[android-auto] Add bookmarks sorting

Signed-off-by: Andrew Shkrob <andrew.shkrob.social@yandex.by>
This commit is contained in:
Andrew Shkrob 2024-08-31 13:06:30 +02:00 committed by Roman Tsisyk
parent 8174ca7e6e
commit 4a1897caca
4 changed files with 316 additions and 29 deletions

View file

@ -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

View file

@ -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<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 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<Icon, CarIcon> 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<Long> bookmarkIds = new ArrayList<>();
for (final SortedBlock block : sortedBlocks)
bookmarkIds.addAll(block.getBookmarkIds());
loadBookmarks(bookmarkIds);
}
}

View file

@ -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();
}
}
}

View file

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