From 4a1897cacae9e372f4615ce5136d4eb504361b7e Mon Sep 17 00:00:00 2001 From: Andrew Shkrob Date: Sat, 31 Aug 2024 13:06:30 +0200 Subject: [PATCH] [android-auto] Add bookmarks sorting Signed-off-by: Andrew Shkrob --- .../bookmarks/data/BookmarkManager.java | 2 +- .../screens/bookmarks/BookmarksLoader.java | 133 ++++++++++++--- .../screens/bookmarks/BookmarksScreen.java | 51 +++++- .../car/screens/bookmarks/SortingScreen.java | 159 ++++++++++++++++++ 4 files changed, 316 insertions(+), 29 deletions(-) create mode 100644 android/app/src/main/java/app/organicmaps/car/screens/bookmarks/SortingScreen.java diff --git a/android/app/src/main/java/app/organicmaps/bookmarks/data/BookmarkManager.java b/android/app/src/main/java/app/organicmaps/bookmarks/data/BookmarkManager.java index 1453e0b928..c30bfdd7b7 100644 --- a/android/app/src/main/java/app/organicmaps/bookmarks/data/BookmarkManager.java +++ b/android/app/src/main/java/app/organicmaps/bookmarks/data/BookmarkManager.java @@ -916,7 +916,7 @@ public enum BookmarkManager public interface BookmarksSortingListener { void onBookmarksSortingCompleted(@NonNull SortedBlock[] sortedBlocks, long timestamp); - void onBookmarksSortingCancelled(long timestamp); + default void onBookmarksSortingCancelled(long timestamp) {}; } public interface BookmarksSharingListener diff --git a/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksLoader.java b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksLoader.java index 1063116d3d..c5440cd975 100644 --- a/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksLoader.java +++ b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksLoader.java @@ -21,6 +21,7 @@ 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; @@ -29,11 +30,14 @@ 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; -public class BookmarksLoader +class BookmarksLoader implements BookmarkManager.BookmarksSortingListener { public interface OnBookmarksLoaded { @@ -47,32 +51,102 @@ public class BookmarksLoader // each row contains a unique icon, resulting in serialization of each icon. private static final int MAX_BOOKMARKS_SIZE = 50; - public static void load(@NonNull CarContext carContext, @NonNull BookmarkCategory bookmarkCategory, @NonNull OnBookmarksLoaded onBookmarksLoaded) + @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) { - UiThread.run(() -> { - final ConstraintManager constraintManager = carContext.getCarService(ConstraintManager.class); - final int maxCategoriesSize = constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST); - final long bookmarkCategoryId = bookmarkCategory.getId(); - final int bookmarkCategoriesSize = Math.min(bookmarkCategory.getBookmarksCount(), Math.min(maxCategoriesSize, MAX_BOOKMARKS_SIZE)); + final ConstraintManager constraintManager = carContext.getCarService(ConstraintManager.class); + final int maxCategoriesSize = constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST); - final BookmarkInfo[] bookmarks = new BookmarkInfo[bookmarkCategoriesSize]; - for (int i = 0; i < bookmarkCategoriesSize; ++i) - { - final long id = BookmarkManager.INSTANCE.getBookmarkIdByPosition(bookmarkCategoryId, i); - bookmarks[i] = new BookmarkInfo(bookmarkCategoryId, id); - } + mCarContext = carContext; + mOnBookmarksLoaded = onBookmarksLoaded; + mBookmarkCategoryId = bookmarkCategory.getId(); + mBookmarksListSize = Math.min(bookmarkCategory.getBookmarksCount(), Math.min(maxCategoriesSize, MAX_BOOKMARKS_SIZE)); + } - ThreadPool.getWorker().submit(() -> { - final ItemList bookmarksList = createBookmarksList(carContext, bookmarks); - UiThread.run(() -> onBookmarksLoaded.onBookmarksLoaded(bookmarksList)); + public void load() + { + UiThread.runLater(() -> { + BookmarkManager.INSTANCE.addSortingListener(this); + if (sortBookmarks()) + return; + + final List 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 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 static ItemList createBookmarksList(@NonNull CarContext carContext, @NonNull BookmarkInfo[] bookmarks) + private ItemList createBookmarksList(@NonNull BookmarkInfo[] bookmarks) { - final Location location = LocationHelper.from(carContext).getSavedLocation(); + final Location location = LocationHelper.from(mCarContext).getSavedLocation(); final ItemList.Builder builder = new ItemList.Builder(); final Map iconsCache = new HashMap<>(); for (final BookmarkInfo bookmarkInfo : bookmarks) @@ -91,7 +165,7 @@ public class BookmarksLoader R.dimen.track_circle_size, icon.getResId(), R.dimen.bookmark_icon_size, - carContext); + mCarContext); final CarIcon carIcon = new CarIcon.Builder(IconCompat.createWithBitmap(Graphics.drawableToBitmap(drawable))).build(); iconsCache.put(icon, carIcon); } @@ -105,20 +179,31 @@ public class BookmarksLoader @NonNull private static CharSequence getDescription(@NonNull BookmarkInfo bookmark, @Nullable Location location) { - final SpannableStringBuilder result = new SpannableStringBuilder(" "); + 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 (!bookmark.getFeatureType().isEmpty()) + { + if (result.length() > 0) result.append(" • "); - result.append(bookmark.getFeatureType()); - } + result.append(bookmark.getFeatureType()); } return result; } + + @Override + public void onBookmarksSortingCompleted(@NonNull SortedBlock[] sortedBlocks, long timestamp) + { + final List bookmarkIds = new ArrayList<>(); + for (final SortedBlock block : sortedBlocks) + bookmarkIds.addAll(block.getBookmarkIds()); + loadBookmarks(bookmarkIds); + } } diff --git a/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksScreen.java b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksScreen.java index fcaf96ebbd..1263596781 100644 --- a/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksScreen.java +++ b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksScreen.java @@ -4,12 +4,16 @@ 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; @@ -20,13 +24,19 @@ 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 @@ -39,12 +49,20 @@ public class BookmarksScreen extends BaseMapScreen 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(); } @@ -57,14 +75,39 @@ public class BookmarksScreen extends BaseMapScreen if (mBookmarksList == null) { builder.setLoading(true); - BookmarksLoader.load(getCarContext(), mBookmarkCategory, (bookmarksList) -> { - mBookmarksList = bookmarksList; - invalidate(); - }); + 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(); + } + } } diff --git a/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/SortingScreen.java b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/SortingScreen.java new file mode 100644 index 0000000000..0771c09372 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/SortingScreen.java @@ -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; + } +}