diff --git a/android/app/src/main/java/app/organicmaps/bookmarks/data/Icon.java b/android/app/src/main/java/app/organicmaps/bookmarks/data/Icon.java index 7ef8693449..2a6b01d7e0 100644 --- a/android/app/src/main/java/app/organicmaps/bookmarks/data/Icon.java +++ b/android/app/src/main/java/app/organicmaps/bookmarks/data/Icon.java @@ -7,6 +7,8 @@ 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; @@ -232,17 +234,17 @@ public class Icon implements Parcelable @Override public boolean equals(Object o) { - if (o == null || !(o instanceof Icon)) - return false; - final Icon comparedIcon = (Icon) o; - return mColor == comparedIcon.getColor(); + if (this == o) + return true; + if (o instanceof Icon comparedIcon) + return mColor == comparedIcon.mColor && mType == comparedIcon.mType; + return false; } @Override - @PredefinedColor public int hashCode() { - return mColor; + return Objects.hashCode(mColor, mType); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator<>() diff --git a/android/app/src/main/java/app/organicmaps/car/screens/BookmarksScreen.java b/android/app/src/main/java/app/organicmaps/car/screens/BookmarksScreen.java deleted file mode 100644 index 479a18f3be..0000000000 --- a/android/app/src/main/java/app/organicmaps/car/screens/BookmarksScreen.java +++ /dev/null @@ -1,169 +0,0 @@ -package app.organicmaps.car.screens; - -import static java.util.Objects.requireNonNull; - -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.Action; -import androidx.car.app.model.CarIcon; -import androidx.car.app.model.DistanceSpan; -import androidx.car.app.model.ForegroundCarColorSpan; -import androidx.car.app.model.Header; -import androidx.car.app.model.ItemList; -import androidx.car.app.model.Row; -import androidx.car.app.model.Template; -import androidx.car.app.navigation.model.MapTemplate; -import androidx.core.graphics.drawable.IconCompat; - -import app.organicmaps.R; -import app.organicmaps.bookmarks.data.BookmarkCategory; -import app.organicmaps.bookmarks.data.BookmarkInfo; -import app.organicmaps.bookmarks.data.BookmarkManager; -import app.organicmaps.car.SurfaceRenderer; -import app.organicmaps.car.screens.base.BaseMapScreen; -import app.organicmaps.car.util.Colors; -import app.organicmaps.car.util.RoutingHelpers; -import app.organicmaps.car.util.UiHelpers; -import app.organicmaps.location.LocationHelper; -import app.organicmaps.util.Distance; -import app.organicmaps.util.Graphics; - -import java.util.ArrayList; -import java.util.List; - -public class BookmarksScreen extends BaseMapScreen -{ - private final int MAX_CATEGORIES_SIZE; - - @Nullable - private BookmarkCategory mBookmarkCategory; - - public BookmarksScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer) - { - super(carContext, surfaceRenderer); - final ConstraintManager constraintManager = getCarContext().getCarService(ConstraintManager.class); - MAX_CATEGORIES_SIZE = constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST); - } - - private BookmarksScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer, @NonNull BookmarkCategory bookmarkCategory) - { - this(carContext, surfaceRenderer); - mBookmarkCategory = bookmarkCategory; - } - - @NonNull - @Override - public Template onGetTemplate() - { - final MapTemplate.Builder builder = new MapTemplate.Builder(); - builder.setHeader(createHeader()); - builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer())); - builder.setItemList(mBookmarkCategory == null ? createBookmarkCategoriesList() : createBookmarksList()); - return builder.build(); - } - - @NonNull - private Header createHeader() - { - final Header.Builder builder = new Header.Builder(); - builder.setStartHeaderAction(Action.BACK); - builder.setTitle(mBookmarkCategory == null ? getCarContext().getString(R.string.bookmarks) : mBookmarkCategory.getName()); - return builder.build(); - } - - @NonNull - private ItemList createBookmarkCategoriesList() - { - final List bookmarkCategories = getBookmarks(); - final int categoriesSize = Math.min(bookmarkCategories.size(), MAX_CATEGORIES_SIZE); - - ItemList.Builder builder = new ItemList.Builder(); - for (int i = 0; i < categoriesSize; ++i) - { - final BookmarkCategory bookmarkCategory = bookmarkCategories.get(i); - - Row.Builder itemBuilder = new Row.Builder(); - itemBuilder.setTitle(bookmarkCategory.getName()); - itemBuilder.addText(bookmarkCategory.getDescription()); - itemBuilder.setOnClickListener(() -> getScreenManager().push(new BookmarksScreen(getCarContext(), getSurfaceRenderer(), bookmarkCategory))); - itemBuilder.setBrowsable(true); - builder.addItem(itemBuilder.build()); - } - return builder.build(); - } - - @NonNull - private ItemList createBookmarksList() - { - final long bookmarkCategoryId = requireNonNull(mBookmarkCategory).getId(); - final int bookmarkCategoriesSize = Math.min(mBookmarkCategory.getBookmarksCount(), MAX_CATEGORIES_SIZE); - - ItemList.Builder builder = new ItemList.Builder(); - for (int i = 0; i < bookmarkCategoriesSize; ++i) - { - final long bookmarkId = BookmarkManager.INSTANCE.getBookmarkIdByPosition(bookmarkCategoryId, i); - final BookmarkInfo bookmarkInfo = new BookmarkInfo(bookmarkCategoryId, bookmarkId); - - final Row.Builder itemBuilder = new Row.Builder(); - itemBuilder.setTitle(bookmarkInfo.getName()); - if (!bookmarkInfo.getAddress().isEmpty()) - itemBuilder.addText(bookmarkInfo.getAddress()); - final CharSequence description = getDescription(bookmarkInfo); - if (description.length() != 0) - itemBuilder.addText(description); - final Drawable icon = Graphics.drawCircleAndImage(bookmarkInfo.getIcon().argb(), - R.dimen.track_circle_size, - bookmarkInfo.getIcon().getResId(), - R.dimen.bookmark_icon_size, - getCarContext()); - itemBuilder.setImage(new CarIcon.Builder(IconCompat.createWithBitmap(Graphics.drawableToBitmap(icon))).build()); - itemBuilder.setOnClickListener(() -> BookmarkManager.INSTANCE.showBookmarkOnMap(bookmarkId)); - builder.addItem(itemBuilder.build()); - } - return builder.build(); - } - - @NonNull - private CharSequence getDescription(final BookmarkInfo bookmark) - { - final SpannableStringBuilder result = new SpannableStringBuilder(" "); - final Location loc = LocationHelper.from(getCarContext()).getSavedLocation(); - if (loc != null) - { - final Distance distance = bookmark.getDistance(loc.getLatitude(), loc.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 (loc != null && !bookmark.getFeatureType().isEmpty()) - { - result.append(" • "); - result.append(bookmark.getFeatureType()); - } - - return result; - } - - @NonNull - private static List getBookmarks() - { - final List bookmarkCategories = new ArrayList<>(BookmarkManager.INSTANCE.getCategories()); - - final List toRemove = new ArrayList<>(); - for (final BookmarkCategory bookmarkCategory : bookmarkCategories) - { - if (bookmarkCategory.getBookmarksCount() == 0 || !bookmarkCategory.isVisible()) - toRemove.add(bookmarkCategory); - } - bookmarkCategories.removeAll(toRemove); - - return bookmarkCategories; - } -} diff --git a/android/app/src/main/java/app/organicmaps/car/screens/MapScreen.java b/android/app/src/main/java/app/organicmaps/car/screens/MapScreen.java index efdfb266ef..7e986540b5 100644 --- a/android/app/src/main/java/app/organicmaps/car/screens/MapScreen.java +++ b/android/app/src/main/java/app/organicmaps/car/screens/MapScreen.java @@ -16,6 +16,7 @@ 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.util.SuggestionsHelpers; import app.organicmaps.car.util.UiHelpers; @@ -127,6 +128,6 @@ public class MapScreen extends BaseMapScreen // Details in UiHelpers.createSettingsAction() if (getScreenManager().getTop() != this) return; - getScreenManager().push(new BookmarksScreen(getCarContext(), getSurfaceRenderer())); + getScreenManager().push(new BookmarkCategoriesScreen(getCarContext(), getSurfaceRenderer())); } } diff --git a/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarkCategoriesScreen.java b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarkCategoriesScreen.java new file mode 100644 index 0000000000..1aee66a34f --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarkCategoriesScreen.java @@ -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.Row; +import androidx.car.app.model.Template; +import androidx.car.app.navigation.model.MapTemplate; + +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 MapTemplate.Builder builder = new MapTemplate.Builder(); + builder.setHeader(createHeader()); + builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer())); + builder.setItemList(createBookmarkCategoriesList()); + 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 ItemList createBookmarkCategoriesList() + { + final List 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 builder.build(); + } + + @NonNull + private static List getBookmarks() + { + final List bookmarkCategories = new ArrayList<>(BookmarkManager.INSTANCE.getCategories()); + + final List toRemove = new ArrayList<>(); + for (final BookmarkCategory bookmarkCategory : bookmarkCategories) + { + if (bookmarkCategory.getBookmarksCount() == 0 || !bookmarkCategory.isVisible()) + toRemove.add(bookmarkCategory); + } + bookmarkCategories.removeAll(toRemove); + + return bookmarkCategories; + } +} 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 new file mode 100644 index 0000000000..1063116d3d --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksLoader.java @@ -0,0 +1,124 @@ +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.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.HashMap; +import java.util.Map; +import java.util.Objects; + +public class BookmarksLoader +{ + 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; + + public static void load(@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 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); + } + + ThreadPool.getWorker().submit(() -> { + final ItemList bookmarksList = createBookmarksList(carContext, bookmarks); + UiThread.run(() -> onBookmarksLoaded.onBookmarksLoaded(bookmarksList)); + }); + }); + } + + @NonNull + private static ItemList createBookmarksList(@NonNull CarContext carContext, @NonNull BookmarkInfo[] bookmarks) + { + final Location location = LocationHelper.from(carContext).getSavedLocation(); + final ItemList.Builder builder = new ItemList.Builder(); + final Map 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, + carContext); + 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) + { + 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()) + { + result.append(" • "); + result.append(bookmark.getFeatureType()); + } + } + + return result; + } +} 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 new file mode 100644 index 0000000000..7a9aca331b --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/car/screens/bookmarks/BookmarksScreen.java @@ -0,0 +1,61 @@ +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.Header; +import androidx.car.app.model.ItemList; +import androidx.car.app.model.Pane; +import androidx.car.app.model.Template; +import androidx.car.app.navigation.model.MapTemplate; + +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; + + @Nullable + private ItemList mBookmarksList = null; + + public BookmarksScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer, @NonNull BookmarkCategory bookmarkCategory) + { + super(carContext, surfaceRenderer); + mBookmarkCategory = bookmarkCategory; + } + + @NonNull + @Override + public Template onGetTemplate() + { + final MapTemplate.Builder builder = new MapTemplate.Builder(); + + builder.setHeader(createHeader()); + builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer())); + if (mBookmarksList == null) + { + builder.setPane(new Pane.Builder().setLoading(true).build()); + BookmarksLoader.load(getCarContext(), mBookmarkCategory, (bookmarksList) -> { + mBookmarksList = bookmarksList; + invalidate(); + }); + } + else + builder.setItemList(mBookmarksList); + return builder.build(); + } + + @NonNull + private Header createHeader() + { + final Header.Builder builder = new Header.Builder(); + builder.setStartHeaderAction(Action.BACK); + builder.setTitle(mBookmarkCategory.getName()); + return builder.build(); + } +}