mirror of
https://codeberg.org/comaps/comaps
synced 2025-12-27 08:23:38 +00:00
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:
255
android/app/src/main/java/app/organicmaps/ChartController.java
Normal file
255
android/app/src/main/java/app/organicmaps/ChartController.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
487
android/app/src/main/java/app/organicmaps/Framework.java
Normal file
487
android/app/src/main/java/app/organicmaps/Framework.java
Normal 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);
|
||||
}
|
||||
419
android/app/src/main/java/app/organicmaps/Map.java
Normal file
419
android/app/src/main/java/app/organicmaps/Map.java
Normal 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);
|
||||
}
|
||||
206
android/app/src/main/java/app/organicmaps/MapFragment.java
Normal file
206
android/app/src/main/java/app/organicmaps/MapFragment.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
2474
android/app/src/main/java/app/organicmaps/MwmActivity.java
Normal file
2474
android/app/src/main/java/app/organicmaps/MwmActivity.java
Normal file
File diff suppressed because it is too large
Load Diff
364
android/app/src/main/java/app/organicmaps/MwmApplication.java
Normal file
364
android/app/src/main/java/app/organicmaps/MwmApplication.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
137
android/app/src/main/java/app/organicmaps/PanelAnimator.java
Normal file
137
android/app/src/main/java/app/organicmaps/PanelAnimator.java
Normal 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);
|
||||
}
|
||||
}
|
||||
217
android/app/src/main/java/app/organicmaps/SplashActivity.java
Normal file
217
android/app/src/main/java/app/organicmaps/SplashActivity.java
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
23
android/app/src/main/java/app/organicmaps/api/Const.java
Normal file
23
android/app/src/main/java/app/organicmaps/api/Const.java
Normal 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() {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package app.organicmaps.bookmarks;
|
||||
|
||||
interface CategoryListCallback
|
||||
{
|
||||
void onAddButtonClick();
|
||||
void onImportButtonClick();
|
||||
void onExportButtonClick();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package app.organicmaps.bookmarks;
|
||||
|
||||
public interface DataChangedListener
|
||||
{
|
||||
void onChanged();
|
||||
}
|
||||
486
android/app/src/main/java/app/organicmaps/bookmarks/Holders.java
Normal file
486
android/app/src/main/java/app/organicmaps/bookmarks/Holders.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package app.organicmaps.bookmarks.data;
|
||||
|
||||
import android.widget.ImageView;
|
||||
|
||||
public interface IconClickListener
|
||||
{
|
||||
void onItemClick(ImageView v, int position);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package app.organicmaps.bookmarks.data;
|
||||
|
||||
public enum RoadWarningMarkType
|
||||
{
|
||||
TOLL,
|
||||
FERRY,
|
||||
DIRTY,
|
||||
UNKNOWN
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
289
android/app/src/main/java/app/organicmaps/car/CarAppSession.java
Normal file
289
android/app/src/main/java/app/organicmaps/car/CarAppSession.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package app.organicmaps.car.screens.base;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.car.app.CarContext;
|
||||
import androidx.car.app.Screen;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
|
||||
public abstract class BaseScreen extends Screen implements DefaultLifecycleObserver
|
||||
{
|
||||
public BaseScreen(@NonNull CarContext carContext)
|
||||
{
|
||||
super(carContext);
|
||||
getLifecycle().addObserver(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user