Organic Maps sources as of 02.04.2025 (fad26bbf22ac3da75e01e62aa01e5c8e11861005)

To expand with full Organic Maps and Maps.ME commits history run:
  git remote add om-historic [om-historic.git repo url]
  git fetch --tags om-historic
  git replace squashed-history historic-commits
This commit is contained in:
Konstantin Pastbin
2025-04-13 16:37:30 +07:00
commit e3e4a1985a
12931 changed files with 13195100 additions and 0 deletions

View File

@@ -0,0 +1,255 @@
package app.organicmaps;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.github.mikephil.charting.charts.LineChart;
import com.github.mikephil.charting.components.Legend;
import com.github.mikephil.charting.components.MarkerView;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import com.github.mikephil.charting.formatter.ValueFormatter;
import com.github.mikephil.charting.highlight.Highlight;
import com.github.mikephil.charting.listener.OnChartValueSelectedListener;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.bookmarks.data.ElevationInfo;
import app.organicmaps.widget.placepage.AxisValueFormatter;
import app.organicmaps.widget.placepage.CurrentLocationMarkerView;
import app.organicmaps.widget.placepage.FloatingMarkerView;
import app.organicmaps.util.ThemeUtils;
import app.organicmaps.util.Utils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ChartController implements OnChartValueSelectedListener,
BookmarkManager.OnElevationActivePointChangedListener,
BookmarkManager.OnElevationCurrentPositionChangedListener
{
private static final int CHART_Y_LABEL_COUNT = 3;
private static final int CHART_X_LABEL_COUNT = 6;
private static final int CHART_ANIMATION_DURATION = 1500;
private static final int CHART_FILL_ALPHA = (int) (0.12 * 255);
private static final int CHART_AXIS_GRANULARITY = 100;
private static final float CUBIC_INTENSITY = 0.2f;
private static final int CURRENT_POSITION_OUT_OF_TRACK = -1;
@SuppressWarnings("NullableProblems")
@NonNull
private LineChart mChart;
@SuppressWarnings("NullableProblems")
@NonNull
private FloatingMarkerView mFloatingMarkerView;
@SuppressWarnings("NullableProblems")
@NonNull
private MarkerView mCurrentLocationMarkerView;
@SuppressWarnings("NullableProblems")
@NonNull
private TextView mMaxAltitude;
@SuppressWarnings("NullableProblems")
@NonNull
private TextView mMinAltitude;
@NonNull
private final Context mContext;
private long mTrackId = Utils.INVALID_ID;
private boolean mCurrentPositionOutOfTrack = true;
public ChartController(@NonNull Context context)
{
mContext = context;
}
public void initialize(@NonNull View view)
{
BookmarkManager.INSTANCE.setElevationActivePointChangedListener(this);
BookmarkManager.INSTANCE.setElevationCurrentPositionChangedListener(this);
final Resources resources = mContext.getResources();
mChart = view.findViewById(R.id.elevation_profile_chart);
mFloatingMarkerView = view.findViewById(R.id.floating_marker);
mCurrentLocationMarkerView = new CurrentLocationMarkerView(mContext);
mFloatingMarkerView.setChartView(mChart);
mCurrentLocationMarkerView.setChartView(mChart);
mMaxAltitude = view.findViewById(R.id.highest_altitude);
mMinAltitude = view.findViewById(R.id.lowest_altitude);
mChart.setBackgroundColor(ThemeUtils.getColor(mContext, R.attr.cardBackground));
mChart.setTouchEnabled(true);
mChart.setOnChartValueSelectedListener(this);
mChart.setDrawGridBackground(false);
mChart.setScaleXEnabled(true);
mChart.setScaleYEnabled(false);
mChart.setExtraTopOffset(0);
int sideOffset = resources.getDimensionPixelSize(R.dimen.margin_base);
int topOffset = 0;
mChart.setViewPortOffsets(sideOffset, topOffset, sideOffset,
resources.getDimensionPixelSize(R.dimen.margin_base_plus_quarter));
mChart.getDescription().setEnabled(false);
mChart.setDrawBorders(false);
Legend l = mChart.getLegend();
l.setEnabled(false);
initAxises();
}
@SuppressWarnings("unused")
public void destroy()
{
BookmarkManager.INSTANCE.setElevationActivePointChangedListener(null);
BookmarkManager.INSTANCE.setElevationCurrentPositionChangedListener(null);
}
private void highlightChartCurrentLocation()
{
mChart.highlightValues(Collections.singletonList(getCurrentPosHighlight()),
Collections.singletonList(mCurrentLocationMarkerView));
}
private void initAxises()
{
XAxis x = mChart.getXAxis();
x.setLabelCount(CHART_X_LABEL_COUNT, false);
x.setDrawGridLines(false);
x.setGranularity(CHART_AXIS_GRANULARITY);
x.setGranularityEnabled(true);
x.setTextColor(ThemeUtils.getColor(mContext, R.attr.elevationProfileAxisLabelColor));
x.setPosition(XAxis.XAxisPosition.BOTTOM);
x.setAxisLineColor(ThemeUtils.getColor(mContext, androidx.appcompat.R.attr.dividerHorizontal));
x.setAxisLineWidth(mContext.getResources().getDimensionPixelSize(R.dimen.divider_height));
ValueFormatter xAxisFormatter = new AxisValueFormatter(mChart);
x.setValueFormatter(xAxisFormatter);
YAxis y = mChart.getAxisLeft();
y.setLabelCount(CHART_Y_LABEL_COUNT, false);
y.setPosition(YAxis.YAxisLabelPosition.INSIDE_CHART);
y.setDrawGridLines(true);
y.setGridColor(ContextCompat.getColor(mContext, R.color.black_12));
y.setEnabled(true);
y.setTextColor(Color.TRANSPARENT);
y.setAxisLineColor(Color.TRANSPARENT);
int lineLength = mContext.getResources().getDimensionPixelSize(R.dimen.margin_eighth);
y.enableGridDashedLine(lineLength, 2 * lineLength, 0);
mChart.getAxisRight().setEnabled(false);
}
public void setData(@NonNull ElevationInfo info)
{
mTrackId = info.getId();
List<Entry> values = new ArrayList<>();
for (ElevationInfo.Point point: info.getPoints())
values.add(new Entry((float) point.getDistance(), point.getAltitude()));
LineDataSet set = new LineDataSet(values, "Elevation_profile_points");
set.setMode(LineDataSet.Mode.CUBIC_BEZIER);
set.setCubicIntensity(CUBIC_INTENSITY);
set.setDrawFilled(true);
set.setDrawCircles(false);
int lineThickness = mContext.getResources().getDimensionPixelSize(R.dimen.divider_width);
set.setLineWidth(lineThickness);
int color = ThemeUtils.getColor(mContext, R.attr.elevationProfileColor);
set.setCircleColor(color);
set.setColor(color);
set.setFillAlpha(CHART_FILL_ALPHA);
set.setFillColor(color);
set.setDrawHorizontalHighlightIndicator(false);
set.setHighlightLineWidth(lineThickness);
set.setHighLightColor(ContextCompat.getColor(mContext, R.color.base_accent_transparent));
LineData data = new LineData(set);
data.setValueTextSize(mContext.getResources().getDimensionPixelSize(R.dimen.text_size_icon_title));
data.setDrawValues(false);
mChart.setData(data);
mChart.animateX(CHART_ANIMATION_DURATION);
mMinAltitude.setText(Framework.nativeFormatAltitude(info.getMinAltitude()));
mMaxAltitude.setText(Framework.nativeFormatAltitude(info.getMaxAltitude()));
highlightActivePointManually();
}
@Override
public void onValueSelected(Entry e, Highlight h) {
mFloatingMarkerView.updateOffsets(e, h);
Highlight curPos = getCurrentPosHighlight();
if (mCurrentPositionOutOfTrack)
mChart.highlightValues(Collections.singletonList(h), Collections.singletonList(mFloatingMarkerView));
else
mChart.highlightValues(Arrays.asList(curPos, h), Arrays.asList(mCurrentLocationMarkerView,
mFloatingMarkerView));
if (mTrackId == Utils.INVALID_ID)
return;
BookmarkManager.INSTANCE.setElevationActivePoint(mTrackId, e.getX());
}
@NonNull
private Highlight getCurrentPosHighlight()
{
double activeX = BookmarkManager.INSTANCE.getElevationCurPositionDistance(mTrackId);
return new Highlight((float) activeX, 0f, 0);
}
@Override
public void onNothingSelected()
{
if (mCurrentPositionOutOfTrack)
return;
highlightChartCurrentLocation();
}
@Override
public void onCurrentPositionChanged()
{
if (mTrackId == Utils.INVALID_ID)
return;
double distance = BookmarkManager.INSTANCE.getElevationCurPositionDistance(mTrackId);
mCurrentPositionOutOfTrack = distance == CURRENT_POSITION_OUT_OF_TRACK;
highlightActivePointManually();
}
@Override
public void onElevationActivePointChanged()
{
if (mTrackId == Utils.INVALID_ID)
return;
highlightActivePointManually();
}
private void highlightActivePointManually()
{
Highlight highlight = getActivePoint();
mChart.highlightValue(highlight, true);
}
@NonNull
private Highlight getActivePoint()
{
double activeX = BookmarkManager.INSTANCE.getElevationActivePointDistance(mTrackId);
return new Highlight((float) activeX, 0f, 0);
}
public void onHide()
{
mChart.fitScreen();
mTrackId = Utils.INVALID_ID;
}
}

View File

@@ -0,0 +1,463 @@
package app.organicmaps;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.ComponentName;
import android.content.Intent;
import android.location.Location;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.CallSuper;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import androidx.core.view.ViewCompat;
import app.organicmaps.base.BaseMwmFragmentActivity;
import app.organicmaps.downloader.CountryItem;
import app.organicmaps.downloader.MapManager;
import app.organicmaps.intent.Factory;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.location.LocationListener;
import app.organicmaps.util.Config;
import app.organicmaps.util.ConnectionState;
import app.organicmaps.util.StringUtils;
import app.organicmaps.util.UiUtils;
import app.organicmaps.util.Utils;
import app.organicmaps.util.WindowInsetUtils.PaddingInsetsListener;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import java.util.List;
import java.util.Objects;
@SuppressLint("StringFormatMatches")
public class DownloadResourcesLegacyActivity extends BaseMwmFragmentActivity
{
private static final String TAG = DownloadResourcesLegacyActivity.class.getSimpleName();
// Error codes, should match the same codes in JNI
private static final int ERR_DOWNLOAD_SUCCESS = 0;
private static final int ERR_DISK_ERROR = -1;
private static final int ERR_NOT_ENOUGH_FREE_SPACE = -2;
private static final int ERR_STORAGE_DISCONNECTED = -3;
private static final int ERR_DOWNLOAD_ERROR = -4;
private static final int ERR_NO_MORE_FILES = -5;
private static final int ERR_FILE_IN_PROGRESS = -6;
private TextView mTvMessage;
private LinearProgressIndicator mProgress;
private Button mBtnDownload;
private CheckBox mChbDownloadCountry;
private String mCurrentCountry;
@Nullable
private Dialog mAlertDialog;
@NonNull
private ActivityResultLauncher<Intent> mApiRequest;
private boolean mAreResourcesDownloaded;
private static final int DOWNLOAD = 0;
private static final int PAUSE = 1;
private static final int RESUME = 2;
private static final int TRY_AGAIN = 3;
private static final int PROCEED_TO_MAP = 4;
private static final int BTN_COUNT = 5;
private View.OnClickListener[] mBtnListeners;
private String[] mBtnNames;
private int mCountryDownloadListenerSlot;
private interface Listener
{
// Called by JNI.
@Keep
@SuppressWarnings("unused")
void onProgress(int percent);
// Called by JNI.
@Keep
@SuppressWarnings("unused")
void onFinish(int errorCode);
}
private final LocationListener mLocationListener = new LocationListener()
{
@Override
public void onLocationUpdated(Location location)
{
if (mCurrentCountry != null)
return;
final double lat = location.getLatitude();
final double lon = location.getLongitude();
mCurrentCountry = MapManager.nativeFindCountry(lat, lon);
if (TextUtils.isEmpty(mCurrentCountry))
{
mCurrentCountry = null;
return;
}
int status = MapManager.nativeGetStatus(mCurrentCountry);
String name = MapManager.nativeGetName(mCurrentCountry);
if (status != CountryItem.STATUS_DONE)
{
UiUtils.show(mChbDownloadCountry);
String checkBoxText;
if (status == CountryItem.STATUS_UPDATABLE)
checkBoxText = String.format(getString(R.string.update_country_ask), name);
else
checkBoxText = String.format(getString(R.string.download_country_ask), name);
mChbDownloadCountry.setText(checkBoxText);
}
LocationHelper.from(DownloadResourcesLegacyActivity.this).removeListener(this);
}
};
private final Listener mResourcesDownloadListener = new Listener()
{
@Override
public void onProgress(final int percent)
{
if (!isFinishing())
mProgress.setProgressCompat(percent, true);
}
@Override
public void onFinish(final int errorCode)
{
if (isFinishing())
return;
if (errorCode == ERR_DOWNLOAD_SUCCESS)
{
final int res = nativeStartNextFileDownload(mResourcesDownloadListener);
if (res == ERR_NO_MORE_FILES)
finishFilesDownload(res);
}
else
finishFilesDownload(errorCode);
}
};
private final MapManager.StorageCallback mCountryDownloadListener = new MapManager.StorageCallback()
{
@Override
public void onStatusChanged(List<MapManager.StorageCallbackData> data)
{
for (MapManager.StorageCallbackData item : data)
{
if (!item.isLeafNode)
continue;
switch (item.newStatus)
{
case CountryItem.STATUS_DONE:
mAreResourcesDownloaded = true;
showMap();
return;
case CountryItem.STATUS_FAILED:
MapManager.showError(DownloadResourcesLegacyActivity.this, item, null);
return;
}
}
}
@Override
public void onProgress(String countryId, long localSize, long remoteSize)
{
mProgress.setProgressCompat((int) localSize, true);
}
};
@CallSuper
@Override
protected void onSafeCreate(@Nullable Bundle savedInstanceState)
{
super.onSafeCreate(savedInstanceState);
UiUtils.setLightStatusBar(this, true);
setContentView(R.layout.activity_download_resources);
final View view = getWindow().getDecorView().findViewById(android.R.id.content);
ViewCompat.setOnApplyWindowInsetsListener(view, PaddingInsetsListener.allSides());
initViewsAndListeners();
mApiRequest = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
setResult(result.getResultCode(), result.getData());
finish();
});
if (prepareFilesDownload(false))
{
Utils.keepScreenOn(true, getWindow());
setAction(DOWNLOAD);
return;
}
showMap();
}
@CallSuper
@Override
protected void onSafeDestroy()
{
super.onSafeDestroy();
mApiRequest.unregister();
mApiRequest = null;
Utils.keepScreenOn(Config.isKeepScreenOnEnabled(), getWindow());
if (mCountryDownloadListenerSlot != 0)
{
MapManager.nativeUnsubscribe(mCountryDownloadListenerSlot);
mCountryDownloadListenerSlot = 0;
}
}
@CallSuper
@Override
protected void onResume()
{
super.onResume();
if (!isFinishing())
LocationHelper.from(this).addListener(mLocationListener);
}
@Override
protected void onPause()
{
super.onPause();
LocationHelper.from(this).removeListener(mLocationListener);
if (mAlertDialog != null && mAlertDialog.isShowing())
mAlertDialog.dismiss();
mAlertDialog = null;
}
private void setDownloadMessage(int bytesToDownload)
{
mTvMessage.setText(getString(R.string.download_resources,
StringUtils.getFileSizeString(this, bytesToDownload)));
}
private boolean prepareFilesDownload(boolean showMap)
{
final int bytes = nativeGetBytesToDownload();
if (bytes == 0)
{
mAreResourcesDownloaded = true;
if (showMap)
showMap();
return false;
}
if (bytes > 0)
{
setDownloadMessage(bytes);
mProgress.setMax(bytes);
mProgress.setProgressCompat(0, true);
}
else
finishFilesDownload(bytes);
return true;
}
private void initViewsAndListeners()
{
mTvMessage = findViewById(R.id.download_message);
mProgress = findViewById(R.id.progressbar);
mBtnDownload = findViewById(R.id.btn_download_resources);
mChbDownloadCountry = findViewById(R.id.chb_download_country);
mBtnListeners = new View.OnClickListener[BTN_COUNT];
mBtnNames = new String[BTN_COUNT];
mBtnListeners[DOWNLOAD] = v -> onDownloadClicked();
mBtnNames[DOWNLOAD] = getString(R.string.download);
mBtnListeners[PAUSE] = v -> onPauseClicked();
mBtnNames[PAUSE] = getString(R.string.pause);
mBtnListeners[RESUME] = v -> onResumeClicked();
mBtnNames[RESUME] = getString(R.string.continue_button);
mBtnListeners[TRY_AGAIN] = v -> onTryAgainClicked();
mBtnNames[TRY_AGAIN] = getString(R.string.try_again);
mBtnListeners[PROCEED_TO_MAP] = v -> onProceedToMapClicked();
mBtnNames[PROCEED_TO_MAP] = getString(R.string.download_resources_continue);
}
private void setAction(int action)
{
mBtnDownload.setOnClickListener(mBtnListeners[action]);
mBtnDownload.setText(mBtnNames[action]);
}
private void doDownload()
{
if (nativeStartNextFileDownload(mResourcesDownloadListener) == ERR_NO_MORE_FILES)
finishFilesDownload(ERR_NO_MORE_FILES);
}
private void onDownloadClicked()
{
setAction(PAUSE);
doDownload();
}
private void onPauseClicked()
{
setAction(RESUME);
nativeCancelCurrentFile();
}
private void onResumeClicked()
{
setAction(PAUSE);
doDownload();
}
private void onTryAgainClicked()
{
if (prepareFilesDownload(true))
{
setAction(PAUSE);
doDownload();
}
}
private void onProceedToMapClicked()
{
mAreResourcesDownloaded = true;
showMap();
}
public void showMap()
{
if (!mAreResourcesDownloaded)
return;
// Re-use original intent to retain all flags and payload.
// https://github.com/organicmaps/organicmaps/issues/6944
final Intent intent = Objects.requireNonNull(getIntent());
intent.setComponent(new ComponentName(this, MwmActivity.class));
// Disable animation because MwmActivity should appear exactly over this one
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_CLEAR_TOP);
// See {@link SplashActivity.processNavigation()}
if (Factory.isStartedForApiResult(intent))
{
// Wait for the result from MwmActivity for API callers.
mApiRequest.launch(intent);
return;
}
startActivity(intent);
finish();
}
private void finishFilesDownload(int result)
{
if (result == ERR_NO_MORE_FILES)
{
// World and WorldCoasts has been downloaded, we should register maps again to correctly add them to the model.
Framework.nativeReloadWorldMaps();
if (mCurrentCountry != null && mChbDownloadCountry.isChecked())
{
CountryItem item = CountryItem.fill(mCurrentCountry);
UiUtils.hide(mChbDownloadCountry);
mTvMessage.setText(getString(R.string.downloading_country_can_proceed, item.name));
mProgress.setMax((int)item.totalSize);
mProgress.setProgressCompat(0, true);
mCountryDownloadListenerSlot = MapManager.nativeSubscribe(mCountryDownloadListener);
MapManager.startDownload(mCurrentCountry);
setAction(PROCEED_TO_MAP);
}
else
{
mAreResourcesDownloaded = true;
showMap();
}
}
else
{
showErrorDialog(result);
}
}
private void showErrorDialog(int result)
{
if (mAlertDialog != null && mAlertDialog.isShowing())
return;
@StringRes final int titleId;
@StringRes final int messageId = switch (result)
{
case ERR_NOT_ENOUGH_FREE_SPACE ->
{
titleId = R.string.routing_not_enough_space;
yield R.string.not_enough_free_space_on_sdcard;
}
case ERR_STORAGE_DISCONNECTED ->
{
titleId = R.string.disconnect_usb_cable_title;
yield R.string.disconnect_usb_cable;
}
case ERR_DOWNLOAD_ERROR ->
{
titleId = R.string.connection_failure;
yield (ConnectionState.INSTANCE.isConnected() ? R.string.download_has_failed
: R.string.common_check_internet_connection_dialog);
}
case ERR_DISK_ERROR ->
{
titleId = R.string.disk_error_title;
yield R.string.disk_error;
}
default -> throw new AssertionError("Unexpected result code = " + result);
};
mAlertDialog = new MaterialAlertDialogBuilder(this, R.style.MwmTheme_AlertDialog)
.setTitle(titleId)
.setMessage(messageId)
.setCancelable(true)
.setOnCancelListener((dialog) -> setAction(PAUSE))
.setPositiveButton(R.string.try_again, (dialog, which) -> {
setAction(TRY_AGAIN);
onTryAgainClicked();
})
.setOnDismissListener(dialog -> mAlertDialog = null)
.show();
}
@Override
@StyleRes
public int getThemeResourceId(@NonNull String theme)
{
return R.style.MwmTheme_DownloadResourcesLegacy;
}
private static native int nativeGetBytesToDownload();
private static native int nativeStartNextFileDownload(Listener listener);
private static native void nativeCancelCurrentFile();
}

View File

@@ -0,0 +1,487 @@
package app.organicmaps;
import android.graphics.Bitmap;
import androidx.annotation.IntDef;
import androidx.annotation.Keep;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Size;
import app.organicmaps.api.ParsedRoutingData;
import app.organicmaps.api.ParsedSearchRequest;
import app.organicmaps.api.RequestType;
import app.organicmaps.bookmarks.data.DistanceAndAzimut;
import app.organicmaps.bookmarks.data.FeatureId;
import app.organicmaps.bookmarks.data.MapObject;
import app.organicmaps.products.Product;
import app.organicmaps.products.ProductsConfig;
import app.organicmaps.routing.JunctionInfo;
import app.organicmaps.routing.RouteMarkData;
import app.organicmaps.routing.RoutePointInfo;
import app.organicmaps.routing.RoutingInfo;
import app.organicmaps.routing.TransitRouteInfo;
import app.organicmaps.settings.SettingsPrefsFragment;
import app.organicmaps.widget.placepage.PlacePageData;
import app.organicmaps.util.Constants;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* This class wraps android::Framework.cpp class
* via static methods
*/
public class Framework
{
@Retention(RetentionPolicy.SOURCE)
@IntDef({MAP_STYLE_CLEAR, MAP_STYLE_DARK, MAP_STYLE_VEHICLE_CLEAR, MAP_STYLE_VEHICLE_DARK, MAP_STYLE_OUTDOORS_CLEAR, MAP_STYLE_OUTDOORS_DARK})
public @interface MapStyle {}
public static final int MAP_STYLE_CLEAR = 0;
public static final int MAP_STYLE_DARK = 1;
public static final int MAP_STYLE_VEHICLE_CLEAR = 3;
public static final int MAP_STYLE_VEHICLE_DARK = 4;
public static final int MAP_STYLE_OUTDOORS_CLEAR = 5;
public static final int MAP_STYLE_OUTDOORS_DARK = 6;
@Retention(RetentionPolicy.SOURCE)
@IntDef({ ROUTER_TYPE_VEHICLE, ROUTER_TYPE_PEDESTRIAN, ROUTER_TYPE_BICYCLE, ROUTER_TYPE_TRANSIT, ROUTER_TYPE_RULER })
public @interface RouterType {}
public static final int ROUTER_TYPE_VEHICLE = 0;
public static final int ROUTER_TYPE_PEDESTRIAN = 1;
public static final int ROUTER_TYPE_BICYCLE = 2;
public static final int ROUTER_TYPE_TRANSIT = 3;
public static final int ROUTER_TYPE_RULER = 4;
@Retention(RetentionPolicy.SOURCE)
@IntDef({ROUTE_REBUILD_AFTER_POINTS_LOADING})
public @interface RouteRecommendationType {}
public static final int ROUTE_REBUILD_AFTER_POINTS_LOADING = 0;
public interface PlacePageActivationListener
{
// Called from JNI.
@Keep
@SuppressWarnings("unused")
void onPlacePageActivated(@NonNull PlacePageData data);
// Called from JNI
@Keep
@SuppressWarnings("unused")
void onPlacePageDeactivated();
// Called from JNI
@Keep
@SuppressWarnings("unused")
void onSwitchFullScreenMode();
}
public interface RoutingListener
{
// Called from JNI
@Keep
@SuppressWarnings("unused")
@MainThread
void onRoutingEvent(int resultCode, String[] missingMaps);
}
public interface RoutingProgressListener
{
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
void onRouteBuildingProgress(float progress);
}
public interface RoutingRecommendationListener
{
// Called from JNI.
@Keep
@SuppressWarnings("unused")
void onRecommend(@RouteRecommendationType int recommendation);
}
public interface RoutingLoadPointsListener
{
// Called from JNI.
@Keep
@SuppressWarnings("unused")
void onRoutePointsLoaded(boolean success);
}
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public static class Params3dMode
{
public boolean enabled;
public boolean buildings;
}
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public static class RouteAltitudeLimits
{
public int totalAscent;
public int totalDescent;
public String totalAscentString;
public String totalDescentString;
public boolean isMetricUnits;
}
// this class is just bridge between Java and C++ worlds, we must not create it
private Framework() {}
public static String getHttpGe0Url(double lat, double lon, double zoomLevel, String name)
{
return nativeGetGe0Url(lat, lon, zoomLevel, name).replaceFirst(
Constants.Url.SHORT_SHARE_PREFIX, Constants.Url.HTTP_SHARE_PREFIX);
}
/**
* Generates Bitmap with route altitude image chart taking into account current map style.
* @param width is width of the image.
* @param height is height of the image.
* @return Bitmap if there's pedestrian or bicycle route and null otherwise.
*/
@Nullable
public static Bitmap generateRouteAltitudeChart(int width, int height,
@NonNull RouteAltitudeLimits limits)
{
if (width <= 0 || height <= 0)
return null;
final int[] altitudeChartBits = Framework.nativeGenerateRouteAltitudeChartBits(width, height,
limits);
if (altitudeChartBits == null)
return null;
return Bitmap.createBitmap(altitudeChartBits, width, height, Bitmap.Config.ARGB_8888);
}
public static void setSpeedCamerasMode(@NonNull SettingsPrefsFragment.SpeedCameraMode mode)
{
nativeSetSpeedCamManagerMode(mode.ordinal());
}
public static native void nativeShowTrackRect(long track);
public static native int nativeGetDrawScale();
public static native void nativePokeSearchInViewport();
@Size(2)
public static native double[] nativeGetScreenRectCenter();
public static native DistanceAndAzimut nativeGetDistanceAndAzimuth(double dstMerX, double dstMerY, double srcLat, double srcLon, double north);
public static native DistanceAndAzimut nativeGetDistanceAndAzimuthFromLatLon(double dstLat, double dstLon, double srcLat, double srcLon, double north);
public static native String nativeFormatLatLon(double lat, double lon, int coordFormat);
public static native String nativeFormatAltitude(double alt);
public static native String nativeFormatSpeed(double speed);
public static native String nativeGetGe0Url(double lat, double lon, double zoomLevel, String name);
public static native String nativeGetGeoUri(double lat, double lon, double zoomLevel, String name);
public static native String nativeGetAddress(double lat, double lon);
public static native void nativePlacePageActivationListener(@NonNull PlacePageActivationListener listener);
public static native void nativeRemovePlacePageActivationListener(@NonNull PlacePageActivationListener listener);
// @UiThread
// public static native String nativeGetOutdatedCountriesString();
//
// @UiThread
// @NonNull
// public static native String[] nativeGetOutdatedCountries();
//
// @UiThread
// @DoAfterUpdate
// public static native int nativeToDoAfterUpdate();
//
// public static native boolean nativeIsDataVersionChanged();
//
// public static native void nativeUpdateSavedDataVersion();
private static native long nativeGetDataVersion();
public static Date getDataVersion()
{
long dataVersion = nativeGetDataVersion();
final SimpleDateFormat format = new SimpleDateFormat("yyMMdd", Locale.ENGLISH);
try
{
return format.parse(String.valueOf(dataVersion));
}
catch (ParseException e)
{
throw new AssertionError("Invalid data version code: " + dataVersion);
}
}
public static native void nativeClearApiPoints();
@NonNull
public static native @RequestType int nativeParseAndSetApiUrl(String url);
public static native ParsedRoutingData nativeGetParsedRoutingData();
public static native ParsedSearchRequest nativeGetParsedSearchRequest();
public static native @Nullable String nativeGetParsedAppName();
public static native @Nullable String nativeGetParsedOAuth2Code();
@Nullable @Size(2)
public static native double[] nativeGetParsedCenterLatLon();
public static native @Nullable String nativeGetParsedBackUrl();
public static native void nativeDeactivatePopup();
public static native void nativeDeactivateMapSelectionCircle();
public static native String nativeGetDataFileExt();
public static native String[] nativeGetMovableFilesExts();
public static native String[] nativeGetBookmarksFilesExts();
public static native String nativeGetBookmarkDir();
public static native String nativeGetSettingsDir();
public static native String nativeGetWritableDir();
public static native void nativeChangeWritableDir(String newPath);
// Routing.
public static native boolean nativeIsRoutingActive();
public static native boolean nativeIsRouteBuilt();
public static native boolean nativeIsRouteBuilding();
public static native void nativeCloseRouting();
public static native void nativeBuildRoute();
public static native void nativeRemoveRoute();
public static native void nativeFollowRoute();
public static native void nativeDisableFollowing();
@Nullable
public static native RoutingInfo nativeGetRouteFollowingInfo();
@Nullable
public static native JunctionInfo[] nativeGetRouteJunctionPoints();
@Nullable
public static native final int[] nativeGenerateRouteAltitudeChartBits(int width, int height, RouteAltitudeLimits routeAltitudeLimits);
// When an end user is going to a turn he gets sound turn instructions.
// If C++ part wants the client to pronounce an instruction nativeGenerateTurnNotifications returns
// an array of one of more strings. C++ part assumes that all these strings shall be pronounced by the client's TTS.
// For example if C++ part wants the client to pronounce "Make a right turn." this method returns
// an array with one string "Make a right turn.". The next call of the method returns nothing.
// nativeGenerateTurnNotifications shall be called by the client when a new position is available.
@Nullable
public static native String[] nativeGenerateNotifications(boolean announceStreets);
private static native void nativeSetSpeedCamManagerMode(int mode);
public static native void nativeSetRoutingListener(RoutingListener listener);
public static native void nativeSetRouteProgressListener(RoutingProgressListener listener);
public static native void nativeSetRoutingRecommendationListener(RoutingRecommendationListener listener);
public static native void nativeSetRoutingLoadPointsListener(
@Nullable RoutingLoadPointsListener listener);
public static native void nativeShowCountry(String countryId, boolean zoomToDownloadButton);
public static native void nativeSetMapStyle(int mapStyle);
@MapStyle
public static native int nativeGetMapStyle();
/**
* This method allows to set new map style without immediate applying. It can be used before
* engine recreation instead of nativeSetMapStyle to avoid huge flow of OpenGL invocations.
* @param mapStyle style index
*/
public static native void nativeMarkMapStyle(int mapStyle);
public static native void nativeSetRouter(@RouterType int routerType);
@RouterType
public static native int nativeGetRouter();
@RouterType
public static native int nativeGetLastUsedRouter();
@RouterType
public static native int nativeGetBestRouter(double srcLat, double srcLon,
double dstLat, double dstLon);
public static void addRoutePoint(RouteMarkData point)
{
Framework.nativeAddRoutePoint(point.mTitle, point.mSubtitle, point.mPointType,
point.mIntermediateIndex, point.mIsMyPosition,
point.mLat, point.mLon);
}
public static native void nativeAddRoutePoint(String title, String subtitle,
@RoutePointInfo.RouteMarkType int markType,
int intermediateIndex, boolean isMyPosition,
double lat, double lon);
public static native void nativeRemoveRoutePoints();
public static native void nativeRemoveRoutePoint(@RoutePointInfo.RouteMarkType int markType,
int intermediateIndex);
public static native void nativeRemoveIntermediateRoutePoints();
public static native boolean nativeCouldAddIntermediatePoint();
@NonNull
public static native RouteMarkData[] nativeGetRoutePoints();
public static native void nativeMoveRoutePoint(int currentIndex, int targetIndex);
@NonNull
public static native TransitRouteInfo nativeGetTransitRouteInfo();
/**
* Registers all maps(.mwms). Adds them to the models, generates indexes and does all necessary stuff.
*/
public static native void nativeReloadWorldMaps();
/**
* Determines if currently is day or night at the given location. Used to switch day/night styles.
* @param utcTimeSeconds Unix time in seconds.
* @param lat latitude of the current location.
* @param lon longitude of the current location.
* @return {@code true} if it is day now or {@code false} otherwise.
*/
public static native boolean nativeIsDayTime(long utcTimeSeconds, double lat, double lon);
public static native void nativeGet3dMode(Params3dMode result);
public static native void nativeSet3dMode(boolean allow3d, boolean allow3dBuildings);
public static native boolean nativeGetAutoZoomEnabled();
public static native void nativeSetAutoZoomEnabled(boolean enabled);
public static native void nativeSetTransitSchemeEnabled(boolean enabled);
public static native void nativeSaveSettingSchemeEnabled(boolean enabled);
public static native boolean nativeIsTransitSchemeEnabled();
public static native void nativeSetIsolinesLayerEnabled(boolean enabled);
public static native boolean nativeIsIsolinesLayerEnabled();
public static native void nativeSetOutdoorsLayerEnabled(boolean enabled);
public static native boolean nativeIsOutdoorsLayerEnabled();
@NonNull
public static native MapObject nativeDeleteBookmarkFromMapObject();
@NonNull
public static native String nativeGetPoiContactUrl(int metadataType);
public static native void nativeZoomToPoint(double lat, double lon, int zoom, boolean animate);
@Retention(RetentionPolicy.SOURCE)
@IntDef({ChoosePositionMode.NONE, ChoosePositionMode.EDITOR, ChoosePositionMode.API})
public @interface ChoosePositionMode
{
// Keep in sync with `enum ChoosePositionMode` in Framework.hpp.
public static final int NONE = 0;
public static final int EDITOR = 1;
public static final int API = 2;
}
/**
* @param mode - see ChoosePositionMode values.
* @param isBusiness selection area will be bounded by building borders, if its true (eg. true for businesses in buildings).
* @param applyPosition if true, map'll be animated to currently selected object.
*/
public static native void nativeSetChoosePositionMode(@ChoosePositionMode int mode, boolean isBusiness,
boolean applyPosition);
public static native @ChoosePositionMode int nativeGetChoosePositionMode();
public static native boolean nativeIsDownloadedMapAtScreenCenter();
public static native String nativeGetActiveObjectFormattedCuisine();
public static native void nativeSetVisibleRect(int left, int top, int right, int bottom);
// Navigation.
public static native boolean nativeIsRouteFinished();
public static native void nativeRunFirstLaunchAnimation();
public static native int nativeOpenRoutePointsTransaction();
public static native void nativeApplyRoutePointsTransaction(int transactionId);
public static native void nativeCancelRoutePointsTransaction(int transactionId);
public static native int nativeInvalidRoutePointsTransactionId();
public static native boolean nativeHasSavedRoutePoints();
public static native void nativeLoadRoutePoints();
public static native void nativeSaveRoutePoints();
public static native void nativeDeleteSavedRoutePoints();
public static native void nativeShowFeature(@NonNull FeatureId featureId);
public static native void nativeMakeCrash();
public static native void nativeSetPowerManagerFacility(int facilityType, boolean state);
public static native int nativeGetPowerManagerScheme();
public static native void nativeSetPowerManagerScheme(int schemeType);
public static native void nativeSetViewportCenter(double lat, double lon, int zoom);
public static native void nativeStopLocationFollow();
public static native void nativeSetSearchViewport(double lat, double lon, int zoom);
/**
* In case of the app was dumped by system to the hard drive, Java map object can be
* restored from parcelable, but c++ framework is created from scratch and internal
* place page object is not initialized. So, do not restore place page in this case.
*
* @return true if c++ framework has initialized internal place page object, otherwise - false.
*/
public static native boolean nativeHasPlacePageInfo();
public static native void nativeMemoryWarning();
/**
* @param countryIsoCode Two-letter ISO country code to use country-specific Kayak.com domain.
* @param uri `$HOTEL_NAME,-c$CITY_ID-h$HOTEL_ID` URI.
* @param firstDaySec the epoch seconds of the first day of planned stay.
* @param lastDaySec the epoch seconds of the last day of planned stay.
* @return a URL to Kayak's hotel page.
*/
@Nullable
public static native String nativeGetKayakHotelLink(@NonNull String countryIsoCode, @NonNull String uri,
long firstDaySec, long lastDaySec);
public static native boolean nativeShouldShowProducts();
@Nullable
public static native ProductsConfig nativeGetProductsConfiguration();
public static native void nativeDidCloseProductsPopup(String reason);
public static native void nativeDidSelectProduct(String title, String link);
}

View File

@@ -0,0 +1,419 @@
package app.organicmaps;
import android.content.Context;
import android.graphics.Rect;
import android.view.MotionEvent;
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.ROMUtils;
import app.organicmaps.util.UiUtils;
import app.organicmaps.util.concurrency.UiThread;
import app.organicmaps.util.log.Logger;
public final class Map
{
public interface CallbackUnsupported
{
void report();
}
public static final String ARG_LAUNCH_BY_DEEP_LINK = "launch_by_deep_link";
private static final String TAG = Map.class.getSimpleName();
// Should correspond to android::MultiTouchAction from Framework.cpp
public static final int NATIVE_ACTION_UP = 0x01;
public static final int NATIVE_ACTION_DOWN = 0x02;
public static final int NATIVE_ACTION_MOVE = 0x03;
public static final int NATIVE_ACTION_CANCEL = 0x04;
// Should correspond to gui::EWidget from skin.hpp
public static final int WIDGET_RULER = 0x01;
public static final int WIDGET_COMPASS = 0x02;
public static final int WIDGET_COPYRIGHT = 0x04;
public static final int WIDGET_SCALE_FPS_LABEL = 0x08;
// Should correspond to dp::Anchor from drape_global.hpp
public static final int ANCHOR_CENTER = 0x00;
public static final int ANCHOR_LEFT = 0x01;
public static final int ANCHOR_RIGHT = (ANCHOR_LEFT << 1);
public static final int ANCHOR_TOP = (ANCHOR_RIGHT << 1);
public static final int ANCHOR_BOTTOM = (ANCHOR_TOP << 1);
public static final int ANCHOR_LEFT_TOP = (ANCHOR_LEFT | ANCHOR_TOP);
public static final int ANCHOR_RIGHT_TOP = (ANCHOR_RIGHT | ANCHOR_TOP);
public static final int ANCHOR_LEFT_BOTTOM = (ANCHOR_LEFT | ANCHOR_BOTTOM);
public static final int ANCHOR_RIGHT_BOTTOM = (ANCHOR_RIGHT | ANCHOR_BOTTOM);
// Should correspond to df::TouchEvent::INVALID_MASKED_POINTER from user_event_stream.cpp
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;
private int mBottomWidgetOffsetY;
private int mHeight;
private int mWidth;
private boolean mRequireResize;
private boolean mSurfaceCreated;
private boolean mSurfaceAttached;
private boolean mLaunchByDeepLink;
@Nullable
private String mUiThemeOnPause;
@Nullable
private MapRenderingListener mMapRenderingListener;
@Nullable
private CallbackUnsupported mCallbackUnsupported;
private static int sCurrentDpi = 0;
public Map(DisplayType mapType)
{
mDisplayType = mapType;
onCreate(false);
}
/**
* Moves the map compass using the given offsets.
*
* @param context Context.
* @param offsetX Pixel offset from the top. -1 to keep the previous value.
* @param offsetY Pixel offset from the right. -1 to keep the previous value.
* @param forceRedraw True to force the compass to redraw
*/
public void updateCompassOffset(final Context context, int offsetX, int offsetY, boolean forceRedraw)
{
final int x = offsetX < 0 ? mCurrentCompassOffsetX : offsetX;
final int y = offsetY < 0 ? mCurrentCompassOffsetY : offsetY;
final int navPadding = UiUtils.dimen(context, R.dimen.nav_frame_padding);
final int marginX = UiUtils.dimen(context, R.dimen.margin_compass) + navPadding;
final int marginY = UiUtils.dimen(context, R.dimen.margin_compass_top) + navPadding;
nativeSetupWidget(WIDGET_COMPASS, mWidth - x - marginX, y + marginY, ANCHOR_CENTER);
if (forceRedraw && mSurfaceCreated)
nativeApplyWidgets();
mCurrentCompassOffsetX = x;
mCurrentCompassOffsetY = y;
}
public static void onCompassUpdated(double north, boolean forceRedraw)
{
nativeCompassUpdated(north, forceRedraw);
}
/**
* Moves the ruler and copyright using the given offsets.
*
* @param context Context.
* @param offsetX Pixel offset from the left. -1 to keep the previous value.
* @param offsetY Pixel offset from the bottom. -1 to keep the previous value.
*/
public void updateBottomWidgetsOffset(final Context context, int offsetX, int offsetY)
{
final int x = offsetX < 0 ? mBottomWidgetOffsetX : offsetX;
final int y = offsetY < 0 ? mBottomWidgetOffsetY : offsetY;
updateRulerOffset(context, x, y);
updateAttributionOffset(context, x, y);
mBottomWidgetOffsetX = x;
mBottomWidgetOffsetY = y;
}
/**
* Moves my position arrow to the given offset.
*
* @param offsetY Pixel offset from the bottom.
*/
public void updateMyPositionRoutingOffset(int offsetY)
{
nativeUpdateMyPositionRoutingOffset(offsetY);
}
public void onSurfaceCreated(final Context context, final Surface surface, Rect surfaceFrame, int surfaceDpi)
{
if (isThemeChangingProcess(context))
{
Logger.d(TAG, "Theme changing process, skip 'onSurfaceCreated' callback");
return;
}
Logger.d(TAG, "mSurfaceCreated = " + mSurfaceCreated);
if (nativeIsEngineCreated())
{
if (sCurrentDpi != surfaceDpi)
{
nativeUpdateEngineDpi(surfaceDpi);
sCurrentDpi = surfaceDpi;
setupWidgets(context, surfaceFrame.width(), surfaceFrame.height());
}
if (!nativeAttachSurface(surface))
{
if (mCallbackUnsupported != null)
mCallbackUnsupported.report();
return;
}
mSurfaceCreated = true;
mSurfaceAttached = true;
mRequireResize = true;
nativeResumeSurfaceRendering();
return;
}
mRequireResize = false;
setupWidgets(context, surfaceFrame.width(), surfaceFrame.height());
final LocationHelper locationHelper = LocationHelper.from(context);
final boolean firstStart = locationHelper.isInFirstRun();
if (!nativeCreateEngine(surface, surfaceDpi, firstStart, mLaunchByDeepLink,
BuildConfig.VERSION_CODE, ROMUtils.isCustomROM()))
{
if (mCallbackUnsupported != null)
mCallbackUnsupported.report();
return;
}
sCurrentDpi = surfaceDpi;
if (firstStart)
UiThread.runLater(locationHelper::onExitFromFirstRun);
mSurfaceCreated = true;
mSurfaceAttached = true;
nativeResumeSurfaceRendering();
if (mMapRenderingListener != null)
mMapRenderingListener.onRenderingCreated();
}
public void onSurfaceChanged(final Context context, final Surface surface, Rect surfaceFrame, boolean isSurfaceCreating)
{
if (isThemeChangingProcess(context))
{
Logger.d(TAG, "Theme changing process, skip 'onSurfaceChanged' callback");
return;
}
Logger.d(TAG, "mSurfaceCreated = " + mSurfaceCreated);
if (!mSurfaceCreated || (!mRequireResize && isSurfaceCreating))
return;
nativeSurfaceChanged(surface, surfaceFrame.width(), surfaceFrame.height());
mRequireResize = false;
setupWidgets(context, surfaceFrame.width(), surfaceFrame.height());
nativeApplyWidgets();
if (mMapRenderingListener != null)
mMapRenderingListener.onRenderingRestored();
}
public void onSurfaceDestroyed(boolean activityIsChangingConfigurations, boolean isAdded)
{
Logger.d(TAG, "mSurfaceCreated = " + mSurfaceCreated + ", mSurfaceAttached = " + mSurfaceAttached + ", isAdded = " + isAdded);
if (!mSurfaceCreated || !mSurfaceAttached || !isAdded)
return;
nativeDetachSurface(!activityIsChangingConfigurations);
mSurfaceCreated = !nativeDestroySurfaceOnDetach();
mSurfaceAttached = false;
}
public void setMapRenderingListener(MapRenderingListener mapRenderingListener)
{
mMapRenderingListener = mapRenderingListener;
}
public void setCallbackUnsupported(CallbackUnsupported callback)
{
mCallbackUnsupported = callback;
}
public void onCreate(boolean launchByDeeplink)
{
mLaunchByDeepLink = launchByDeeplink;
mCurrentCompassOffsetX = 0;
mCurrentCompassOffsetY = 0;
mBottomWidgetOffsetX = 0;
mBottomWidgetOffsetY = 0;
}
public void onStart()
{
nativeSetRenderingInitializationFinishedListener(mMapRenderingListener);
}
public void onStop()
{
nativeSetRenderingInitializationFinishedListener(null);
}
public void onPause(final Context context)
{
mUiThemeOnPause = Config.getCurrentUiTheme(context);
// Pause/Resume can be called without surface creation/destroy.
if (mSurfaceAttached)
nativePauseSurfaceRendering();
}
public void onResume()
{
// Pause/Resume can be called without surface creation/destroy.
if (mSurfaceAttached)
nativeResumeSurfaceRendering();
}
boolean isContextCreated()
{
return mSurfaceCreated;
}
public void onScroll(double distanceX, double distanceY)
{
Map.nativeOnScroll(distanceX, distanceY);
}
public static void zoomIn()
{
nativeScalePlus();
}
public static void zoomOut()
{
nativeScaleMinus();
}
public static void onScale(double factor, double focusX, double focusY, boolean isAnim)
{
nativeOnScale(factor, focusX, focusY, isAnim);
}
public static void onTouch(int actionType, MotionEvent event, int pointerIndex)
{
if (event.getPointerCount() == 1)
{
nativeOnTouch(actionType, event.getPointerId(0), event.getX(), event.getY(), Map.INVALID_TOUCH_ID, 0, 0, 0);
}
else
{
nativeOnTouch(actionType,
event.getPointerId(0), event.getX(0), event.getY(0),
event.getPointerId(1), event.getX(1), event.getY(1), pointerIndex);
}
}
public static void onClick(float x, float y)
{
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()
{
return nativeIsEngineCreated();
}
public static void executeMapApiRequest()
{
nativeExecuteMapApiRequest();
}
private void setupWidgets(final Context context, int width, int height)
{
mHeight = height;
mWidth = width;
nativeCleanWidgets();
updateBottomWidgetsOffset(context, mBottomWidgetOffsetX, mBottomWidgetOffsetY);
if (mDisplayType == DisplayType.Device)
{
nativeSetupWidget(WIDGET_SCALE_FPS_LABEL, UiUtils.dimen(context, R.dimen.margin_base), UiUtils.dimen(context, R.dimen.margin_base) * 2, ANCHOR_LEFT_TOP);
updateCompassOffset(context, mCurrentCompassOffsetX, mCurrentCompassOffsetY, false);
}
else
{
nativeSetupWidget(WIDGET_SCALE_FPS_LABEL, (float) mWidth / 2 + UiUtils.dimen(context, R.dimen.margin_base) * 2, UiUtils.dimen(context, R.dimen.margin_base), ANCHOR_LEFT_TOP);
updateCompassOffset(context, mWidth, mCurrentCompassOffsetY, true);
}
}
private void updateRulerOffset(final Context context, int offsetX, int offsetY)
{
nativeSetupWidget(WIDGET_RULER,
UiUtils.dimen(context, R.dimen.margin_ruler) + offsetX,
mHeight - UiUtils.dimen(context, R.dimen.margin_ruler) - offsetY,
ANCHOR_LEFT_BOTTOM);
if (mSurfaceCreated)
nativeApplyWidgets();
}
private void updateAttributionOffset(final Context context, int offsetX, int offsetY)
{
nativeSetupWidget(WIDGET_COPYRIGHT,
UiUtils.dimen(context, R.dimen.margin_ruler) + offsetX,
mHeight - UiUtils.dimen(context, R.dimen.margin_ruler) - offsetY,
ANCHOR_LEFT_BOTTOM);
if (mSurfaceCreated)
nativeApplyWidgets();
}
private boolean isThemeChangingProcess(final Context context)
{
return mUiThemeOnPause != null && !mUiThemeOnPause.equals(Config.getCurrentUiTheme(context));
}
// Engine
private static native boolean nativeCreateEngine(Surface surface, int density,
boolean firstLaunch,
boolean isLaunchByDeepLink,
int appVersionCode,
boolean isCustomROM);
private static native boolean nativeIsEngineCreated();
private static native void nativeUpdateEngineDpi(int dpi);
private static native void nativeSetRenderingInitializationFinishedListener(
@Nullable MapRenderingListener listener);
private static native void nativeExecuteMapApiRequest();
// Surface
private static native boolean nativeAttachSurface(Surface surface);
private static native void nativeDetachSurface(boolean destroySurface);
private static native void nativeSurfaceChanged(Surface surface, int w, int h);
private static native boolean nativeDestroySurfaceOnDetach();
private static native void nativePauseSurfaceRendering();
private static native void nativeResumeSurfaceRendering();
// Widgets
private static native void nativeApplyWidgets();
private static native void nativeCleanWidgets();
private static native void nativeUpdateMyPositionRoutingOffset(int offsetY);
private static native void nativeSetupWidget(int widget, float x, float y, int anchor);
private static native void nativeCompassUpdated(double north, boolean forceRedraw);
// Events
private static native void nativeScalePlus();
private static native void nativeScaleMinus();
private static native void nativeOnScroll(double distanceX, double distanceY);
private static native void nativeOnScale(double factor, double focusX, double focusY, boolean isAnim);
private static native void nativeOnTouch(int actionType, int id1, float x1, float y1, int id2, float x2, float y2, int maskedPointer);
}

View File

@@ -0,0 +1,206 @@
package app.organicmaps;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
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(DisplayType.Device);
public void updateCompassOffset(int offsetX, int offsetY)
{
mMap.updateCompassOffset(requireContext(), offsetX, offsetY, true);
}
public void updateBottomWidgetsOffset(int offsetX, int offsetY)
{
mMap.updateBottomWidgetsOffset(requireContext(), offsetX, offsetY);
}
public void updateMyPositionRoutingOffset(int offsetY)
{
mMap.updateMyPositionRoutingOffset(offsetY);
}
public void destroySurface()
{
mMap.onSurfaceDestroyed(requireActivity().isChangingConfigurations(), isAdded());
}
public boolean isContextCreated()
{
return mMap.isContextCreated();
}
@Override
public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder)
{
Logger.d(TAG);
int densityDpi;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
densityDpi = ConfigurationHelper.getDensityDpi(requireContext().getResources());
else
densityDpi = getDensityDpiOld();
mMap.onSurfaceCreated(requireContext(), surfaceHolder.getSurface(), surfaceHolder.getSurfaceFrame(), densityDpi);
}
@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(@NonNull SurfaceHolder surfaceHolder)
{
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);
}
@Override
public void onDetach()
{
Logger.d(TAG);
super.onDetach();
mMap.setMapRenderingListener(null);
mMap.setCallbackUnsupported(null);
}
@Override
public void onCreate(Bundle b)
{
Logger.d(TAG);
super.onCreate(b);
setRetainInstance(true);
boolean launchByDeepLink = false;
Bundle args = getArguments();
if (args != null)
launchByDeepLink = args.getBoolean(Map.ARG_LAUNCH_BY_DEEP_LINK);
mMap.onCreate(launchByDeepLink);
}
@Override
public void onStart()
{
Logger.d(TAG);
super.onStart();
mMap.onStart();
}
@Override
public void onStop()
{
Logger.d(TAG);
super.onStop();
mMap.onStop();
}
@Override
public void onPause()
{
Logger.d(TAG);
super.onPause();
mMap.onPause(requireContext());
}
@Override
public void onResume()
{
Logger.d(TAG);
super.onResume();
mMap.onResume();
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
{
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;
}
@Override
public boolean onTouch(View view, MotionEvent event)
{
int action = event.getActionMasked();
int pointerIndex = event.getActionIndex();
switch (action)
{
case MotionEvent.ACTION_POINTER_UP -> action = Map.NATIVE_ACTION_UP;
case MotionEvent.ACTION_UP ->
{
action = Map.NATIVE_ACTION_UP;
pointerIndex = 0;
}
case MotionEvent.ACTION_POINTER_DOWN -> action = Map.NATIVE_ACTION_DOWN;
case MotionEvent.ACTION_DOWN ->
{
action = Map.NATIVE_ACTION_DOWN;
pointerIndex = 0;
}
case MotionEvent.ACTION_MOVE ->
{
action = Map.NATIVE_ACTION_MOVE;
pointerIndex = Map.INVALID_POINTER_MASK;
}
case MotionEvent.ACTION_CANCEL -> action = Map.NATIVE_ACTION_CANCEL;
}
Map.onTouch(action, event, pointerIndex);
return true;
}
public void notifyOnSurfaceDestroyed(@NonNull Runnable task)
{
mMap.onSurfaceDestroyed(false, true);
task.run();
}
private void reportUnsupported()
{
new MaterialAlertDialogBuilder(requireContext(), R.style.MwmTheme_AlertDialog)
.setMessage(R.string.unsupported_phone)
.setCancelable(false)
.setPositiveButton(R.string.close, (dlg, which) -> requireActivity().moveTaskToBack(true))
.show();
}
private int getDensityDpiOld()
{
final DisplayMetrics metrics = new DisplayMetrics();
requireActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
return metrics.densityDpi;
}
}

View File

@@ -0,0 +1,50 @@
package app.organicmaps;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.organicmaps.base.BaseMwmFragmentActivity;
import app.organicmaps.display.DisplayChangedListener;
import app.organicmaps.display.DisplayManager;
import app.organicmaps.display.DisplayType;
public class MapPlaceholderActivity extends BaseMwmFragmentActivity implements DisplayChangedListener
{
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private DisplayManager mDisplayManager;
private boolean mRemoveDisplayListener = true;
@Override
protected void onSafeCreate(@Nullable Bundle savedInstanceState)
{
super.onSafeCreate(savedInstanceState);
setContentView(R.layout.activity_map_placeholder);
mDisplayManager = DisplayManager.from(this);
mDisplayManager.addListener(DisplayType.Device, this);
findViewById(R.id.btn_continue).setOnClickListener((unused) -> mDisplayManager.changeDisplay(DisplayType.Device));
}
@Override
public void onDisplayChangedToDevice(@NonNull Runnable onTaskFinishedCallback)
{
mRemoveDisplayListener = false;
startActivity(new Intent(this, MwmActivity.class)
.putExtra(MwmActivity.EXTRA_UPDATE_THEME, true));
finish();
onTaskFinishedCallback.run();
}
@Override
protected void onSafeDestroy()
{
super.onSafeDestroy();
if (mRemoveDisplayListener)
mDisplayManager.removeListener(DisplayType.Device);
}
}

View File

@@ -0,0 +1,15 @@
package app.organicmaps;
import androidx.annotation.Keep;
public interface MapRenderingListener
{
default void onRenderingCreated() {}
default void onRenderingRestored() {}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
default void onRenderingInitializationFinished() {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,364 @@
package app.organicmaps;
import static app.organicmaps.location.LocationState.LOCATION_TAG;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import java.io.IOException;
import java.lang.ref.WeakReference;
import app.organicmaps.background.OsmUploadWork;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.display.DisplayManager;
import app.organicmaps.downloader.Android7RootCertificateWorkaround;
import app.organicmaps.downloader.DownloaderNotifier;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.location.LocationState;
import app.organicmaps.location.SensorHelper;
import app.organicmaps.location.TrackRecorder;
import app.organicmaps.location.TrackRecordingService;
import app.organicmaps.maplayer.isolines.IsolinesManager;
import app.organicmaps.maplayer.subway.SubwayManager;
import app.organicmaps.maplayer.traffic.TrafficManager;
import app.organicmaps.routing.NavigationService;
import app.organicmaps.routing.RoutingController;
import app.organicmaps.sdk.search.SearchEngine;
import app.organicmaps.settings.StoragePathManager;
import app.organicmaps.sound.TtsPlayer;
import app.organicmaps.util.Config;
import app.organicmaps.util.ConnectionState;
import app.organicmaps.util.SharedPropertiesUtils;
import app.organicmaps.util.StorageUtils;
import app.organicmaps.util.ThemeSwitcher;
import app.organicmaps.util.UiUtils;
import app.organicmaps.util.Utils;
import app.organicmaps.util.log.Logger;
import app.organicmaps.util.log.LogsManager;
public class MwmApplication extends Application implements Application.ActivityLifecycleCallbacks
{
@NonNull
private static final String TAG = MwmApplication.class.getSimpleName();
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private SubwayManager mSubwayManager;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private IsolinesManager mIsolinesManager;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private LocationHelper mLocationHelper;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private SensorHelper mSensorHelper;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private DisplayManager mDisplayManager;
private volatile boolean mFrameworkInitialized;
private volatile boolean mPlatformInitialized;
@Nullable
private WeakReference<Activity> mTopActivity;
@UiThread
@Nullable
public Activity getTopActivity()
{
return mTopActivity != null ? mTopActivity.get() : null;
}
@NonNull
public SubwayManager getSubwayManager()
{
return mSubwayManager;
}
@NonNull
public IsolinesManager getIsolinesManager()
{
return mIsolinesManager;
}
@NonNull
public LocationHelper getLocationHelper()
{
return mLocationHelper;
}
@NonNull
public SensorHelper getSensorHelper()
{
return mSensorHelper;
}
@NonNull
public DisplayManager getDisplayManager()
{
return mDisplayManager;
}
@NonNull
public static MwmApplication from(@NonNull Context context)
{
return (MwmApplication) context.getApplicationContext();
}
@NonNull
public static MwmApplication sInstance;
@NonNull
public static SharedPreferences prefs(@NonNull Context context)
{
return context.getSharedPreferences(context.getString(R.string.pref_file_name), MODE_PRIVATE);
}
@Override
public void onCreate()
{
super.onCreate();
Logger.i(TAG, "Initializing application");
sInstance = this;
LogsManager.INSTANCE.initFileLogging(this);
Android7RootCertificateWorkaround.initializeIfNeeded(this);
// Set configuration directory as early as possible.
// Other methods may explicitly use Config, which requires settingsDir to be set.
final String settingsPath = StorageUtils.getSettingsPath(this);
if (!StorageUtils.createDirectory(settingsPath))
throw new AssertionError("Can't create settingsDir " + settingsPath);
Logger.d(TAG, "Settings path = " + settingsPath);
nativeSetSettingsDir(settingsPath);
Config.init(this);
ConnectionState.INSTANCE.initialize(this);
DownloaderNotifier.createNotificationChannel(this);
NavigationService.createNotificationChannel(this);
TrackRecordingService.createNotificationChannel(this);
registerActivityLifecycleCallbacks(this);
mSubwayManager = new SubwayManager(this);
mIsolinesManager = new IsolinesManager(this);
mLocationHelper = new LocationHelper(this);
mSensorHelper = new SensorHelper(this);
mDisplayManager = new DisplayManager();
}
/**
* Initialize native core of application: platform and framework.
*
* @throws IOException - if failed to create directories. Caller must handle
* the exception and do nothing with native code if initialization is failed.
*/
public boolean init(@NonNull Runnable onComplete) throws IOException
{
initNativePlatform();
return initNativeFramework(onComplete);
}
private void initNativePlatform() throws IOException
{
if (mPlatformInitialized)
return;
final String apkPath = StorageUtils.getApkPath(this);
Logger.d(TAG, "Apk path = " + apkPath);
// Note: StoragePathManager uses Config, which requires SettingsDir to be set.
final String writablePath = StoragePathManager.findMapsStorage(this);
Logger.d(TAG, "Writable path = " + writablePath);
final String privatePath = StorageUtils.getPrivatePath(this);
Logger.d(TAG, "Private path = " + privatePath);
final String tempPath = StorageUtils.getTempPath(this);
Logger.d(TAG, "Temp path = " + tempPath);
// If platform directories are not created it means that native part of app will not be able
// to work at all. So, we just ignore native part initialization in this case, e.g. when the
// external storage is damaged or not available (read-only).
createPlatformDirectories(writablePath, privatePath, tempPath);
nativeInitPlatform(getApplicationContext(),
apkPath,
writablePath,
privatePath,
tempPath,
app.organicmaps.BuildConfig.FLAVOR,
app.organicmaps.BuildConfig.BUILD_TYPE, UiUtils.isTablet(this));
Config.setStoragePath(writablePath);
Config.setStatisticsEnabled(SharedPropertiesUtils.isStatisticsEnabled(this));
mPlatformInitialized = true;
Logger.i(TAG, "Platform initialized");
}
private void createPlatformDirectories(@NonNull String writablePath,
@NonNull String privatePath,
@NonNull String tempPath) throws IOException
{
SharedPropertiesUtils.emulateBadExternalStorage(this);
StorageUtils.requireDirectory(writablePath);
StorageUtils.requireDirectory(privatePath);
StorageUtils.requireDirectory(tempPath);
}
private boolean initNativeFramework(@NonNull Runnable onComplete)
{
if (mFrameworkInitialized)
return false;
nativeInitFramework(onComplete);
initNativeStrings();
ThemeSwitcher.INSTANCE.initialize(this);
SearchEngine.INSTANCE.initialize();
BookmarkManager.loadBookmarks();
TtsPlayer.INSTANCE.initialize(this);
ThemeSwitcher.INSTANCE.restart(false);
RoutingController.get().initialize(this);
TrafficManager.INSTANCE.initialize();
SubwayManager.from(this).initialize();
IsolinesManager.from(this).initialize();
ProcessLifecycleOwner.get().getLifecycle().addObserver(mProcessLifecycleObserver);
Logger.i(TAG, "Framework initialized");
mFrameworkInitialized = true;
return true;
}
private void initNativeStrings()
{
nativeAddLocalization("core_entrance", getString(R.string.core_entrance));
nativeAddLocalization("core_exit", getString(R.string.core_exit));
nativeAddLocalization("core_my_places", getString(R.string.core_my_places));
nativeAddLocalization("core_my_position", getString(R.string.core_my_position));
nativeAddLocalization("core_placepage_unknown_place", getString(R.string.core_placepage_unknown_place));
nativeAddLocalization("postal_code", getString(R.string.postal_code));
nativeAddLocalization("wifi", getString(R.string.category_wifi));
}
public boolean arePlatformAndCoreInitialized()
{
return mFrameworkInitialized && mPlatformInitialized;
}
static
{
System.loadLibrary("organicmaps");
}
private static native void nativeSetSettingsDir(String settingsPath);
private static native void nativeInitPlatform(Context context, String apkPath, String writablePath,
String privatePath, String tmpPath, String flavorName,
String buildType, boolean isTablet);
private static native void nativeInitFramework(@NonNull Runnable onComplete);
private static native void nativeAddLocalization(String name, String value);
private static native void nativeOnTransit(boolean foreground);
private final LifecycleObserver mProcessLifecycleObserver = new DefaultLifecycleObserver() {
@Override
public void onStart(@NonNull LifecycleOwner owner)
{
MwmApplication.this.onForeground();
}
@Override
public void onStop(@NonNull LifecycleOwner owner)
{
MwmApplication.this.onBackground();
}
};
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState)
{}
@Override
public void onActivityStarted(@NonNull Activity activity)
{}
@Override
public void onActivityResumed(@NonNull Activity activity)
{
Logger.d(TAG, "activity = " + activity);
Utils.showOnLockScreen(Config.isShowOnLockScreenEnabled(), activity);
mSensorHelper.setRotation(activity.getWindowManager().getDefaultDisplay().getRotation());
mTopActivity = new WeakReference<>(activity);
}
@Override
public void onActivityPaused(@NonNull Activity activity)
{
Logger.d(TAG, "activity = " + activity);
mTopActivity = null;
}
@Override
public void onActivityStopped(@NonNull Activity activity)
{}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState)
{
Logger.d(TAG, "activity = " + activity + " outState = " + outState);
}
@Override
public void onActivityDestroyed(@NonNull Activity activity)
{
Logger.d(TAG, "activity = " + activity);
}
private void onForeground()
{
Logger.d(TAG);
nativeOnTransit(true);
mLocationHelper.resumeLocationInForeground();
}
private void onBackground()
{
Logger.d(TAG);
nativeOnTransit(false);
OsmUploadWork.startActionUploadOsmChanges(this);
if (!mDisplayManager.isDeviceDisplayUsed())
Logger.i(LOCATION_TAG, "Android Auto is active, keeping location in the background");
else if (RoutingController.get().isNavigating())
Logger.i(LOCATION_TAG, "Navigation is in progress, keeping location in the background");
else if (!Map.isEngineCreated() || LocationState.getMode() == LocationState.PENDING_POSITION)
Logger.i(LOCATION_TAG, "PENDING_POSITION mode, keeping location in the background");
else if (TrackRecorder.nativeIsTrackRecordingEnabled())
Logger.i(LOCATION_TAG, "Track Recordr is active, keeping location in the background");
else
{
Logger.i(LOCATION_TAG, "Stopping location in the background");
mLocationHelper.stop();
}
}
}

View File

@@ -0,0 +1,137 @@
package app.organicmaps;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.os.Bundle;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import androidx.annotation.IntegerRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.chromium.base.ObserverList;
import app.organicmaps.util.UiUtils;
class PanelAnimator
{
private final MwmActivity mActivity;
private final ObserverList<MwmActivity.LeftAnimationTrackListener> mAnimationTrackListeners = new ObserverList<>();
private final ObserverList.RewindableIterator<MwmActivity.LeftAnimationTrackListener> mAnimationTrackIterator = mAnimationTrackListeners.rewindableIterator();
private final View mPanel;
private final int mWidth;
@IntegerRes
private final int mDuration;
PanelAnimator(MwmActivity activity)
{
mActivity = activity;
mWidth = UiUtils.dimen(activity.getApplicationContext(), R.dimen.panel_width);
mPanel = mActivity.findViewById(R.id.fragment_container);
mDuration = mActivity.getResources().getInteger(R.integer.anim_panel);
}
void registerListener(@NonNull MwmActivity.LeftAnimationTrackListener animationTrackListener)
{
mAnimationTrackListeners.addObserver(animationTrackListener);
}
private void track(ValueAnimator animation)
{
float offset = (Float) animation.getAnimatedValue();
mPanel.setTranslationX(offset);
mPanel.setAlpha(offset / mWidth + 1.0f);
mAnimationTrackIterator.rewind();
while (mAnimationTrackIterator.hasNext())
mAnimationTrackIterator.next().onTrackLeftAnimation(offset + mWidth);
}
/** @param completionListener will be called before the fragment becomes actually visible */
public void show(final Class<? extends Fragment> clazz, final Bundle args, @Nullable final Runnable completionListener)
{
if (isVisible())
{
if (mActivity.getFragment(clazz) != null)
{
if (completionListener != null)
completionListener.run();
return;
}
hide(() -> show(clazz, args, completionListener));
return;
}
mActivity.replaceFragmentInternal(clazz, args);
if (completionListener != null)
completionListener.run();
UiUtils.show(mPanel);
mAnimationTrackIterator.rewind();
while (mAnimationTrackIterator.hasNext())
mAnimationTrackIterator.next().onTrackStarted(false);
ValueAnimator animator = ValueAnimator.ofFloat(-mWidth, 0.0f);
animator.addUpdateListener(this::track);
animator.addListener(new UiUtils.SimpleAnimatorListener()
{
@Override
public void onAnimationEnd(Animator animation)
{
mAnimationTrackIterator.rewind();
while (mAnimationTrackIterator.hasNext())
mAnimationTrackIterator.next().onTrackStarted(true);
}
});
animator.setDuration(mDuration);
animator.setInterpolator(new AccelerateInterpolator());
animator.start();
}
public void hide(@Nullable final Runnable completionListener)
{
if (!isVisible())
{
if (completionListener != null)
completionListener.run();
return;
}
mAnimationTrackIterator.rewind();
while (mAnimationTrackIterator.hasNext())
mAnimationTrackIterator.next().onTrackStarted(true);
ValueAnimator animator = ValueAnimator.ofFloat(0.0f, -mWidth);
animator.addUpdateListener(this::track);
animator.addListener(new UiUtils.SimpleAnimatorListener()
{
@Override
public void onAnimationEnd(Animator animation)
{
UiUtils.hide(mPanel);
mAnimationTrackIterator.rewind();
while (mAnimationTrackIterator.hasNext())
mAnimationTrackIterator.next().onTrackStarted(false);
if (completionListener != null)
completionListener.run();
}
});
animator.setDuration(mDuration);
animator.setInterpolator(new AccelerateInterpolator());
animator.start();
}
public boolean isVisible()
{
return UiUtils.isVisible(mPanel);
}
}

View File

@@ -0,0 +1,217 @@
package app.organicmaps;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static app.organicmaps.api.Const.EXTRA_PICK_POINT;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity;
import app.organicmaps.display.DisplayManager;
import app.organicmaps.downloader.DownloaderActivity;
import app.organicmaps.intent.Factory;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.util.Config;
import app.organicmaps.util.LocationUtils;
import app.organicmaps.util.SharingUtils;
import app.organicmaps.util.ThemeUtils;
import app.organicmaps.util.Utils;
import app.organicmaps.util.concurrency.UiThread;
import app.organicmaps.util.log.Logger;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.io.IOException;
import java.util.Objects;
public class SplashActivity extends AppCompatActivity
{
private static final String TAG = SplashActivity.class.getSimpleName();
private static final long DELAY = 100;
private boolean mCanceled = false;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private ActivityResultLauncher<Intent> mApiRequest;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private ActivityResultLauncher<String[]> mPermissionRequest;
@NonNull
private ActivityResultLauncher<SharingUtils.SharingIntent> mShareLauncher;
@NonNull
private final Runnable mInitCoreDelayedTask = this::init;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
final Context context = getApplicationContext();
final String theme = Config.getCurrentUiTheme(context);
if (ThemeUtils.isDefaultTheme(context, theme))
setTheme(R.style.MwmTheme_Splash);
else if (ThemeUtils.isNightTheme(context, theme))
setTheme(R.style.MwmTheme_Night_Splash);
else
throw new IllegalArgumentException("Attempt to apply unsupported theme: " + theme);
UiThread.cancelDelayedTasks(mInitCoreDelayedTask);
setContentView(R.layout.activity_splash);
mPermissionRequest = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(),
result -> Config.setLocationRequested());
mApiRequest = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
setResult(result.getResultCode(), result.getData());
finish();
});
mShareLauncher = SharingUtils.RegisterLauncher(this);
if (DisplayManager.from(this).isCarDisplayUsed())
{
startActivity(new Intent(this, MapPlaceholderActivity.class));
finish();
}
}
@Override
protected void onResume()
{
super.onResume();
if (mCanceled)
return;
if (!Config.isLocationRequested() && !LocationUtils.checkLocationPermission(this))
{
Logger.d(TAG, "Requesting location permissions");
mPermissionRequest.launch(new String[]{
ACCESS_COARSE_LOCATION,
ACCESS_FINE_LOCATION
});
return;
}
UiThread.runLater(mInitCoreDelayedTask, DELAY);
}
@Override
protected void onPause()
{
super.onPause();
UiThread.cancelDelayedTasks(mInitCoreDelayedTask);
}
@Override
protected void onDestroy()
{
super.onDestroy();
mPermissionRequest.unregister();
mPermissionRequest = null;
mApiRequest.unregister();
mApiRequest = null;
}
private void showFatalErrorDialog(@StringRes int titleId, @StringRes int messageId, Exception error)
{
mCanceled = true;
new MaterialAlertDialogBuilder(this, R.style.MwmTheme_AlertDialog)
.setTitle(titleId)
.setMessage(messageId)
.setPositiveButton(
R.string.report_a_bug,
(dialog, which) -> Utils.sendBugReport(
mShareLauncher,
this,
"Fatal Error",
Log.getStackTraceString(error)
)
)
.setCancelable(false)
.show();
}
private void init()
{
MwmApplication app = MwmApplication.from(this);
boolean asyncContinue = false;
try
{
asyncContinue = app.init(this::processNavigation);
} catch (IOException error)
{
showFatalErrorDialog(R.string.dialog_error_storage_title, R.string.dialog_error_storage_message, error);
return;
}
if (Config.isFirstLaunch(this) && LocationUtils.checkLocationPermission(this))
{
final LocationHelper locationHelper = app.getLocationHelper();
locationHelper.onEnteredIntoFirstRun();
if (!locationHelper.isActive())
locationHelper.start();
}
if (!asyncContinue)
processNavigation();
}
// Called from MwmApplication::nativeInitFramework like callback.
@Keep
@SuppressWarnings({"unused", "unchecked"})
public void processNavigation()
{
if (isDestroyed())
{
Logger.w(TAG, "Ignore late callback from core because activity is already destroyed");
return;
}
// Re-use original intent with the known safe subset of flags to retain security permissions.
// https://github.com/organicmaps/organicmaps/issues/6944
final Intent intent = Objects.requireNonNull(getIntent());
if (isManageSpaceActivity(intent)) {
intent.setComponent(new ComponentName(this, DownloaderActivity.class));
} else {
intent.setComponent(new ComponentName(this, DownloadResourcesLegacyActivity.class));
}
// FLAG_ACTIVITY_NEW_TASK and FLAG_ACTIVITY_RESET_TASK_IF_NEEDED break the cold start.
// https://github.com/organicmaps/organicmaps/pull/7287
// FORWARD_RESULT_FLAG conflicts with the ActivityResultLauncher.
// https://github.com/organicmaps/organicmaps/issues/8984
intent.setFlags(intent.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (Factory.isStartedForApiResult(intent))
{
// Wait for the result from MwmActivity for API callers.
mApiRequest.launch(intent);
return;
}
Config.setFirstStartDialogSeen(this);
startActivity(intent);
finish();
}
private boolean isManageSpaceActivity(Intent intent) {
var component = intent.getComponent();
if (!Intent.ACTION_VIEW.equals(intent.getAction())) return false;
if (component == null) return false;
var manageSpaceActivityName = BuildConfig.APPLICATION_ID + ".ManageSpaceActivity";
return manageSpaceActivityName.equals(component.getClassName());
}
}

View File

@@ -0,0 +1,78 @@
package app.organicmaps;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.net.MailTo;
import android.net.Uri;
import android.view.View;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import app.organicmaps.base.OnBackPressListener;
import app.organicmaps.util.UiUtils;
public abstract class WebContainerDelegate implements OnBackPressListener
{
private final WebView mWebView;
private final View mProgress;
@SuppressLint("SetJavaScriptEnabled")
private void initWebView(String url)
{
mWebView.setWebViewClient(new WebViewClient()
{
@Override
public void onPageFinished(WebView view, String url)
{
UiUtils.show(mWebView);
UiUtils.hide(mProgress);
}
@Override
public boolean shouldOverrideUrlLoading(WebView v, String url)
{
if (MailTo.isMailTo(url))
{
MailTo parser = MailTo.parse(url);
doStartActivity(new Intent(Intent.ACTION_SEND)
.putExtra(Intent.EXTRA_EMAIL, new String[] { parser.getTo() })
.putExtra(Intent.EXTRA_TEXT, parser.getBody())
.putExtra(Intent.EXTRA_SUBJECT, parser.getSubject())
.putExtra(Intent.EXTRA_CC, parser.getCc())
.setType("message/rfc822"));
v.reload();
return true;
}
doStartActivity(new Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(url)));
return true;
}
});
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.getSettings().setDefaultTextEncodingName("utf-8");
mWebView.loadUrl(url);
}
public WebContainerDelegate(@NonNull View frame, @NonNull String url)
{
mWebView = frame.findViewById(R.id.webview);
mProgress = frame.findViewById(R.id.progress);
initWebView(url);
}
@Override
public boolean onBackPressed()
{
if (!mWebView.canGoBack())
return false;
mWebView.goBack();
return true;
}
protected abstract void doStartActivity(Intent intent);
}

View File

@@ -0,0 +1,37 @@
package app.organicmaps.adapter;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.SimpleExpandableListAdapter;
import java.util.List;
import java.util.Map;
/**
* Disables child selections, also fixes bug with SimpleExpandableListAdapter not switching expandedGroupLayout and collapsedGroupLayout correctly.
*/
public class DisabledChildSimpleExpandableListAdapter extends SimpleExpandableListAdapter
{
public DisabledChildSimpleExpandableListAdapter(Context context, List<? extends Map<String, ?>> groupData, int expandedGroupLayout, int collapsedGroupLayout, String[] groupFrom, int[] groupTo, List<? extends List<? extends Map<String, ?>>> childData, int childLayout, String[] childFrom, int[] childTo)
{
super(context, groupData, expandedGroupLayout, collapsedGroupLayout, groupFrom, groupTo, childData, childLayout, childFrom, childTo);
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition)
{
return false;
}
/*
* Quick bugfix, pass convertView param null always to change expanded-collapsed groupview correctly.
* See http://stackoverflow.com/questions/19520037/simpleexpandablelistadapter-and-expandedgrouplayout for details
*/
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
ViewGroup parent)
{
return super.getGroupView(groupPosition, isExpanded, null, parent);
}
}

View File

@@ -0,0 +1,10 @@
package app.organicmaps.adapter;
import android.view.View;
import androidx.annotation.NonNull;
public interface OnItemClickListener<T>
{
void onItemClick(@NonNull View v, @NonNull T item);
}

View File

@@ -0,0 +1,23 @@
package app.organicmaps.api;
public class Const
{
// Common
public static final String API_SCHEME = "om";
public static final String AUTHORITY = "app.organicmaps.api";
public static final String EXTRA_PREFIX = AUTHORITY + ".extra";
public static final String ACTION_PREFIX = AUTHORITY + ".action";
// Request extras
public static final String EXTRA_PICK_POINT = EXTRA_PREFIX + ".PICK_POINT";
// Response extras
public static final String EXTRA_POINT_NAME = EXTRA_PREFIX + ".POINT_NAME";
public static final String EXTRA_POINT_LAT = EXTRA_PREFIX + ".POINT_LAT";
public static final String EXTRA_POINT_LON = EXTRA_PREFIX + ".POINT_LON";
public static final String EXTRA_POINT_ID = EXTRA_PREFIX + ".POINT_ID";
public static final String EXTRA_ZOOM_LEVEL = EXTRA_PREFIX + ".ZOOM_LEVEL";
private Const() {}
}

View File

@@ -0,0 +1,23 @@
package app.organicmaps.api;
import androidx.annotation.Keep;
import app.organicmaps.Framework;
/**
* Represents Framework::ParsedRoutingData from core.
*/
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class ParsedRoutingData
{
public final RoutePoint[] mPoints;
@Framework.RouterType
public final int mRouterType;
public ParsedRoutingData(RoutePoint[] points, int routerType) {
this.mPoints = points;
this.mRouterType = routerType;
}
}

View File

@@ -0,0 +1,31 @@
package app.organicmaps.api;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Represents url_scheme::SearchRequest from core.
*/
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public final class ParsedSearchRequest
{
@NonNull
public final String mQuery;
@Nullable
public final String mLocale;
public final double mLat;
public final double mLon;
public final boolean mIsSearchOnMap;
public ParsedSearchRequest(@NonNull String query, @Nullable String locale, double lat, double lon, boolean isSearchOnMap) {
this.mQuery = query;
this.mLocale = locale;
this.mLat = lat;
this.mLon = lon;
this.mIsSearchOnMap = isSearchOnMap;
}
}

View File

@@ -0,0 +1,21 @@
package app.organicmaps.api;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.SOURCE)
@IntDef({RequestType.INCORRECT, RequestType.MAP, RequestType.ROUTE, RequestType.SEARCH, RequestType.CROSSHAIR, RequestType.OAUTH2, RequestType.MENU, RequestType.SETTINGS})
public @interface RequestType
{
// Represents url_scheme::ParsedMapApi::UrlType from c++ part.
public static final int INCORRECT = 0;
public static final int MAP = 1;
public static final int ROUTE = 2;
public static final int SEARCH = 3;
public static final int CROSSHAIR = 4;
public static final int OAUTH2 = 5;
public static final int MENU = 6;
public static final int SETTINGS = 7;
}

View File

@@ -0,0 +1,23 @@
package app.organicmaps.api;
import androidx.annotation.Keep;
/**
* Represents url_scheme::RoutePoint from core.
*/
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class RoutePoint
{
public final double mLat;
public final double mLon;
public final String mName;
public RoutePoint(double lat, double lon, String name)
{
mLat = lat;
mLon = lon;
mName = name;
}
}

View File

@@ -0,0 +1,58 @@
package app.organicmaps.background;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.Constraints;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import androidx.work.WorkRequest;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import app.organicmaps.MwmApplication;
import app.organicmaps.editor.Editor;
import app.organicmaps.editor.OsmOAuth;
import app.organicmaps.util.log.Logger;
public class OsmUploadWork extends Worker
{
private static final String TAG = OsmUploadWork.class.getSimpleName();
private final Context mContext;
private final WorkerParameters mWorkerParameters;
public OsmUploadWork(@NonNull Context context, @NonNull WorkerParameters workerParams)
{
super(context, workerParams);
this.mContext = context;
this.mWorkerParameters = workerParams;
}
/**
* Starts this worker to upload map edits to osm servers.
*/
public static void startActionUploadOsmChanges(@NonNull Context context)
{
if (Editor.nativeHasSomethingToUpload() && OsmOAuth.isAuthorized(context))
{
final Constraints c = new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();
final WorkRequest wr = new OneTimeWorkRequest.Builder(OsmUploadWork.class).setConstraints(c).build();
WorkManager.getInstance(context).enqueue(wr);
}
}
@NonNull
@Override
public Result doWork()
{
final MwmApplication app = MwmApplication.from(mContext);
if (!app.arePlatformAndCoreInitialized())
{
Logger.w(TAG, "Application is not initialized, ignoring " + mWorkerParameters);
return Result.failure();
}
Editor.uploadChanges(mContext);
return Result.success();
}
}

View File

@@ -0,0 +1,66 @@
package app.organicmaps.base;
import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.fragment.app.DialogFragment;
import app.organicmaps.R;
import app.organicmaps.util.ThemeUtils;
public class BaseMwmDialogFragment extends DialogFragment
{
@StyleRes
protected final int getFullscreenTheme()
{
return ThemeUtils.isNightTheme(requireContext()) ? getFullscreenDarkTheme() : getFullscreenLightTheme();
}
protected int getStyle()
{
return STYLE_NORMAL;
}
protected @StyleRes int getCustomTheme()
{
return 0;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
int style = getStyle();
int theme = getCustomTheme();
if (style != STYLE_NORMAL || theme != 0)
//noinspection WrongConstant
setStyle(style, theme);
}
@StyleRes
protected int getFullscreenLightTheme()
{
return R.style.MwmTheme_DialogFragment_Fullscreen;
}
@StyleRes
protected int getFullscreenDarkTheme()
{
return R.style.MwmTheme_DialogFragment_Fullscreen_Night;
}
@NonNull
protected Application getAppContextOrThrow()
{
Context context = requireContext();
if (context == null)
throw new IllegalStateException("Before call this method make sure that the context exists");
return (Application) context.getApplicationContext();
}
}

View File

@@ -0,0 +1,24 @@
package app.organicmaps.base;
import android.content.Context;
import androidx.fragment.app.Fragment;
import app.organicmaps.util.Utils;
public class BaseMwmFragment extends Fragment implements OnBackPressListener
{
@Override
public void onAttach(Context context)
{
super.onAttach(context);
Utils.detachFragmentIfCoreNotInitialized(context, this);
}
@Override
public boolean onBackPressed()
{
return false;
}
}

View File

@@ -0,0 +1,259 @@
package app.organicmaps.base;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.media.AudioManager;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.activity.EdgeToEdge;
import androidx.activity.SystemBarStyle;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentFactory;
import androidx.fragment.app.FragmentManager;
import app.organicmaps.MwmApplication;
import app.organicmaps.R;
import app.organicmaps.SplashActivity;
import app.organicmaps.util.Config;
import app.organicmaps.util.RtlUtils;
import app.organicmaps.util.ThemeUtils;
import app.organicmaps.util.concurrency.UiThread;
import app.organicmaps.util.log.Logger;
import java.util.Objects;
public abstract class BaseMwmFragmentActivity extends AppCompatActivity
{
private static final String TAG = BaseMwmFragmentActivity.class.getSimpleName();
private boolean mSafeCreated;
@NonNull
private String mThemeName;
@StyleRes
protected int getThemeResourceId(@NonNull String theme)
{
Context context = getApplicationContext();
if (ThemeUtils.isDefaultTheme(context, theme))
return R.style.MwmTheme;
if (ThemeUtils.isNightTheme(context, theme))
return R.style.MwmTheme_Night;
throw new IllegalArgumentException("Attempt to apply unsupported theme: " + theme);
}
/**
* Shows splash screen and initializes the core in case when it was not initialized.
*
* Do not override this method!
* Use {@link #onSafeCreate(Bundle savedInstanceState)}
*/
@CallSuper
@Override
protected final void onCreate(@Nullable Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
mThemeName = Config.getCurrentUiTheme(getApplicationContext());
setTheme(getThemeResourceId(mThemeName));
EdgeToEdge.enable(this, SystemBarStyle.dark(Color.TRANSPARENT));
RtlUtils.manageRtl(this);
if (!MwmApplication.from(this).arePlatformAndCoreInitialized())
{
final Intent intent = Objects.requireNonNull(getIntent());
intent.setComponent(new ComponentName(this, SplashActivity.class));
startActivity(intent);
finish();
return;
}
onSafeCreate(savedInstanceState);
}
/**
* Use this safe method instead of {@link #onCreate(Bundle savedInstanceState)}.
* When this method is called, the core is already initialized.
*/
@CallSuper
protected void onSafeCreate(@Nullable Bundle savedInstanceState)
{
setVolumeControlStream(AudioManager.STREAM_MUSIC);
final int layoutId = getContentLayoutResId();
if (layoutId != 0)
setContentView(layoutId);
attachDefaultFragment();
mSafeCreated = true;
}
@CallSuper
@Override
protected final void onDestroy()
{
super.onDestroy();
if (!mSafeCreated)
return;
onSafeDestroy();
}
/**
* Use this safe method instead of {@link #onDestroy()}.
* When this method is called, the core is already initialized and
* {@link #onSafeCreate(Bundle savedInstanceState)} was called.
*/
@CallSuper
protected void onSafeDestroy()
{
mSafeCreated = false;
}
@CallSuper
@Override
public void onPostResume()
{
super.onPostResume();
if (!mThemeName.equals(Config.getCurrentUiTheme(getApplicationContext())))
{
// Workaround described in https://code.google.com/p/android/issues/detail?id=93731
UiThread.runLater(this::recreate);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
if (item.getItemId() == android.R.id.home)
{
onHomeOptionItemSelected();
return true;
}
return super.onOptionsItemSelected(item);
}
protected void onHomeOptionItemSelected()
{
onBackPressed();
}
protected Toolbar getToolbar()
{
return findViewById(R.id.toolbar);
}
protected void displayToolbarAsActionBar()
{
setSupportActionBar(getToolbar());
}
@Override
public void onBackPressed()
{
if (getFragmentClass() == null)
{
super.onBackPressed();
return;
}
FragmentManager manager = getSupportFragmentManager();
String name = getFragmentClass().getName();
Fragment fragment = manager.findFragmentByTag(name);
if (fragment == null)
{
super.onBackPressed();
return;
}
if (onBackPressedInternal(fragment))
return;
super.onBackPressed();
}
private boolean onBackPressedInternal(@NonNull Fragment currentFragment)
{
try
{
OnBackPressListener listener = (OnBackPressListener) currentFragment;
return listener.onBackPressed();
}
catch (ClassCastException e)
{
Logger.i(TAG, "Fragment '" + currentFragment + "' doesn't handle back press by itself.");
return false;
}
}
/**
* Override to set custom content view.
* @return layout resId.
*/
protected int getContentLayoutResId()
{
return 0;
}
protected void attachDefaultFragment()
{
Class<? extends Fragment> clazz = getFragmentClass();
if (clazz != null)
replaceFragment(clazz, getIntent().getExtras(), null);
}
/**
* Replace attached fragment with the new one.
*/
public void replaceFragment(@NonNull Class<? extends Fragment> fragmentClass, @Nullable Bundle args, @Nullable Runnable completionListener)
{
final int resId = getFragmentContentResId();
if (resId <= 0 || findViewById(resId) == null)
throw new IllegalStateException("Fragment can't be added, since getFragmentContentResId() isn't implemented or returns wrong resourceId.");
String name = fragmentClass.getName();
Fragment potentialInstance = getSupportFragmentManager().findFragmentByTag(name);
if (potentialInstance == null)
{
final FragmentManager manager = getSupportFragmentManager();
final FragmentFactory factory = manager.getFragmentFactory();
final Fragment fragment = factory.instantiate(getClassLoader(), name);
fragment.setArguments(args);
manager.beginTransaction()
.replace(resId, fragment, name)
.commitAllowingStateLoss();
manager.executePendingTransactions();
if (completionListener != null)
completionListener.run();
}
}
/**
* Override to automatically attach fragment in onCreate. Tag applied to fragment in back stack is set to fragment name, too.
* WARNING : if custom layout for activity is set, getFragmentContentResId() must be implemented, too.
* @return class of the fragment, eg FragmentClass.getClass()
*/
protected Class<? extends Fragment> getFragmentClass()
{
return null;
}
/**
* Get resource id for the fragment. That must be implemented to return correct resource id, if custom layout is set.
* @return resourceId for the fragment
*/
protected int getFragmentContentResId()
{
return android.R.id.content;
}
}

View File

@@ -0,0 +1,123 @@
package app.organicmaps.base;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.CallSuper;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import app.organicmaps.R;
import app.organicmaps.util.UiUtils;
import app.organicmaps.util.Utils;
import app.organicmaps.util.WindowInsetUtils.ScrollableContentInsetsListener;
import app.organicmaps.widget.PlaceholderView;
public abstract class BaseMwmRecyclerFragment<T extends RecyclerView.Adapter> extends Fragment
{
private Toolbar mToolbar;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private RecyclerView mRecycler;
@Nullable
private PlaceholderView mPlaceholder;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private T mAdapter;
@NonNull
private final View.OnClickListener mNavigationClickListener
= view -> Utils.navigateToParent(requireActivity());
@NonNull
protected abstract T createAdapter();
@LayoutRes
protected int getLayoutRes()
{
return R.layout.fragment_recycler;
}
@NonNull
protected T getAdapter()
{
return mAdapter;
}
@Override
public void onAttach(Context context)
{
super.onAttach(context);
Utils.detachFragmentIfCoreNotInitialized(context, this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
return inflater.inflate(getLayoutRes(), container, false);
}
@CallSuper
@Override
public void onViewCreated(View view, Bundle savedInstanceState)
{
super.onViewCreated(view, savedInstanceState);
mToolbar = view.findViewById(R.id.toolbar);
if (mToolbar != null)
UiUtils.setupNavigationIcon(mToolbar, mNavigationClickListener);
mRecycler = view.findViewById(R.id.recycler);
if (mRecycler == null)
throw new IllegalStateException("RecyclerView not found in layout");
LinearLayoutManager manager = new LinearLayoutManager(view.getContext());
manager.setSmoothScrollbarEnabled(true);
mRecycler.setLayoutManager(manager);
mRecycler.setClipToPadding(false);
mAdapter = createAdapter();
mRecycler.setAdapter(mAdapter);
ViewCompat.setOnApplyWindowInsetsListener(mRecycler, new ScrollableContentInsetsListener(mRecycler));
mPlaceholder = view.findViewById(R.id.placeholder);
setupPlaceholder(mPlaceholder);
}
public RecyclerView getRecyclerView()
{
return mRecycler;
}
@NonNull
public PlaceholderView requirePlaceholder()
{
if (mPlaceholder != null)
return mPlaceholder;
throw new IllegalStateException("Placeholder not found in layout");
}
protected void setupPlaceholder(@Nullable PlaceholderView placeholder) {}
public void setupPlaceholder()
{
setupPlaceholder(mPlaceholder);
}
public void showPlaceholder(boolean show)
{
if (mPlaceholder != null)
UiUtils.showIf(show, mPlaceholder);
}
}

View File

@@ -0,0 +1,34 @@
package app.organicmaps.base;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.organicmaps.widget.ToolbarController;
public class BaseMwmToolbarFragment extends BaseMwmFragment
{
@SuppressWarnings("NullableProblems")
@NonNull
private ToolbarController mToolbarController;
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
{
mToolbarController = onCreateToolbarController(view);
}
@NonNull
protected ToolbarController onCreateToolbarController(@NonNull View root)
{
return new ToolbarController(root, requireActivity());
}
@NonNull
protected ToolbarController getToolbarController()
{
return mToolbarController;
}
}

View File

@@ -0,0 +1,114 @@
package app.organicmaps.base;
import android.os.Bundle;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentFactory;
import androidx.fragment.app.FragmentManager;
import app.organicmaps.R;
import app.organicmaps.util.UiUtils;
import app.organicmaps.util.WindowInsetUtils.PaddingInsetsListener;
public abstract class BaseToolbarActivity extends BaseMwmFragmentActivity
{
@Nullable
private String mLastTitle;
@CallSuper
@Override
protected void onSafeCreate(@Nullable Bundle savedInstanceState)
{
super.onSafeCreate(savedInstanceState);
Toolbar toolbar = getToolbar();
if (toolbar != null)
{
int title = getToolbarTitle();
if (title == 0)
toolbar.setTitle(getTitle());
else
toolbar.setTitle(title);
setupHomeButton(toolbar);
displayToolbarAsActionBar();
ViewCompat.setOnApplyWindowInsetsListener(toolbar, PaddingInsetsListener.excludeBottom());
}
}
protected void setupHomeButton(@NonNull Toolbar toolbar)
{
UiUtils.showHomeUpButton(toolbar);
}
@StringRes
protected int getToolbarTitle()
{
return 0;
}
@Override
protected Class<? extends Fragment> getFragmentClass()
{
throw new RuntimeException("Must be implemented in child classes!");
}
@Override
protected int getContentLayoutResId()
{
return R.layout.activity_fragment_and_toolbar;
}
@Override
protected int getFragmentContentResId()
{
return R.id.fragment_container;
}
public Fragment stackFragment(@NonNull Class<? extends Fragment> fragmentClass,
@Nullable String title, @Nullable Bundle args)
{
final int resId = getFragmentContentResId();
if (resId <= 0 || findViewById(resId) == null)
throw new IllegalStateException("Fragment can't be added, since getFragmentContentResId() " +
"isn't implemented or returns wrong resourceId.");
String name = fragmentClass.getName();
final FragmentManager manager = getSupportFragmentManager();
final FragmentFactory factory = manager.getFragmentFactory();
final Fragment fragment = factory.instantiate(getClassLoader(), name);
fragment.setArguments(args);
manager.beginTransaction()
.replace(resId, fragment, name)
.addToBackStack(null)
.commitAllowingStateLoss();
manager.executePendingTransactions();
if (title != null)
{
Toolbar toolbar = getToolbar();
if (toolbar != null && toolbar.getTitle() != null)
{
mLastTitle = toolbar.getTitle().toString();
toolbar.setTitle(title);
}
}
return fragment;
}
@Override
public void onBackPressed()
{
if (mLastTitle != null)
getToolbar().setTitle(mLastTitle);
super.onBackPressed();
}
}

View File

@@ -0,0 +1,11 @@
package app.organicmaps.base;
public interface OnBackPressListener
{
/**
* Fragment tries to process back button press.
*
* @return true, if back was processed & fragment shouldn't be closed. false otherwise.
*/
boolean onBackPressed();
}

View File

@@ -0,0 +1,59 @@
package app.organicmaps.bookmarks;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import java.util.List;
public abstract class BaseBookmarkCategoryAdapter<V extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<V>
{
@NonNull
private final Context mContext;
@NonNull
private List<BookmarkCategory> mItems;
BaseBookmarkCategoryAdapter(@NonNull Context context, @NonNull List<BookmarkCategory> items)
{
mContext = context;
mItems = items;
}
public void setItems(@NonNull List<BookmarkCategory> items)
{
mItems = items;
notifyDataSetChanged();
}
@NonNull
protected Context requireContext()
{
return mContext;
}
@NonNull
public List<BookmarkCategory> getBookmarkCategories()
{
return mItems;
}
@Override
public int getItemCount()
{
return getBookmarkCategories().size();
}
@NonNull
public BookmarkCategory getCategoryByPosition(int position)
{
List<BookmarkCategory> categories = getBookmarkCategories();
if (position < 0 || position > categories.size() - 1)
throw new ArrayIndexOutOfBoundsException(position);
return categories.get(position);
}
}

View File

@@ -0,0 +1,75 @@
package app.organicmaps.bookmarks;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.fragment.app.Fragment;
import app.organicmaps.R;
import app.organicmaps.base.BaseToolbarActivity;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.util.ThemeUtils;
public class BookmarkCategoriesActivity extends BaseToolbarActivity
{
@CallSuper
@Override
public void onResume()
{
super.onResume();
// Disable all notifications in BM on appearance of this activity.
// It allows to significantly improve performance in case of bookmarks
// modification. All notifications will be sent on activity's disappearance.
BookmarkManager.INSTANCE.setNotificationsEnabled(false);
}
@CallSuper
@Override
public void onPause()
{
// Allow to send all notifications in BM.
BookmarkManager.INSTANCE.setNotificationsEnabled(true);
super.onPause();
}
@Override
@StyleRes
public int getThemeResourceId(@NonNull String theme)
{
return ThemeUtils.getWindowBgThemeResourceId(getApplicationContext(), theme);
}
@Override
protected Class<? extends Fragment> getFragmentClass()
{
return BookmarkCategoriesFragment.class;
}
@Override
protected int getContentLayoutResId()
{
return R.layout.bookmarks_activity;
}
public static void start(@NonNull Activity context, @Nullable BookmarkCategory category)
{
Bundle args = new Bundle();
args.putParcelable(BookmarksListFragment.EXTRA_CATEGORY, category);
Intent intent = new Intent(context, BookmarkCategoriesActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP).putExtras(args);
context.startActivity(intent);
}
public static void start(@NonNull Activity context)
{
start(context, null);
}
}

View File

@@ -0,0 +1,305 @@
package app.organicmaps.bookmarks;
import static app.organicmaps.bookmarks.Holders.CategoryViewHolder;
import static app.organicmaps.bookmarks.Holders.HeaderViewHolder;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import app.organicmaps.R;
import app.organicmaps.adapter.OnItemClickListener;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.bookmarks.data.BookmarkManager;
import java.util.List;
public class BookmarkCategoriesAdapter extends BaseBookmarkCategoryAdapter<RecyclerView.ViewHolder>
{
private final static int HEADER_POSITION = 0;
private final static int TYPE_ACTION_HEADER = 0;
private final static int TYPE_CATEGORY_ITEM = 1;
private final static int TYPE_ACTION_ADD = 2;
private final static int TYPE_ACTION_IMPORT = 3;
private final static int TYPE_ACTION_EXPORT_ALL_AS_KMZ = 4;
@Nullable
private OnItemLongClickListener<BookmarkCategory> mLongClickListener;
@Nullable
private OnItemClickListener<BookmarkCategory> mClickListener;
@Nullable
private OnItemMoreClickListener<BookmarkCategory> mMoreClickListener;
@Nullable
private CategoryListCallback mCategoryListCallback;
@NonNull
private final MassOperationAction mMassOperationAction = new MassOperationAction();
BookmarkCategoriesAdapter(@NonNull Context context, @NonNull List<BookmarkCategory> categories)
{
super(context.getApplicationContext(), categories);
}
public void setOnClickListener(@Nullable OnItemClickListener<BookmarkCategory> listener)
{
mClickListener = listener;
}
public void setOnMoreClickListener(@Nullable OnItemMoreClickListener<BookmarkCategory> listener)
{
mMoreClickListener = listener;
}
void setOnLongClickListener(@Nullable OnItemLongClickListener<BookmarkCategory> listener)
{
mLongClickListener = listener;
}
void setCategoryListCallback(@Nullable CategoryListCallback listener)
{
mCategoryListCallback = listener;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (viewType)
{
case TYPE_ACTION_HEADER ->
{
View header = inflater.inflate(R.layout.item_bookmark_group_list_header, parent, false);
return new HeaderViewHolder(header);
}
case TYPE_CATEGORY_ITEM ->
{
View view = inflater.inflate(R.layout.item_bookmark_category, parent, false);
final CategoryViewHolder holder = new CategoryViewHolder(view);
view.setOnClickListener(new CategoryItemClickListener(holder));
view.setOnLongClickListener(new LongClickListener(holder));
return holder;
}
case TYPE_ACTION_ADD ->
{
View item = inflater.inflate(R.layout.item_bookmark_button, parent, false);
item.setOnClickListener(v -> {
if (mCategoryListCallback != null)
mCategoryListCallback.onAddButtonClick();
});
return new Holders.GeneralViewHolder(item);
}
case TYPE_ACTION_IMPORT ->
{
View item = inflater.inflate(R.layout.item_bookmark_button, parent, false);
item.setOnClickListener(v -> {
if (mCategoryListCallback != null)
mCategoryListCallback.onImportButtonClick();
});
return new Holders.GeneralViewHolder(item);
}
case TYPE_ACTION_EXPORT_ALL_AS_KMZ ->
{
View item = inflater.inflate(R.layout.item_bookmark_button, parent, false);
item.setOnClickListener(v -> {
if (mCategoryListCallback != null)
mCategoryListCallback.onExportButtonClick();
});
return new Holders.GeneralViewHolder(item);
}
default -> throw new AssertionError("Invalid item type: " + viewType);
}
}
@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position)
{
int type = getItemViewType(position);
switch (type)
{
case TYPE_ACTION_HEADER ->
{
HeaderViewHolder headerViewHolder = (HeaderViewHolder) holder;
headerViewHolder.setAction(mMassOperationAction,
BookmarkManager.INSTANCE.areAllCategoriesInvisible());
headerViewHolder.getText().setText(R.string.bookmark_lists);
}
case TYPE_CATEGORY_ITEM ->
{
final BookmarkCategory category = getCategoryByPosition(toCategoryPosition(position));
CategoryViewHolder categoryHolder = (CategoryViewHolder) holder;
categoryHolder.setEntity(category);
categoryHolder.setName(category.getName());
categoryHolder.setSize();
categoryHolder.setVisibilityState(category.isVisible());
ToggleVisibilityClickListener visibilityListener = new ToggleVisibilityClickListener(categoryHolder);
categoryHolder.setVisibilityListener(visibilityListener);
CategoryItemMoreClickListener moreClickListener = new CategoryItemMoreClickListener(categoryHolder);
categoryHolder.setMoreButtonClickListener(moreClickListener);
}
case TYPE_ACTION_ADD ->
{
Holders.GeneralViewHolder generalViewHolder = (Holders.GeneralViewHolder) holder;
generalViewHolder.getImage().setImageResource(R.drawable.ic_add_list);
generalViewHolder.getText().setText(R.string.bookmarks_create_new_group);
}
case TYPE_ACTION_IMPORT ->
{
Holders.GeneralViewHolder generalViewHolder = (Holders.GeneralViewHolder) holder;
generalViewHolder.getImage().setImageResource(R.drawable.ic_import);
generalViewHolder.getText().setText(R.string.bookmarks_import);
}
case TYPE_ACTION_EXPORT_ALL_AS_KMZ ->
{
Holders.GeneralViewHolder generalViewHolder = (Holders.GeneralViewHolder) holder;
generalViewHolder.getImage().setImageResource(R.drawable.ic_export);
generalViewHolder.getText().setText(R.string.bookmarks_export);
}
default -> throw new AssertionError("Invalid item type: " + type);
}
}
@Override
public int getItemViewType(int position)
{
/*
* Adapter content:
* - TYPE_ACTION_HEADER = 0
* - TYPE_CATEGORY_ITEM 0 = 1
* - TYPE_CATEGORY_ITEM n = n + 1
* - TYPE_ACTION_ADD = count - 3
* - TYPE_ACTION_IMPORT = count - 2
* - TYPE_ACTION_EXPORT_ALL_AS_KMZ = count - 1
*/
if (position == 0)
return TYPE_ACTION_HEADER;
if (position == getItemCount() - 3)
return TYPE_ACTION_ADD;
if (position == getItemCount() - 2)
return TYPE_ACTION_IMPORT;
if (position == getItemCount() - 1)
return TYPE_ACTION_EXPORT_ALL_AS_KMZ;
return TYPE_CATEGORY_ITEM;
}
private int toCategoryPosition(int adapterPosition)
{
int type = getItemViewType(adapterPosition);
if (type != TYPE_CATEGORY_ITEM)
throw new AssertionError("An element at specified position is not category!");
// The header "Hide All" is located at first index, so subtraction is needed.
return adapterPosition - 1;
}
@Override
public int getItemCount()
{
int count = super.getItemCount();
if (count == 0)
return 0;
return 1 /* header */ + count + 1 /* add button */ + 1 /* import button */ + 1 /* export button */;
}
private class LongClickListener implements View.OnLongClickListener
{
@NonNull
private final CategoryViewHolder mHolder;
LongClickListener(@NonNull CategoryViewHolder holder)
{
mHolder = holder;
}
@Override
public boolean onLongClick(View view)
{
if (mLongClickListener != null)
{
mLongClickListener.onItemLongClick(view, mHolder.getEntity());
}
return true;
}
}
private class MassOperationAction implements HeaderViewHolder.HeaderAction
{
@Override
public void onHideAll()
{
BookmarkManager.INSTANCE.setAllCategoriesVisibility(false);
notifyDataSetChanged();
}
@Override
public void onShowAll()
{
BookmarkManager.INSTANCE.setAllCategoriesVisibility(true);
notifyDataSetChanged();
}
}
private class CategoryItemMoreClickListener implements View.OnClickListener
{
@NonNull
private final CategoryViewHolder mHolder;
CategoryItemMoreClickListener(@NonNull CategoryViewHolder holder)
{
mHolder = holder;
}
@Override
public void onClick(View v)
{
if (mMoreClickListener != null)
mMoreClickListener.onItemMoreClick(v, mHolder.getEntity());
}
}
private class CategoryItemClickListener implements View.OnClickListener
{
@NonNull
private final CategoryViewHolder mHolder;
CategoryItemClickListener(@NonNull CategoryViewHolder holder)
{
mHolder = holder;
}
@Override
public void onClick(View v)
{
if (mClickListener != null)
mClickListener.onItemClick(v, mHolder.getEntity());
}
}
private class ToggleVisibilityClickListener implements View.OnClickListener
{
@NonNull
private final CategoryViewHolder mHolder;
ToggleVisibilityClickListener(@NonNull CategoryViewHolder holder)
{
mHolder = holder;
}
@Override
public void onClick(View v)
{
BookmarkCategory category = mHolder.getEntity();
BookmarkManager.INSTANCE.toggleCategoryVisibility(category);
notifyItemChanged(mHolder.getBindingAdapterPosition());
notifyItemChanged(HEADER_POSITION);
}
}
}

View File

@@ -0,0 +1,394 @@
package app.organicmaps.bookmarks;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.view.View;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.CallSuper;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import app.organicmaps.MwmApplication;
import app.organicmaps.R;
import app.organicmaps.adapter.OnItemClickListener;
import app.organicmaps.base.BaseMwmRecyclerFragment;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.bookmarks.data.BookmarkSharingResult;
import app.organicmaps.bookmarks.data.KmlFileType;
import app.organicmaps.dialog.EditTextDialogFragment;
import app.organicmaps.util.SharingUtils;
import app.organicmaps.util.Utils;
import app.organicmaps.widget.PlaceholderView;
import app.organicmaps.widget.recycler.DividerItemDecorationWithPadding;
import app.organicmaps.util.StorageUtils;
import app.organicmaps.util.bottomsheet.MenuBottomSheetFragment;
import app.organicmaps.util.bottomsheet.MenuBottomSheetItem;
import app.organicmaps.util.concurrency.ThreadPool;
import app.organicmaps.util.concurrency.UiThread;
import app.organicmaps.util.log.Logger;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class BookmarkCategoriesFragment extends BaseMwmRecyclerFragment<BookmarkCategoriesAdapter>
implements BookmarkManager.BookmarksLoadingListener,
CategoryListCallback,
OnItemClickListener<BookmarkCategory>,
OnItemMoreClickListener<BookmarkCategory>,
OnItemLongClickListener<BookmarkCategory>,
BookmarkManager.BookmarksSharingListener,
MenuBottomSheetFragment.MenuBottomSheetInterface
{
private static final String TAG = BookmarkCategoriesFragment.class.getSimpleName();
private static final int MAX_CATEGORY_NAME_LENGTH = 60;
public static final String BOOKMARKS_CATEGORIES_MENU_ID = "BOOKMARKS_CATEGORIES_BOTTOM_SHEET";
private ActivityResultLauncher<SharingUtils.SharingIntent> shareLauncher;
@Nullable
private BookmarkCategory mSelectedCategory;
@Nullable
private CategoryEditor mCategoryEditor;
@SuppressWarnings("NullableProblems")
@NonNull
private DataChangedListener mCategoriesAdapterObserver;
private final ActivityResultLauncher<Intent> startBookmarkListForResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), activityResult -> {
if( activityResult.getResultCode() == Activity.RESULT_OK)
onDeleteActionSelected(getSelectedCategory());
});
private final ActivityResultLauncher<Intent> startImportDirectoryForResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), activityResult ->
{
if( activityResult.getResultCode() == Activity.RESULT_OK)
onImportDirectoryResult(activityResult.getData());
});
private final ActivityResultLauncher<Intent> startBookmarkSettingsForResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), activityResult -> {
// not handled at the moment
});
@Override
@LayoutRes
protected int getLayoutRes()
{
return R.layout.fragment_bookmark_categories;
}
@NonNull
@Override
protected BookmarkCategoriesAdapter createAdapter()
{
List<BookmarkCategory> items = BookmarkManager.INSTANCE.getCategories();
return new BookmarkCategoriesAdapter(requireContext(), items);
}
@CallSuper
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
{
super.onViewCreated(view, savedInstanceState);
onPrepareControllers(view);
getAdapter().setOnClickListener(this);
getAdapter().setOnLongClickListener(this);
getAdapter().setOnMoreClickListener(this);
getAdapter().setCategoryListCallback(this);
RecyclerView rw = getRecyclerView();
if (rw == null) return;
rw.setNestedScrollingEnabled(false);
RecyclerView.ItemDecoration decor = new DividerItemDecorationWithPadding(requireContext());
rw.addItemDecoration(decor);
mCategoriesAdapterObserver = this::onCategoriesChanged;
BookmarkManager.INSTANCE.addCategoriesUpdatesListener(mCategoriesAdapterObserver);
shareLauncher = SharingUtils.RegisterLauncher(this);
}
protected void onPrepareControllers(@NonNull View view)
{
// No op
}
@Override
public void onPreparedFileForSharing(@NonNull BookmarkSharingResult result)
{
BookmarksSharingHelper.INSTANCE.onPreparedFileForSharing(requireActivity(), shareLauncher, result);
}
@Override
public void onStart()
{
super.onStart();
BookmarkManager.INSTANCE.addLoadingListener(this);
BookmarkManager.INSTANCE.addSharingListener(this);
}
@Override
public void onStop()
{
super.onStop();
BookmarkManager.INSTANCE.removeLoadingListener(this);
BookmarkManager.INSTANCE.removeSharingListener(this);
}
@Override
public void onResume()
{
super.onResume();
getAdapter().notifyDataSetChanged();
}
@Override
public void onPause()
{
super.onPause();
}
@Override
public void onDestroyView()
{
super.onDestroyView();
BookmarkManager.INSTANCE.removeCategoriesUpdatesListener(mCategoriesAdapterObserver);
}
protected final void showBottomMenu(@NonNull BookmarkCategory item)
{
mSelectedCategory = item;
MenuBottomSheetFragment.newInstance(BOOKMARKS_CATEGORIES_MENU_ID, item.getName())
.show(getChildFragmentManager(), BOOKMARKS_CATEGORIES_MENU_ID);
}
@Override
@Nullable
public ArrayList<MenuBottomSheetItem> getMenuBottomSheetItems(String id)
{
ArrayList<MenuBottomSheetItem> items = new ArrayList<>();
if (mSelectedCategory != null)
{
items.add(new MenuBottomSheetItem(
R.string.edit,
R.drawable.ic_settings,
() -> onSettingsActionSelected(mSelectedCategory)));
items.add(new MenuBottomSheetItem(
mSelectedCategory.isVisible() ? R.string.hide : R.string.show,
mSelectedCategory.isVisible() ? R.drawable.ic_hide : R.drawable.ic_show,
() -> onShowActionSelected(mSelectedCategory)));
items.add(new MenuBottomSheetItem(
R.string.export_file,
R.drawable.ic_file_kmz,
() -> onShareActionSelected(mSelectedCategory, KmlFileType.Text)));
items.add(new MenuBottomSheetItem(
R.string.export_file_gpx,
R.drawable.ic_file_gpx,
() -> onShareActionSelected(mSelectedCategory, KmlFileType.Gpx)));
// Disallow deleting the last category
if (getAdapter().getBookmarkCategories().size() > 1)
items.add(new MenuBottomSheetItem(
R.string.delete,
R.drawable.ic_delete,
() -> onDeleteActionSelected(mSelectedCategory)));
}
return items;
}
@Override
protected void setupPlaceholder(@Nullable PlaceholderView placeholder)
{
// A placeholder is no needed on this screen.
}
@Override
public void onBookmarksLoadingFinished()
{
getAdapter().notifyDataSetChanged();
}
@Override
public void onBookmarksFileImportFailed()
{
// TODO: Is there a way to display several failure notifications?
// TODO: It would be helpful to see the file name that failed to import.
final View view = getView();
// TODO: how to get import button view to show snackbar above it?
if (view != null)
Utils.showSnackbar(requireActivity(), view, R.string.load_kmz_failed);
}
@Override
public void onAddButtonClick()
{
mCategoryEditor = BookmarkManager.INSTANCE::createCategory;
EditTextDialogFragment dialogFragment =
EditTextDialogFragment.show(getString(R.string.bookmarks_create_new_group),
getString(R.string.bookmarks_new_list_hint),
getString(R.string.bookmark_set_name),
getString(R.string.create),
getString(R.string.cancel),
MAX_CATEGORY_NAME_LENGTH,
this,
new CategoryValidator());
dialogFragment.setTextSaveListener(this::onSaveText);
}
@Override
public void onImportButtonClick()
{
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
// Sic: EXTRA_INITIAL_URI doesn't work
// https://stackoverflow.com/questions/65326605/extra-initial-uri-will-not-work-no-matter-what-i-do
// intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initial);
// Enable "Show SD card option"
// http://stackoverflow.com/a/31334967/1615876
intent.putExtra("android.content.extra.SHOW_ADVANCED", true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
intent.putExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, true);
PackageManager packageManager = requireActivity().getPackageManager();
if (intent.resolveActivity(packageManager) != null)
startImportDirectoryForResult.launch(intent);
else
showNoFileManagerError();
}
private void showNoFileManagerError() {
new AlertDialog.Builder(requireActivity())
.setMessage(R.string.error_no_file_manager_app)
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.show();
}
@Override
public void onItemClick(@NonNull View v, @NonNull BookmarkCategory category)
{
mSelectedCategory = category;
BookmarkListActivity.startForResult(this, startBookmarkListForResult, category);
}
@Override
public void onExportButtonClick()
{
BookmarksSharingHelper.INSTANCE.prepareBookmarkCategoriesForSharing(requireActivity());
}
private void onShowActionSelected(@NonNull BookmarkCategory category)
{
BookmarkManager.INSTANCE.toggleCategoryVisibility(category);
getAdapter().notifyDataSetChanged();
}
protected void onShareActionSelected(@NonNull BookmarkCategory category, KmlFileType kmlFileType)
{
BookmarksSharingHelper.INSTANCE.prepareBookmarkCategoryForSharing(requireActivity(), category.getId(), kmlFileType);
}
private void onDeleteActionSelected(@NonNull BookmarkCategory category)
{
BookmarkManager.INSTANCE.deleteCategory(category.getId());
getAdapter().notifyDataSetChanged();
}
private void onSettingsActionSelected(@NonNull BookmarkCategory category)
{
BookmarkCategorySettingsActivity.startForResult(this, startBookmarkSettingsForResult, category);
}
private void onImportDirectoryResult(Intent data)
{
if (data == null)
throw new AssertionError("Data is null");
final Context context = requireActivity();
final Uri rootUri = data.getData();
final ProgressDialog dialog = new ProgressDialog(context, R.style.MwmTheme_ProgressDialog);
dialog.setMessage(getString(R.string.wait_several_minutes));
dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
dialog.setIndeterminate(true);
dialog.setCancelable(false);
dialog.show();
Logger.d(TAG, "Importing bookmarks from " + rootUri);
MwmApplication app = MwmApplication.from(context);
final File tempDir = new File(StorageUtils.getTempPath(app));
final ContentResolver resolver = context.getContentResolver();
ThreadPool.getStorage().execute(() -> {
AtomicInteger found = new AtomicInteger(0);
StorageUtils.listContentProviderFilesRecursively(
resolver, rootUri, uri -> {
if (BookmarkManager.INSTANCE.importBookmarksFile(resolver, uri, tempDir))
found.incrementAndGet();
});
UiThread.run(() -> {
if (dialog.isShowing())
dialog.dismiss();
int found_val = found.get();
String message = context.getResources().getQuantityString(
R.plurals.bookmarks_detect_message, found_val, found_val);
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show();
});
});
}
@Override
public void onItemLongClick(@NonNull View v, @NonNull BookmarkCategory category)
{
showBottomMenu(category);
}
public void onItemMoreClick(@NonNull View v, @NonNull BookmarkCategory category)
{
showBottomMenu(category);
}
private void onSaveText(@NonNull String text)
{
if (mCategoryEditor != null)
mCategoryEditor.commit(text);
getAdapter().notifyDataSetChanged();
}
@NonNull
protected BookmarkCategory getSelectedCategory()
{
if (mSelectedCategory == null)
throw new AssertionError("Invalid attempt to use null selected category.");
return mSelectedCategory;
}
interface CategoryEditor
{
void commit(@NonNull String newName);
}
private void onCategoriesChanged()
{
getAdapter().setItems(BookmarkManager.INSTANCE.getCategories());
}
}

View File

@@ -0,0 +1,42 @@
package app.organicmaps.bookmarks;
import android.content.Intent;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import app.organicmaps.R;
import app.organicmaps.base.BaseMwmFragmentActivity;
import app.organicmaps.bookmarks.data.BookmarkCategory;
public class BookmarkCategorySettingsActivity extends BaseMwmFragmentActivity
{
public static final String EXTRA_BOOKMARK_CATEGORY = "bookmark_category";
@Override
protected int getContentLayoutResId()
{
return R.layout.fragment_container_layout;
}
@Override
protected int getFragmentContentResId()
{
return R.id.fragment_container;
}
@Override
protected Class<? extends Fragment> getFragmentClass()
{
return BookmarkCategorySettingsFragment.class;
}
public static void startForResult(@NonNull Fragment fragment, ActivityResultLauncher<Intent> startBookmarkSettingsForResult,
@NonNull BookmarkCategory category)
{
android.content.Intent intent = new Intent(fragment.requireActivity(), BookmarkCategorySettingsActivity.class)
.putExtra(EXTRA_BOOKMARK_CATEGORY, category);
startBookmarkSettingsForResult.launch(intent);
}
}

View File

@@ -0,0 +1,181 @@
package app.organicmaps.bookmarks;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.organicmaps.R;
import app.organicmaps.base.BaseMwmToolbarFragment;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.util.Utils;
import app.organicmaps.util.InputUtils;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import java.util.Objects;
public class BookmarkCategorySettingsFragment extends BaseMwmToolbarFragment
{
private static final int TEXT_LENGTH_LIMIT = 60;
@SuppressWarnings("NullableProblems")
@NonNull
private BookmarkCategory mCategory;
@SuppressWarnings("NullableProblems")
@NonNull
private TextInputEditText mEditDescView;
@SuppressWarnings("NullableProblems")
@NonNull
private TextInputEditText mEditCategoryNameView;
@Override
public void onCreate(@Nullable Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
final Bundle args = requireArguments();
mCategory = Objects.requireNonNull(Utils.getParcelable(args,
BookmarkCategorySettingsActivity.EXTRA_BOOKMARK_CATEGORY, BookmarkCategory.class));
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
View root = inflater.inflate(R.layout.fragment_bookmark_category_settings, container, false);
setHasOptionsMenu(true);
initViews(root);
return root;
}
private void initViews(@NonNull View root)
{
mEditCategoryNameView = root.findViewById(R.id.edit_list_name_view);
TextInputLayout clearNameBtn = root.findViewById(R.id.edit_list_name_input);
clearNameBtn.setEndIconOnClickListener(v -> clearAndFocus(mEditCategoryNameView));
mEditCategoryNameView.setText(mCategory.getName());
InputFilter[] f = { new InputFilter.LengthFilter(TEXT_LENGTH_LIMIT) };
mEditCategoryNameView.setFilters(f);
mEditCategoryNameView.requestFocus();
mEditCategoryNameView.addTextChangedListener(new TextWatcher()
{
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2)
{
clearNameBtn.setEndIconVisible(charSequence.length() > 0);
}
@Override
public void afterTextChanged(Editable editable) {}
});
mEditDescView = root.findViewById(R.id.edit_description);
mEditDescView.setText(mCategory.getDescription());
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater)
{
inflater.inflate(R.menu.menu_done, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
if (item.getItemId() == R.id.done)
{
onEditDoneClicked();
return true;
}
return super.onOptionsItemSelected(item);
}
private void onEditDoneClicked()
{
final String newCategoryName = getEditableCategoryName();
if (!validateCategoryName(newCategoryName))
return;
if (isCategoryNameChanged())
BookmarkManager.INSTANCE.setCategoryName(mCategory.getId(), newCategoryName);
if (isCategoryDescChanged())
BookmarkManager.INSTANCE.setCategoryDescription(mCategory.getId(), getEditableCategoryDesc());
requireActivity().finish();
}
private boolean isCategoryNameChanged()
{
String categoryName = getEditableCategoryName();
return !TextUtils.equals(categoryName, mCategory.getName());
}
private boolean validateCategoryName(@Nullable String name)
{
if (TextUtils.isEmpty(name))
{
new MaterialAlertDialogBuilder(requireActivity(), R.style.MwmTheme_AlertDialog)
.setTitle(R.string.bookmarks_error_title_empty_list_name)
.setMessage(R.string.bookmarks_error_message_empty_list_name)
.setPositiveButton(R.string.ok, null)
.show();
return false;
}
if (BookmarkManager.INSTANCE.isUsedCategoryName(name) && !TextUtils.equals(name, mCategory.getName()))
{
new MaterialAlertDialogBuilder(requireActivity(), R.style.MwmTheme_AlertDialog)
.setTitle(R.string.bookmarks_error_title_list_name_already_taken)
.setMessage(R.string.bookmarks_error_message_list_name_already_taken)
.setPositiveButton(R.string.ok, null)
.show();
return false;
}
return true;
}
@NonNull
private String getEditableCategoryName()
{
return mEditCategoryNameView.getEditableText().toString().trim();
}
@NonNull
private String getEditableCategoryDesc()
{
return mEditDescView.getEditableText().toString().trim();
}
private boolean isCategoryDescChanged()
{
String categoryDesc = getEditableCategoryDesc();
return !TextUtils.equals(mCategory.getDescription(), categoryDesc);
}
private void clearAndFocus(TextView textView)
{
textView.getEditableText().clear();
textView.requestFocus();
InputUtils.showKeyboard(textView);
}
}

View File

@@ -0,0 +1,254 @@
package app.organicmaps.bookmarks;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import app.organicmaps.R;
import app.organicmaps.adapter.OnItemClickListener;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.util.UiUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
public class BookmarkCollectionAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
{
@Retention(RetentionPolicy.SOURCE)
@IntDef({ TYPE_HEADER_ITEM, TYPE_CATEGORY_ITEM })
public @interface SectionType { }
private final static int TYPE_CATEGORY_ITEM = BookmarkManager.CATEGORY;
private final static int TYPE_HEADER_ITEM = 3;
@NonNull
private final BookmarkCategory mBookmarkCategory;
@NonNull
private List<BookmarkCategory> mItemsCategory;
private int mSectionCount;
private int mCategorySectionIndex = SectionPosition.INVALID_POSITION;
private boolean mVisible;
@Nullable
private OnItemClickListener<BookmarkCategory> mClickListener;
@NonNull
private final MassOperationAction mMassOperationAction = new MassOperationAction();
private class ToggleVisibilityClickListener implements View.OnClickListener
{
@NonNull
private final Holders.CollectionViewHolder mHolder;
ToggleVisibilityClickListener(@NonNull Holders.CollectionViewHolder holder)
{
mHolder = holder;
}
@Override
public void onClick(View v)
{
BookmarkCategory category = mHolder.getEntity();
BookmarkManager.INSTANCE.toggleCategoryVisibility(category);
notifyItemChanged(0);
}
}
BookmarkCollectionAdapter(@NonNull BookmarkCategory bookmarkCategory,
@NonNull List<BookmarkCategory> itemsCategories)
{
mBookmarkCategory = bookmarkCategory;
//noinspection AssignmentOrReturnOfFieldWithMutableType
mItemsCategory = itemsCategories;
mSectionCount = 0;
if (!mItemsCategory.isEmpty())
mCategorySectionIndex = mSectionCount++;
}
public int getItemsCount(int sectionIndex)
{
if (sectionIndex == mCategorySectionIndex)
return mItemsCategory.size();
return 0;
}
@SectionType
public int getItemsType(int sectionIndex)
{
if (sectionIndex == mCategorySectionIndex)
return TYPE_CATEGORY_ITEM;
throw new AssertionError("Invalid section index: " + sectionIndex);
}
@NonNull
private List<BookmarkCategory> getItemsListByType(@SectionType int type)
{
return mItemsCategory;
}
@NonNull
public BookmarkCategory getGroupByPosition(SectionPosition sp, @SectionType int type)
{
List<BookmarkCategory> categories = getItemsListByType(type);
int itemIndex = sp.getItemIndex();
if (sp.getItemIndex() > categories.size() - 1)
throw new ArrayIndexOutOfBoundsException(itemIndex);
return categories.get(itemIndex);
}
public void setOnClickListener(@Nullable OnItemClickListener<BookmarkCategory> listener)
{
mClickListener = listener;
}
private SectionPosition getSectionPosition(int position)
{
int startSectionRow = 0;
for (int i = 0; i < mSectionCount; ++i)
{
int sectionRowsCount = getItemsCount(i) + /* header */ 1;
if (startSectionRow == position)
return new SectionPosition(i, SectionPosition.INVALID_POSITION);
if (startSectionRow + sectionRowsCount > position)
return new SectionPosition(i, position - startSectionRow - /* header */ 1);
startSectionRow += sectionRowsCount;
}
return new SectionPosition(SectionPosition.INVALID_POSITION, SectionPosition.INVALID_POSITION);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
@SectionType int viewType)
{
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
RecyclerView.ViewHolder holder = null;
if (viewType == TYPE_HEADER_ITEM)
holder = new Holders.HeaderViewHolder(inflater.inflate(R.layout.item_bookmark_group_list_header,
parent, false));
if (viewType == TYPE_CATEGORY_ITEM)
holder = new Holders.CollectionViewHolder(inflater.inflate(R.layout.item_bookmark_collection,
parent, false));
if (holder == null)
throw new AssertionError("Unsupported view type: " + viewType);
return holder;
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position)
{
SectionPosition sectionPosition = getSectionPosition(position);
if (sectionPosition.isTitlePosition())
bindHeaderHolder(holder, sectionPosition.getSectionIndex());
else
bindCollectionHolder(holder, sectionPosition, getItemsType(sectionPosition.getSectionIndex()));
}
@Override
@SectionType
public int getItemViewType(int position)
{
SectionPosition sectionPosition = getSectionPosition(position);
if (sectionPosition.isTitlePosition())
return TYPE_HEADER_ITEM;
if (sectionPosition.isItemPosition())
return getItemsType(sectionPosition.getSectionIndex());
throw new AssertionError("Position not found: " + position);
}
private void bindCollectionHolder(RecyclerView.ViewHolder holder, SectionPosition position,
@SectionType int type)
{
final BookmarkCategory category = getGroupByPosition(position, type);
Holders.CollectionViewHolder collectionViewHolder = (Holders.CollectionViewHolder) holder;
collectionViewHolder.setEntity(category);
collectionViewHolder.setName(category.getName());
collectionViewHolder.setSize();
collectionViewHolder.setVisibilityState(category.isVisible());
collectionViewHolder.setOnClickListener(mClickListener);
ToggleVisibilityClickListener listener = new ToggleVisibilityClickListener(collectionViewHolder);
collectionViewHolder.setVisibilityListener(listener);
updateVisibility(collectionViewHolder.itemView);
}
private void bindHeaderHolder(@NonNull RecyclerView.ViewHolder holder, int nextSectionPosition)
{
Holders.HeaderViewHolder headerViewHolder = (Holders.HeaderViewHolder) holder;
headerViewHolder.getText()
.setText(holder.itemView.getResources().getString(R.string.bookmarks));
final boolean visibility = !BookmarkManager.INSTANCE.areAllCategoriesVisible();
headerViewHolder.setAction(mMassOperationAction, visibility);
updateVisibility(headerViewHolder.itemView);
}
private void updateVisibility(@NonNull View itemView)
{
UiUtils.showRecyclerItemView(mVisible, itemView);
}
@Override
public long getItemId(int position)
{
return position;
}
@Override
public int getItemCount()
{
int itemCount = 0;
for (int i = 0; i < mSectionCount; ++i)
{
int sectionItemsCount = getItemsCount(i);
if (sectionItemsCount == 0)
continue;
itemCount += sectionItemsCount + /* header */ 1;
}
return itemCount;
}
private void updateAllItems()
{
mItemsCategory = BookmarkManager.INSTANCE.getChildrenCategories(mBookmarkCategory.getId());
}
void show(boolean visible)
{
mVisible = visible;
notifyDataSetChanged();
}
class MassOperationAction implements Holders.HeaderViewHolder.HeaderActionChildCategories
{
@Override
public void onHideAll()
{
BookmarkManager.INSTANCE.setChildCategoriesVisibility(mBookmarkCategory.getId(), false);
updateAllItems();
notifyDataSetChanged();
}
@Override
public void onShowAll()
{
BookmarkManager.INSTANCE.setChildCategoriesVisibility(mBookmarkCategory.getId(), true);
updateAllItems();
notifyDataSetChanged();
}
}
}

View File

@@ -0,0 +1,68 @@
package app.organicmaps.bookmarks;
import android.content.Intent;
import android.os.Bundle;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;
import androidx.fragment.app.Fragment;
import app.organicmaps.R;
import app.organicmaps.base.BaseToolbarActivity;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.util.ThemeUtils;
public class BookmarkListActivity extends BaseToolbarActivity
{
@CallSuper
@Override
public void onResume()
{
super.onResume();
// Disable all notifications in BM on appearance of this activity.
// It allows to significantly improve performance in case of bookmarks
// modification. All notifications will be sent on activity's disappearance.
BookmarkManager.INSTANCE.setNotificationsEnabled(false);
}
@CallSuper
@Override
public void onPause()
{
// Allow to send all notifications in BM.
BookmarkManager.INSTANCE.setNotificationsEnabled(true);
super.onPause();
}
@Override
@StyleRes
public int getThemeResourceId(@NonNull String theme)
{
return ThemeUtils.getCardBgThemeResourceId(getApplicationContext(), theme);
}
@Override
protected Class<? extends Fragment> getFragmentClass()
{
return BookmarksListFragment.class;
}
@Override
protected int getContentLayoutResId()
{
return R.layout.bookmarks_activity;
}
static void startForResult(@NonNull Fragment fragment, ActivityResultLauncher<Intent> startBookmarkListForResult, @NonNull BookmarkCategory category)
{
Bundle args = new Bundle();
Intent intent = new Intent(fragment.requireActivity(), BookmarkListActivity.class);
intent.putExtra(BookmarksListFragment.EXTRA_CATEGORY, category);
startBookmarkListForResult.launch(intent);
}
}

View File

@@ -0,0 +1,575 @@
package app.organicmaps.bookmarks;
import android.content.res.Resources;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
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.bookmarks.data.IconClickListener;
import app.organicmaps.bookmarks.data.SortedBlock;
import app.organicmaps.content.DataSource;
import app.organicmaps.widget.recycler.RecyclerClickListener;
import app.organicmaps.widget.recycler.RecyclerLongClickListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class BookmarkListAdapter extends RecyclerView.Adapter<Holders.BaseBookmarkHolder>
{
// view types
static final int TYPE_TRACK = 0;
static final int TYPE_BOOKMARK = 1;
static final int TYPE_SECTION = 2;
static final int TYPE_DESC = 3;
static final int MAX_VISIBLE_LINES = 2;
@NonNull
private final DataSource<BookmarkCategory> mDataSource;
@Nullable
private List<Long> mSearchResults;
@Nullable
private List<SortedBlock> mSortedResults;
@SuppressWarnings("NullableProblems")
@NonNull
private SectionsDataSource mSectionsDataSource;
@Nullable
private RecyclerClickListener mClickListener;
@Nullable
private RecyclerLongClickListener mLongClickListener;
private RecyclerClickListener mMoreClickListener;
private IconClickListener mIconClickListener;
public static abstract class SectionsDataSource
{
@NonNull
private final DataSource<BookmarkCategory> mDataSource;
SectionsDataSource(@NonNull DataSource<BookmarkCategory> dataSource)
{
mDataSource = dataSource;
}
public BookmarkCategory getCategory() { return mDataSource.getData(); }
boolean hasDescription()
{
return (!mDataSource.getData().getAnnotation().isEmpty() ||
!mDataSource.getData().getDescription().isEmpty());
}
void invalidate()
{
mDataSource.invalidate();
}
public abstract int getSectionsCount();
public abstract boolean isEditable(int sectionIndex);
public abstract boolean hasTitle(int sectionIndex);
@Nullable
public abstract String getTitle(int sectionIndex, @NonNull Resources rs);
public abstract int getItemsCount(int sectionIndex);
public abstract int getItemsType(int sectionIndex);
public abstract long getBookmarkId(@NonNull SectionPosition pos);
public abstract long getTrackId(@NonNull SectionPosition pos);
public abstract void onDelete(@NonNull SectionPosition pos);
}
private static class CategorySectionsDataSource extends SectionsDataSource
{
private int mSectionsCount;
private int mBookmarksSectionIndex;
private int mTracksSectionIndex;
private int mDescriptionSectionIndex;
CategorySectionsDataSource(@NonNull DataSource<BookmarkCategory> dataSource)
{
super(dataSource);
calculateSections();
}
private void calculateSections()
{
mBookmarksSectionIndex = SectionPosition.INVALID_POSITION;
mTracksSectionIndex = SectionPosition.INVALID_POSITION;
mSectionsCount = 0;
// We must always show the description, even if it's blank.
mDescriptionSectionIndex = mSectionsCount++;
if (getCategory().getTracksCount() > 0)
mTracksSectionIndex = mSectionsCount++;
if (getCategory().getBookmarksCount() > 0)
mBookmarksSectionIndex = mSectionsCount++;
}
@Override
public int getSectionsCount() { return mSectionsCount; }
@Override
public boolean isEditable(int sectionIndex)
{
return sectionIndex != mDescriptionSectionIndex;
}
@Override
public boolean hasTitle(int sectionIndex) { return true; }
@Nullable
public String getTitle(int sectionIndex, @NonNull Resources rs)
{
if (sectionIndex == mDescriptionSectionIndex)
return rs.getString(R.string.description);
if (sectionIndex == mTracksSectionIndex)
return rs.getString(R.string.tracks_title);
return rs.getString(R.string.bookmarks);
}
@Override
public int getItemsCount(int sectionIndex)
{
if (sectionIndex == mDescriptionSectionIndex)
return 1;
if (sectionIndex == mTracksSectionIndex)
return getCategory().getTracksCount();
if (sectionIndex == mBookmarksSectionIndex)
return getCategory().getBookmarksCount();
return 0;
}
@Override
public int getItemsType(int sectionIndex)
{
if (sectionIndex == mDescriptionSectionIndex)
return TYPE_DESC;
if (sectionIndex == mTracksSectionIndex)
return TYPE_TRACK;
if (sectionIndex == mBookmarksSectionIndex)
return TYPE_BOOKMARK;
throw new AssertionError("Invalid section index: " + sectionIndex);
}
@Override
public void onDelete(@NonNull SectionPosition pos)
{
// we must invalidate datasource before calculate sections
invalidate();
calculateSections();
}
@Override
public long getBookmarkId(@NonNull SectionPosition pos)
{
return BookmarkManager.INSTANCE.getBookmarkIdByPosition(getCategory().getId(),
pos.getItemIndex());
}
@Override
public long getTrackId(@NonNull SectionPosition pos)
{
return BookmarkManager.INSTANCE.getTrackIdByPosition(getCategory().getId(),
pos.getItemIndex());
}
}
private static class SearchResultsSectionsDataSource extends SectionsDataSource
{
@NonNull
private final List<Long> mSearchResults;
SearchResultsSectionsDataSource(@NonNull DataSource<BookmarkCategory> dataSource,
@NonNull List<Long> searchResults)
{
super(dataSource);
mSearchResults = searchResults;
}
@Override
public int getSectionsCount() { return 1; }
@Override
public boolean isEditable(int sectionIndex) { return true; }
@Override
public boolean hasTitle(int sectionIndex) { return false; }
@Nullable
public String getTitle(int sectionIndex, @NonNull Resources rs) { return null; }
@Override
public int getItemsCount(int sectionIndex) { return mSearchResults.size(); }
@Override
public int getItemsType(int sectionIndex) { return TYPE_BOOKMARK; }
@Override
public void onDelete(@NonNull SectionPosition pos)
{
mSearchResults.remove(pos.getItemIndex());
}
@Override
public long getBookmarkId(@NonNull SectionPosition pos)
{
return mSearchResults.get(pos.getItemIndex());
}
@Override
public long getTrackId(@NonNull SectionPosition pos)
{
throw new AssertionError("Tracks unsupported in search results.");
}
}
private static class SortedSectionsDataSource extends SectionsDataSource
{
@NonNull
private final List<SortedBlock> mSortedBlocks;
SortedSectionsDataSource(@NonNull DataSource<BookmarkCategory> dataSource,
@NonNull List<SortedBlock> sortedBlocks)
{
super(dataSource);
mSortedBlocks = sortedBlocks;
}
private boolean isDescriptionSection(int sectionIndex)
{
return hasDescription() && sectionIndex == 0;
}
@NonNull
private SortedBlock getSortedBlock(int sectionIndex)
{
if (isDescriptionSection(sectionIndex))
throw new IllegalArgumentException("Invalid section index for sorted block.");
int blockIndex = sectionIndex - (hasDescription() ? 1 : 0);
return mSortedBlocks.get(blockIndex);
}
@Override
public int getSectionsCount()
{
return mSortedBlocks.size() + (hasDescription() ? 1 : 0);
}
@Override
public boolean isEditable(int sectionIndex)
{
return !isDescriptionSection(sectionIndex);
}
@Override
public boolean hasTitle(int sectionIndex) { return true; }
@Nullable
public String getTitle(int sectionIndex, @NonNull Resources rs)
{
if (isDescriptionSection(sectionIndex))
return rs.getString(R.string.description);
return getSortedBlock(sectionIndex).getName();
}
@Override
public int getItemsCount(int sectionIndex)
{
if (isDescriptionSection(sectionIndex))
return 1;
SortedBlock block = getSortedBlock(sectionIndex);
if (block.isBookmarksBlock())
return block.getBookmarkIds().size();
return block.getTrackIds().size();
}
@Override
public int getItemsType(int sectionIndex)
{
if (isDescriptionSection(sectionIndex))
return TYPE_DESC;
if (getSortedBlock(sectionIndex).isBookmarksBlock())
return TYPE_BOOKMARK;
return TYPE_TRACK;
}
@Override
public void onDelete(@NonNull SectionPosition pos)
{
if (isDescriptionSection(pos.getSectionIndex()))
throw new IllegalArgumentException("Delete failed. Invalid section index.");
int blockIndex = pos.getSectionIndex() - (hasDescription() ? 1 : 0);
SortedBlock block = mSortedBlocks.get(blockIndex);
if (block.isBookmarksBlock())
{
block.getBookmarkIds().remove(pos.getItemIndex());
if (block.getBookmarkIds().isEmpty())
mSortedBlocks.remove(blockIndex);
return;
}
block.getTrackIds().remove(pos.getItemIndex());
if (block.getTrackIds().isEmpty())
mSortedBlocks.remove(blockIndex);
}
public long getBookmarkId(@NonNull SectionPosition pos)
{
return getSortedBlock(pos.getSectionIndex()).getBookmarkIds().get(pos.getItemIndex());
}
public long getTrackId(@NonNull SectionPosition pos)
{
return getSortedBlock(pos.getSectionIndex()).getTrackIds().get(pos.getItemIndex());
}
}
BookmarkListAdapter(@NonNull DataSource<BookmarkCategory> dataSource)
{
mDataSource = dataSource;
refreshSections();
}
private void refreshSections()
{
if (mSearchResults != null)
mSectionsDataSource = new SearchResultsSectionsDataSource(mDataSource, mSearchResults);
else if (mSortedResults != null)
mSectionsDataSource = new SortedSectionsDataSource(mDataSource, mSortedResults);
else
mSectionsDataSource = new CategorySectionsDataSource(mDataSource);
}
private SectionPosition getSectionPosition(int position)
{
int startSectionRow = 0;
boolean hasTitle;
int sectionsCount = mSectionsDataSource.getSectionsCount();
for (int i = 0; i < sectionsCount; ++i)
{
hasTitle = mSectionsDataSource.hasTitle(i);
int sectionRowsCount = mSectionsDataSource.getItemsCount(i) + (hasTitle ? 1 : 0);
if (startSectionRow == position && hasTitle)
return new SectionPosition(i, SectionPosition.INVALID_POSITION);
if (startSectionRow + sectionRowsCount > position)
return new SectionPosition(i, position - startSectionRow - (hasTitle ? 1 : 0));
startSectionRow += sectionRowsCount;
}
return new SectionPosition(SectionPosition.INVALID_POSITION, SectionPosition.INVALID_POSITION);
}
void setSearchResults(@Nullable long[] searchResults)
{
if (searchResults != null)
{
mSearchResults = new ArrayList<>(searchResults.length);
for (long id : searchResults)
mSearchResults.add(id);
}
else
{
mSearchResults = null;
}
refreshSections();
}
void setSortedResults(@Nullable SortedBlock[] sortedResults)
{
if (sortedResults != null)
mSortedResults = new ArrayList<>(Arrays.asList(sortedResults));
else
mSortedResults = null;
refreshSections();
}
public void setOnClickListener(@Nullable RecyclerClickListener listener)
{
mClickListener = listener;
}
void setOnLongClickListener(@Nullable RecyclerLongClickListener listener)
{
mLongClickListener = listener;
}
public void setMoreListener(@Nullable RecyclerClickListener listener)
{
mMoreClickListener = listener;
}
public void setIconClickListener(IconClickListener listener)
{
mIconClickListener = listener;
}
@Override
@NonNull
public Holders.BaseBookmarkHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType)
{
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
Holders.BaseBookmarkHolder holder = null;
switch (viewType)
{
case TYPE_TRACK:
Holders.TrackViewHolder trackHolder =
new Holders.TrackViewHolder(inflater.inflate(R.layout.item_track, parent,
false));
trackHolder.setOnClickListener(mClickListener);
trackHolder.setOnLongClickListener(mLongClickListener);
trackHolder.setTrackIconClickListener(mIconClickListener);
trackHolder.setMoreButtonClickListener(mMoreClickListener);
holder = trackHolder;
break;
case TYPE_BOOKMARK:
Holders.BookmarkViewHolder bookmarkHolder =
new Holders.BookmarkViewHolder(inflater.inflate(R.layout.item_bookmark, parent,
false));
bookmarkHolder.setOnClickListener(mClickListener);
bookmarkHolder.setOnLongClickListener(mLongClickListener);
holder = bookmarkHolder;
break;
case TYPE_SECTION:
TextView tv = (TextView) inflater.inflate(R.layout.item_category_title, parent, false);
holder = new Holders.SectionViewHolder(tv);
break;
case TYPE_DESC:
View desc = inflater.inflate(R.layout.item_category_description, parent, false);
TextView moreBtn = desc.findViewById(R.id.more_btn);
TextView text = desc.findViewById(R.id.text);
TextView title = desc.findViewById(R.id.title);
setMoreButtonVisibility(text, moreBtn);
holder = new Holders.DescriptionViewHolder(desc, mSectionsDataSource.getCategory());
text.setOnClickListener(v -> onMoreButtonClicked(text, moreBtn));
moreBtn.setOnClickListener(v -> onMoreButtonClicked(text, moreBtn));
title.setOnClickListener(v -> onMoreButtonClicked(text, moreBtn));
break;
}
if (holder == null)
throw new AssertionError("Unsupported view type: " + viewType);
return holder;
}
@Override
public void onBindViewHolder(@NonNull Holders.BaseBookmarkHolder holder, int position)
{
SectionPosition sp = getSectionPosition(position);
holder.bind(sp, mSectionsDataSource);
}
@Override
public int getItemViewType(int position)
{
SectionPosition sp = getSectionPosition(position);
if (sp.isTitlePosition())
return TYPE_SECTION;
if (sp.isItemPosition())
return mSectionsDataSource.getItemsType(sp.getSectionIndex());
throw new IllegalArgumentException("Position not found: " + position);
}
@Override
public long getItemId(int position)
{
return position;
}
@Override
public int getItemCount()
{
int itemCount = 0;
int sectionsCount = mSectionsDataSource.getSectionsCount();
for (int i = 0; i < sectionsCount; ++i)
{
int sectionItemsCount = mSectionsDataSource.getItemsCount(i);
if (sectionItemsCount == 0)
continue;
itemCount += sectionItemsCount;
if (mSectionsDataSource.hasTitle(i))
++itemCount;
}
return itemCount;
}
void onDelete(int position)
{
SectionPosition sp = getSectionPosition(position);
mSectionsDataSource.onDelete(sp);
// In case of the search results editing reset cached sorted blocks.
if (isSearchResults())
mSortedResults = null;
}
boolean isSearchResults()
{
return mSearchResults != null;
}
public Object getItem(int position)
{
if (getItemViewType(position) == TYPE_DESC)
throw new UnsupportedOperationException("Not supported here! Position = " + position);
SectionPosition pos = getSectionPosition(position);
if (getItemViewType(position) == TYPE_TRACK)
{
final long trackId = mSectionsDataSource.getTrackId(pos);
return BookmarkManager.INSTANCE.getTrack(trackId);
}
else
{
final long bookmarkId = mSectionsDataSource.getBookmarkId(pos);
BookmarkInfo info = BookmarkManager.INSTANCE.getBookmarkInfo(bookmarkId);
if (info == null)
throw new RuntimeException("Bookmark no longer exists " + bookmarkId);
return info;
}
}
private void setMoreButtonVisibility(TextView text, TextView moreBtn)
{
text.post(() -> setShortModeDescription(text, moreBtn));
}
private void onMoreButtonClicked(TextView textView, TextView moreBtn)
{
if (isShortModeDescription(textView))
{
setExpandedModeDescription(textView, moreBtn);
}
else
{
setShortModeDescription(textView, moreBtn);
}
}
private boolean isShortModeDescription(TextView text)
{
return text.getMaxLines() == MAX_VISIBLE_LINES;
}
private void setExpandedModeDescription(TextView textView, TextView moreBtn)
{
textView.setMaxLines(Integer.MAX_VALUE);
moreBtn.setVisibility(View.GONE);
}
private void setShortModeDescription(TextView textView, TextView moreBtn)
{
textView.setMaxLines(MAX_VISIBLE_LINES);
boolean isDescriptionTooLong = textView.getLineCount() > MAX_VISIBLE_LINES;
moreBtn.setVisibility(isDescriptionTooLong ? View.VISIBLE : View.GONE);
}
}

View File

@@ -0,0 +1,869 @@
package app.organicmaps.bookmarks;
import android.app.Activity;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.FragmentFactory;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.ConcatAdapter;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator;
import app.organicmaps.MwmActivity;
import app.organicmaps.R;
import app.organicmaps.base.BaseMwmRecyclerFragment;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.bookmarks.data.BookmarkInfo;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.bookmarks.data.BookmarkSharingResult;
import app.organicmaps.bookmarks.data.CategoryDataSource;
import app.organicmaps.bookmarks.data.Icon;
import app.organicmaps.bookmarks.data.KmlFileType;
import app.organicmaps.bookmarks.data.SortedBlock;
import app.organicmaps.bookmarks.data.Track;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.sdk.search.BookmarkSearchListener;
import app.organicmaps.sdk.search.SearchEngine;
import app.organicmaps.util.Graphics;
import app.organicmaps.util.SharingUtils;
import app.organicmaps.util.UiUtils;
import app.organicmaps.util.Utils;
import app.organicmaps.util.WindowInsetUtils;
import app.organicmaps.util.bottomsheet.MenuBottomSheetFragment;
import app.organicmaps.util.bottomsheet.MenuBottomSheetItem;
import app.organicmaps.widget.SearchToolbarController;
import app.organicmaps.widget.placepage.BookmarkColorDialogFragment;
import app.organicmaps.widget.placepage.EditBookmarkFragment;
import app.organicmaps.widget.recycler.DividerItemDecorationWithPadding;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class BookmarksListFragment extends BaseMwmRecyclerFragment<ConcatAdapter>
implements BookmarkManager.BookmarksSharingListener,
BookmarkManager.BookmarksSortingListener,
BookmarkManager.BookmarksLoadingListener,
BookmarkSearchListener,
ChooseBookmarksSortingTypeFragment.ChooseSortingTypeListener,
MenuBottomSheetFragment.MenuBottomSheetInterface
{
public static final String TAG = BookmarksListFragment.class.getSimpleName();
public static final String EXTRA_CATEGORY = "bookmark_category";
private static final int INDEX_BOOKMARKS_COLLECTION_ADAPTER = 0;
private static final int INDEX_BOOKMARKS_LIST_ADAPTER = 1;
private static final String BOOKMARKS_MENU_ID = "BOOKMARKS_MENU_BOTTOM_SHEET";
private static final String TRACK_MENU_ID = "TRACK_MENU_BOTTOM_SHEET";
private static final String OPTIONS_MENU_ID = "OPTIONS_MENU_BOTTOM_SHEET";
private ActivityResultLauncher<SharingUtils.SharingIntent> shareLauncher;
private final ActivityResultLauncher<Intent> startBookmarkListForResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), activityResult -> {
System.out.println("resultCode: " + activityResult.getResultCode());
handleActivityResult();
});
private final ActivityResultLauncher<Intent> startBookmarkSettingsForResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), activityResult -> {
System.out.println("resultCode: " + activityResult.getResultCode());
handleActivityResult();
});
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private SearchToolbarController mToolbarController;
private long mLastQueryTimestamp = 0;
private long mLastSortTimestamp = 0;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private CategoryDataSource mCategoryDataSource;
private int mSelectedPosition;
private boolean mSearchMode = false;
private boolean mNeedUpdateSorting = true;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private ViewGroup mSearchContainer;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private ExtendedFloatingActionButton mFabViewOnMap;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private final RecyclerView.OnScrollListener mRecyclerListener = new RecyclerView.OnScrollListener()
{
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState)
{
if (newState == RecyclerView.SCROLL_STATE_DRAGGING)
mToolbarController.deactivate();
}
};
@Nullable
private Bundle mSavedInstanceState;
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
BookmarkCategory category = getCategoryOrThrow();
mCategoryDataSource = new CategoryDataSource(category);
shareLauncher = SharingUtils.RegisterLauncher(this);
}
@NonNull
private BookmarkCategory getCategoryOrThrow()
{
final Bundle args = requireArguments();
return Objects.requireNonNull(Utils.getParcelable(args, EXTRA_CATEGORY, BookmarkCategory.class));
}
@NonNull
@Override
protected ConcatAdapter createAdapter()
{
BookmarkCategory category = mCategoryDataSource.getData();
return new ConcatAdapter(initAndGetCollectionAdapter(category.getId()),
new BookmarkListAdapter(mCategoryDataSource));
}
@NonNull
private RecyclerView.Adapter<RecyclerView.ViewHolder> initAndGetCollectionAdapter(long categoryId)
{
List<BookmarkCategory> mCategoryItems = BookmarkManager.INSTANCE.getChildrenCategories(categoryId);
BookmarkCollectionAdapter adapter = new BookmarkCollectionAdapter(getCategoryOrThrow(),
mCategoryItems);
adapter.setOnClickListener((v, item) -> {
BookmarkListActivity.startForResult(this, startBookmarkListForResult, item);
});
return adapter;
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.fragment_bookmark_list, container, false);
}
@CallSuper
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
{
if (BookmarkManager.INSTANCE.isAsyncBookmarksLoadingInProgress())
{
mSavedInstanceState = savedInstanceState;
updateLoadingPlaceholder(view, true);
return;
}
super.onViewCreated(view, savedInstanceState);
onViewCreatedInternal(view);
}
private void onViewCreatedInternal(@NonNull View view)
{
configureBookmarksListAdapter();
configureFab(view);
setHasOptionsMenu(true);
ActionBar bar = ((AppCompatActivity) requireActivity()).getSupportActionBar();
if (bar != null)
bar.setTitle(mCategoryDataSource.getData().getName());
ViewGroup toolbar = requireActivity().findViewById(R.id.toolbar);
mSearchContainer = toolbar.findViewById(R.id.search_container);
UiUtils.hide(mSearchContainer, R.id.back);
mToolbarController = new BookmarksToolbarController(toolbar, requireActivity(), this);
mToolbarController.setHint(R.string.search_in_the_list);
configureRecyclerAnimations();
configureRecyclerDividers();
// recycler view already has an InsetListener in BaseMwmRecyclerFragment
// here we must reset it, because the logic is different from a common use case
ViewCompat.setOnApplyWindowInsetsListener(
getRecyclerView(),
new WindowInsetUtils.ScrollableContentInsetsListener(getRecyclerView(), mFabViewOnMap));
updateLoadingPlaceholder(view, false);
}
@Override
public void onStart()
{
super.onStart();
SearchEngine.INSTANCE.addBookmarkListener(this);
BookmarkManager.INSTANCE.addLoadingListener(this);
BookmarkManager.INSTANCE.addSortingListener(this);
BookmarkManager.INSTANCE.addSharingListener(this);
}
@Override
public void onResume()
{
super.onResume();
if (BookmarkManager.INSTANCE.isAsyncBookmarksLoadingInProgress())
return;
BookmarkListAdapter adapter = getBookmarkListAdapter();
adapter.notifyDataSetChanged();
updateSorting();
updateSearchVisibility();
updateRecyclerVisibility();
}
@Override
public void onPause()
{
super.onPause();
}
@Override
public void onStop()
{
super.onStop();
SearchEngine.INSTANCE.removeBookmarkListener(this);
BookmarkManager.INSTANCE.removeLoadingListener(this);
BookmarkManager.INSTANCE.removeSortingListener(this);
BookmarkManager.INSTANCE.removeSharingListener(this);
}
private void configureBookmarksListAdapter()
{
BookmarkListAdapter adapter = getBookmarkListAdapter();
adapter.registerAdapterDataObserver(mCategoryDataSource);
adapter.setOnClickListener((v, position) -> onItemClick(position));
adapter.setOnLongClickListener((v, position) -> onItemMore(position));
adapter.setMoreListener((v, position) -> onItemMore(position));
adapter.setIconClickListener(this::showColorDialog);
}
private void configureFab(@NonNull View view)
{
mFabViewOnMap = view.findViewById(R.id.show_on_map_fab);
mFabViewOnMap.setOnClickListener(v ->
{
final Intent i = makeMwmActivityIntent();
i.putExtra(MwmActivity.EXTRA_CATEGORY_ID, mCategoryDataSource.getData().getId());
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(i);
});
}
private void configureRecyclerAnimations()
{
RecyclerView.ItemAnimator itemAnimator = getRecyclerView().getItemAnimator();
if (itemAnimator != null)
((SimpleItemAnimator) itemAnimator).setSupportsChangeAnimations(false);
}
private void configureRecyclerDividers()
{
RecyclerView.ItemDecoration decorWithPadding = new DividerItemDecorationWithPadding(requireContext());
getRecyclerView().addItemDecoration(decorWithPadding);
getRecyclerView().addOnScrollListener(mRecyclerListener);
}
private void updateRecyclerVisibility()
{
if (isEmptySearchResults())
{
requirePlaceholder().setContent(R.string.search_not_found,
R.string.search_not_found_query);
}
else if (isEmpty())
{
requirePlaceholder().setContent(R.string.bookmarks_empty_list_title,
R.string.bookmarks_empty_list_message);
}
boolean isEmptyRecycler = isEmpty() || isEmptySearchResults();
showPlaceholder(isEmptyRecycler);
getBookmarkCollectionAdapter().show(!getBookmarkListAdapter().isSearchResults());
UiUtils.showIf(!isEmptyRecycler, getRecyclerView(), mFabViewOnMap);
requireActivity().invalidateOptionsMenu();
}
private void updateSearchVisibility()
{
if (isEmpty())
{
UiUtils.hide(mSearchContainer);
}
else
{
UiUtils.showIf(mSearchMode, mSearchContainer);
if (mSearchMode)
mToolbarController.activate();
else
mToolbarController.deactivate();
}
requireActivity().invalidateOptionsMenu();
}
public void runSearch(@NonNull String query)
{
SearchEngine.INSTANCE.cancel();
mLastQueryTimestamp = System.nanoTime();
if (SearchEngine.INSTANCE.searchInBookmarks(query,
mCategoryDataSource.getData().getId(),
mLastQueryTimestamp))
{
mToolbarController.showProgress(true);
}
}
@Override
public void onBookmarkSearchResultsUpdate(@Nullable long[] bookmarkIds, long timestamp)
{
if (!isAdded() || !mToolbarController.hasQuery() || mLastQueryTimestamp != timestamp)
return;
updateSearchResults(bookmarkIds);
}
@Override
public void onBookmarkSearchResultsEnd(@Nullable long[] bookmarkIds, long timestamp)
{
if (!isAdded() || !mToolbarController.hasQuery() || mLastQueryTimestamp != timestamp)
return;
mLastQueryTimestamp = 0;
mToolbarController.showProgress(false);
updateSearchResults(bookmarkIds);
}
private void updateSearchResults(@Nullable long[] bookmarkIds)
{
BookmarkListAdapter adapter = getBookmarkListAdapter();
adapter.setSearchResults(bookmarkIds);
adapter.notifyDataSetChanged();
updateRecyclerVisibility();
}
public void cancelSearch()
{
mLastQueryTimestamp = 0;
SearchEngine.INSTANCE.cancel();
mToolbarController.showProgress(false);
updateSearchResults(null);
updateSorting();
}
public void activateSearch()
{
mSearchMode = true;
BookmarkManager.INSTANCE.setNotificationsEnabled(true);
BookmarkManager.INSTANCE.prepareForSearch(mCategoryDataSource.getData().getId());
updateSearchVisibility();
}
public void deactivateSearch()
{
mSearchMode = false;
BookmarkManager.INSTANCE.setNotificationsEnabled(false);
updateSearchVisibility();
}
@Override
public void onBookmarksSortingCompleted(@NonNull SortedBlock[] sortedBlocks, long timestamp)
{
if (mLastSortTimestamp != timestamp)
return;
mLastSortTimestamp = 0;
BookmarkListAdapter adapter = getBookmarkListAdapter();
adapter.setSortedResults(sortedBlocks);
adapter.notifyDataSetChanged();
updateSortingProgressBar();
}
@Override
public void onBookmarksSortingCancelled(long timestamp)
{
if (mLastSortTimestamp != timestamp)
return;
mLastSortTimestamp = 0;
BookmarkListAdapter adapter = getBookmarkListAdapter();
adapter.setSortedResults(null);
adapter.notifyDataSetChanged();
updateSortingProgressBar();
}
@Override
public void onSort(@BookmarkManager.SortingType int sortingType)
{
mLastSortTimestamp = System.nanoTime();
final Location loc = LocationHelper.from(requireContext()).getSavedLocation();
final boolean hasMyPosition = loc != null;
if (!hasMyPosition && sortingType == BookmarkManager.SORT_BY_DISTANCE)
return;
final long catId = mCategoryDataSource.getData().getId();
final double lat = hasMyPosition ? loc.getLatitude() : 0;
final double lon = hasMyPosition ? loc.getLongitude() : 0;
BookmarkManager.INSTANCE.setLastSortingType(catId, sortingType);
BookmarkManager.INSTANCE.getSortedCategory(catId, sortingType, hasMyPosition, lat, lon,
mLastSortTimestamp);
updateSortingProgressBar();
}
@NonNull
private BookmarkListAdapter getBookmarkListAdapter()
{
return (BookmarkListAdapter) getAdapter().getAdapters().get(INDEX_BOOKMARKS_LIST_ADAPTER);
}
@NonNull
private BookmarkCollectionAdapter getBookmarkCollectionAdapter()
{
return (BookmarkCollectionAdapter) getAdapter().getAdapters()
.get(INDEX_BOOKMARKS_COLLECTION_ADAPTER);
}
@Override
public void onResetSorting()
{
mLastSortTimestamp = 0;
long catId = mCategoryDataSource.getData().getId();
BookmarkManager.INSTANCE.resetLastSortingType(catId);
BookmarkListAdapter adapter = getBookmarkListAdapter();
adapter.setSortedResults(null);
adapter.notifyDataSetChanged();
}
private void updateSorting()
{
if (!mNeedUpdateSorting)
return;
mNeedUpdateSorting = false;
// Do nothing in case of sorting has already started and we are waiting for results.
if (mLastSortTimestamp != 0)
return;
long catId = mCategoryDataSource.getData().getId();
if (!BookmarkManager.INSTANCE.hasLastSortingType(catId))
return;
int currentType = getLastAvailableSortingType();
if (currentType >= 0)
onSort(currentType);
}
private void forceUpdateSorting()
{
mLastSortTimestamp = 0;
mNeedUpdateSorting = true;
updateSorting();
}
private void resetSearchAndSort()
{
BookmarkListAdapter adapter = getBookmarkListAdapter();
adapter.setSortedResults(null);
adapter.setSearchResults(null);
adapter.notifyDataSetChanged();
if (mSearchMode)
{
cancelSearch();
deactivateSearch();
}
forceUpdateSorting();
updateRecyclerVisibility();
}
@NonNull
@BookmarkManager.SortingType
private int[] getAvailableSortingTypes()
{
final long catId = mCategoryDataSource.getData().getId();
final Location loc = LocationHelper.from(requireContext()).getSavedLocation();
final boolean hasMyPosition = loc != null;
return BookmarkManager.INSTANCE.getAvailableSortingTypes(catId, hasMyPosition);
}
private int getLastSortingType()
{
final long catId = mCategoryDataSource.getData().getId();
if (BookmarkManager.INSTANCE.hasLastSortingType(catId))
return BookmarkManager.INSTANCE.getLastSortingType(catId);
return -1;
}
private int getLastAvailableSortingType()
{
int currentType = getLastSortingType();
@BookmarkManager.SortingType int[] types = getAvailableSortingTypes();
for (@BookmarkManager.SortingType int type : types)
{
if (type == currentType)
return currentType;
}
return -1;
}
private boolean isEmpty()
{
return !getBookmarkListAdapter().isSearchResults()
&& getBookmarkListAdapter().getItemCount() == 0;
}
private boolean isEmptySearchResults()
{
return getBookmarkListAdapter().isSearchResults()
&& getBookmarkListAdapter().getItemCount() == 0;
}
private boolean isLastOwnedCategory()
{
return BookmarkManager.INSTANCE.getCategoriesCount() == 1;
}
private void updateSortingProgressBar()
{
requireActivity().invalidateOptionsMenu();
}
public void onItemClick(int position)
{
final Intent intent = makeMwmActivityIntent();
BookmarkListAdapter adapter = getBookmarkListAdapter();
switch (adapter.getItemViewType(position))
{
case BookmarkListAdapter.TYPE_SECTION, BookmarkListAdapter.TYPE_DESC ->
{
return;
}
case BookmarkListAdapter.TYPE_BOOKMARK -> onBookmarkClicked(position, intent, adapter);
case BookmarkListAdapter.TYPE_TRACK -> onTrackClicked(position, intent, adapter);
}
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
@NonNull
private Intent makeMwmActivityIntent()
{
return new Intent(requireActivity(), MwmActivity.class);
}
private void onTrackClicked(int position, @NonNull Intent i, @NonNull BookmarkListAdapter adapter)
{
final Track track = (Track) adapter.getItem(position);
i.putExtra(MwmActivity.EXTRA_CATEGORY_ID, track.getCategoryId());
i.putExtra(MwmActivity.EXTRA_TRACK_ID, track.getTrackId());
}
private void onBookmarkClicked(int position, @NonNull Intent i,
@NonNull BookmarkListAdapter adapter)
{
final BookmarkInfo bookmark = (BookmarkInfo) adapter.getItem(position);
i.putExtra(MwmActivity.EXTRA_CATEGORY_ID, bookmark.getCategoryId());
i.putExtra(MwmActivity.EXTRA_BOOKMARK_ID, bookmark.getBookmarkId());
}
private void showColorDialog(ImageView v, int position)
{
BookmarkListAdapter adapter = getBookmarkListAdapter();
mSelectedPosition = position;
final Track mTrack = (Track) adapter.getItem(mSelectedPosition);
if (mTrack == null) return;
final Bundle args = new Bundle();
args.putInt(BookmarkColorDialogFragment.ICON_TYPE, Icon.getColorPosition(mTrack.getColor()));
final FragmentManager manager = getChildFragmentManager();
String className = BookmarkColorDialogFragment.class.getName();
final FragmentFactory factory = manager.getFragmentFactory();
final BookmarkColorDialogFragment dialogFragment =
(BookmarkColorDialogFragment) factory.instantiate(getContext().getClassLoader(), className);
dialogFragment.setArguments(args);
dialogFragment.setOnColorSetListener((colorPos) -> {
int from = mTrack.getColor();
int to = BookmarkManager.ICONS.get(colorPos).argb();
if (from == to)
return;
BookmarkManager.INSTANCE.changeTrackColor(mTrack.getTrackId(), to);
Drawable circle = Graphics.drawCircle(to,
R.dimen.track_circle_size,
requireContext().getResources());
v.setImageDrawable(circle);
});
dialogFragment.show(requireActivity().getSupportFragmentManager(), null);
}
public void onItemMore(int position)
{
BookmarkListAdapter adapter = getBookmarkListAdapter();
mSelectedPosition = position;
int type = adapter.getItemViewType(mSelectedPosition);
switch (type)
{
case BookmarkListAdapter.TYPE_SECTION:
case BookmarkListAdapter.TYPE_DESC:
// Do nothing here?
break;
case BookmarkListAdapter.TYPE_BOOKMARK:
final BookmarkInfo bookmark = (BookmarkInfo) adapter.getItem(mSelectedPosition);
MenuBottomSheetFragment.newInstance(BOOKMARKS_MENU_ID, bookmark.getName())
.show(getChildFragmentManager(), BOOKMARKS_MENU_ID);
break;
case BookmarkListAdapter.TYPE_TRACK:
final Track track = (Track) adapter.getItem(mSelectedPosition);
MenuBottomSheetFragment.newInstance(TRACK_MENU_ID, track.getName())
.show(getChildFragmentManager(), TRACK_MENU_ID);
break;
}
}
private void onDeleteTrackSelected(long trackId)
{
BookmarkManager.INSTANCE.deleteTrack(trackId);
getBookmarkListAdapter().onDelete(mSelectedPosition);
getBookmarkListAdapter().notifyDataSetChanged();
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater)
{
inflater.inflate(R.menu.option_menu_bookmarks, menu);
MenuItem itemSearch = menu.findItem(R.id.bookmarks_search);
itemSearch.setVisible(!isEmpty());
}
@Override
public void onPrepareOptionsMenu(@NonNull Menu menu)
{
super.onPrepareOptionsMenu(menu);
boolean visible = !mSearchMode && !isEmpty();
MenuItem itemSearch = menu.findItem(R.id.bookmarks_search);
itemSearch.setVisible(visible);
MenuItem itemMore = menu.findItem(R.id.bookmarks_more);
if (mLastSortTimestamp != 0)
itemMore.setActionView(R.layout.toolbar_menu_progressbar);
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
if (item.getItemId() == R.id.bookmarks_search)
{
activateSearch();
return true;
}
if (item.getItemId() == R.id.bookmarks_more)
{
MenuBottomSheetFragment.newInstance(OPTIONS_MENU_ID, mCategoryDataSource.getData().getName())
.show(getChildFragmentManager(), OPTIONS_MENU_ID);
return true;
}
return super.onOptionsItemSelected(item);
}
private void onShareActionSelected()
{
BookmarkInfo info = (BookmarkInfo) getBookmarkListAdapter().getItem(mSelectedPosition);
SharingUtils.shareBookmark(requireContext(), info);
}
private void onEditActionSelected()
{
BookmarkListAdapter adapter = getBookmarkListAdapter();
BookmarkInfo info = (BookmarkInfo) adapter.getItem(mSelectedPosition);
EditBookmarkFragment.editBookmark(
info.getCategoryId(), info.getBookmarkId(), requireActivity(), getChildFragmentManager(),
(bookmarkId, movedFromCategory) ->
{
if (movedFromCategory)
resetSearchAndSort();
else
adapter.notifyDataSetChanged();
});
}
private void onTrackEditActionSelected()
{
Track track = (Track) getBookmarkListAdapter().getItem(mSelectedPosition);
EditBookmarkFragment.editTrack(
track.getCategoryId(), track.getTrackId(), requireActivity(), getChildFragmentManager(),
(trackId, movedFromCategory) ->
{
if (movedFromCategory)
resetSearchAndSort();
else
getBookmarkListAdapter().notifyDataSetChanged();
});
}
private void onDeleteActionSelected()
{
BookmarkListAdapter adapter = getBookmarkListAdapter();
BookmarkInfo info = (BookmarkInfo) getBookmarkListAdapter().getItem(mSelectedPosition);
adapter.onDelete(mSelectedPosition);
BookmarkManager.INSTANCE.deleteBookmark(info.getBookmarkId());
adapter.notifyDataSetChanged();
if (mSearchMode)
mNeedUpdateSorting = true;
updateSearchVisibility();
updateRecyclerVisibility();
}
private void onSortOptionSelected()
{
ChooseBookmarksSortingTypeFragment.chooseSortingType(getAvailableSortingTypes(),
getLastSortingType(), requireActivity(), getChildFragmentManager());
}
private void onShareOptionSelected(KmlFileType kmlFileType)
{
long catId = mCategoryDataSource.getData().getId();
BookmarksSharingHelper.INSTANCE.prepareBookmarkCategoryForSharing(requireActivity(), catId, kmlFileType);
}
private void onSettingsOptionSelected()
{
BookmarkCategorySettingsActivity.startForResult(this, startBookmarkSettingsForResult, mCategoryDataSource.getData());
}
private void onDeleteOptionSelected()
{
requireActivity().setResult(Activity.RESULT_OK);
requireActivity().finish();
}
private ArrayList<MenuBottomSheetItem> getOptionsMenuItems()
{
@BookmarkManager.SortingType int[] types = getAvailableSortingTypes();
ArrayList<MenuBottomSheetItem> items = new ArrayList<>();
if (!isEmpty())
{
if (types.length > 0)
items.add(new MenuBottomSheetItem(R.string.sort, R.drawable.ic_sort, this::onSortOptionSelected));
items.add(new MenuBottomSheetItem(R.string.export_file, R.drawable.ic_file_kmz, () -> onShareOptionSelected(KmlFileType.Text)));
items.add(new MenuBottomSheetItem(R.string.export_file_gpx, R.drawable.ic_file_gpx, () -> onShareOptionSelected(KmlFileType.Gpx)));
}
items.add(new MenuBottomSheetItem(R.string.edit, R.drawable.ic_settings, this::onSettingsOptionSelected));
if (!isLastOwnedCategory())
items.add(new MenuBottomSheetItem(R.string.delete_list, R.drawable.ic_delete, this::onDeleteOptionSelected));
return items;
}
private ArrayList<MenuBottomSheetItem> getBookmarkMenuItems()
{
ArrayList<MenuBottomSheetItem> items = new ArrayList<>();
items.add(new MenuBottomSheetItem(R.string.share, R.drawable.ic_share, this::onShareActionSelected));
items.add(new MenuBottomSheetItem(R.string.edit, R.drawable.ic_edit, this::onEditActionSelected));
items.add(new MenuBottomSheetItem(R.string.delete, R.drawable.ic_delete, this::onDeleteActionSelected));
return items;
}
private ArrayList<MenuBottomSheetItem> getTrackMenuItems(final Track track)
{
ArrayList<MenuBottomSheetItem> items = new ArrayList<>();
items.add(new MenuBottomSheetItem(R.string.edit, R.drawable.ic_edit, this::onTrackEditActionSelected));
items.add(new MenuBottomSheetItem(R.string.export_file, R.drawable.ic_file_kmz, () -> onShareTrackSelected(track.getTrackId(), KmlFileType.Text)));
items.add(new MenuBottomSheetItem(R.string.export_file_gpx, R.drawable.ic_file_gpx, () -> onShareTrackSelected(track.getTrackId(), KmlFileType.Gpx)));
items.add(new MenuBottomSheetItem(R.string.delete, R.drawable.ic_delete, () -> onDeleteTrackSelected(track.getTrackId())));
return items;
}
private void onShareTrackSelected(long trackId, KmlFileType kmlFileType)
{
BookmarksSharingHelper.INSTANCE.prepareTrackForSharing(requireActivity(), trackId, kmlFileType);
}
@Override
public void onPreparedFileForSharing(@NonNull BookmarkSharingResult result)
{
BookmarksSharingHelper.INSTANCE.onPreparedFileForSharing(requireActivity(), shareLauncher, result);
}
private void handleActivityResult()
{
getBookmarkListAdapter().notifyDataSetChanged();
ActionBar actionBar = ((AppCompatActivity) requireActivity()).getSupportActionBar();
actionBar.setTitle(mCategoryDataSource.getData().getName());
}
@Override
public void onBookmarksLoadingFinished()
{
View view = getView();
if (view == null)
return;
super.onViewCreated(view, mSavedInstanceState);
onViewCreatedInternal(view);
updateRecyclerVisibility();
updateLoadingPlaceholder(view, false);
}
private void updateLoadingPlaceholder(@NonNull View root, boolean isShowLoadingPlaceholder)
{
View loadingPlaceholder = root.findViewById(R.id.placeholder_loading);
UiUtils.showIf(!isShowLoadingPlaceholder, root, R.id.show_on_map_fab);
UiUtils.showIf(isShowLoadingPlaceholder, loadingPlaceholder);
}
@Override
@Nullable
public ArrayList<MenuBottomSheetItem> getMenuBottomSheetItems(String id)
{
if (id.equals(BOOKMARKS_MENU_ID))
return getBookmarkMenuItems();
if (id.equals(TRACK_MENU_ID))
{
final Track track = (Track) getBookmarkListAdapter().getItem(mSelectedPosition);
return getTrackMenuItems(track);
}
if (id.equals(OPTIONS_MENU_ID))
return getOptionsMenuItems();
return null;
}
}

View File

@@ -0,0 +1,97 @@
package app.organicmaps.bookmarks;
import android.app.Activity;
import android.app.ProgressDialog;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import app.organicmaps.R;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.bookmarks.data.BookmarkSharingResult;
import app.organicmaps.bookmarks.data.KmlFileType;
import app.organicmaps.util.SharingUtils;
import app.organicmaps.util.log.Logger;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.ArrayList;
import java.util.List;
public enum BookmarksSharingHelper
{
INSTANCE;
private static final String TAG = BookmarksSharingHelper.class.getSimpleName();
@Nullable
private ProgressDialog mProgressDialog;
public void prepareBookmarkCategoryForSharing(@NonNull Activity context, long catId, KmlFileType kmlFileType)
{
showProgressDialog(context);
BookmarkManager.INSTANCE.prepareCategoriesForSharing(new long[]{catId}, kmlFileType);
}
public void prepareTrackForSharing(@NonNull Activity context, long trackId, KmlFileType kmlFileType)
{
showProgressDialog(context);
BookmarkManager.INSTANCE.prepareTrackForSharing(trackId, kmlFileType);
}
private void showProgressDialog(@NonNull Activity context)
{
mProgressDialog = new ProgressDialog(context, R.style.MwmTheme_ProgressDialog);
mProgressDialog.setMessage(context.getString(R.string.please_wait));
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
mProgressDialog.setIndeterminate(true);
mProgressDialog.setCancelable(false);
mProgressDialog.show();
}
public void onPreparedFileForSharing(@NonNull FragmentActivity context,
@NonNull ActivityResultLauncher<SharingUtils.SharingIntent> launcher,
@NonNull BookmarkSharingResult result)
{
if (mProgressDialog != null && mProgressDialog.isShowing())
mProgressDialog.dismiss();
switch (result.getCode())
{
case BookmarkSharingResult.SUCCESS ->
SharingUtils.shareBookmarkFile(context, launcher, result.getSharingPath(), result.getMimeType());
case BookmarkSharingResult.EMPTY_CATEGORY ->
new MaterialAlertDialogBuilder(context, R.style.MwmTheme_AlertDialog)
.setTitle(R.string.bookmarks_error_title_share_empty)
.setMessage(R.string.bookmarks_error_message_share_empty)
.setPositiveButton(R.string.ok, null)
.show();
case BookmarkSharingResult.ARCHIVE_ERROR, BookmarkSharingResult.FILE_ERROR ->
{
new MaterialAlertDialogBuilder(context, R.style.MwmTheme_AlertDialog)
.setTitle(R.string.dialog_routing_system_error)
.setMessage(R.string.bookmarks_error_message_share_general)
.setPositiveButton(R.string.ok, null)
.show();
List<String> names = new ArrayList<>();
for (long categoryId : result.getCategoriesIds())
names.add(BookmarkManager.INSTANCE.getCategoryById(categoryId).getName());
Logger.e(TAG, "Failed to share bookmark categories " + names + ", error code: " + result.getCode());
}
default -> throw new AssertionError("Unsupported bookmark sharing code: " + result.getCode());
}
}
public void prepareBookmarkCategoriesForSharing(@NonNull Activity context)
{
showProgressDialog(context);
List<BookmarkCategory> categories = BookmarkManager.INSTANCE.getCategories();
long[] categoryIds = new long[categories.size()];
for (int i = 0; i < categories.size(); i++)
categoryIds[i] = categories.get(i).getId();
BookmarkManager.INSTANCE.prepareCategoriesForSharing(categoryIds, KmlFileType.Text);
}
}

View File

@@ -0,0 +1,49 @@
package app.organicmaps.bookmarks;
import android.app.Activity;
import android.view.View;
import androidx.annotation.NonNull;
import app.organicmaps.widget.SearchToolbarController;
public class BookmarksToolbarController extends SearchToolbarController
{
@NonNull
private final BookmarksListFragment mFragment;
BookmarksToolbarController(@NonNull View root, @NonNull Activity activity,
@NonNull BookmarksListFragment fragment)
{
super(root, activity);
mFragment = fragment;
}
@Override
protected boolean alwaysShowClearButton()
{
return true;
}
@Override
protected void onClearClick()
{
super.onClearClick();
mFragment.deactivateSearch();
}
@Override
protected void onTextChanged(String query)
{
if (hasQuery())
mFragment.runSearch(getQuery());
else
mFragment.cancelSearch();
}
@Override
protected boolean showBackButton()
{
return false;
}
}

View File

@@ -0,0 +1,8 @@
package app.organicmaps.bookmarks;
interface CategoryListCallback
{
void onAddButtonClick();
void onImportButtonClick();
void onExportButtonClick();
}

View File

@@ -0,0 +1,27 @@
package app.organicmaps.bookmarks;
import android.app.Activity;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.organicmaps.R;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.dialog.EditTextDialogFragment;
class CategoryValidator implements EditTextDialogFragment.Validator
{
@Nullable
@Override
public String validate(@NonNull Activity activity, @Nullable String text)
{
if (TextUtils.isEmpty(text))
return activity.getString(R.string.bookmarks_error_title_empty_list_name);
if (BookmarkManager.INSTANCE.isUsedCategoryName(text))
return activity.getString(R.string.bookmarks_error_title_list_name_already_taken);
return null;
}
}

View File

@@ -0,0 +1,114 @@
package app.organicmaps.bookmarks;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RadioButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import app.organicmaps.R;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import java.util.List;
public class ChooseBookmarkCategoryAdapter extends BaseBookmarkCategoryAdapter<ChooseBookmarkCategoryAdapter.SingleChoiceHolder>
{
public static final int VIEW_TYPE_CATEGORY = 0;
public static final int VIEW_TYPE_ADD_NEW = 1;
private int mCheckedPosition;
public interface CategoryListener
{
void onCategorySet(int categoryPosition);
void onCategoryCreate();
}
private CategoryListener mListener;
public ChooseBookmarkCategoryAdapter(Context context, int pos,
@NonNull List<BookmarkCategory> categories)
{
super(context, categories);
mCheckedPosition = pos;
}
public void setListener(CategoryListener listener)
{
mListener = listener;
}
@Override
public SingleChoiceHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
View view;
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
if (viewType == VIEW_TYPE_CATEGORY)
view = inflater.inflate(R.layout.item_bookmark_category_choose, parent, false);
else
view = inflater.inflate(R.layout.item_bookmark_category_create, parent, false);
final SingleChoiceHolder holder = new SingleChoiceHolder(view);
view.setOnClickListener(v -> {
if (mListener == null)
return;
if (holder.getItemViewType() == VIEW_TYPE_ADD_NEW)
mListener.onCategoryCreate();
else
mListener.onCategorySet(holder.getBindingAdapterPosition());
});
return holder;
}
@Override
public void onBindViewHolder(SingleChoiceHolder holder, int position)
{
if (holder.getItemViewType() == VIEW_TYPE_CATEGORY)
{
BookmarkCategory category = getCategoryByPosition(position);
holder.name.setText(category.getName());
holder.checked.setChecked(mCheckedPosition == position);
}
}
@Override
public int getItemViewType(int position)
{
return position == getItemCount() - 1 ? VIEW_TYPE_ADD_NEW : VIEW_TYPE_CATEGORY;
}
@Override
public int getItemCount()
{
return super.getItemCount() + 1;
}
public void chooseItem(int position)
{
final int oldPosition = mCheckedPosition;
mCheckedPosition = position;
notifyItemChanged(oldPosition);
notifyItemChanged(mCheckedPosition);
}
static class SingleChoiceHolder extends RecyclerView.ViewHolder
{
TextView name;
RadioButton checked;
public SingleChoiceHolder(View convertView)
{
super(convertView);
name = convertView.findViewById(R.id.tv__set_name);
checked = convertView.findViewById(R.id.rb__selected);
}
}
}

View File

@@ -0,0 +1,138 @@
package app.organicmaps.bookmarks;
import android.app.Activity;
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 androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import app.organicmaps.R;
import app.organicmaps.base.BaseMwmDialogFragment;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.dialog.EditTextDialogFragment;
import java.util.List;
public class ChooseBookmarkCategoryFragment extends BaseMwmDialogFragment
implements ChooseBookmarkCategoryAdapter.CategoryListener
{
public static final String CATEGORY_POSITION = "ExtraCategoryPosition";
private ChooseBookmarkCategoryAdapter mAdapter;
private RecyclerView mRecycler;
public interface Listener
{
void onCategoryChanged(@NonNull BookmarkCategory newCategory);
}
private Listener mListener;
@Override
protected int getStyle()
{
return STYLE_NO_TITLE;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View root = inflater.inflate(R.layout.choose_bookmark_category_fragment, container, false);
mRecycler = root.findViewById(R.id.recycler);
mRecycler.setLayoutManager(new LinearLayoutManager(requireActivity()));
return root;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState)
{
super.onViewCreated(view, savedInstanceState);
final Bundle args = getArguments();
final int catPosition = args.getInt(CATEGORY_POSITION, 0);
List<BookmarkCategory> items = BookmarkManager.INSTANCE.getCategories();
mAdapter = new ChooseBookmarkCategoryAdapter(requireActivity(), catPosition, items);
mAdapter.setListener(this);
mRecycler.setAdapter(mAdapter);
}
@Override
public void onAttach(Activity activity)
{
if (mListener == null)
{
final Fragment parent = getParentFragment();
if (parent instanceof Listener)
mListener = (Listener) parent;
else if (activity instanceof Listener)
mListener = (Listener) activity;
}
super.onAttach(activity);
}
private void createCategory(@NonNull String name)
{
BookmarkManager.INSTANCE.createCategory(name);
List<BookmarkCategory> bookmarkCategories = mAdapter.getBookmarkCategories();
if (bookmarkCategories.isEmpty())
throw new AssertionError("BookmarkCategories are empty");
int categoryPosition = -1;
for (int i = 0; i < bookmarkCategories.size(); i++)
{
if (bookmarkCategories.get(i).getName().equals(name))
{
categoryPosition = i;
break;
}
}
if (categoryPosition == -1)
throw new AssertionError("No selected category in the list");
mAdapter.chooseItem(categoryPosition);
if (mListener != null)
{
BookmarkCategory newCategory = bookmarkCategories.get(categoryPosition);
mListener.onCategoryChanged(newCategory);
}
dismiss();
}
@Override
public void onCategorySet(int categoryPosition)
{
mAdapter.chooseItem(categoryPosition);
if (mListener != null)
{
final BookmarkCategory category = mAdapter.getBookmarkCategories().get(categoryPosition);
mListener.onCategoryChanged(category);
}
dismiss();
}
@Override
public void onCategoryCreate()
{
EditTextDialogFragment dialogFragment =
EditTextDialogFragment.show(getString(R.string.bookmark_set_name),
null,
getString(R.string.ok),
null,
this,
new CategoryValidator());
dialogFragment.setTextSaveListener(this::createCategory);
}
}

View File

@@ -0,0 +1,159 @@
package app.organicmaps.bookmarks;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RadioGroup;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import app.organicmaps.R;
import app.organicmaps.base.BaseMwmDialogFragment;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.util.UiUtils;
public class ChooseBookmarksSortingTypeFragment extends BaseMwmDialogFragment
implements RadioGroup.OnCheckedChangeListener
{
private static final String EXTRA_SORTING_TYPES = "sorting_types";
private static final String EXTRA_CURRENT_SORT_TYPE = "current_sort_type";
@Nullable
private ChooseSortingTypeListener mListener;
public interface ChooseSortingTypeListener
{
void onResetSorting();
void onSort(@BookmarkManager.SortingType int sortingType);
}
public static void chooseSortingType(@NonNull @BookmarkManager.SortingType int[] availableTypes,
int currentType, @NonNull Context context,
@NonNull FragmentManager manager)
{
Bundle args = new Bundle();
args.putIntArray(EXTRA_SORTING_TYPES, availableTypes);
args.putInt(EXTRA_CURRENT_SORT_TYPE, currentType);
String name = ChooseBookmarksSortingTypeFragment.class.getName();
final ChooseBookmarksSortingTypeFragment fragment = (ChooseBookmarksSortingTypeFragment) manager
.getFragmentFactory()
.instantiate(context.getClassLoader(), name);
fragment.setArguments(args);
fragment.show(manager, name);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.dialog_sorting_types, container, false);
}
@Override
protected int getStyle()
{
return STYLE_NO_TITLE;
}
@IdRes
private int getViewId(int sortingType)
{
if (sortingType >= 0)
{
switch (sortingType)
{
case BookmarkManager.SORT_BY_TYPE:
return R.id.sort_by_type;
case BookmarkManager.SORT_BY_DISTANCE:
return R.id.sort_by_distance;
case BookmarkManager.SORT_BY_TIME:
return R.id.sort_by_time;
case BookmarkManager.SORT_BY_NAME:
return R.id.sort_by_name;
}
}
return R.id.sort_by_default;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
{
super.onViewCreated(view, savedInstanceState);
final Bundle args = getArguments();
if (args == null)
throw new AssertionError("Arguments of choose sorting type view can't be null.");
UiUtils.hide(view, R.id.sort_by_type, R.id.sort_by_distance, R.id.sort_by_time, R.id.sort_by_name);
@BookmarkManager.SortingType
int[] availableSortingTypes = args.getIntArray(EXTRA_SORTING_TYPES);
if (availableSortingTypes == null)
throw new AssertionError("Available sorting types can't be null.");
for (@BookmarkManager.SortingType int type : availableSortingTypes)
UiUtils.show(view.findViewById(getViewId(type)));
int currentType = args.getInt(EXTRA_CURRENT_SORT_TYPE);
RadioGroup radioGroup = view.findViewById(R.id.sorting_types);
radioGroup.clearCheck();
radioGroup.check(getViewId(currentType));
radioGroup.setOnCheckedChangeListener(this);
}
@Override
public void onAttach(Context context)
{
super.onAttach(context);
onAttachInternal();
}
private void onAttachInternal()
{
mListener = (ChooseSortingTypeListener) (getParentFragment() == null ? getTargetFragment()
: getParentFragment());
}
@Override
public void onDetach()
{
super.onDetach();
mListener = null;
}
private void resetSorting()
{
if (mListener != null)
mListener.onResetSorting();
dismiss();
}
private void setSortingType(@BookmarkManager.SortingType int sortingType)
{
if (mListener != null)
mListener.onSort(sortingType);
dismiss();
}
@Override
public void onCheckedChanged(RadioGroup group, @IdRes int id)
{
if (id == R.id.sort_by_default)
resetSorting();
else if (id == R.id.sort_by_type)
setSortingType(BookmarkManager.SORT_BY_TYPE);
else if (id == R.id.sort_by_distance)
setSortingType(BookmarkManager.SORT_BY_DISTANCE);
else if (id == R.id.sort_by_time)
setSortingType(BookmarkManager.SORT_BY_TIME);
else if (id == R.id.sort_by_name)
setSortingType(BookmarkManager.SORT_BY_NAME);
}
}

View File

@@ -0,0 +1,6 @@
package app.organicmaps.bookmarks;
public interface DataChangedListener
{
void onChanged();
}

View File

@@ -0,0 +1,486 @@
package app.organicmaps.bookmarks;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.View;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.PluralsRes;
import androidx.recyclerview.widget.RecyclerView;
import app.organicmaps.R;
import app.organicmaps.adapter.OnItemClickListener;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.bookmarks.data.BookmarkInfo;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.bookmarks.data.IconClickListener;
import app.organicmaps.bookmarks.data.Track;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.util.Utils;
import app.organicmaps.widget.recycler.RecyclerClickListener;
import app.organicmaps.widget.recycler.RecyclerLongClickListener;
import app.organicmaps.util.Graphics;
import app.organicmaps.util.UiUtils;
public class Holders
{
public static class GeneralViewHolder extends RecyclerView.ViewHolder
{
@NonNull
private final TextView mText;
@NonNull
private final ImageView mImage;
GeneralViewHolder(@NonNull View itemView)
{
super(itemView);
mImage = itemView.findViewById(R.id.image);
mText = itemView.findViewById(R.id.text);
}
@NonNull
public TextView getText()
{
return mText;
}
@NonNull
public ImageView getImage()
{
return mImage;
}
}
public static class HeaderViewHolder extends RecyclerView.ViewHolder
{
@NonNull
private final TextView mButton;
@NonNull
private final TextView mText;
HeaderViewHolder(@NonNull View itemView)
{
super(itemView);
mButton = itemView.findViewById(R.id.button);
mText = itemView.findViewById(R.id.text_message);
}
@NonNull
public TextView getText()
{
return mText;
}
@NonNull
public TextView getButton()
{
return mButton;
}
void setAction(@NonNull HeaderAction action,
final boolean showAll)
{
mButton.setText(showAll
? R.string.bookmark_lists_show_all
: R.string.bookmark_lists_hide_all);
mButton.setOnClickListener(new ToggleShowAllClickListener(action, showAll));
}
void setAction(@NonNull HeaderActionChildCategories action,
final boolean showAll)
{
mButton.setText(showAll
? R.string.bookmark_lists_show_all
: R.string.bookmark_lists_hide_all);
mButton.setOnClickListener(new ToggleShowAllChildCategoryClickListener(
action, showAll));
}
public interface HeaderAction
{
void onHideAll();
void onShowAll();
}
public interface HeaderActionChildCategories
{
void onHideAll();
void onShowAll();
}
private static class ToggleShowAllChildCategoryClickListener implements View.OnClickListener
{
private final HeaderActionChildCategories mAction;
private final boolean mShowAll;
ToggleShowAllChildCategoryClickListener(@NonNull HeaderActionChildCategories action,
boolean showAll)
{
mAction = action;
mShowAll = showAll;
}
@Override
public void onClick(View view)
{
if (mShowAll)
mAction.onShowAll();
else
mAction.onHideAll();
}
}
private static class ToggleShowAllClickListener implements View.OnClickListener
{
private final HeaderAction mAction;
private final boolean mShowAll;
ToggleShowAllClickListener(@NonNull HeaderAction action, boolean showAll)
{
mAction = action;
mShowAll = showAll;
}
@Override
public void onClick(View view)
{
if (mShowAll)
mAction.onShowAll();
else
mAction.onHideAll();
}
}
}
static class CategoryViewHolderBase extends RecyclerView.ViewHolder
{
@Nullable
protected BookmarkCategory mEntity;
@NonNull
protected final TextView mSize;
public CategoryViewHolderBase(@NonNull View root)
{
super(root);
mSize = root.findViewById(R.id.size);
}
protected void setSize()
{
if (mEntity == null)
return;
mSize.setText(getSizeString());
}
private String getSizeString()
{
final Resources resources = mSize.getResources();
final int bookmarksCount = mEntity.getBookmarksCount();
final int tracksCount = mEntity.getTracksCount();
if ((bookmarksCount == 0 && tracksCount == 0) || (bookmarksCount > 0 && tracksCount > 0))
{
final String bookmarks = getQuantified(resources, R.plurals.bookmarks_places, bookmarksCount);
final String tracks = getQuantified(resources, R.plurals.tracks, tracksCount);
final String template = resources.getString(R.string.comma_separated_pair);
return String.format(template, bookmarks, tracks);
}
if (bookmarksCount > 0)
return getQuantified(resources, R.plurals.bookmarks_places, bookmarksCount);
return getQuantified(resources, R.plurals.tracks, tracksCount);
}
void setEntity(@NonNull BookmarkCategory entity)
{
mEntity = entity;
}
@NonNull
public BookmarkCategory getEntity()
{
if (mEntity == null)
throw new AssertionError("BookmarkCategory is null");
return mEntity;
}
private String getQuantified(Resources resources, @PluralsRes int plural, int size)
{
return resources.getQuantityString(plural, size, size);
}
}
static class CollectionViewHolder extends CategoryViewHolderBase
{
@NonNull
private final View mView;
@NonNull
private final TextView mName;
@NonNull
private final CheckBox mVisibilityMarker;
CollectionViewHolder(@NonNull View root)
{
super(root);
mView = root;
mName = root.findViewById(R.id.name);
mVisibilityMarker = root.findViewById(R.id.checkbox);
}
void setOnClickListener(@Nullable OnItemClickListener<BookmarkCategory> listener)
{
mView.setOnClickListener(v -> {
if (listener != null && mEntity != null)
listener.onItemClick(v, mEntity);
});
}
void setVisibilityState(boolean visible)
{
mVisibilityMarker.setChecked(visible);
}
void setVisibilityListener(@Nullable View.OnClickListener listener)
{
mVisibilityMarker.setOnClickListener(listener);
}
void setName(@NonNull String name)
{
mName.setText(name);
}
}
static class CategoryViewHolder extends CategoryViewHolderBase
{
@NonNull
private final TextView mName;
@NonNull
CheckBox mVisibilityMarker;
@NonNull
ImageView mMoreButton;
CategoryViewHolder(@NonNull View root)
{
super(root);
mName = root.findViewById(R.id.name);
mVisibilityMarker = root.findViewById(R.id.checkbox);
mMoreButton = root.findViewById(R.id.more);
}
void setVisibilityState(boolean visible)
{
mVisibilityMarker.setChecked(visible);
}
void setVisibilityListener(@Nullable View.OnClickListener listener)
{
mVisibilityMarker.setOnClickListener(listener);
}
void setMoreButtonClickListener(@Nullable View.OnClickListener listener)
{
mMoreButton.setOnClickListener(listener);
}
void setName(@NonNull String name)
{
mName.setText(name);
}
}
static abstract class BaseBookmarkHolder extends RecyclerView.ViewHolder
{
@NonNull
private final View mView;
BaseBookmarkHolder(@NonNull View itemView)
{
super(itemView);
mView = itemView;
}
abstract void bind(@NonNull SectionPosition position,
@NonNull BookmarkListAdapter.SectionsDataSource sectionsDataSource);
void setOnClickListener(@Nullable RecyclerClickListener listener)
{
mView.setOnClickListener(v -> {
if (listener != null)
listener.onItemClick(v, getBindingAdapterPosition());
});
}
void setOnLongClickListener(@Nullable RecyclerLongClickListener listener)
{
mView.setOnLongClickListener(v -> {
if (listener != null)
listener.onLongItemClick(v, getBindingAdapterPosition());
return true;
});
}
}
static class BookmarkViewHolder extends BaseBookmarkHolder
{
@NonNull
private final ImageView mIcon;
@NonNull
private final TextView mName;
@NonNull
private final TextView mDistance;
BookmarkViewHolder(@NonNull View itemView)
{
super(itemView);
mIcon = itemView.findViewById(R.id.iv__bookmark_color);
mName = itemView.findViewById(R.id.tv__bookmark_name);
mDistance = itemView.findViewById(R.id.tv__bookmark_distance);
}
@Override
void bind(@NonNull SectionPosition position,
@NonNull BookmarkListAdapter.SectionsDataSource sectionsDataSource)
{
final long bookmarkId = sectionsDataSource.getBookmarkId(position);
BookmarkInfo bookmark = new BookmarkInfo(sectionsDataSource.getCategory().getId(),
bookmarkId);
mName.setText(bookmark.getName());
final Location loc = LocationHelper.from(mIcon.getContext()).getSavedLocation();
String distanceValue = loc == null ? "" : bookmark.getDistance(loc.getLatitude(),
loc.getLongitude(), 0.0).toString(mDistance.getContext());
String separator = "";
if (!distanceValue.isEmpty() && !bookmark.getFeatureType().isEmpty())
separator = "";
String subtitleValue = distanceValue.concat(separator).concat(bookmark.getFeatureType());
mDistance.setText(subtitleValue);
UiUtils.hideIf(TextUtils.isEmpty(subtitleValue), mDistance);
mIcon.setImageResource(bookmark.getIcon().getResId());
Drawable circle = Graphics.drawCircleAndImage(bookmark.getIcon().argb(),
R.dimen.track_circle_size,
bookmark.getIcon().getResId(),
R.dimen.bookmark_icon_size,
mIcon.getContext());
mIcon.setImageDrawable(circle);
}
}
static class TrackViewHolder extends BaseBookmarkHolder
{
@NonNull
private final ImageView mIcon;
@NonNull
private final TextView mName;
@NonNull
private final TextView mDistance;
private final ImageView mMoreButton;
TrackViewHolder(@NonNull View itemView)
{
super(itemView);
mIcon = itemView.findViewById(R.id.iv__bookmark_color);
mName = itemView.findViewById(R.id.tv__bookmark_name);
mDistance = itemView.findViewById(R.id.tv__bookmark_distance);
mMoreButton = itemView.findViewById(R.id.more);
}
@Override
void bind(@NonNull SectionPosition position,
@NonNull BookmarkListAdapter.SectionsDataSource sectionsDataSource)
{
final long trackId = sectionsDataSource.getTrackId(position);
Track track = BookmarkManager.INSTANCE.getTrack(trackId);
mName.setText(track.getName());
mDistance.setText(new StringBuilder().append(mDistance.getContext()
.getString(R.string.length))
.append(" ")
.append(track.getLength().toString(mDistance.getContext()))
.toString());
Drawable circle = Graphics.drawCircle(track.getColor(), R.dimen.track_circle_size,
mIcon.getContext().getResources());
mIcon.setImageDrawable(circle);
}
public void setMoreButtonClickListener(RecyclerClickListener listener)
{
mMoreButton.setOnClickListener(v -> listener.onItemClick(v, getBindingAdapterPosition()));
}
public void setTrackIconClickListener(IconClickListener listener)
{
mIcon.setOnClickListener(v -> listener.onItemClick((ImageView) v, getBindingAdapterPosition()));
}
}
public static class SectionViewHolder extends BaseBookmarkHolder
{
@NonNull
private final TextView mView;
SectionViewHolder(@NonNull TextView itemView)
{
super(itemView);
mView = itemView;
}
@Override
void bind(@NonNull SectionPosition position,
@NonNull BookmarkListAdapter.SectionsDataSource sectionsDataSource)
{
mView.setText(sectionsDataSource.getTitle(position.getSectionIndex(), mView.getResources()));
}
}
static class DescriptionViewHolder extends BaseBookmarkHolder
{
static final float SPACING_MULTIPLE = 1.0f;
static final float SPACING_ADD = 0.0f;
@NonNull
private final TextView mTitle;
@NonNull
private final TextView mDescText;
DescriptionViewHolder(@NonNull View itemView, @NonNull BookmarkCategory category)
{
super(itemView);
mDescText = itemView.findViewById(R.id.text);
mTitle = itemView.findViewById(R.id.title);
}
@Override
void bind(@NonNull SectionPosition position,
@NonNull BookmarkListAdapter.SectionsDataSource sectionsDataSource)
{
mTitle.setText(sectionsDataSource.getCategory().getName());
bindDescription(sectionsDataSource.getCategory());
}
private void bindDescription(@NonNull BookmarkCategory category)
{
String desc = TextUtils.isEmpty(category.getAnnotation())
? category.getDescription()
: category.getAnnotation();
Spanned spannedDesc = Utils.fromHtml(desc);
mDescText.setText(spannedDesc);
UiUtils.showIf(!TextUtils.isEmpty(spannedDesc), mDescText);
}
}
}

View File

@@ -0,0 +1,77 @@
package app.organicmaps.bookmarks;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import app.organicmaps.R;
import app.organicmaps.bookmarks.data.Icon;
import app.organicmaps.util.Graphics;
import java.util.List;
public class IconsAdapter extends ArrayAdapter<Icon>
{
private int mCheckedIconColor;
public IconsAdapter(Context context, List<Icon> list)
{
super(context, 0, 0, list);
}
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
SpinnerViewHolder holder;
if (convertView == null)
{
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.color_row, parent, false);
holder = new SpinnerViewHolder(convertView);
convertView.setTag(holder);
}
else
holder = (SpinnerViewHolder) convertView.getTag();
final Icon icon = getItem(position);
Drawable circle;
if (icon.getColor() == mCheckedIconColor)
{
circle = Graphics.drawCircleAndImage(getItem(position).argb(),
R.dimen.track_circle_size,
R.drawable.ic_bookmark_none,
R.dimen.bookmark_icon_size,
getContext());
}
else
{
circle = Graphics.drawCircle(getItem(position).argb(),
R.dimen.select_color_circle_size,
getContext().getResources());
}
holder.icon.setImageDrawable(circle);
return convertView;
}
private static class SpinnerViewHolder
{
final ImageView icon;
SpinnerViewHolder(View convertView)
{
icon = convertView.findViewById(R.id.iv__color);
}
}
public void chooseItem(int position)
{
mCheckedIconColor = position;
notifyDataSetChanged();
}
}

View File

@@ -0,0 +1,10 @@
package app.organicmaps.bookmarks;
import android.view.View;
import androidx.annotation.NonNull;
public interface OnItemLongClickListener<T>
{
void onItemLongClick(@NonNull View v, @NonNull T item);
}

View File

@@ -0,0 +1,10 @@
package app.organicmaps.bookmarks;
import android.view.View;
import androidx.annotation.NonNull;
public interface OnItemMoreClickListener<T>
{
void onItemMoreClick(@NonNull View v, @NonNull T item);
}

View File

@@ -0,0 +1,67 @@
package app.organicmaps.bookmarks;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import androidx.core.os.ParcelCompat;
import app.organicmaps.bookmarks.data.Error;
import app.organicmaps.bookmarks.data.Result;
public class OperationStatus implements Parcelable
{
@Nullable
private final Result mResult;
@Nullable
private final Error mError;
OperationStatus(@Nullable Result result, @Nullable Error error)
{
mResult = result;
mError = error;
}
private OperationStatus(Parcel in)
{
mResult = ParcelCompat.readParcelable(in, Result.class.getClassLoader(), Result.class);
mError = ParcelCompat.readParcelable(in, Error.class.getClassLoader(), Error.class);
}
public static final Creator<OperationStatus> CREATOR = new Creator<>()
{
@Override
public OperationStatus createFromParcel(Parcel in)
{
return new OperationStatus(in);
}
@Override
public OperationStatus[] newArray(int size)
{
return new OperationStatus[size];
}
};
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeParcelable(mResult, flags);
dest.writeParcelable(mError, flags);
}
@Override
public String toString()
{
return "OperationStatus{" +
"mResult=" + mResult +
", mError=" + mError +
'}';
}
}

View File

@@ -0,0 +1,35 @@
package app.organicmaps.bookmarks;
public class SectionPosition
{
static final int INVALID_POSITION = -1;
private final int mSectionIndex;
private final int mItemIndex;
SectionPosition(int sectionInd, int itemInd)
{
mSectionIndex = sectionInd;
mItemIndex = itemInd;
}
int getSectionIndex()
{
return mSectionIndex;
}
int getItemIndex()
{
return mItemIndex;
}
boolean isTitlePosition()
{
return mSectionIndex != INVALID_POSITION && mItemIndex == INVALID_POSITION;
}
boolean isItemPosition()
{
return mSectionIndex != INVALID_POSITION && mItemIndex != INVALID_POSITION;
}
}

View File

@@ -0,0 +1,151 @@
package app.organicmaps.bookmarks.data;
import android.annotation.SuppressLint;
import android.os.Parcel;
import androidx.annotation.IntRange;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.ParcelCompat;
import app.organicmaps.Framework;
import app.organicmaps.routing.RoutePointInfo;
import app.organicmaps.sdk.search.Popularity;
import app.organicmaps.util.Constants;
// TODO consider refactoring to remove hack with MapObject unmarshalling itself and Bookmark at the same time.
// Used by JNI.
@Keep
@SuppressWarnings("unused")
@SuppressLint("ParcelCreator")
public class Bookmark extends MapObject
{
private Icon mIcon;
private long mCategoryId;
private final long mBookmarkId;
private final double mMerX;
private final double mMerY;
public Bookmark(@NonNull FeatureId featureId, @IntRange(from = 0) long categoryId,
@IntRange(from = 0) long bookmarkId, String title, @Nullable String secondaryTitle,
@Nullable String subtitle, @Nullable String address, @Nullable RoutePointInfo routePointInfo,
@OpeningMode int openingMode, @NonNull Popularity popularity, @NonNull String description,
@Nullable String[] rawTypes)
{
super(featureId, BOOKMARK, title, secondaryTitle, subtitle, address, 0, 0, "",
routePointInfo, openingMode, popularity, description, RoadWarningMarkType.UNKNOWN.ordinal(), rawTypes);
mCategoryId = categoryId;
mBookmarkId = bookmarkId;
mIcon = getIconInternal();
final ParcelablePointD ll = BookmarkManager.INSTANCE.getBookmarkXY(mBookmarkId);
mMerX = ll.x;
mMerY = ll.y;
initXY();
}
private void initXY()
{
setLat(Math.toDegrees(2.0 * Math.atan(Math.exp(Math.toRadians(mMerY))) - Math.PI / 2.0));
setLon(mMerX);
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
super.writeToParcel(dest, flags);
dest.writeLong(mCategoryId);
dest.writeLong(mBookmarkId);
dest.writeParcelable(mIcon, flags);
dest.writeDouble(mMerX);
dest.writeDouble(mMerY);
}
// Do not use Core while restoring from Parcel! In some cases this constructor is called before
// the App is completely initialized.
// TODO: Method restoreHasCurrentPermission causes this strange behaviour, needs to be investigated.
protected Bookmark(@MapObjectType int type, Parcel source)
{
super(type, source);
mCategoryId = source.readLong();
mBookmarkId = source.readLong();
mIcon = ParcelCompat.readParcelable(source, Icon.class.getClassLoader(), Icon.class);
mMerX = source.readDouble();
mMerY = source.readDouble();
initXY();
}
@Override
public double getScale()
{
return BookmarkManager.INSTANCE.getBookmarkScale(mBookmarkId);
}
public DistanceAndAzimut getDistanceAndAzimuth(double cLat, double cLon, double north)
{
return Framework.nativeGetDistanceAndAzimuth(mMerX, mMerY, cLat, cLon, north);
}
private Icon getIconInternal()
{
return new Icon(BookmarkManager.INSTANCE.getBookmarkColor(mBookmarkId),
BookmarkManager.INSTANCE.getBookmarkIcon(mBookmarkId));
}
@Nullable
public Icon getIcon()
{
return mIcon;
}
public String getCategoryName()
{
return BookmarkManager.INSTANCE.getCategoryById(mCategoryId).getName();
}
public void setCategoryId(@IntRange(from = 0) long catId)
{
BookmarkManager.INSTANCE.notifyCategoryChanging(this, catId);
mCategoryId = catId;
}
public void setParams(@NonNull String title, @Nullable Icon icon, @NonNull String description)
{
BookmarkManager.INSTANCE.notifyParametersUpdating(this, title, icon, description);
if (icon != null)
mIcon = icon;
setTitle(title);
setDescription(description);
}
public long getCategoryId()
{
return mCategoryId;
}
public long getBookmarkId()
{
return mBookmarkId;
}
@NonNull
public String getBookmarkDescription()
{
return BookmarkManager.INSTANCE.getBookmarkDescription(mBookmarkId);
}
@NonNull
public String getGe0Url(boolean addName)
{
return BookmarkManager.INSTANCE.encode2Ge0Url(mBookmarkId, addName);
}
@NonNull
public String getHttpGe0Url(boolean addName)
{
return getGe0Url(addName).replaceFirst(Constants.Url.SHORT_SHARE_PREFIX, Constants.Url.HTTP_SHARE_PREFIX);
}
}

View File

@@ -0,0 +1,16 @@
package app.organicmaps.bookmarks.data;
import androidx.annotation.NonNull;
import java.util.List;
public interface BookmarkCategoriesDataProvider
{
@NonNull
List<BookmarkCategory> getCategories();
int getCategoriesCount();
@NonNull
List<BookmarkCategory> getChildrenCategories(long parentId);
@NonNull
BookmarkCategory getCategoryById(long categoryId);
}

View File

@@ -0,0 +1,193 @@
package app.organicmaps.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.DrawableRes;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import app.organicmaps.R;
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class BookmarkCategory implements Parcelable
{
private final long mId;
@NonNull
private final String mName;
@NonNull
private final String mAnnotation;
@NonNull
private final String mDescription;
private final int mTracksCount;
private final int mBookmarksCount;
private boolean mIsVisible;
public BookmarkCategory(long id, @NonNull String name, @NonNull String annotation,
@NonNull String description, int tracksCount, int bookmarksCount,
boolean isVisible)
{
mId = id;
mName = name;
mAnnotation = annotation;
mDescription = description;
mTracksCount = tracksCount;
mBookmarksCount = bookmarksCount;
mIsVisible = isVisible;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BookmarkCategory that = (BookmarkCategory) o;
return mId == that.mId;
}
@Override
public int hashCode()
{
return (int)(mId ^ (mId >>> 32));
}
public long getId()
{
return mId;
}
@NonNull
public String getName()
{
return mName;
}
public int getTracksCount()
{
return mTracksCount;
}
public int getBookmarksCount()
{
return mBookmarksCount;
}
public boolean isVisible()
{
return mIsVisible;
}
public void setVisible(boolean isVisible)
{
mIsVisible = isVisible;
}
public int size()
{
return getBookmarksCount() + getTracksCount();
}
@NonNull
public String getAnnotation()
{
return mAnnotation;
}
@NonNull
public String getDescription()
{
return mDescription;
}
@Override
public String toString()
{
final StringBuilder sb = new StringBuilder("BookmarkCategory{");
sb.append("mId=").append(mId);
sb.append(", mName='").append(mName).append('\'');
sb.append(", mAnnotation='").append(mAnnotation).append('\'');
sb.append(", mDescription='").append(mDescription).append('\'');
sb.append(", mTracksCount=").append(mTracksCount);
sb.append(", mBookmarksCount=").append(mBookmarksCount);
sb.append(", mIsVisible=").append(mIsVisible);
sb.append('}');
return sb.toString();
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeLong(this.mId);
dest.writeString(this.mName);
dest.writeString(this.mAnnotation);
dest.writeString(this.mDescription);
dest.writeInt(this.mTracksCount);
dest.writeInt(this.mBookmarksCount);
dest.writeByte(this.mIsVisible ? (byte) 1 : (byte) 0);
}
protected BookmarkCategory(Parcel in)
{
this.mId = in.readLong();
this.mName = in.readString();
this.mAnnotation = in.readString();
this.mDescription = in.readString();
this.mTracksCount = in.readInt();
this.mBookmarksCount = in.readInt();
this.mIsVisible = in.readByte() != 0;
}
public static final Creator<BookmarkCategory> CREATOR = new Creator<>()
{
@Override
public BookmarkCategory createFromParcel(Parcel source)
{
return new BookmarkCategory(source);
}
@Override
public BookmarkCategory[] newArray(int size)
{
return new BookmarkCategory[size];
}
};
public enum AccessRules
{
ACCESS_RULES_LOCAL(R.string.not_shared, R.drawable.ic_lock),
ACCESS_RULES_PUBLIC(R.string.public_access, R.drawable.ic_public_inline),
ACCESS_RULES_DIRECT_LINK(R.string.limited_access, R.drawable.ic_link_inline),
ACCESS_RULES_AUTHOR_ONLY(R.string.access_rules_author_only, R.drawable.ic_lock);
private final int mResId;
private final int mDrawableResId;
AccessRules(int resId, int drawableResId)
{
mResId = resId;
mDrawableResId = drawableResId;
}
@DrawableRes
public int getDrawableResId()
{
return mDrawableResId;
}
@StringRes
public int getNameResId()
{
return mResId;
}
}
}

View File

@@ -0,0 +1,104 @@
package app.organicmaps.bookmarks.data;
import androidx.annotation.IntRange;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import app.organicmaps.Framework;
import app.organicmaps.util.Distance;
import app.organicmaps.util.GeoUtils;
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public class BookmarkInfo
{
private final long mCategoryId;
private final long mBookmarkId;
@NonNull
private final String mTitle;
@NonNull
private final String mFeatureType;
@NonNull
private final Icon mIcon;
private final double mMerX;
private final double mMerY;
private final double mScale;
@NonNull
private final String mAddress;
@NonNull
private final ParcelablePointD mLatLonPoint;
public BookmarkInfo(@IntRange(from = 0) long categoryId, @IntRange(from = 0) long bookmarkId)
{
mCategoryId = categoryId;
mBookmarkId = bookmarkId;
mTitle = BookmarkManager.INSTANCE.getBookmarkName(mBookmarkId);
mFeatureType = BookmarkManager.INSTANCE.getBookmarkFeatureType(mBookmarkId);
mIcon = new Icon(BookmarkManager.INSTANCE.getBookmarkColor(mBookmarkId),
BookmarkManager.INSTANCE.getBookmarkIcon(mBookmarkId));
final ParcelablePointD ll = BookmarkManager.INSTANCE.getBookmarkXY(mBookmarkId);
mMerX = ll.x;
mMerY = ll.y;
mScale = BookmarkManager.INSTANCE.getBookmarkScale(mBookmarkId);
mAddress = BookmarkManager.INSTANCE.getBookmarkAddress(mBookmarkId);
mLatLonPoint = GeoUtils.toLatLon(mMerX, mMerY);
}
public long getCategoryId()
{
return mCategoryId;
}
public long getBookmarkId()
{
return mBookmarkId;
}
public DistanceAndAzimut getDistanceAndAzimuth(double cLat, double cLon, double north)
{
return Framework.nativeGetDistanceAndAzimuth(mMerX, mMerY, cLat, cLon, north);
}
@NonNull
public String getFeatureType() { return mFeatureType; }
@NonNull
public String getName()
{
return mTitle;
}
@NonNull
public Icon getIcon()
{
return mIcon;
}
@NonNull
public Distance getDistance(double latitude, double longitude, double v)
{
return getDistanceAndAzimuth(latitude, longitude, v).getDistance();
}
public double getLat()
{
return mLatLonPoint.x;
}
public double getLon()
{
return mLatLonPoint.y;
}
public double getScale()
{
return mScale;
}
@NonNull
public String getAddress()
{
return mAddress;
}
}

View File

@@ -0,0 +1,987 @@
package app.organicmaps.bookmarks.data;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.Keep;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import app.organicmaps.Framework;
import app.organicmaps.bookmarks.DataChangedListener;
import app.organicmaps.util.KeyValue;
import app.organicmaps.util.StorageUtils;
import app.organicmaps.util.concurrency.UiThread;
import app.organicmaps.util.log.Logger;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@MainThread
public enum BookmarkManager
{
INSTANCE;
@Retention(RetentionPolicy.SOURCE)
@IntDef({ SORT_BY_TYPE, SORT_BY_DISTANCE, SORT_BY_TIME, SORT_BY_NAME })
public @interface SortingType {}
public static final int SORT_BY_TYPE = 0;
public static final int SORT_BY_DISTANCE = 1;
public static final int SORT_BY_TIME = 2;
public static final int SORT_BY_NAME = 3;
// These values have to match the values of kml::CompilationType from kml/types.hpp
public static final int CATEGORY = 0;
public static final List<Icon> ICONS = new ArrayList<>();
private static final String[] BOOKMARKS_EXTENSIONS = Framework.nativeGetBookmarksFilesExts();
private static final String TAG = BookmarkManager.class.getSimpleName();
@NonNull
private final BookmarkCategoriesDataProvider mCategoriesCoreDataProvider
= new CoreBookmarkCategoriesDataProvider();
@NonNull
private BookmarkCategoriesDataProvider mCurrentDataProvider = mCategoriesCoreDataProvider;
private final BookmarkCategoriesCache mBookmarkCategoriesCache = new BookmarkCategoriesCache();
@NonNull
private final List<BookmarksLoadingListener> mListeners = new ArrayList<>();
@NonNull
private final List<BookmarksSortingListener> mSortingListeners = new ArrayList<>();
@NonNull
private final List<BookmarksSharingListener> mSharingListeners = new ArrayList<>();
@Nullable
private OnElevationCurrentPositionChangedListener mOnElevationCurrentPositionChangedListener;
@Nullable
private OnElevationActivePointChangedListener mOnElevationActivePointChangedListener;
static
{
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_RED, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_PINK, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_PURPLE, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_DEEPPURPLE, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_BLUE, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_LIGHTBLUE, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_CYAN, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_TEAL, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_GREEN, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_LIME, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_YELLOW, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_ORANGE, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_DEEPORANGE, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_BROWN, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_GRAY, Icon.BOOKMARK_ICON_TYPE_NONE));
ICONS.add(new Icon(Icon.PREDEFINED_COLOR_BLUEGRAY, Icon.BOOKMARK_ICON_TYPE_NONE));
}
public void toggleCategoryVisibility(@NonNull BookmarkCategory category)
{
boolean isVisible = isVisible(category.getId());
setVisibility(category.getId(), !isVisible);
}
@Nullable
public Bookmark addNewBookmark(double lat, double lon)
{
return nativeAddBookmarkToLastEditedCategory(lat, lon);
}
public void addLoadingListener(@NonNull BookmarksLoadingListener listener)
{
mListeners.add(listener);
}
public void removeLoadingListener(@NonNull BookmarksLoadingListener listener)
{
mListeners.remove(listener);
}
public void addSortingListener(@NonNull BookmarksSortingListener listener)
{
mSortingListeners.add(listener);
}
public void removeSortingListener(@NonNull BookmarksSortingListener listener)
{
mSortingListeners.remove(listener);
}
public void addSharingListener(@NonNull BookmarksSharingListener listener)
{
mSharingListeners.add(listener);
}
public void removeSharingListener(@NonNull BookmarksSharingListener listener)
{
mSharingListeners.remove(listener);
}
public void setElevationActivePointChangedListener(
@Nullable OnElevationActivePointChangedListener listener)
{
if (listener != null)
nativeSetElevationActiveChangedListener();
else
nativeRemoveElevationActiveChangedListener();
mOnElevationActivePointChangedListener = listener;
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onBookmarksChanged()
{
updateCache();
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onBookmarksLoadingStarted()
{
for (BookmarksLoadingListener listener : mListeners)
listener.onBookmarksLoadingStarted();
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onBookmarksLoadingFinished()
{
updateCache();
mCurrentDataProvider = new CacheBookmarkCategoriesDataProvider();
for (BookmarksLoadingListener listener : mListeners)
listener.onBookmarksLoadingFinished();
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onBookmarksSortingCompleted(@NonNull SortedBlock[] sortedBlocks, long timestamp)
{
for (BookmarksSortingListener listener : mSortingListeners)
listener.onBookmarksSortingCompleted(sortedBlocks, timestamp);
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onBookmarksSortingCancelled(long timestamp)
{
for (BookmarksSortingListener listener : mSortingListeners)
listener.onBookmarksSortingCancelled(timestamp);
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onBookmarksFileLoaded(boolean success, @NonNull String fileName,
boolean isTemporaryFile)
{
// Android could create temporary file with bookmarks in some cases (KML/KMZ file is a blob
// in the intent, so we have to create a temporary file on the disk). Here we can delete it.
if (isTemporaryFile)
{
File tmpFile = new File(fileName);
tmpFile.delete();
}
if (success)
{
for (BookmarksLoadingListener listener : mListeners)
listener.onBookmarksFileImportSuccessful();
}
else
{
for (BookmarksLoadingListener listener : mListeners)
listener.onBookmarksFileImportFailed();
}
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onPreparedFileForSharing(BookmarkSharingResult result)
{
for (BookmarksSharingListener listener : mSharingListeners)
listener.onPreparedFileForSharing(result);
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onElevationCurrentPositionChanged()
{
if (mOnElevationCurrentPositionChangedListener != null)
mOnElevationCurrentPositionChangedListener.onCurrentPositionChanged();
}
public void setElevationCurrentPositionChangedListener(@Nullable OnElevationCurrentPositionChangedListener listener)
{
if (listener != null)
nativeSetElevationCurrentPositionChangedListener();
else
nativeRemoveElevationCurrentPositionChangedListener();
mOnElevationCurrentPositionChangedListener = listener;
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onElevationActivePointChanged()
{
if (mOnElevationActivePointChangedListener != null)
mOnElevationActivePointChangedListener.onElevationActivePointChanged();
}
public boolean isVisible(long catId)
{
return nativeIsVisible(catId);
}
public void setVisibility(long catId, boolean visible)
{
nativeSetVisibility(catId, visible);
}
public void setCategoryName(long catId, @NonNull String name)
{
nativeSetCategoryName(catId, name);
}
public void setCategoryDescription(long id, @NonNull String categoryDesc)
{
nativeSetCategoryDescription(id, categoryDesc);
}
@Nullable
public Bookmark updateBookmarkPlacePage(long bmkId)
{
return nativeUpdateBookmarkPlacePage(bmkId);
}
@Nullable
public BookmarkInfo getBookmarkInfo(long bmkId)
{
return nativeGetBookmarkInfo(bmkId);
}
public long getBookmarkIdByPosition(long catId, int positionInCategory)
{
return nativeGetBookmarkIdByPosition(catId, positionInCategory);
}
@NonNull
public Track getTrack(long trackId)
{
return nativeGetTrack(trackId, Track.class);
}
public long getTrackIdByPosition(long catId, int positionInCategory)
{
return nativeGetTrackIdByPosition(catId, positionInCategory);
}
public static void loadBookmarks() { nativeLoadBookmarks(); }
public void deleteCategory(long catId) { nativeDeleteCategory(catId); }
public void deleteTrack(long trackId)
{
nativeDeleteTrack(trackId);
}
public void deleteBookmark(long bmkId)
{
nativeDeleteBookmark(bmkId);
}
public long createCategory(@NonNull String name) { return nativeCreateCategory(name); }
public void showBookmarkOnMap(long bmkId) { nativeShowBookmarkOnMap(bmkId); }
public void showBookmarkCategoryOnMap(long catId) { nativeShowBookmarkCategoryOnMap(catId); }
@Icon.PredefinedColor
public int getLastEditedColor() { return nativeGetLastEditedColor(); }
@MainThread
public void loadBookmarksFile(@NonNull String path, boolean isTemporaryFile)
{
Logger.d(TAG, "Loading bookmarks file from: " + path);
nativeLoadBookmarksFile(path, isTemporaryFile);
}
static @Nullable String getBookmarksFilenameFromUri(@NonNull ContentResolver resolver, @NonNull Uri uri)
{
String filename = null;
final String scheme = uri.getScheme();
if (scheme.equals("content"))
{
try (Cursor cursor = resolver.query(uri, null, null, null, null))
{
if (cursor != null && cursor.moveToFirst())
{
final int columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (columnIndex >= 0)
filename = cursor.getString(columnIndex);
}
}
}
if (filename == null)
{
filename = uri.getPath();
if (filename == null)
return null;
final int cut = filename.lastIndexOf('/');
if (cut != -1)
filename = filename.substring(cut + 1);
}
// See IsBadCharForPath()
filename = filename.replaceAll("[:/\\\\<>\"|?*]", "");
final String lowerCaseFilename = filename.toLowerCase(java.util.Locale.ROOT);
// Check that filename contains bookmarks extension.
for (String ext: BOOKMARKS_EXTENSIONS)
{
if (lowerCaseFilename.endsWith(ext))
return filename;
}
// Samsung browser adds .xml extension to downloaded gpx files.
// Duplicate files have " (1).xml", " (2).xml" suffixes added.
final String gpxExt = ".gpx";
final int gpxStart = lowerCaseFilename.lastIndexOf(gpxExt);
if (gpxStart != -1)
return filename.substring(0, gpxStart + gpxExt.length());
// Try get guess extension from the mime type.
final String mime = resolver.getType(uri);
if (mime != null)
{
final int i = mime.lastIndexOf('.');
if (i != -1)
{
final String type = mime.substring(i + 1);
if (type.equalsIgnoreCase("kmz"))
return filename + ".kmz";
else if (type.equalsIgnoreCase("kml+xml"))
return filename + ".kml";
}
if (mime.endsWith("gpx+xml") || mime.endsWith("gpx")) // match application/gpx, application/gpx+xml
return filename + ".gpx";
}
// WhatsApp doesn't provide correct mime type and extension for GPX files.
if (uri.getHost().contains("com.whatsapp.provider.media"))
return filename + ".gpx";
return null;
}
@WorkerThread
public boolean importBookmarksFile(@NonNull ContentResolver resolver, @NonNull Uri uri, @NonNull File tempDir)
{
Logger.w(TAG, "Importing bookmarks from " + uri);
try
{
String filename = getBookmarksFilenameFromUri(resolver, uri);
if (filename == null)
{
Logger.w(TAG, "Could not find a supported file type in " + uri);
UiThread.run(() -> {
for (BookmarksLoadingListener listener : mListeners)
listener.onBookmarksFileUnsupported(uri);
});
return false;
}
Logger.d(TAG, "Downloading bookmarks file from " + uri + " into " + filename);
final File tempFile = new File(tempDir, filename);
StorageUtils.copyFile(resolver, uri, tempFile);
Logger.d(TAG, "Downloaded bookmarks file from " + uri + " into " + filename);
UiThread.run(() -> loadBookmarksFile(tempFile.getAbsolutePath(), true));
return true;
}
catch (IOException|SecurityException e)
{
Logger.e(TAG, "Could not download bookmarks file from " + uri, e);
UiThread.run(() -> {
for (BookmarksLoadingListener listener : mListeners)
listener.onBookmarksFileDownloadFailed(uri, e.toString());
});
return false;
}
}
@WorkerThread
public void importBookmarksFiles(@NonNull ContentResolver resolver, @NonNull List<Uri> uris, @NonNull File tempDir)
{
for (Uri uri: uris)
importBookmarksFile(resolver, uri, tempDir);
}
public boolean isAsyncBookmarksLoadingInProgress()
{
return nativeIsAsyncBookmarksLoadingInProgress();
}
@NonNull
public List<BookmarkCategory> getCategories()
{
return mCurrentDataProvider.getCategories();
}
public int getCategoriesCount()
{
return mCurrentDataProvider.getCategoriesCount();
}
@NonNull
BookmarkCategoriesCache getBookmarkCategoriesCache()
{
return mBookmarkCategoriesCache;
}
private void updateCache()
{
getBookmarkCategoriesCache().update(mCategoriesCoreDataProvider.getCategories());
}
public void addCategoriesUpdatesListener(@NonNull DataChangedListener listener)
{
getBookmarkCategoriesCache().registerListener(listener);
}
public void removeCategoriesUpdatesListener(@NonNull DataChangedListener listener)
{
getBookmarkCategoriesCache().unregisterListener(listener);
}
@NonNull
public BookmarkCategory getCategoryById(long categoryId)
{
return mCurrentDataProvider.getCategoryById(categoryId);
}
public boolean isUsedCategoryName(@NonNull String name)
{
return nativeIsUsedCategoryName(name);
}
public void prepareForSearch(long catId) { nativePrepareForSearch(catId); }
public boolean areAllCategoriesVisible()
{
return nativeAreAllCategoriesVisible();
}
public boolean areAllCategoriesInvisible()
{
return nativeAreAllCategoriesInvisible();
}
public void setAllCategoriesVisibility(boolean visible)
{
nativeSetAllCategoriesVisibility(visible);
}
public void setChildCategoriesVisibility(long catId, boolean visible)
{
nativeSetChildCategoriesVisibility(catId, visible);
}
public void prepareCategoriesForSharing(long[] catIds, KmlFileType kmlFileType)
{
nativePrepareFileForSharing(catIds, kmlFileType.ordinal());
}
public void prepareTrackForSharing(long trackId, KmlFileType kmlFileType)
{
nativePrepareTrackFileForSharing(trackId, kmlFileType.ordinal());
}
public void setNotificationsEnabled(boolean enabled)
{
nativeSetNotificationsEnabled(enabled);
}
public boolean hasLastSortingType(long catId) { return nativeHasLastSortingType(catId); }
@SortingType
public int getLastSortingType(long catId) { return nativeGetLastSortingType(catId); }
public void setLastSortingType(long catId, @SortingType int sortingType)
{
nativeSetLastSortingType(catId, sortingType);
}
public void resetLastSortingType(long catId) { nativeResetLastSortingType(catId); }
@NonNull
@SortingType
public int[] getAvailableSortingTypes(long catId, boolean hasMyPosition)
{
return nativeGetAvailableSortingTypes(catId, hasMyPosition);
}
public void getSortedCategory(long catId, @SortingType int sortingType,
boolean hasMyPosition, double lat, double lon,
long timestamp)
{
nativeGetSortedCategory(catId, sortingType, hasMyPosition, lat, lon, timestamp);
}
@NonNull
public List<BookmarkCategory> getChildrenCategories(long catId)
{
return mCurrentDataProvider.getChildrenCategories(catId);
}
@NonNull
native BookmarkCategory nativeGetBookmarkCategory(long catId);
@NonNull
native BookmarkCategory[] nativeGetBookmarkCategories();
native int nativeGetBookmarkCategoriesCount();
@NonNull
native BookmarkCategory[] nativeGetChildrenCategories(long catId);
@NonNull
public String getBookmarkName(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkName(bookmarkId);
}
@NonNull
public String getBookmarkFeatureType(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkFeatureType(bookmarkId);
}
@NonNull
public ParcelablePointD getBookmarkXY(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkXY(bookmarkId);
}
@Icon.PredefinedColor
public int getBookmarkColor(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkColor(bookmarkId);
}
public int getBookmarkIcon(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkIcon(bookmarkId);
}
@NonNull
public String getBookmarkDescription(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkDescription(bookmarkId);
}
public String getTrackDescription(@IntRange(from = 0) long trackId)
{
return nativeGetTrackDescription(trackId);
}
public double getBookmarkScale(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkScale(bookmarkId);
}
@NonNull
public String encode2Ge0Url(@IntRange(from = 0) long bookmarkId, boolean addName)
{
return nativeEncode2Ge0Url(bookmarkId, addName);
}
public void setBookmarkParams(@IntRange(from = 0) long bookmarkId, @NonNull String name,
@Icon.PredefinedColor int color, @NonNull String descr)
{
nativeSetBookmarkParams(bookmarkId, name, color, descr);
}
public void setTrackParams(@IntRange(from = 0) long trackId, @NonNull String name,
int color, @NonNull String descr)
{
nativeSetTrackParams(trackId, name, color, descr);
}
public void changeTrackColor(@IntRange(from = 0) long trackId, int color)
{
nativeChangeTrackColor(trackId, color);
}
public void changeBookmarkCategory(@IntRange(from = 0) long oldCatId,
@IntRange(from = 0) long newCatId,
@IntRange(from = 0) long bookmarkId)
{
nativeChangeBookmarkCategory(oldCatId, newCatId, bookmarkId);
}
public void changeTrackCategory(@IntRange(from = 0) long oldCatId,
@IntRange(from = 0) long newCatId,
@IntRange(from = 0) long trackId)
{
nativeChangeTrackCategory(oldCatId, newCatId, trackId);
}
@NonNull
public String getBookmarkAddress(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkAddress(bookmarkId);
}
public void notifyCategoryChanging(@NonNull BookmarkInfo bookmarkInfo,
@IntRange(from = 0) long catId)
{
if (catId == bookmarkInfo.getCategoryId())
return;
changeBookmarkCategory(bookmarkInfo.getCategoryId(), catId, bookmarkInfo.getBookmarkId());
}
public void notifyCategoryChanging(@NonNull Track track,
@IntRange(from = 0) long catId)
{
if (catId == track.getCategoryId())
return;
changeTrackCategory(track.getCategoryId(), catId, track.getTrackId());
}
public void notifyCategoryChanging(@NonNull Bookmark bookmark, @IntRange(from = 0) long catId)
{
if (catId == bookmark.getCategoryId())
return;
changeBookmarkCategory(bookmark.getCategoryId(), catId, bookmark.getBookmarkId());
}
public void notifyParametersUpdating(@NonNull BookmarkInfo bookmarkInfo, @NonNull String name,
@Nullable Icon icon, @NonNull String description)
{
if (icon == null)
icon = bookmarkInfo.getIcon();
if (!name.equals(bookmarkInfo.getName()) || !icon.equals(bookmarkInfo.getIcon()) ||
!description.equals(getBookmarkDescription(bookmarkInfo.getBookmarkId())))
{
setBookmarkParams(bookmarkInfo.getBookmarkId(), name, icon.getColor(), description);
}
}
public void notifyParametersUpdating(@NonNull Bookmark bookmark, @NonNull String name,
@Nullable Icon icon, @NonNull String description)
{
if (icon == null)
icon = bookmark.getIcon();
if (!name.equals(bookmark.getName()) || !icon.equals(bookmark.getIcon()) ||
!description.equals(getBookmarkDescription(bookmark.getBookmarkId())))
{
setBookmarkParams(bookmark.getBookmarkId(), name,
icon != null ? icon.getColor() : getLastEditedColor(), description);
}
}
public void notifyParametersUpdating(@NonNull Track track, @NonNull String name,
@Nullable int color, @NonNull String description)
{
if (!name.equals(track.getName()) || !(color == track.getColor()) ||
!description.equals(getTrackDescription(track.getTrackId())))
{
setTrackParams(track.getTrackId(), name, color, description);
}
}
public double getElevationCurPositionDistance(long trackId)
{
return nativeGetElevationCurPositionDistance(trackId);
}
public void setElevationActivePoint(long trackId, double distance)
{
nativeSetElevationActivePoint(trackId, distance);
}
public double getElevationActivePointDistance(long trackId)
{
return nativeGetElevationActivePointDistance(trackId);
}
@Nullable
private native Bookmark nativeUpdateBookmarkPlacePage(long bmkId);
@Nullable
private native BookmarkInfo nativeGetBookmarkInfo(long bmkId);
private native long nativeGetBookmarkIdByPosition(long catId, int position);
@NonNull
private native Track nativeGetTrack(long trackId, Class<Track> trackClazz);
private native long nativeGetTrackIdByPosition(long catId, int position);
private native boolean nativeIsVisible(long catId);
private native void nativeSetVisibility(long catId, boolean visible);
private native void nativeSetCategoryName(long catId, @NonNull String n);
private native void nativeSetCategoryDescription(long catId, @NonNull String desc);
private native void nativeSetCategoryTags(long catId, @NonNull String[] tagsIds);
private native void nativeSetCategoryAccessRules(long catId, int accessRules);
private native void nativeSetCategoryCustomProperty(long catId, String key, String value);
private static native void nativeLoadBookmarks();
private native boolean nativeDeleteCategory(long catId);
private native void nativeDeleteTrack(long trackId);
private native void nativeDeleteBookmark(long bmkId);
/**
* @return category Id
*/
private native long nativeCreateCategory(@NonNull String name);
private native void nativeShowBookmarkOnMap(long bmkId);
private native void nativeShowBookmarkCategoryOnMap(long catId);
@Nullable
private native Bookmark nativeAddBookmarkToLastEditedCategory(double lat, double lon);
@Icon.PredefinedColor
private native int nativeGetLastEditedColor();
private static native void nativeLoadBookmarksFile(@NonNull String path, boolean isTemporaryFile);
private static native boolean nativeIsAsyncBookmarksLoadingInProgress();
private static native boolean nativeIsUsedCategoryName(@NonNull String name);
private static native void nativePrepareForSearch(long catId);
private static native boolean nativeAreAllCategoriesVisible();
private static native boolean nativeAreAllCategoriesInvisible();
private static native void nativeSetChildCategoriesVisibility(long catId, boolean visible);
private static native void nativeSetAllCategoriesVisibility(boolean visible);
private static native void nativePrepareFileForSharing(long[] catIds, int kmlFileType);
private static native void nativePrepareTrackFileForSharing(long trackId, int kmlFileType);
private static native boolean nativeIsCategoryEmpty(long catId);
private static native void nativeSetNotificationsEnabled(boolean enabled);
@NonNull
private static native String nativeGetCatalogDeeplink(long catId);
@NonNull
private static native String nativeGetCatalogPublicLink(long catId);
@NonNull
private static native String nativeGetWebEditorUrl(@NonNull String serverId);
@NonNull
private static native KeyValue[] nativeGetCatalogHeaders();
private static native void nativeRequestCatalogCustomProperties();
private native boolean nativeHasLastSortingType(long catId);
@SortingType
private native int nativeGetLastSortingType(long catId);
private native void nativeSetLastSortingType(long catId, @SortingType int sortingType);
private native void nativeResetLastSortingType(long catId);
@NonNull
@SortingType
private native int[] nativeGetAvailableSortingTypes(long catId, boolean hasMyPosition);
private native void nativeGetSortedCategory(long catId, @SortingType int sortingType,
boolean hasMyPosition, double lat, double lon,
long timestamp);
@NonNull
private static native String nativeGetBookmarkName(@IntRange(from = 0) long bookmarkId);
@NonNull
private static native String nativeGetBookmarkFeatureType(@IntRange(from = 0) long bookmarkId);
@NonNull
private static native ParcelablePointD nativeGetBookmarkXY(@IntRange(from = 0) long bookmarkId);
@Icon.PredefinedColor
private static native int nativeGetBookmarkColor(@IntRange(from = 0) long bookmarkId);
private static native int nativeGetBookmarkIcon(@IntRange(from = 0) long bookmarkId);
@NonNull
private static native String nativeGetBookmarkDescription(@IntRange(from = 0) long bookmarkId);
private static native String nativeGetTrackDescription(@IntRange(from = 0) long trackId);
private static native double nativeGetBookmarkScale(@IntRange(from = 0) long bookmarkId);
@NonNull
private static native String nativeEncode2Ge0Url(@IntRange(from = 0) long bookmarkId,
boolean addName);
private static native void nativeSetBookmarkParams(@IntRange(from = 0) long bookmarkId,
@NonNull String name,
@Icon.PredefinedColor int color,
@NonNull String descr);
private static native void nativeChangeTrackColor(@IntRange(from = 0) long trackId,
@Icon.PredefinedColor int color);
private static native void nativeSetTrackParams(@IntRange(from = 0) long trackId,
@NonNull String name,
@Icon.PredefinedColor int color,
@NonNull String descr);
private static native void nativeChangeBookmarkCategory(@IntRange(from = 0) long oldCatId,
@IntRange(from = 0) long newCatId,
@IntRange(from = 0) long bookmarkId);
private static native void nativeChangeTrackCategory(@IntRange(from = 0) long oldCatId,
@IntRange(from = 0) long newCatId,
@IntRange(from = 0) long trackId);
@NonNull
private static native String nativeGetBookmarkAddress(@IntRange(from = 0) long bookmarkId);
private static native double nativeGetElevationCurPositionDistance(long trackId);
private static native void nativeSetElevationCurrentPositionChangedListener();
public static native void nativeRemoveElevationCurrentPositionChangedListener();
private static native void nativeSetElevationActivePoint(long trackId, double distanceInMeters);
private static native double nativeGetElevationActivePointDistance(long trackId);
private static native void nativeSetElevationActiveChangedListener();
public static native void nativeRemoveElevationActiveChangedListener();
public interface BookmarksLoadingListener
{
default void onBookmarksLoadingStarted() {}
default void onBookmarksLoadingFinished() {}
default void onBookmarksFileUnsupported(@NonNull Uri uri) {}
default void onBookmarksFileDownloadFailed(@NonNull Uri uri, @NonNull String string) {}
default void onBookmarksFileImportSuccessful() {}
default void onBookmarksFileImportFailed() {}
}
public interface BookmarksSortingListener
{
void onBookmarksSortingCompleted(@NonNull SortedBlock[] sortedBlocks, long timestamp);
default void onBookmarksSortingCancelled(long timestamp) {};
}
public interface BookmarksSharingListener
{
void onPreparedFileForSharing(@NonNull BookmarkSharingResult result);
}
public interface OnElevationActivePointChangedListener
{
void onElevationActivePointChanged();
}
public interface OnElevationCurrentPositionChangedListener
{
void onCurrentPositionChanged();
}
static class BookmarkCategoriesCache
{
@NonNull
private final List<BookmarkCategory> mCategories = new ArrayList<>();
@NonNull
private final List<DataChangedListener> mListeners = new ArrayList<>();
void update(@NonNull List<BookmarkCategory> categories)
{
mCategories.clear();
mCategories.addAll(categories);
notifyChanged();
}
@NonNull
public List<BookmarkCategory> getCategories()
{
return Collections.unmodifiableList(mCategories);
}
public void registerListener(@NonNull DataChangedListener listener)
{
if (mListeners.contains(listener))
throw new IllegalStateException("Observer " + listener + " is already registered.");
mListeners.add(listener);
}
public void unregisterListener(@NonNull DataChangedListener listener)
{
int index = mListeners.indexOf(listener);
if (index == -1)
throw new IllegalStateException("Observer " + listener + " was not registered.");
mListeners.remove(index);
}
protected void notifyChanged()
{
for (DataChangedListener item : mListeners)
item.onChanged();
}
}
}

View File

@@ -0,0 +1,71 @@
package app.organicmaps.bookmarks.data;
import androidx.annotation.IntDef;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public class BookmarkSharingResult
{
@Retention(RetentionPolicy.SOURCE)
@IntDef({ SUCCESS, EMPTY_CATEGORY, ARCHIVE_ERROR, FILE_ERROR })
public @interface Code {}
public static final int SUCCESS = 0;
public static final int EMPTY_CATEGORY = 1;
public static final int ARCHIVE_ERROR = 2;
public static final int FILE_ERROR = 3;
private final long[] mCategoriesIds;
@Code
private final int mCode;
@NonNull
private final String mSharingPath;
@NonNull
@SuppressWarnings("unused")
private final String mErrorString;
@NonNull
@SuppressWarnings("unused")
private final String mMimeType;
public BookmarkSharingResult(long[] categoriesIds, @Code int code, @NonNull String sharingPath, @NonNull String mimeType, @NonNull String errorString)
{
mCategoriesIds = categoriesIds;
mCode = code;
mSharingPath = sharingPath;
mErrorString = errorString;
mMimeType = mimeType;
}
public long[] getCategoriesIds()
{
return mCategoriesIds;
}
public int getCode()
{
return mCode;
}
@NonNull
public String getSharingPath()
{
return mSharingPath;
}
@NonNull
public String getMimeType()
{
return mMimeType;
}
@NonNull
public String getErrorString()
{
return mErrorString;
}
}

View File

@@ -0,0 +1,45 @@
package app.organicmaps.bookmarks.data;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.List;
class CacheBookmarkCategoriesDataProvider implements BookmarkCategoriesDataProvider
{
@NonNull
@Override
public BookmarkCategory getCategoryById(long categoryId)
{
BookmarkManager.BookmarkCategoriesCache cache
= BookmarkManager.INSTANCE.getBookmarkCategoriesCache();
List<BookmarkCategory> categories = cache.getCategories();
for (BookmarkCategory category: categories)
if (category.getId() == categoryId)
return category;
return BookmarkManager.INSTANCE.nativeGetBookmarkCategory(categoryId);
}
@NonNull
@Override
public List<BookmarkCategory> getCategories()
{
return BookmarkManager.INSTANCE.getBookmarkCategoriesCache().getCategories();
}
@Override
public int getCategoriesCount()
{
return BookmarkManager.INSTANCE.nativeGetBookmarkCategoriesCount();
}
@NonNull
@Override
public List<BookmarkCategory> getChildrenCategories(long parentId)
{
BookmarkCategory[] categories = BookmarkManager.INSTANCE.nativeGetChildrenCategories(parentId);
return Arrays.asList(categories);
}
}

View File

@@ -0,0 +1,43 @@
package app.organicmaps.bookmarks.data;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import app.organicmaps.content.DataSource;
import java.util.List;
public class CategoryDataSource extends RecyclerView.AdapterDataObserver implements
DataSource<BookmarkCategory>
{
@NonNull
private BookmarkCategory mCategory;
public CategoryDataSource(@NonNull BookmarkCategory category)
{
mCategory = category;
}
@NonNull
@Override
public BookmarkCategory getData()
{
return mCategory;
}
@Override
public void onChanged()
{
super.onChanged();
List<BookmarkCategory> categories = BookmarkManager.INSTANCE.getCategories();
int index = categories.indexOf(mCategory);
if (index >= 0)
mCategory = categories.get(index);
}
@Override
public void invalidate()
{
onChanged();
}
}

View File

@@ -0,0 +1,38 @@
package app.organicmaps.bookmarks.data;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.List;
class CoreBookmarkCategoriesDataProvider implements BookmarkCategoriesDataProvider
{
@NonNull
@Override
public BookmarkCategory getCategoryById(long categoryId)
{
return BookmarkManager.INSTANCE.nativeGetBookmarkCategory(categoryId);
}
@NonNull
@Override
public List<BookmarkCategory> getCategories()
{
BookmarkCategory[] categories = BookmarkManager.INSTANCE.nativeGetBookmarkCategories();
return Arrays.asList(categories);
}
@Override
public int getCategoriesCount()
{
return BookmarkManager.INSTANCE.nativeGetBookmarkCategoriesCount();
}
@NonNull
@Override
public List<BookmarkCategory> getChildrenCategories(long parentId)
{
BookmarkCategory[] categories = BookmarkManager.INSTANCE.nativeGetChildrenCategories(parentId);
return Arrays.asList(categories);
}
}

View File

@@ -0,0 +1,30 @@
package app.organicmaps.bookmarks.data;
import androidx.annotation.Keep;
import app.organicmaps.util.Distance;
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class DistanceAndAzimut
{
private final Distance mDistance;
private final double mAzimuth;
public Distance getDistance()
{
return mDistance;
}
public double getAzimuth()
{
return mAzimuth;
}
public DistanceAndAzimut(Distance distance, double azimuth)
{
mDistance = distance;
mAzimuth = azimuth;
}
}

View File

@@ -0,0 +1,211 @@
package app.organicmaps.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import app.organicmaps.widget.placepage.PlacePageData;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class ElevationInfo implements PlacePageData
{
private final long mId;
@NonNull
private final String mName;
@NonNull
private final List<Point> mPoints;
private final int mAscent;
private final int mDescent;
private final int mMinAltitude;
private final int mMaxAltitude;
private final int mDifficulty;
private final long mDuration;
public ElevationInfo(long trackId, @NonNull String name,
@NonNull Point[] points, int ascent, int descent, int minAltitude,
int maxAltitude, int difficulty, long duration)
{
mId = trackId;
mName = name;
mPoints = Arrays.asList(points);
mAscent = ascent;
mDescent = descent;
mMinAltitude = minAltitude;
mMaxAltitude = maxAltitude;
mDifficulty = difficulty;
mDuration = duration;
}
protected ElevationInfo(Parcel in)
{
mId = in.readLong();
mName = in.readString();
mAscent = in.readInt();
mDescent = in.readInt();
mMinAltitude = in.readInt();
mMaxAltitude = in.readInt();
mDifficulty = in.readInt();
mDuration = in.readLong();
mPoints = readPoints(in);
}
@NonNull
private static List<Point> readPoints(@NonNull Parcel in)
{
List<Point> points = new ArrayList<>();
in.readTypedList(points, Point.CREATOR);
return points;
}
public long getId()
{
return mId;
}
@NonNull
public String getName()
{
return mName;
}
@NonNull
public List<Point> getPoints()
{
return Collections.unmodifiableList(mPoints);
}
public int getAscent()
{
return mAscent;
}
public int getDescent()
{
return mDescent;
}
public int getMinAltitude()
{
return mMinAltitude;
}
public int getMaxAltitude()
{
return mMaxAltitude;
}
public int getDifficulty()
{
return mDifficulty;
}
public long getDuration()
{
return mDuration;
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeLong(mId);
dest.writeString(mName);
dest.writeInt(mAscent);
dest.writeInt(mDescent);
dest.writeInt(mMinAltitude);
dest.writeInt(mMaxAltitude);
dest.writeInt(mDifficulty);
dest.writeLong(mDuration);
// All collections are deserialized AFTER non-collection and primitive type objects,
// so collections must be always serialized at the end.
dest.writeTypedList(mPoints);
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public static class Point implements Parcelable
{
private final double mDistance;
private final int mAltitude;
public Point(double distance, int altitude)
{
mDistance = distance;
mAltitude = altitude;
}
protected Point(Parcel in)
{
mDistance = in.readDouble();
mAltitude = in.readInt();
}
public static final Creator<Point> CREATOR = new Creator<>()
{
@Override
public Point createFromParcel(Parcel in)
{
return new Point(in);
}
@Override
public Point[] newArray(int size)
{
return new Point[size];
}
};
public double getDistance()
{
return mDistance;
}
public int getAltitude()
{
return mAltitude;
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeDouble(mDistance);
dest.writeInt(mAltitude);
}
}
public static final Creator<ElevationInfo> CREATOR = new Creator<>()
{
@Override
public ElevationInfo createFromParcel(Parcel in)
{
return new ElevationInfo(in);
}
@Override
public ElevationInfo[] newArray(int size)
{
return new ElevationInfo[size];
}
};
}

View File

@@ -0,0 +1,69 @@
package app.organicmaps.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import java.net.HttpURLConnection;
public class Error implements Parcelable
{
private final int mHttpCode;
@Nullable
private final String mMessage;
public Error(int httpCode, @Nullable String message)
{
mHttpCode = httpCode;
mMessage = message;
}
public Error(@Nullable String message)
{
this(HttpURLConnection.HTTP_UNAVAILABLE, message);
}
protected Error(Parcel in)
{
mHttpCode = in.readInt();
mMessage = in.readString();
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeInt(mHttpCode);
dest.writeString(mMessage);
}
@Override
public String toString()
{
return "Error{" +
"mHttpCode=" + mHttpCode +
", mMessage='" + mMessage + '\'' +
'}';
}
public static final Creator<Error> CREATOR = new Creator<>()
{
@Override
public Error createFromParcel(Parcel in)
{
return new Error(in);
}
@Override
public Error[] newArray(int size)
{
return new Error[size];
}
};
}

View File

@@ -0,0 +1,127 @@
package app.organicmaps.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
// Used by JNI.
@Keep
/// @todo Review using of this class, because seems like it has no any useful purpose.
/// Just creating in JNI and assigning ..
public class FeatureId implements Parcelable
{
public static final Creator<FeatureId> CREATOR = new Creator<>()
{
@Override
public FeatureId createFromParcel(Parcel in)
{
return new FeatureId(in);
}
@Override
public FeatureId[] newArray(int size)
{
return new FeatureId[size];
}
};
@NonNull
public static final FeatureId EMPTY = new FeatureId("", 0L, 0);
@NonNull
private final String mMwmName;
private final long mMwmVersion;
private final int mFeatureIndex;
@NonNull
public static FeatureId fromFeatureIdString(@NonNull String id)
{
if (TextUtils.isEmpty(id))
throw new AssertionError("Feature id string is empty");
String[] parts = id.split(":");
if (parts.length != 3)
throw new AssertionError("Wrong feature id string format");
return new FeatureId(parts[1], Long.parseLong(parts[0]), Integer.parseInt(parts[2]));
}
public FeatureId(@NonNull String mwmName, long mwmVersion, int featureIndex)
{
mMwmName = mwmName;
mMwmVersion = mwmVersion;
mFeatureIndex = featureIndex;
}
private FeatureId(Parcel in)
{
mMwmName = in.readString();
mMwmVersion = in.readLong();
mFeatureIndex = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeString(mMwmName);
dest.writeLong(mMwmVersion);
dest.writeInt(mFeatureIndex);
}
@Override
public int describeContents()
{
return 0;
}
@NonNull
public String getMwmName()
{
return mMwmName;
}
public long getMwmVersion()
{
return mMwmVersion;
}
public int getFeatureIndex()
{
return mFeatureIndex;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FeatureId featureId = (FeatureId) o;
if (mMwmVersion != featureId.mMwmVersion) return false;
if (mFeatureIndex != featureId.mFeatureIndex) return false;
return mMwmName.equals(featureId.mMwmName);
}
@Override
public int hashCode()
{
int result = mMwmName.hashCode();
result = 31 * result + (int) (mMwmVersion ^ (mMwmVersion >>> 32));
result = 31 * result + mFeatureIndex;
return result;
}
@Override
public String toString()
{
return "FeatureId{" +
"mMwmName='" + mMwmName + '\'' +
", mMwmVersion=" + mMwmVersion +
", mFeatureIndex=" + mFeatureIndex +
'}';
}
}

View File

@@ -0,0 +1,197 @@
package app.organicmaps.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import com.google.common.base.Objects;
import app.organicmaps.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class Icon implements Parcelable
{
@Retention(RetentionPolicy.SOURCE)
@IntDef({ PREDEFINED_COLOR_NONE, PREDEFINED_COLOR_RED, PREDEFINED_COLOR_BLUE,
PREDEFINED_COLOR_PURPLE, PREDEFINED_COLOR_YELLOW, PREDEFINED_COLOR_PINK,
PREDEFINED_COLOR_BROWN, PREDEFINED_COLOR_GREEN, PREDEFINED_COLOR_ORANGE,
PREDEFINED_COLOR_DEEPPURPLE, PREDEFINED_COLOR_LIGHTBLUE, PREDEFINED_COLOR_CYAN,
PREDEFINED_COLOR_TEAL, PREDEFINED_COLOR_LIME, PREDEFINED_COLOR_DEEPORANGE,
PREDEFINED_COLOR_GRAY, PREDEFINED_COLOR_BLUEGRAY})
@interface PredefinedColor {}
static final int PREDEFINED_COLOR_NONE = 0;
static final int PREDEFINED_COLOR_RED = 1;
static final int PREDEFINED_COLOR_BLUE = 2;
static final int PREDEFINED_COLOR_PURPLE = 3;
static final int PREDEFINED_COLOR_YELLOW = 4;
static final int PREDEFINED_COLOR_PINK = 5;
static final int PREDEFINED_COLOR_BROWN = 6;
static final int PREDEFINED_COLOR_GREEN = 7;
static final int PREDEFINED_COLOR_ORANGE = 8;
static final int PREDEFINED_COLOR_DEEPPURPLE = 9;
static final int PREDEFINED_COLOR_LIGHTBLUE = 10;
static final int PREDEFINED_COLOR_CYAN = 11;
static final int PREDEFINED_COLOR_TEAL = 12;
static final int PREDEFINED_COLOR_LIME = 13;
static final int PREDEFINED_COLOR_DEEPORANGE = 14;
static final int PREDEFINED_COLOR_GRAY = 15;
static final int PREDEFINED_COLOR_BLUEGRAY = 16;
private static int shift(int v, int bitCount) { return v << bitCount; }
private static int toARGB(int r, int g, int b)
{
return shift(255, 24) + shift(r, 16) + shift(g, 8) + b;
}
/// @note Important! Should be synced with kml/types.hpp/PredefinedColor
/// @todo Values can be taken from Core.
private static final int[] ARGB_COLORS = { toARGB(229, 27, 35), // none
toARGB(229, 27, 35), // red
toARGB(0, 110, 199), // blue
toARGB(156, 39, 176), // purple
toARGB(255, 200, 0), // yellow
toARGB(255, 65, 130), // pink
toARGB(121, 85, 72), // brown
toARGB(56, 142, 60), // green
toARGB(255, 160, 0), // orange
toARGB(102, 57, 191), // deeppurple
toARGB(36, 156, 242), // lightblue
toARGB(20, 190, 205), // cyan
toARGB(0, 165, 140), // teal
toARGB(147, 191, 57), // lime
toARGB(240, 100, 50), // deeporange
toARGB(115, 115, 115), // gray
toARGB(89, 115, 128) }; // bluegray
static final int BOOKMARK_ICON_TYPE_NONE = 0;
/// @note Important! Should be synced with kml/types.hpp/BookmarkIcon
/// @todo Can make better: take name-by-type from Core and make a concat: "R.drawable.ic_bookmark_" + name.
// First icon should be "none" <-> BOOKMARK_ICON_TYPE_NONE.
@DrawableRes
private static final int[] TYPE_ICONS = { R.drawable.ic_bookmark_none,
R.drawable.ic_bookmark_hotel,
R.drawable.ic_bookmark_animals,
R.drawable.ic_bookmark_buddhism,
R.drawable.ic_bookmark_building,
R.drawable.ic_bookmark_christianity,
R.drawable.ic_bookmark_entertainment,
R.drawable.ic_bookmark_money,
R.drawable.ic_bookmark_food,
R.drawable.ic_bookmark_gas,
R.drawable.ic_bookmark_judaism,
R.drawable.ic_bookmark_medicine,
R.drawable.ic_bookmark_mountain,
R.drawable.ic_bookmark_museum,
R.drawable.ic_bookmark_islam,
R.drawable.ic_bookmark_park,
R.drawable.ic_bookmark_parking,
R.drawable.ic_bookmark_shop,
R.drawable.ic_bookmark_sights,
R.drawable.ic_bookmark_swim,
R.drawable.ic_bookmark_water,
R.drawable.ic_bookmark_bar,
R.drawable.ic_bookmark_transport,
R.drawable.ic_bookmark_viewpoint,
R.drawable.ic_bookmark_sport,
R.drawable.ic_bookmark_none, // pub
R.drawable.ic_bookmark_none, // art
R.drawable.ic_bookmark_none, // bank
R.drawable.ic_bookmark_none, // cafe
R.drawable.ic_bookmark_none, // pharmacy
R.drawable.ic_bookmark_none, // stadium
R.drawable.ic_bookmark_none, // theatre
R.drawable.ic_bookmark_none // information
};
@PredefinedColor
private final int mColor;
private final int mType;
public Icon(@PredefinedColor int color, int type)
{
mColor = color;
mType = type;
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeInt(mColor);
dest.writeInt(mType);
}
private Icon(Parcel in)
{
mColor = in.readInt();
mType = in.readInt();
}
@PredefinedColor
public int getColor()
{
return mColor;
}
public int argb()
{
return ARGB_COLORS[mColor];
}
public static int getColorPosition(int color)
{
for (int index = 1; index < ARGB_COLORS.length; index++)
{
if (ARGB_COLORS[index] == color)
return index;
}
return -1;
}
@DrawableRes
public int getResId()
{
return TYPE_ICONS[mType];
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (o instanceof Icon comparedIcon)
return mColor == comparedIcon.mColor && mType == comparedIcon.mType;
return false;
}
@Override
public int hashCode()
{
return Objects.hashCode(mColor, mType);
}
public static final Parcelable.Creator<Icon> CREATOR = new Parcelable.Creator<>()
{
public Icon createFromParcel(Parcel in)
{
return new Icon(in);
}
public Icon[] newArray(int size)
{
return new Icon[size];
}
};
}

View File

@@ -0,0 +1,8 @@
package app.organicmaps.bookmarks.data;
import android.widget.ImageView;
public interface IconClickListener
{
void onItemClick(ImageView v, int position);
}

View File

@@ -0,0 +1,8 @@
package app.organicmaps.bookmarks.data;
// Need to be in sync with KmlFileType (map/bookmark_helpers.hpp)
public enum KmlFileType {
Text,
Binary,
Gpx
}

View File

@@ -0,0 +1,429 @@
package app.organicmaps.bookmarks.data;
import android.net.Uri;
import android.os.Parcel;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.ParcelCompat;
import app.organicmaps.Framework;
import app.organicmaps.routing.RoutePointInfo;
import app.organicmaps.sdk.search.Popularity;
import app.organicmaps.util.Utils;
import app.organicmaps.widget.placepage.PlacePageData;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
// TODO(yunikkk): Refactor. Displayed information is different from edited information, and it's better to
// separate them. Simple getters from jni place_page::Info and osm::EditableFeature should be enough.
// Used from JNI.
@Keep
public class MapObject implements PlacePageData
{
@Retention(RetentionPolicy.SOURCE)
@IntDef({ POI, API_POINT, BOOKMARK, MY_POSITION, SEARCH })
public @interface MapObjectType
{
}
public static final int POI = 0;
public static final int API_POINT = 1;
public static final int BOOKMARK = 2;
public static final int MY_POSITION = 3;
public static final int SEARCH = 4;
@Retention(RetentionPolicy.SOURCE)
@IntDef({ OPENING_MODE_PREVIEW, OPENING_MODE_PREVIEW_PLUS, OPENING_MODE_DETAILS, OPENING_MODE_FULL })
public @interface OpeningMode {}
public static final int OPENING_MODE_PREVIEW = 0;
public static final int OPENING_MODE_PREVIEW_PLUS = 1;
public static final int OPENING_MODE_DETAILS = 2;
public static final int OPENING_MODE_FULL = 3;
private static final String kHttp = "http://";
private static final String kHttps = "https://";
@NonNull
private final FeatureId mFeatureId;
@MapObjectType
private final int mMapObjectType;
private String mTitle;
@Nullable
private final String mSecondaryTitle;
private final String mSubtitle;
private double mLat;
private double mLon;
private final String mAddress;
@NonNull
private final Metadata mMetadata;
private final String mApiId;
private final RoutePointInfo mRoutePointInfo;
@OpeningMode
private final int mOpeningMode;
// @NonNull
// private final Popularity mPopularity;
@NonNull
private final RoadWarningMarkType mRoadWarningMarkType;
@NonNull
private String mDescription;
@Nullable
private List<String> mRawTypes;
public MapObject(@NonNull FeatureId featureId, @MapObjectType int mapObjectType, String title,
@Nullable String secondaryTitle, String subtitle, String address,
double lat, double lon, String apiId, @Nullable RoutePointInfo routePointInfo,
@OpeningMode int openingMode, Popularity popularity, @NonNull String description,
int roadWarningType, @Nullable String[] rawTypes)
{
this(featureId, mapObjectType, title, secondaryTitle,
subtitle, address, lat, lon, new Metadata(), apiId,
routePointInfo, openingMode, popularity, description,
roadWarningType, rawTypes);
}
public MapObject(@NonNull FeatureId featureId, @MapObjectType int mapObjectType,
String title, @Nullable String secondaryTitle, String subtitle, String address,
double lat, double lon, Metadata metadata, String apiId,
@Nullable RoutePointInfo routePointInfo, @OpeningMode int openingMode, Popularity popularity,
@NonNull String description, int roadWarningType, @Nullable String[] rawTypes)
{
mFeatureId = featureId;
mMapObjectType = mapObjectType;
mTitle = title;
mSecondaryTitle = secondaryTitle;
mSubtitle = subtitle;
mAddress = address;
mLat = lat;
mLon = lon;
mMetadata = metadata != null ? metadata : new Metadata();
mApiId = apiId;
mRoutePointInfo = routePointInfo;
mOpeningMode = openingMode;
//mPopularity = popularity;
mDescription = description;
mRoadWarningMarkType = RoadWarningMarkType.values()[roadWarningType];
if (rawTypes != null)
mRawTypes = new ArrayList<>(Arrays.asList(rawTypes));
}
protected MapObject(@MapObjectType int type, Parcel source)
{
this(Objects.requireNonNull(ParcelCompat.readParcelable(source, FeatureId.class.getClassLoader(), FeatureId.class)), // FeatureId
type, // MapObjectType
source.readString(), // Title
source.readString(), // SecondaryTitle
source.readString(), // Subtitle
source.readString(), // Address
source.readDouble(), // Lat
source.readDouble(), // Lon
ParcelCompat.readParcelable(source, Metadata.class.getClassLoader(), Metadata.class),
source.readString(), // ApiId;
ParcelCompat.readParcelable(source, RoutePointInfo.class.getClassLoader(), RoutePointInfo.class), // RoutePointInfo
source.readInt(), // mOpeningMode
Objects.requireNonNull(ParcelCompat.readParcelable(source, Popularity.class.getClassLoader(), Popularity.class)),
Objects.requireNonNull(source.readString()),
source.readInt(),
null // mRawTypes
);
mRawTypes = readRawTypes(source);
}
@NonNull
public static MapObject createMapObject(@NonNull FeatureId featureId, @MapObjectType int mapObjectType,
@NonNull String title, @NonNull String subtitle, double lat, double lon)
{
return new MapObject(featureId, mapObjectType, title,
"", subtitle, "", lat, lon, null,
"", null, OPENING_MODE_PREVIEW,
Popularity.defaultInstance(), "",
RoadWarningMarkType.UNKNOWN.ordinal(), new String[0]);
}
@NonNull
private static List<String> readRawTypes(@NonNull Parcel source)
{
List<String> types = new ArrayList<>();
source.readStringList(types);
return types;
}
/**
* If you override {@link #equals(Object)} it is also required to override {@link #hashCode()}.
* MapObject does not participate in any sets or other collections that need {@code hashCode()}.
* So {@code sameAs()} serves as {@code equals()} but does not break the equals+hashCode contract.
*/
public boolean sameAs(@Nullable MapObject other)
{
if (other == null)
return false;
if (this == other)
return true;
//noinspection SimplifiableIfStatement
if (getClass() != other.getClass())
return false;
if (mFeatureId != FeatureId.EMPTY && other.getFeatureId() != FeatureId.EMPTY)
return mFeatureId.equals(other.getFeatureId());
return Double.doubleToLongBits(mLon) == Double.doubleToLongBits(other.mLon) &&
Double.doubleToLongBits(mLat) == Double.doubleToLongBits(other.mLat);
}
public static boolean same(@Nullable MapObject one, @Nullable MapObject another)
{
//noinspection SimplifiableIfStatement
if (one == null && another == null)
return true;
return (one != null && one.sameAs(another));
}
public double getScale()
{
return 0;
}
@NonNull
public String getTitle()
{
return mTitle;
}
public void setTitle(@NonNull String title)
{
mTitle = title;
}
@NonNull
public String getName()
{
return getTitle();
}
@Nullable
public String getSecondaryTitle()
{
return mSecondaryTitle;
}
@NonNull
public String getSubtitle()
{
return mSubtitle;
}
public double getLat()
{
return mLat;
}
public double getLon()
{
return mLon;
}
@NonNull
public String getAddress()
{
return mAddress;
}
@NonNull
public String getDescription()
{
return mDescription;
}
public void setDescription(@NonNull String description)
{
mDescription = description;
}
@NonNull
public RoadWarningMarkType getRoadWarningMarkType()
{
return mRoadWarningMarkType;
}
@NonNull
public String getMetadata(Metadata.MetadataType type)
{
final String res = mMetadata.getMetadata(type);
return res == null ? "" : res;
}
@NonNull
public String getWebsiteUrl(boolean strip, @NonNull Metadata.MetadataType type)
{
final String website = Uri.decode(getMetadata(type));
final int len = website.length();
if (strip && len > 1)
{
final int start = website.startsWith(kHttps) ? kHttps.length() : (website.startsWith(kHttp) ? kHttp.length() : 0);
final int end = website.endsWith("/") ? len - 1 : len;
return website.substring(start, end);
}
return website;
}
@NonNull
public String getKayakUrl()
{
final String uri = getMetadata(Metadata.MetadataType.FMD_EXTERNAL_URI);
if (TextUtils.isEmpty(uri))
return "";
final Instant firstDay = Instant.now();
final long firstDaySec = firstDay.getEpochSecond();
final long lastDaySec = firstDay.plus(1, ChronoUnit.DAYS).getEpochSecond();
final String res = Framework.nativeGetKayakHotelLink(Utils.getCountryCode(), uri, firstDaySec, lastDaySec);
return res == null ? "" : res;
}
public String getApiId()
{
return mApiId;
}
public void setLat(double lat)
{
mLat = lat;
}
public void setLon(double lon)
{
mLon = lon;
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public void addMetadata(int type, String value)
{
mMetadata.addMetadata(type, value);
}
public boolean hasPhoneNumber()
{
return !TextUtils.isEmpty(getMetadata(Metadata.MetadataType.FMD_PHONE_NUMBER));
}
public boolean hasAtm()
{
return mRawTypes.contains("amenity-atm");
}
public final boolean isMyPosition()
{
return mMapObjectType == MY_POSITION;
}
public final boolean isBookmark()
{
return mMapObjectType == BOOKMARK;
}
@Nullable
public RoutePointInfo getRoutePointInfo()
{
return mRoutePointInfo;
}
@OpeningMode
public int getOpeningMode()
{
return mOpeningMode;
}
@NonNull
public FeatureId getFeatureId()
{
return mFeatureId;
}
private static MapObject readFromParcel(Parcel source)
{
@MapObjectType int type = source.readInt();
if (type == BOOKMARK)
return new Bookmark(type, source);
return new MapObject(type, source);
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
// A map object type must be written first, since it's used in readParcel method to distinguish
// what type of object should be read from the parcel.
dest.writeInt(mMapObjectType);
dest.writeParcelable(mFeatureId, 0);
dest.writeString(mTitle);
dest.writeString(mSecondaryTitle);
dest.writeString(mSubtitle);
dest.writeString(mAddress);
dest.writeDouble(mLat);
dest.writeDouble(mLon);
dest.writeParcelable(mMetadata, 0);
dest.writeString(mApiId);
dest.writeParcelable(mRoutePointInfo, 0);
dest.writeInt(mOpeningMode);
//dest.writeParcelable(mPopularity, 0);
dest.writeString(mDescription);
dest.writeInt(getRoadWarningMarkType().ordinal());
// All collections are deserialized AFTER non-collection and primitive type objects,
// so collections must be always serialized at the end.
dest.writeStringList(mRawTypes);
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MapObject mapObject = (MapObject) o;
return mFeatureId.equals(mapObject.mFeatureId);
}
@Override
public int hashCode()
{
return mFeatureId.hashCode();
}
public static final Creator<MapObject> CREATOR = new Creator<>()
{
@Override
public MapObject createFromParcel(Parcel source)
{
return readFromParcel(source);
}
@Override
public MapObject[] newArray(int size)
{
return new MapObject[size];
}
};
}

View File

@@ -0,0 +1,148 @@
package app.organicmaps.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
public class Metadata implements Parcelable
{
// Values must correspond to the Metadata definition from indexer/feature_meta.hpp.
public enum MetadataType
{
// Defined by classifier types now.
FMD_CUISINE(1),
FMD_OPEN_HOURS(2),
FMD_PHONE_NUMBER(3),
FMD_FAX_NUMBER(4),
FMD_STARS(5),
FMD_OPERATOR(6),
// Removed and is not used in the core. Use FMD_WEBSITE instead.
//FMD_URL(7),
FMD_WEBSITE(8),
FMD_INTERNET(9),
FMD_ELE(10),
FMD_TURN_LANES(11),
FMD_TURN_LANES_FORWARD(12),
FMD_TURN_LANES_BACKWARD(13),
FMD_EMAIL(14),
FMD_POSTCODE(15),
// TODO: It is hacked in jni and returns full Wikipedia url. Should use separate getter instead.
FMD_WIKIPEDIA(16),
// TODO: Skipped now.
FMD_DESCRIPTION(17),
FMD_FLATS(18),
FMD_HEIGHT(19),
FMD_MIN_HEIGHT(20),
FMD_DENOMINATION(21),
FMD_BUILDING_LEVELS(22),
FWD_TEST_ID(23),
FMD_CUSTOM_IDS(24),
FMD_PRICE_RATES(25),
FMD_RATINGS(26),
FMD_EXTERNAL_URI(27),
FMD_LEVEL(28),
FMD_AIRPORT_IATA(29),
FMD_BRAND(30),
FMD_DURATION(31),
FMD_CONTACT_FACEBOOK(32),
FMD_CONTACT_INSTAGRAM(33),
FMD_CONTACT_TWITTER(34),
FMD_CONTACT_VK(35),
FMD_CONTACT_LINE(36),
FMD_DESTINATION(37),
FMD_DESTINATION_REF(38),
FMD_JUNCTION_REF(39),
FMD_BUILDING_MIN_LEVEL(40),
FMD_WIKIMEDIA_COMMONS(41),
FMD_CAPACITY(42),
FMD_WHEELCHAIR(43),
FMD_LOCAL_REF(44),
FMD_DRIVE_THROUGH(45),
FMD_WEBSITE_MENU(46),
FMD_SELF_SERVICE(47),
FMD_OUTDOOR_SEATING(48),
FMD_NETWORK(49);
private final int mMetaType;
MetadataType(int metadataType)
{
mMetaType = metadataType;
}
@NonNull
public static MetadataType fromInt(@IntRange(from = 1, to = 41) int metaType)
{
for (MetadataType type : values())
if (type.mMetaType == metaType)
return type;
throw new IllegalArgumentException("Illegal metaType: " + metaType);
}
public int toInt()
{
return mMetaType;
}
}
private final Map<MetadataType, String> mMetadataMap = new HashMap<>();
public void addMetadata(int metaType, String metaValue)
{
final MetadataType type = MetadataType.fromInt(metaType);
mMetadataMap.put(type, metaValue);
}
@Nullable
String getMetadata(MetadataType type)
{
return mMetadataMap.get(type);
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeInt(mMetadataMap.size());
for (Map.Entry<MetadataType, String> metaEntry : mMetadataMap.entrySet())
{
dest.writeInt(metaEntry.getKey().mMetaType);
dest.writeString(metaEntry.getValue());
}
}
public static Metadata readFromParcel(Parcel source)
{
final Metadata metadata = new Metadata();
final int size = source.readInt();
for (int i = 0; i < size; i++)
metadata.addMetadata(source.readInt(), source.readString());
return metadata;
}
public static final Creator<Metadata> CREATOR = new Creator<>()
{
@Override
public Metadata createFromParcel(Parcel source)
{
return readFromParcel(source);
}
@Override
public Metadata[] newArray(int size)
{
return new Metadata[size];
}
};
}

View File

@@ -0,0 +1,56 @@
package app.organicmaps.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Keep;
// TODO consider removal and usage of platform PointF
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class ParcelablePointD implements Parcelable
{
public final double x;
public final double y;
@Override
public int describeContents()
{
// TODO Auto-generated method stub
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeDouble(x);
dest.writeDouble(y);
}
private ParcelablePointD(Parcel in)
{
x = in.readDouble();
y = in.readDouble();
}
public ParcelablePointD(double x, double y)
{
this.x = x;
this.y = y;
}
public static final Parcelable.Creator<ParcelablePointD> CREATOR = new Parcelable.Creator<>()
{
public ParcelablePointD createFromParcel(Parcel in)
{
return new ParcelablePointD(in);
}
public ParcelablePointD[] newArray(int size)
{
return new ParcelablePointD[size];
}
};
}

View File

@@ -0,0 +1,63 @@
package app.organicmaps.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
public class Result implements Parcelable
{
@Nullable
private final String mFilePath;
@Nullable
private final String mArchiveId;
public Result(@Nullable String filePath, @Nullable String archiveId)
{
mFilePath = filePath;
mArchiveId = archiveId;
}
protected Result(Parcel in)
{
mFilePath = in.readString();
mArchiveId = in.readString();
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeString(mFilePath);
dest.writeString(mArchiveId);
}
@Override
public String toString()
{
return "Result{" +
"mFilePath='" + mFilePath + '\'' +
", mArchiveId='" + mArchiveId + '\'' +
'}';
}
public static final Creator<Result> CREATOR = new Creator<>()
{
@Override
public Result createFromParcel(Parcel in)
{
return new Result(in);
}
@Override
public Result[] newArray(int size)
{
return new Result[size];
}
};
}

View File

@@ -0,0 +1,9 @@
package app.organicmaps.bookmarks.data;
public enum RoadWarningMarkType
{
TOLL,
FERRY,
DIRTY,
UNKNOWN
}

View File

@@ -0,0 +1,41 @@
package app.organicmaps.bookmarks.data;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class SortedBlock
{
@NonNull
private final String mName;
@NonNull
private final List<Long> mBookmarkIds;
@NonNull
private final List<Long> mTrackIds;
public SortedBlock(@NonNull String name, @NonNull Long[] bookmarkIds,
@NonNull Long[] trackIds)
{
mName = name;
mBookmarkIds = new ArrayList<>(Arrays.asList(bookmarkIds));
mTrackIds = new ArrayList<>(Arrays.asList(trackIds));
}
public boolean isBookmarksBlock() { return !mBookmarkIds.isEmpty(); }
@NonNull
public String getName() { return mName; }
@SuppressWarnings("AssignmentOrReturnOfFieldWithMutableType")
@NonNull
public List<Long> getBookmarkIds() { return mBookmarkIds; }
@SuppressWarnings("AssignmentOrReturnOfFieldWithMutableType")
@NonNull
public List<Long> getTrackIds() { return mTrackIds; }
}

View File

@@ -0,0 +1,41 @@
package app.organicmaps.bookmarks.data;
import androidx.annotation.Keep;
import app.organicmaps.util.Distance;
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public class Track
{
private final long mTrackId;
private final long mCategoryId;
private final String mName;
private final Distance mLength;
private final int mColor;
Track(long trackId, long categoryId, String name, Distance length, int color)
{
mTrackId = trackId;
mCategoryId = categoryId;
mName = name;
mLength = length;
mColor = color;
}
public String getName() { return mName; }
public Distance getLength() { return mLength;}
public int getColor() { return mColor; }
public long getTrackId() { return mTrackId; }
public long getCategoryId() { return mCategoryId; }
public String getTrackDescription()
{
return BookmarkManager.INSTANCE.getTrackDescription(mTrackId);
}
}

View File

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

View File

@@ -0,0 +1,289 @@
package app.organicmaps.car;
import android.content.Intent;
import android.content.res.Configuration;
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.MwmApplication;
import app.organicmaps.R;
import app.organicmaps.bookmarks.data.MapObject;
import app.organicmaps.car.screens.ErrorScreen;
import app.organicmaps.car.screens.MapPlaceholderScreen;
import app.organicmaps.car.screens.MapScreen;
import app.organicmaps.car.screens.PlaceScreen;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.download.DownloadMapsScreen;
import app.organicmaps.car.screens.download.DownloadMapsScreenBuilder;
import app.organicmaps.car.screens.download.DownloaderHelpers;
import app.organicmaps.car.screens.permissions.RequestPermissionsScreenBuilder;
import app.organicmaps.car.util.CarSensorsManager;
import app.organicmaps.car.util.CurrentCountryChangedListener;
import app.organicmaps.car.util.IntentUtils;
import app.organicmaps.car.util.ThemeUtils;
import app.organicmaps.car.util.UserActionRequired;
import app.organicmaps.display.DisplayChangedListener;
import app.organicmaps.display.DisplayManager;
import app.organicmaps.display.DisplayType;
import app.organicmaps.location.LocationState;
import app.organicmaps.routing.RoutingController;
import app.organicmaps.util.Config;
import app.organicmaps.util.LocationUtils;
import app.organicmaps.util.log.Logger;
import app.organicmaps.widget.placepage.PlacePageData;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public final class CarAppSession extends Session implements DefaultLifecycleObserver,
LocationState.ModeChangeListener, DisplayChangedListener, Framework.PlacePageActivationListener
{
private static final String TAG = CarAppSession.class.getSimpleName();
@Nullable
private final SessionInfo mSessionInfo;
@NonNull
private final SurfaceRenderer mSurfaceRenderer;
@NonNull
private final ScreenManager mScreenManager;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private CarSensorsManager mSensorsManager;
@NonNull
private final CurrentCountryChangedListener mCurrentCountryChangedListener;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private DisplayManager mDisplayManager;
private boolean mInitFailed = false;
public CarAppSession(@Nullable SessionInfo sessionInfo)
{
getLifecycle().addObserver(this);
mSessionInfo = sessionInfo;
mSurfaceRenderer = new SurfaceRenderer(getCarContext(), getLifecycle());
mScreenManager = getCarContext().getCarService(ScreenManager.class);
mCurrentCountryChangedListener = new CurrentCountryChangedListener();
}
@Override
public void onCarConfigurationChanged(@NonNull Configuration newConfiguration)
{
Logger.d(TAG, "New configuration: " + newConfiguration);
if (mSurfaceRenderer.isRenderingActive())
{
ThemeUtils.update(getCarContext());
mScreenManager.getTop().invalidate();
}
}
@NonNull
@Override
public Screen onCreateScreen(@NonNull Intent intent)
{
Logger.d(TAG);
Logger.i(TAG, "Session info: " + mSessionInfo);
Logger.i(TAG, "API Level: " + getCarContext().getCarAppApiLevel());
if (mSessionInfo != null)
Logger.i(TAG, "Supported templates: " + mSessionInfo.getSupportedTemplates(getCarContext().getCarAppApiLevel()));
Logger.i(TAG, "Host info: " + getCarContext().getHostInfo());
Logger.i(TAG, "Car configuration: " + getCarContext().getResources().getConfiguration());
return prepareScreens();
}
@Override
public void onNewIntent(@NonNull Intent intent)
{
Logger.d(TAG, intent.toString());
IntentUtils.processIntent(getCarContext(), mSurfaceRenderer, intent);
}
@Override
public void onCreate(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
mSensorsManager = new CarSensorsManager(getCarContext());
mDisplayManager = DisplayManager.from(getCarContext());
mDisplayManager.addListener(DisplayType.Car, this);
init();
}
@Override
public void onStart(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
if (mDisplayManager.isCarDisplayUsed())
{
LocationState.nativeSetListener(this);
Framework.nativePlacePageActivationListener(this);
mCurrentCountryChangedListener.onStart(getCarContext());
}
if (LocationUtils.checkFineLocationPermission(getCarContext()))
mSensorsManager.onStart();
if (mDisplayManager.isCarDisplayUsed())
{
ThemeUtils.update(getCarContext());
restoreRoute();
}
}
@Override
public void onStop(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
mSensorsManager.onStop();
if (mDisplayManager.isCarDisplayUsed())
{
LocationState.nativeRemoveListener();
Framework.nativeRemovePlacePageActivationListener(this);
}
mCurrentCountryChangedListener.onStop();
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner)
{
mDisplayManager.removeListener(DisplayType.Car);
}
private void init()
{
mInitFailed = false;
try
{
MwmApplication.from(getCarContext()).init(() -> {
Config.setFirstStartDialogSeen(getCarContext());
if (DownloaderHelpers.isWorldMapsDownloadNeeded())
mScreenManager.push(new DownloadMapsScreenBuilder(getCarContext()).setDownloaderType(DownloadMapsScreenBuilder.DownloaderType.FirstLaunch).build());
});
} catch (IOException e)
{
mInitFailed = true;
Logger.e(TAG, "Failed to initialize the app.");
}
}
@NonNull
private Screen prepareScreens()
{
if (mInitFailed)
return new ErrorScreen.Builder(getCarContext()).setErrorMessage(R.string.dialog_error_storage_message).build();
final List<Screen> screensStack = new ArrayList<>();
screensStack.add(new MapScreen(getCarContext(), mSurfaceRenderer));
if (!LocationUtils.checkFineLocationPermission(getCarContext()))
screensStack.add(RequestPermissionsScreenBuilder.build(getCarContext(), mSensorsManager::onStart));
if (mDisplayManager.isDeviceDisplayUsed())
{
mSurfaceRenderer.disable();
onStop(this);
screensStack.add(new MapPlaceholderScreen(getCarContext()));
}
for (int i = 0; i < screensStack.size() - 1; i++)
mScreenManager.push(screensStack.get(i));
return screensStack.get(screensStack.size() - 1);
}
@Override
public void onMyPositionModeChanged(int newMode)
{
final Screen screen = mScreenManager.getTop();
if (screen instanceof BaseMapScreen)
screen.invalidate();
}
@Override
public void onDisplayChangedToDevice(@NonNull Runnable onTaskFinishedCallback)
{
Logger.d(TAG);
final Screen topScreen = mScreenManager.getTop();
onStop(this);
mSurfaceRenderer.disable();
final MapPlaceholderScreen mapPlaceholderScreen = new MapPlaceholderScreen(getCarContext());
if (topScreen instanceof UserActionRequired)
mScreenManager.popToRoot();
mScreenManager.push(mapPlaceholderScreen);
onTaskFinishedCallback.run();
}
@Override
public void onDisplayChangedToCar(@NonNull Runnable onTaskFinishedCallback)
{
Logger.d(TAG);
onStart(this);
mSurfaceRenderer.enable();
if (mScreenManager.getTop() instanceof MapPlaceholderScreen)
mScreenManager.pop();
onTaskFinishedCallback.run();
}
@Override
public void onPlacePageActivated(@NonNull PlacePageData data)
{
// TODO: How maps downloading can trigger place page activation?
if (DownloadMapsScreen.MARKER.equals(mScreenManager.getTop().getMarker()))
return;
final MapObject mapObject = (MapObject) data;
// Don't display the PlaceScreen for 'MY_POSITION' or during navigation
// TODO (AndrewShkrob): Implement the 'Add stop' functionality
if (mapObject.isMyPosition() || RoutingController.get().isNavigating())
{
Framework.nativeDeactivatePopup();
return;
}
final PlaceScreen placeScreen = new PlaceScreen.Builder(getCarContext(), mSurfaceRenderer).setMapObject(mapObject).build();
mScreenManager.popToRoot();
mScreenManager.push(placeScreen);
}
@Override
public void onPlacePageDeactivated()
{
// The function is called when we close the PlaceScreen or when we enter the navigation mode.
// We only need to handle the first case
if (!(mScreenManager.getTop() instanceof PlaceScreen))
return;
RoutingController.get().cancel();
mScreenManager.popToRoot();
}
@Override
public void onSwitchFullScreenMode()
{
// No fullscreen mode in AndroidAuto. Do nothing.
}
private void restoreRoute()
{
final RoutingController routingController = RoutingController.get();
if (routingController.isPlanning() || routingController.isNavigating() || routingController.hasSavedRoute())
{
final PlaceScreen placeScreen = new PlaceScreen.Builder(getCarContext(), mSurfaceRenderer).setMapObject(routingController.getEndPoint()).build();
mScreenManager.popToRoot();
mScreenManager.push(placeScreen);
}
}
}

View File

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

View File

@@ -0,0 +1,91 @@
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.GridItem;
import androidx.car.app.model.GridTemplate;
import androidx.car.app.model.Header;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapWithContentTemplate;
import androidx.core.graphics.drawable.IconCompat;
import app.organicmaps.R;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.search.SearchOnMapScreen;
import app.organicmaps.car.util.ThemeUtils;
import app.organicmaps.car.util.UiHelpers;
import java.util.Arrays;
import java.util.List;
public class CategoriesScreen extends BaseMapScreen
{
private record CategoryData(@StringRes int nameResId, @DrawableRes int iconResId, @DrawableRes int iconNightResId)
{
}
private static final List<CategoryData> CATEGORIES = Arrays.asList(
new CategoryData(R.string.category_fuel, R.drawable.ic_category_fuel, R.drawable.ic_category_fuel_night),
new CategoryData(R.string.category_parking, R.drawable.ic_category_parking, R.drawable.ic_category_parking_night),
new CategoryData(R.string.category_eat, R.drawable.ic_category_eat, R.drawable.ic_category_eat_night),
new CategoryData(R.string.category_food, R.drawable.ic_category_food, R.drawable.ic_category_food_night),
new CategoryData(R.string.category_hotel, R.drawable.ic_category_hotel, R.drawable.ic_category_hotel_night),
new CategoryData(R.string.category_toilet, R.drawable.ic_category_toilet, R.drawable.ic_category_toilet_night),
new CategoryData(R.string.category_rv, R.drawable.ic_category_rv, R.drawable.ic_category_rv_night)
);
private final int MAX_CATEGORIES_SIZE;
public CategoriesScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
super(carContext, surfaceRenderer);
final ConstraintManager constraintManager = getCarContext().getCarService(ConstraintManager.class);
MAX_CATEGORIES_SIZE = constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST);
}
@NonNull
@Override
public Template onGetTemplate()
{
final MapWithContentTemplate.Builder builder = new MapWithContentTemplate.Builder();
builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer()));
builder.setContentTemplate(createCategoriesListTemplate());
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 GridTemplate createCategoriesListTemplate()
{
final boolean isNightMode = ThemeUtils.isNightMode(getCarContext());
final ItemList.Builder builder = new ItemList.Builder();
final int categoriesSize = Math.min(CATEGORIES.size(), MAX_CATEGORIES_SIZE);
for (int i = 0; i < categoriesSize; ++i)
{
final GridItem.Builder itemBuilder = new GridItem.Builder();
final String title = getCarContext().getString(CATEGORIES.get(i).nameResId);
@DrawableRes final int iconResId = isNightMode ? CATEGORIES.get(i).iconNightResId : CATEGORIES.get(i).iconResId;
itemBuilder.setTitle(title);
itemBuilder.setImage(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), iconResId)).build());
itemBuilder.setOnClickListener(() -> getScreenManager().push(new SearchOnMapScreen.Builder(getCarContext(), getSurfaceRenderer()).setCategory(title).build()));
builder.addItem(itemBuilder.build());
}
return new GridTemplate.Builder().setHeader(createHeader()).setSingleList(builder.build()).build();
}
}

View File

@@ -0,0 +1,145 @@
package app.organicmaps.car.screens;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.car.app.CarContext;
import androidx.car.app.model.Action;
import androidx.car.app.model.Header;
import androidx.car.app.model.MessageTemplate;
import androidx.car.app.model.Template;
import app.organicmaps.R;
import app.organicmaps.car.screens.base.BaseScreen;
import app.organicmaps.car.util.Colors;
import app.organicmaps.car.util.UserActionRequired;
public class ErrorScreen extends BaseScreen implements UserActionRequired
{
@StringRes
private final int mTitle;
@StringRes
private final int mErrorMessage;
@StringRes
private final int mPositiveButtonText;
@Nullable
private final Runnable mPositiveButtonCallback;
@StringRes
private final int mNegativeButtonText;
@Nullable
private final Runnable mNegativeButtonCallback;
private ErrorScreen(@NonNull Builder builder)
{
super(builder.mCarContext);
mTitle = builder.mTitle == -1 ? R.string.app_name : builder.mTitle;
mErrorMessage = builder.mErrorMessage;
mPositiveButtonText = builder.mPositiveButtonText;
mPositiveButtonCallback = builder.mPositiveButtonCallback;
mNegativeButtonText = builder.mNegativeButtonText;
mNegativeButtonCallback = builder.mNegativeButtonCallback;
}
@NonNull
@Override
public Template onGetTemplate()
{
final MessageTemplate.Builder builder = new MessageTemplate.Builder(getCarContext().getString(mErrorMessage));
final Header.Builder headerBuilder = new Header.Builder();
headerBuilder.setStartHeaderAction(Action.APP_ICON);
headerBuilder.setTitle(getCarContext().getString(mTitle));
builder.setHeader(headerBuilder.build());
if (mPositiveButtonText != -1)
{
builder.addAction(new Action.Builder()
.setBackgroundColor(Colors.BUTTON_ACCEPT)
.setTitle(getCarContext().getString(mPositiveButtonText))
.setOnClickListener(this::onPositiveButton).build()
);
}
if (mNegativeButtonText != -1)
{
builder.addAction(new Action.Builder()
.setTitle(getCarContext().getString(mNegativeButtonText))
.setOnClickListener(this::onNegativeButton).build()
);
}
return builder.build();
}
private void onPositiveButton()
{
if (mPositiveButtonCallback != null)
mPositiveButtonCallback.run();
finish();
}
private void onNegativeButton()
{
if (mNegativeButtonCallback != null)
mNegativeButtonCallback.run();
finish();
}
public static class Builder
{
@NonNull
private final CarContext mCarContext;
@StringRes
private int mTitle = -1;
@StringRes
private int mErrorMessage;
@StringRes
private int mPositiveButtonText = -1;
@Nullable
private Runnable mPositiveButtonCallback;
@StringRes
private int mNegativeButtonText = -1;
@Nullable
private Runnable mNegativeButtonCallback;
public Builder(@NonNull CarContext carContext)
{
mCarContext = carContext;
}
public Builder setTitle(@StringRes int title)
{
mTitle = title;
return this;
}
public Builder setErrorMessage(@StringRes int errorMessage)
{
mErrorMessage = errorMessage;
return this;
}
public Builder setPositiveButton(@StringRes int text, @Nullable Runnable callback)
{
mPositiveButtonText = text;
mPositiveButtonCallback = callback;
return this;
}
public Builder setNegativeButton(@StringRes int text, @Nullable Runnable callback)
{
mNegativeButtonText = text;
mNegativeButtonCallback = callback;
return this;
}
public ErrorScreen build()
{
return new ErrorScreen(this);
}
}
}

View File

@@ -0,0 +1,47 @@
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.CarIcon;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.NavigationTemplate;
import androidx.core.graphics.drawable.IconCompat;
import app.organicmaps.R;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.util.UiHelpers;
public class FreeDriveScreen extends BaseMapScreen
{
public FreeDriveScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
super(carContext, surfaceRenderer);
}
@NonNull
@Override
public Template onGetTemplate()
{
final NavigationTemplate.Builder builder = new NavigationTemplate.Builder();
builder.setMapActionStrip(UiHelpers.createMapActionStrip(getCarContext(), getSurfaceRenderer()));
builder.setActionStrip(createActionStrip());
return builder.build();
}
@NonNull
private ActionStrip createActionStrip()
{
final Action.Builder finishActionBuilder = new Action.Builder();
finishActionBuilder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_close)).build());
finishActionBuilder.setOnClickListener(this::finish);
final ActionStrip.Builder builder = new ActionStrip.Builder();
builder.addAction(finishActionBuilder.build());
builder.addAction(UiHelpers.createSettingsAction(this, getSurfaceRenderer()));
return builder.build();
}
}

View File

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

View File

@@ -0,0 +1,162 @@
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.CarIcon;
import androidx.car.app.model.GridItem;
import androidx.car.app.model.GridTemplate;
import androidx.car.app.model.Header;
import androidx.car.app.model.Item;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapWithContentTemplate;
import androidx.core.graphics.drawable.IconCompat;
import app.organicmaps.R;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.bookmarks.BookmarkCategoriesScreen;
import app.organicmaps.car.screens.search.SearchScreen;
import app.organicmaps.car.screens.settings.SettingsScreen;
import app.organicmaps.car.util.SuggestionsHelpers;
import app.organicmaps.car.util.UiHelpers;
public class MapScreen extends BaseMapScreen
{
public MapScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
super(carContext, surfaceRenderer);
}
@NonNull
@Override
public Template onGetTemplate()
{
SuggestionsHelpers.updateSuggestions(getCarContext());
final MapWithContentTemplate.Builder builder = new MapWithContentTemplate.Builder();
builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer()));
builder.setActionStrip(createActionStrip());
builder.setContentTemplate(createGridTemplate());
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 ActionStrip createActionStrip()
{
final Action.Builder freeDriveScreenBuilder = new Action.Builder();
freeDriveScreenBuilder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_steering_wheel)).build());
freeDriveScreenBuilder.setOnClickListener(() -> getScreenManager().push(new FreeDriveScreen(getCarContext(), getSurfaceRenderer())));
final ActionStrip.Builder builder = new ActionStrip.Builder();
builder.addAction(freeDriveScreenBuilder.build());
return builder.build();
}
@NonNull
private GridTemplate createGridTemplate()
{
final GridTemplate.Builder builder = new GridTemplate.Builder();
final ItemList.Builder itemsBuilder = new ItemList.Builder();
itemsBuilder.addItem(createSearchItem());
itemsBuilder.addItem(createCategoriesItem());
itemsBuilder.addItem(createBookmarksItem());
itemsBuilder.addItem(createSettingsItem());
builder.setHeader(createHeader());
builder.setSingleList(itemsBuilder.build());
return builder.build();
}
@NonNull
private Item createSearchItem()
{
final CarIcon iconSearch = new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_search)).build();
final GridItem.Builder builder = new GridItem.Builder();
builder.setTitle(getCarContext().getString(R.string.search));
builder.setImage(iconSearch);
builder.setOnClickListener(this::openSearch);
return builder.build();
}
@NonNull
private Item createCategoriesItem()
{
final CarIcon iconCategories = new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_address)).build();
final GridItem.Builder builder = new GridItem.Builder();
builder.setImage(iconCategories);
builder.setTitle(getCarContext().getString(R.string.categories));
builder.setOnClickListener(this::openCategories);
return builder.build();
}
@NonNull
private Item createBookmarksItem()
{
final CarIcon iconBookmarks = new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_bookmarks)).build();
final GridItem.Builder builder = new GridItem.Builder();
builder.setImage(iconBookmarks);
builder.setTitle(getCarContext().getString(R.string.bookmarks));
builder.setOnClickListener(this::openBookmarks);
return builder.build();
}
@NonNull
private Item createSettingsItem()
{
final CarIcon iconSettings = new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_settings)).build();
final GridItem.Builder builder = new GridItem.Builder();
builder.setImage(iconSettings);
builder.setTitle(getCarContext().getString(R.string.settings));
builder.setOnClickListener(this::openSettings);
return builder.build();
}
private void openSearch()
{
// Details in UiHelpers.createSettingsAction()
if (getScreenManager().getTop() != this)
return;
getScreenManager().push(new SearchScreen.Builder(getCarContext(), getSurfaceRenderer()).build());
}
private void openCategories()
{
// Details in UiHelpers.createSettingsAction()
if (getScreenManager().getTop() != this)
return;
getScreenManager().push(new CategoriesScreen(getCarContext(), getSurfaceRenderer()));
}
private void openBookmarks()
{
// Details in UiHelpers.createSettingsAction()
if (getScreenManager().getTop() != this)
return;
getScreenManager().push(new BookmarkCategoriesScreen(getCarContext(), getSurfaceRenderer()));
}
private void openSettings()
{
// Details in UiHelpers.createSettingsAction()
if (getScreenManager().getTop() != this)
return;
getScreenManager().push(new SettingsScreen(getCarContext(), getSurfaceRenderer()));
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,90 @@
package app.organicmaps.car.screens.bookmarks;
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.Header;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.ListTemplate;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapWithContentTemplate;
import app.organicmaps.R;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.util.UiHelpers;
import java.util.ArrayList;
import java.util.List;
public class BookmarkCategoriesScreen extends BaseMapScreen
{
private final int MAX_CATEGORIES_SIZE;
public BookmarkCategoriesScreen(@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 MapWithContentTemplate.Builder builder = new MapWithContentTemplate.Builder();
builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer()));
builder.setContentTemplate(createBookmarkCategoriesListTemplate());
return builder.build();
}
@NonNull
private Header createHeader()
{
final Header.Builder builder = new Header.Builder();
builder.setStartHeaderAction(Action.BACK);
builder.setTitle(getCarContext().getString(R.string.bookmarks));
return builder.build();
}
@NonNull
private ListTemplate createBookmarkCategoriesListTemplate()
{
final List<BookmarkCategory> bookmarkCategories = getBookmarks();
final int categoriesSize = Math.min(bookmarkCategories.size(), MAX_CATEGORIES_SIZE);
final 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 new ListTemplate.Builder().setHeader(createHeader()).setSingleList(builder.build()).build();
}
@NonNull
private static List<BookmarkCategory> getBookmarks()
{
final List<BookmarkCategory> bookmarkCategories = new ArrayList<>(BookmarkManager.INSTANCE.getCategories());
final List<BookmarkCategory> toRemove = new ArrayList<>();
for (final BookmarkCategory bookmarkCategory : bookmarkCategories)
{
if (bookmarkCategory.getBookmarksCount() == 0 || !bookmarkCategory.isVisible())
toRemove.add(bookmarkCategory);
}
bookmarkCategories.removeAll(toRemove);
return bookmarkCategories;
}
}

View File

@@ -0,0 +1,209 @@
package app.organicmaps.car.screens.bookmarks;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.CarContext;
import androidx.car.app.constraints.ConstraintManager;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.DistanceSpan;
import androidx.car.app.model.ForegroundCarColorSpan;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Row;
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.bookmarks.data.Icon;
import app.organicmaps.bookmarks.data.SortedBlock;
import app.organicmaps.car.util.Colors;
import app.organicmaps.car.util.RoutingHelpers;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.util.Distance;
import app.organicmaps.util.Graphics;
import app.organicmaps.util.concurrency.ThreadPool;
import app.organicmaps.util.concurrency.UiThread;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Future;
class BookmarksLoader implements BookmarkManager.BookmarksSortingListener
{
public interface OnBookmarksLoaded
{
void onBookmarksLoaded(@NonNull ItemList bookmarks);
}
// The maximum size should be equal to ConstraintManager.CONTENT_LIMIT_TYPE_LIST.
// However, having more than 50 items results in android.os.TransactionTooLargeException.
// This exception occurs because the data parcel size is too large to be transferred between services.
// The primary cause of this issue is the icons. Even though we have the maximum Icon.TYPE_ICONS.length icons,
// each row contains a unique icon, resulting in serialization of each icon.
private static final int MAX_BOOKMARKS_SIZE = 50;
@Nullable
private Future<?> mBookmarkLoaderTask = null;
@NonNull
private final CarContext mCarContext;
@NonNull
private final OnBookmarksLoaded mOnBookmarksLoaded;
private final long mBookmarkCategoryId;
private final int mBookmarksListSize;
public BookmarksLoader(@NonNull CarContext carContext, @NonNull BookmarkCategory bookmarkCategory, @NonNull OnBookmarksLoaded onBookmarksLoaded)
{
final ConstraintManager constraintManager = carContext.getCarService(ConstraintManager.class);
final int maxCategoriesSize = constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST);
mCarContext = carContext;
mOnBookmarksLoaded = onBookmarksLoaded;
mBookmarkCategoryId = bookmarkCategory.getId();
mBookmarksListSize = Math.min(bookmarkCategory.getBookmarksCount(), Math.min(maxCategoriesSize, MAX_BOOKMARKS_SIZE));
}
public void load()
{
UiThread.runLater(() -> {
BookmarkManager.INSTANCE.addSortingListener(this);
if (sortBookmarks())
return;
final List<Long> bookmarkIds = new ArrayList<>();
for (int i = 0; i < mBookmarksListSize; ++i)
bookmarkIds.add(BookmarkManager.INSTANCE.getBookmarkIdByPosition(mBookmarkCategoryId, i));
loadBookmarks(bookmarkIds);
});
}
public void cancel()
{
BookmarkManager.INSTANCE.removeSortingListener(this);
if (mBookmarkLoaderTask != null)
{
mBookmarkLoaderTask.cancel(true);
mBookmarkLoaderTask = null;
}
}
/**
* Calls BookmarkManager to sort bookmarks.
*
* @return false if the sorting not needed or can't be done.
*/
private boolean sortBookmarks()
{
if (!BookmarkManager.INSTANCE.hasLastSortingType(mBookmarkCategoryId))
return false;
final int sortingType = BookmarkManager.INSTANCE.getLastSortingType(mBookmarkCategoryId);
if (sortingType < 0)
return false;
final Location loc = LocationHelper.from(mCarContext).getSavedLocation();
final boolean hasMyPosition = loc != null;
if (!hasMyPosition && sortingType == BookmarkManager.SORT_BY_DISTANCE)
return false;
final double lat = hasMyPosition ? loc.getLatitude() : 0;
final double lon = hasMyPosition ? loc.getLongitude() : 0;
BookmarkManager.INSTANCE.getSortedCategory(mBookmarkCategoryId, sortingType, hasMyPosition, lat, lon, 0);
return true;
}
private void loadBookmarks(@NonNull List<Long> bookmarksIds)
{
final BookmarkInfo[] bookmarks = new BookmarkInfo[mBookmarksListSize];
for (int i = 0; i < mBookmarksListSize && i < bookmarksIds.size(); ++i)
{
final long id = bookmarksIds.get(i);
bookmarks[i] = new BookmarkInfo(mBookmarkCategoryId, id);
}
mBookmarkLoaderTask = ThreadPool.getWorker().submit(() -> {
final ItemList bookmarksList = createBookmarksList(bookmarks);
UiThread.run(() -> {
cancel();
mOnBookmarksLoaded.onBookmarksLoaded(bookmarksList);
});
});
}
@NonNull
private ItemList createBookmarksList(@NonNull BookmarkInfo[] bookmarks)
{
final Location location = LocationHelper.from(mCarContext).getSavedLocation();
final ItemList.Builder builder = new ItemList.Builder();
final Map<Icon, CarIcon> iconsCache = new HashMap<>();
for (final BookmarkInfo bookmarkInfo : bookmarks)
{
final Row.Builder itemBuilder = new Row.Builder();
itemBuilder.setTitle(bookmarkInfo.getName());
if (!bookmarkInfo.getAddress().isEmpty())
itemBuilder.addText(bookmarkInfo.getAddress());
final CharSequence description = getDescription(bookmarkInfo, location);
if (description.length() != 0)
itemBuilder.addText(description);
final Icon icon = bookmarkInfo.getIcon();
if (!iconsCache.containsKey(icon))
{
final Drawable drawable = Graphics.drawCircleAndImage(icon.argb(),
R.dimen.track_circle_size,
icon.getResId(),
R.dimen.bookmark_icon_size,
mCarContext);
final CarIcon carIcon = new CarIcon.Builder(IconCompat.createWithBitmap(Graphics.drawableToBitmap(drawable))).build();
iconsCache.put(icon, carIcon);
}
itemBuilder.setImage(Objects.requireNonNull(iconsCache.get(icon)));
itemBuilder.setOnClickListener(() -> BookmarkManager.INSTANCE.showBookmarkOnMap(bookmarkInfo.getBookmarkId()));
builder.addItem(itemBuilder.build());
}
return builder.build();
}
@NonNull
private static CharSequence getDescription(@NonNull BookmarkInfo bookmark, @Nullable Location location)
{
final SpannableStringBuilder result = new SpannableStringBuilder("");
if (location != null)
{
result.append(" ");
final Distance distance = bookmark.getDistance(location.getLatitude(), location.getLongitude(), 0.0);
result.setSpan(DistanceSpan.create(RoutingHelpers.createDistance(distance)), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
result.setSpan(ForegroundCarColorSpan.create(Colors.DISTANCE), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (!bookmark.getFeatureType().isEmpty())
{
if (result.length() > 0)
result.append("");
result.append(bookmark.getFeatureType());
}
return result;
}
@Override
public void onBookmarksSortingCompleted(@NonNull SortedBlock[] sortedBlocks, long timestamp)
{
final List<Long> bookmarkIds = new ArrayList<>();
for (final SortedBlock block : sortedBlocks)
bookmarkIds.addAll(block.getBookmarkIds());
loadBookmarks(bookmarkIds);
}
}

View File

@@ -0,0 +1,113 @@
package app.organicmaps.car.screens.bookmarks;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.ListTemplate;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapWithContentTemplate;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.R;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.util.UiHelpers;
public class BookmarksScreen extends BaseMapScreen
{
@NonNull
private final BookmarkCategory mBookmarkCategory;
@NonNull
private final BookmarksLoader mBookmarksLoader;
@Nullable
private ItemList mBookmarksList = null;
private boolean mIsOnSortingScreen = false;
public BookmarksScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer, @NonNull BookmarkCategory bookmarkCategory)
{
super(carContext, surfaceRenderer);
mBookmarkCategory = bookmarkCategory;
mBookmarksLoader = new BookmarksLoader(carContext, mBookmarkCategory, this::onBookmarksLoaded);
}
@NonNull
@Override
public Template onGetTemplate()
{
final MapWithContentTemplate.Builder builder = new MapWithContentTemplate.Builder();
builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer()));
builder.setContentTemplate(createBookmarksListTemplate());
return builder.build();
}
@Override
public void onStop(@NonNull LifecycleOwner owner)
{
if (!mIsOnSortingScreen)
mBookmarksLoader.cancel();
}
@NonNull
private Header createHeader()
{
final Header.Builder builder = new Header.Builder();
builder.setStartHeaderAction(Action.BACK);
builder.setTitle(mBookmarkCategory.getName());
builder.addEndHeaderAction(createSortingAction());
return builder.build();
}
@NonNull
private ListTemplate createBookmarksListTemplate()
{
final ListTemplate.Builder builder = new ListTemplate.Builder();
builder.setHeader(createHeader());
if (mBookmarksList == null)
{
builder.setLoading(true);
mBookmarksLoader.load();
}
else
builder.setSingleList(mBookmarksList);
return builder.build();
}
@NonNull
private Action createSortingAction()
{
final Action.Builder builder = new Action.Builder();
builder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_sort)).build());
builder.setOnClickListener(() -> {
mIsOnSortingScreen = true;
getScreenManager().pushForResult(new SortingScreen(getCarContext(), getSurfaceRenderer(), mBookmarkCategory), this::onSortingResult);
});
return builder.build();
}
private void onBookmarksLoaded(@NonNull ItemList bookmarksList)
{
mBookmarksList = bookmarksList;
invalidate();
}
private void onSortingResult(final Object result)
{
mIsOnSortingScreen = false;
if (Boolean.TRUE.equals(result))
{
mBookmarksList = null;
invalidate();
}
}
}

View File

@@ -0,0 +1,159 @@
package app.organicmaps.car.screens.bookmarks;
import android.location.Location;
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.ListTemplate;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapWithContentTemplate;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.R;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.util.UiHelpers;
import app.organicmaps.location.LocationHelper;
import java.util.Arrays;
import java.util.stream.IntStream;
class SortingScreen extends BaseMapScreen
{
private static final int DEFAULT_SORTING_TYPE = -1;
@NonNull
private final CarIcon mRadioButtonIcon;
@NonNull
private final CarIcon mRadioButtonSelectedIcon;
private final long mBookmarkCategoryId;
private final @BookmarkManager.SortingType int mLastSortingType;
private @BookmarkManager.SortingType int mNewSortingType;
public SortingScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer, @NonNull BookmarkCategory bookmarkCategory)
{
super(carContext, surfaceRenderer);
mRadioButtonIcon = new CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_radio_button_unchecked)).build();
mRadioButtonSelectedIcon = new CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_radio_button_checked)).build();
mBookmarkCategoryId = bookmarkCategory.getId();
mLastSortingType = mNewSortingType = getLastSortingType();
}
@NonNull
@Override
public Template onGetTemplate()
{
final MapWithContentTemplate.Builder builder = new MapWithContentTemplate.Builder();
builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer()));
builder.setContentTemplate(createSortingTypesListTemplate());
return builder.build();
}
@Override
public void onStop(@NonNull LifecycleOwner owner)
{
super.onStop(owner);
final boolean sortingTypeChanged = mNewSortingType != mLastSortingType;
setResult(sortingTypeChanged);
}
@NonNull
private Header createHeader()
{
final Header.Builder builder = new Header.Builder();
builder.setStartHeaderAction(Action.BACK);
builder.setTitle(getCarContext().getString(R.string.sort_bookmarks));
return builder.build();
}
@NonNull
private ListTemplate createSortingTypesListTemplate()
{
final ListTemplate.Builder builder = new ListTemplate.Builder();
builder.setHeader(createHeader());
builder.setSingleList(createSortingTypesList(getAvailableSortingTypes(), getLastAvailableSortingType()));
return builder.build();
}
@NonNull
private ItemList createSortingTypesList(@NonNull final @BookmarkManager.SortingType int[] availableSortingTypes, final int lastSortingType)
{
final ItemList.Builder builder = new ItemList.Builder();
for (int type : IntStream.concat(IntStream.of(DEFAULT_SORTING_TYPE), Arrays.stream(availableSortingTypes)).toArray())
{
final Row.Builder rowBuilder = new Row.Builder();
rowBuilder.setTitle(getCarContext().getString(sortingTypeToStringRes(type)));
if (type == lastSortingType)
rowBuilder.setImage(mRadioButtonSelectedIcon);
else
{
rowBuilder.setImage(mRadioButtonIcon);
rowBuilder.setOnClickListener(() -> {
if (type == DEFAULT_SORTING_TYPE)
BookmarkManager.INSTANCE.resetLastSortingType(mBookmarkCategoryId);
else
BookmarkManager.INSTANCE.setLastSortingType(mBookmarkCategoryId, type);
mNewSortingType = type;
invalidate();
});
}
builder.addItem(rowBuilder.build());
}
return builder.build();
}
@StringRes
private int sortingTypeToStringRes(@BookmarkManager.SortingType int sortingType)
{
return switch (sortingType)
{
case BookmarkManager.SORT_BY_TYPE -> R.string.by_type;
case BookmarkManager.SORT_BY_DISTANCE -> R.string.by_distance;
case BookmarkManager.SORT_BY_TIME -> R.string.by_date;
case BookmarkManager.SORT_BY_NAME -> R.string.by_name;
default -> R.string.by_default;
};
}
@NonNull
@BookmarkManager.SortingType
private int[] getAvailableSortingTypes()
{
final Location loc = LocationHelper.from(getCarContext()).getSavedLocation();
final boolean hasMyPosition = loc != null;
return BookmarkManager.INSTANCE.getAvailableSortingTypes(mBookmarkCategoryId, hasMyPosition);
}
private int getLastSortingType()
{
if (BookmarkManager.INSTANCE.hasLastSortingType(mBookmarkCategoryId))
return BookmarkManager.INSTANCE.getLastSortingType(mBookmarkCategoryId);
return DEFAULT_SORTING_TYPE;
}
private int getLastAvailableSortingType()
{
int currentType = getLastSortingType();
@BookmarkManager.SortingType int[] types = getAvailableSortingTypes();
for (@BookmarkManager.SortingType int type : types)
{
if (type == currentType)
return currentType;
}
return DEFAULT_SORTING_TYPE;
}
}

View File

@@ -0,0 +1,60 @@
package app.organicmaps.car.screens.download;
import android.location.Location;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.car.app.model.Action;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.R;
import app.organicmaps.downloader.CountryItem;
import app.organicmaps.downloader.MapManager;
import app.organicmaps.location.LocationHelper;
class DownloadMapsForFirstLaunchScreen extends DownloadMapsScreen
{
DownloadMapsForFirstLaunchScreen(@NonNull final DownloadMapsScreenBuilder builder)
{
super(builder);
disableCancelAction();
getMissingMaps().add(CountryItem.fill(DownloaderHelpers.WORLD_MAPS[0]));
getMissingMaps().add(CountryItem.fill(DownloaderHelpers.WORLD_MAPS[1]));
}
@Override
public void onResume(@NonNull LifecycleOwner owner)
{
// Attempting to streamline initial download by including the current country in the list of missing maps for simultaneous retrieval.
final Location location = LocationHelper.from(getCarContext()).getSavedLocation();
if (location == null)
return;
final String countryId = MapManager.nativeFindCountry(location.getLatitude(), location.getLongitude());
if (TextUtils.isEmpty(countryId))
return;
final CountryItem countryItem = CountryItem.fill(countryId);
if (!countryItem.present)
getMissingMaps().add(countryItem);
}
@NonNull
@Override
protected String getTitle()
{
return getCarContext().getString(R.string.download_map_title);
}
@NonNull
@Override
protected String getText(@NonNull String mapsSize)
{
return getCarContext().getString(R.string.download_resources, mapsSize);
}
@NonNull
@Override
protected Action getHeaderAction()
{
return Action.APP_ICON;
}
}

View File

@@ -0,0 +1,47 @@
package app.organicmaps.car.screens.download;
import androidx.annotation.NonNull;
import androidx.car.app.model.Action;
import app.organicmaps.R;
import app.organicmaps.routing.ResultCodesHelper;
import java.util.Objects;
class DownloadMapsForRouteScreen extends DownloadMapsScreen
{
@NonNull
private final String mTitle;
DownloadMapsForRouteScreen(@NonNull final DownloadMapsScreenBuilder builder)
{
super(builder);
mTitle = ResultCodesHelper.getDialogTitleSubtitle(builder.mCarContext, builder.mResultCode, Objects.requireNonNull(builder.mMissingMaps).length)
.getTitleMessage().first;
}
@NonNull
@Override
protected String getTitle()
{
return mTitle;
}
@NonNull
@Override
protected String getText(@NonNull String mapsSize)
{
final int mapsCount = getMissingMaps().size();
if (mapsCount == 1)
return DownloaderHelpers.getCountryName(getMissingMaps().get(0)) + "\n" + mapsSize;
return getCarContext().getString(R.string.downloader_status_maps) + " (" + getMissingMaps().size() + "): " + mapsSize;
}
@NonNull
@Override
protected Action getHeaderAction()
{
return Action.BACK;
}
}

View File

@@ -0,0 +1,35 @@
package app.organicmaps.car.screens.download;
import androidx.annotation.NonNull;
import androidx.car.app.model.Action;
import app.organicmaps.R;
class DownloadMapsForViewScreen extends DownloadMapsScreen
{
DownloadMapsForViewScreen(@NonNull final DownloadMapsScreenBuilder builder)
{
super(builder);
}
@NonNull
@Override
protected String getTitle()
{
return getCarContext().getString(R.string.downloader_download_map);
}
@NonNull
@Override
protected String getText(@NonNull String mapsSize)
{
return DownloaderHelpers.getCountryName(getMissingMaps().get(0)) + "\n" + mapsSize;
}
@NonNull
@Override
protected Action getHeaderAction()
{
return Action.BACK;
}
}

View File

@@ -0,0 +1,114 @@
package app.organicmaps.car.screens.download;
import androidx.annotation.NonNull;
import androidx.car.app.model.Action;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.Header;
import androidx.car.app.model.MessageTemplate;
import androidx.car.app.model.Template;
import androidx.core.graphics.drawable.IconCompat;
import app.organicmaps.R;
import app.organicmaps.car.screens.base.BaseScreen;
import app.organicmaps.car.util.Colors;
import app.organicmaps.downloader.CountryItem;
import app.organicmaps.util.StringUtils;
import java.util.List;
public abstract class DownloadMapsScreen extends BaseScreen
{
public static final String MARKER = "Downloader";
private boolean mIsCancelActionDisabled = false;
@NonNull
private final List<CountryItem> mMissingMaps;
DownloadMapsScreen(@NonNull final DownloadMapsScreenBuilder builder)
{
super(builder.mCarContext);
setMarker(MARKER);
setResult(false);
mMissingMaps = DownloaderHelpers.getCountryItemsFromIds(builder.mMissingMaps);
}
@NonNull
@Override
public final Template onGetTemplate()
{
final MessageTemplate.Builder builder = new MessageTemplate.Builder(getText(getMapsSize(mMissingMaps)));
final Header.Builder headerBuilder = new Header.Builder();
headerBuilder.setStartHeaderAction(getHeaderAction());
headerBuilder.setTitle(getTitle());
builder.setHeader(headerBuilder.build());
builder.setIcon(getIcon());
builder.addAction(getDownloadAction());
if (!mIsCancelActionDisabled)
builder.addAction(getCancelAction());
return builder.build();
}
@NonNull
protected abstract String getTitle();
@NonNull
protected abstract String getText(@NonNull final String mapsSize);
@NonNull
protected abstract Action getHeaderAction();
protected void disableCancelAction()
{
mIsCancelActionDisabled = true;
}
@NonNull
protected List<CountryItem> getMissingMaps()
{
return mMissingMaps;
}
@NonNull
private CarIcon getIcon()
{
return new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_download)).build();
}
@NonNull
private Action getDownloadAction()
{
return new Action.Builder()
.setFlags(Action.FLAG_DEFAULT)
.setTitle(getCarContext().getString(R.string.download))
.setBackgroundColor(Colors.BUTTON_ACCEPT)
.setOnClickListener(this::onDownload)
.build();
}
@NonNull
private Action getCancelAction()
{
return new Action.Builder()
.setTitle(getCarContext().getString(R.string.cancel))
.setOnClickListener(this::finish)
.build();
}
private void onDownload()
{
getScreenManager().pushForResult(new DownloaderScreen(getCarContext(), mMissingMaps, mIsCancelActionDisabled), result -> {
setResult(result);
finish();
});
}
@NonNull
private String getMapsSize(@NonNull final List<CountryItem> countries)
{
return StringUtils.getFileSizeString(getCarContext(), DownloaderHelpers.getMapsSize(countries));
}
}

View File

@@ -0,0 +1,81 @@
package app.organicmaps.car.screens.download;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.CarContext;
import app.organicmaps.routing.ResultCodesHelper;
import java.util.Objects;
public class DownloadMapsScreenBuilder
{
public enum DownloaderType
{
FirstLaunch,
BuildRoute,
View
}
private DownloaderType mDownloaderType = null;
@NonNull
final CarContext mCarContext;
@Nullable
String[] mMissingMaps;
int mResultCode = 0;
public DownloadMapsScreenBuilder(@NonNull CarContext carContext)
{
mCarContext = carContext;
}
@NonNull
public DownloadMapsScreenBuilder setDownloaderType(@NonNull DownloaderType downloaderType)
{
mDownloaderType = downloaderType;
return this;
}
@NonNull
public DownloadMapsScreenBuilder setMissingMaps(@NonNull String[] missingMaps)
{
mMissingMaps = missingMaps;
return this;
}
@NonNull
public DownloadMapsScreenBuilder setResultCode(int resultCode)
{
mResultCode = resultCode;
return this;
}
@NonNull
public DownloadMapsScreen build()
{
Objects.requireNonNull(mDownloaderType);
if (mDownloaderType == DownloaderType.BuildRoute)
{
assert mMissingMaps != null;
assert ResultCodesHelper.isDownloadable(mResultCode, mMissingMaps.length);
}
else if (mDownloaderType == DownloaderType.View)
{
assert mMissingMaps != null;
assert mMissingMaps.length == 1;
}
else if (mDownloaderType == DownloaderType.FirstLaunch)
assert mMissingMaps == null;
return switch (mDownloaderType)
{
case FirstLaunch -> new DownloadMapsForFirstLaunchScreen(this);
case BuildRoute -> new DownloadMapsForRouteScreen(this);
case View -> new DownloadMapsForViewScreen(this);
};
}
}

View File

@@ -0,0 +1,66 @@
package app.organicmaps.car.screens.download;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.organicmaps.BuildConfig;
import app.organicmaps.downloader.CountryItem;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public final class DownloaderHelpers
{
static final String[] WORLD_MAPS = new String[]{"World", "WorldCoasts"};
// World maps may be missing only in the F-Droid build.
@SuppressWarnings("ConstantConditions")
public static boolean isWorldMapsDownloadNeeded()
{
if (BuildConfig.FLAVOR.equals("fdroid"))
return !CountryItem.fill(WORLD_MAPS[0]).present || !CountryItem.fill(WORLD_MAPS[1]).present;
return false;
}
@NonNull
static List<CountryItem> getCountryItemsFromIds(@Nullable final String[] countryIds)
{
final List<CountryItem> countryItems = new ArrayList<>();
if (countryIds != null)
{
for (final String countryId : countryIds)
countryItems.add(CountryItem.fill(countryId));
}
return countryItems;
}
static long getMapsSize(@NonNull final Collection<CountryItem> countries)
{
long totalSize = 0;
for (final CountryItem item : countries)
totalSize += item.totalSize;
return totalSize;
}
@NonNull
static String getCountryName(@NonNull CountryItem country)
{
boolean hasParent = !CountryItem.isRoot(country.topmostParentId) && !TextUtils.isEmpty(country.topmostParentName);
final StringBuilder sb = new StringBuilder();
if (hasParent)
{
sb.append(country.topmostParentName);
sb.append("");
}
sb.append(country.name);
return sb.toString();
}
private DownloaderHelpers() {}
}

View File

@@ -0,0 +1,187 @@
package app.organicmaps.car.screens.download;
import android.text.TextUtils;
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.Header;
import androidx.car.app.model.MessageTemplate;
import androidx.car.app.model.Template;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.R;
import app.organicmaps.car.screens.ErrorScreen;
import app.organicmaps.car.screens.base.BaseScreen;
import app.organicmaps.downloader.CountryItem;
import app.organicmaps.downloader.MapManager;
import app.organicmaps.util.StringUtils;
import app.organicmaps.util.concurrency.UiThread;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class DownloaderScreen extends BaseScreen
{
@NonNull
private final Map<String, CountryItem> mMissingMaps;
private final long mTotalSize;
private final boolean mIsCancelActionDisabled;
private final boolean mIsAppRefreshEnabled;
private long mDownloadedMapsSize = 0;
private int mSubscriptionSlot = 0;
private boolean mIsDownloadFailed = false;
@NonNull
private final MapManager.StorageCallback mStorageCallback = new MapManager.StorageCallback()
{
@Override
public void onStatusChanged(@NonNull final List<MapManager.StorageCallbackData> data)
{
for (final MapManager.StorageCallbackData item : data)
{
if (item.newStatus == CountryItem.STATUS_FAILED)
{
onError(item);
return;
}
final CountryItem map = mMissingMaps.get(item.countryId);
if (map == null)
continue;
map.update();
if (map.present)
{
mDownloadedMapsSize += map.totalSize;
mMissingMaps.remove(map.id);
}
}
if (mMissingMaps.isEmpty())
{
setResult(true);
UiThread.runLater(DownloaderScreen.this::finish);
}
else
invalidate();
}
@Override
public void onProgress(String countryId, long localSize, long remoteSize)
{
if (!mIsAppRefreshEnabled || TextUtils.isEmpty(countryId))
return;
final CountryItem item = mMissingMaps.get(countryId);
if (item != null)
{
item.update();
invalidate();
}
}
};
DownloaderScreen(@NonNull final CarContext carContext, @NonNull final List<CountryItem> missingMaps, final boolean isCancelActionDisabled)
{
super(carContext);
setMarker(DownloadMapsScreen.MARKER);
setResult(false);
MapManager.nativeEnableDownloadOn3g();
mMissingMaps = new HashMap<>();
for (final CountryItem item : missingMaps)
mMissingMaps.put(item.id, item);
mTotalSize = DownloaderHelpers.getMapsSize(mMissingMaps.values());
mIsCancelActionDisabled = isCancelActionDisabled;
mIsAppRefreshEnabled = carContext.getCarService(ConstraintManager.class).isAppDrivenRefreshEnabled();
}
@Override
public void onResume(@NonNull LifecycleOwner owner)
{
if (mSubscriptionSlot == 0)
mSubscriptionSlot = MapManager.nativeSubscribe(mStorageCallback);
for (final var item : mMissingMaps.entrySet())
{
item.getValue().update();
MapManager.startDownload(item.getKey());
}
}
@Override
public void onPause(@NonNull LifecycleOwner owner)
{
if (!mIsDownloadFailed)
cancelMapsDownloading();
if (mSubscriptionSlot != 0)
{
MapManager.nativeUnsubscribe(mSubscriptionSlot);
mSubscriptionSlot = 0;
}
}
@NonNull
@Override
public Template onGetTemplate()
{
final MessageTemplate.Builder builder = new MessageTemplate.Builder(getText());
builder.setLoading(true);
final Header.Builder headerBuilder = new Header.Builder();
if (mIsCancelActionDisabled)
headerBuilder.setStartHeaderAction(Action.APP_ICON);
else
headerBuilder.setStartHeaderAction(Action.BACK);
headerBuilder.setTitle(getCarContext().getString(R.string.notification_channel_downloader));
builder.setHeader(headerBuilder.build());
return builder.build();
}
@NonNull
private String getText()
{
if (!mIsAppRefreshEnabled)
return getCarContext().getString(R.string.downloader_loading_ios);
final long downloadedSize = getDownloadedSize();
final String progressPercent = StringUtils.formatPercent((double) downloadedSize / mTotalSize);
final String totalSizeStr = StringUtils.getFileSizeString(getCarContext(), mTotalSize);
final String downloadedSizeStr = StringUtils.getFileSizeString(getCarContext(), downloadedSize);
return progressPercent + "\n" + downloadedSizeStr + " / " + totalSizeStr;
}
private long getDownloadedSize()
{
long downloadedSize = 0;
for (final CountryItem map : mMissingMaps.values())
downloadedSize += map.downloadedBytes;
return downloadedSize + mDownloadedMapsSize;
}
private void onError(@NonNull final MapManager.StorageCallbackData data)
{
mIsDownloadFailed = true;
final ErrorScreen.Builder builder = new ErrorScreen.Builder(getCarContext())
.setTitle(R.string.country_status_download_failed)
.setErrorMessage(MapManager.getErrorCodeStrRes(data.errorCode))
.setPositiveButton(R.string.downloader_retry, null);
if (!mIsCancelActionDisabled)
builder.setNegativeButton(R.string.cancel, this::finish);
getScreenManager().push(builder.build());
}
private void cancelMapsDownloading()
{
for (final String map : mMissingMaps.keySet())
MapManager.nativeCancel(map);
}
}

View File

@@ -0,0 +1,72 @@
package app.organicmaps.car.screens.permissions;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationManagerCompat;
import app.organicmaps.R;
import app.organicmaps.base.BaseMwmFragmentActivity;
import app.organicmaps.util.LocationUtils;
import java.util.Objects;
public class RequestPermissionsActivity extends BaseMwmFragmentActivity
{
private static final String[] LOCATION_PERMISSIONS = new String[]{ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION};
@Nullable
private ActivityResultLauncher<String[]> mPermissionsRequest;
@Override
protected void onSafeCreate(@Nullable Bundle savedInstanceState)
{
super.onSafeCreate(savedInstanceState);
setContentView(R.layout.activity_request_permissions);
findViewById(R.id.btn_grant_permissions).setOnClickListener(unused -> openAppPermissionSettings());
mPermissionsRequest = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(),
(grantedPermissions) -> closeIfPermissionsGranted());
mPermissionsRequest.launch(LOCATION_PERMISSIONS);
}
@Override
protected void onResume()
{
super.onResume();
closeIfPermissionsGranted();
}
@Override
protected void onSafeDestroy()
{
super.onSafeDestroy();
Objects.requireNonNull(mPermissionsRequest).unregister();
mPermissionsRequest = null;
}
private void openAppPermissionSettings()
{
final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", getPackageName(), null));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
private void closeIfPermissionsGranted()
{
if (!LocationUtils.checkLocationPermission(this))
return;
NotificationManagerCompat.from(this).cancel(RequestPermissionsScreenWithNotification.NOTIFICATION_ID);
finish();
}
}

View File

@@ -0,0 +1,29 @@
package app.organicmaps.car.screens.permissions;
import static android.Manifest.permission.POST_NOTIFICATIONS;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
import androidx.car.app.Screen;
import androidx.core.content.ContextCompat;
import app.organicmaps.util.log.Logger;
public class RequestPermissionsScreenBuilder
{
private static final String TAG = RequestPermissionsScreenBuilder.class.getSimpleName();
public static Screen build(@NonNull CarContext carContext, @NonNull Runnable permissionsGrantedCallback)
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(carContext, POST_NOTIFICATIONS) != PERMISSION_GRANTED)
{
Logger.w(TAG, "Permission POST_NOTIFICATIONS is not granted, using API-based permissions request");
return new RequestPermissionsScreenWithApi(carContext, permissionsGrantedCallback);
}
return new RequestPermissionsScreenWithNotification(carContext, permissionsGrantedCallback);
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More