diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 80b7df683e..9fa285fc6b 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -40,6 +40,7 @@ Code contributions: Konstantin Pastbin Nishant Bhandari Sebastiao Sousa + Fábio Gomes Porting to Tizen platform: Sergey Pisarchik diff --git a/android/app/src/main/cpp/app/organicmaps/Framework.cpp b/android/app/src/main/cpp/app/organicmaps/Framework.cpp index 371d503a20..643a87a92d 100644 --- a/android/app/src/main/cpp/app/organicmaps/Framework.cpp +++ b/android/app/src/main/cpp/app/organicmaps/Framework.cpp @@ -1854,6 +1854,43 @@ Java_app_organicmaps_Framework_nativeDeleteSavedRoutePoints(JNIEnv *, jclass) frm()->GetRoutingManager().DeleteSavedRoutePoints(); } +JNIEXPORT jboolean JNICALL +Java_app_organicmaps_Framework_nativeHasSavedUserRoute(JNIEnv * env, jclass, jstring routeName) +{ + return frm()->GetRoutingManager().HasSavedUserRoute(jni::ToNativeString(env, routeName)); +} + +JNIEXPORT void JNICALL +Java_app_organicmaps_Framework_nativeLoadUserRoutePoints(JNIEnv * env, jclass, jstring routeName) +{ + frm()->GetRoutingManager().LoadUserRoutePoints(g_loadRouteHandler, jni::ToNativeString(env, routeName)); +} + +JNIEXPORT void JNICALL +Java_app_organicmaps_Framework_nativeSaveUserRoutePoints(JNIEnv * env, jclass, jstring routeName) +{ + frm()->GetRoutingManager().SaveUserRoutePoints(jni::ToNativeString(env, routeName)); +} + +JNIEXPORT void JNICALL +Java_app_organicmaps_Framework_nativeDeleteUserRoute(JNIEnv * env, jclass, jstring routeName) +{ + frm()->GetRoutingManager().DeleteUserRoute(jni::ToNativeString(env, routeName)); +} + +JNIEXPORT void JNICALL +Java_app_organicmaps_Framework_nativeRenameUserRoute(JNIEnv * env, jclass, jstring oldRouteName, jstring newRouteName) +{ + frm()->GetRoutingManager().RenameUserRoute(jni::ToNativeString(env, oldRouteName), jni::ToNativeString(env, newRouteName)); +} + +JNIEXPORT jobjectArray JNICALL +Java_app_organicmaps_Framework_nativeGetUserRouteNames(JNIEnv * env, jclass) +{ + auto routeNames = frm()->GetRoutingManager().GetUserRouteNames(); + return jni::ToJavaStringArray(env, routeNames); +} + JNIEXPORT void JNICALL Java_app_organicmaps_Framework_nativeShowFeature(JNIEnv * env, jclass, jobject featureId) { diff --git a/android/app/src/main/java/app/organicmaps/Framework.java b/android/app/src/main/java/app/organicmaps/Framework.java index 76c09d6d7b..8ca94cc63f 100644 --- a/android/app/src/main/java/app/organicmaps/Framework.java +++ b/android/app/src/main/java/app/organicmaps/Framework.java @@ -421,6 +421,13 @@ public class Framework public static native void nativeSaveRoutePoints(); public static native void nativeDeleteSavedRoutePoints(); + public static native boolean nativeHasSavedUserRoute(@NonNull String routeName); + public static native void nativeLoadUserRoutePoints(@NonNull String routeName); + public static native void nativeSaveUserRoutePoints(@NonNull String routeName); + public static native void nativeDeleteUserRoute(@NonNull String routeName); + public static native void nativeRenameUserRoute(@NonNull String oldRouteName, @NonNull String newRouteName); + public static native String[] nativeGetUserRouteNames(); + public static native void nativeShowFeature(@NonNull FeatureId featureId); public static native void nativeMakeCrash(); diff --git a/android/app/src/main/java/app/organicmaps/MwmActivity.java b/android/app/src/main/java/app/organicmaps/MwmActivity.java index b584bbb15f..1f834e35ce 100644 --- a/android/app/src/main/java/app/organicmaps/MwmActivity.java +++ b/android/app/src/main/java/app/organicmaps/MwmActivity.java @@ -72,6 +72,7 @@ import app.organicmaps.location.SensorHelper; import app.organicmaps.location.SensorListener; import app.organicmaps.maplayer.MapButtonsController; import app.organicmaps.maplayer.MapButtonsViewModel; +import app.organicmaps.maplayer.MyRoutesFragment; import app.organicmaps.maplayer.ToggleMapLayerFragment; import app.organicmaps.maplayer.isolines.IsolinesManager; import app.organicmaps.maplayer.isolines.IsolinesState; @@ -156,6 +157,7 @@ public class MwmActivity extends BaseMwmFragmentActivity private static final String MAIN_MENU_ID = "MAIN_MENU_BOTTOM_SHEET"; private static final String LAYERS_MENU_ID = "LAYERS_MENU_BOTTOM_SHEET"; + private static final String MYROUTES_MENU_ID = "MYROUTES_MENU_BOTTOM_SHEET"; @Nullable private MapFragment mMapFragment; @@ -774,6 +776,11 @@ public class MwmActivity extends BaseMwmFragmentActivity showBottomSheet(MAIN_MENU_ID); } case help -> showHelp(); + case myRoutes -> + { + closeFloatingPanels(); + showBottomSheet(MYROUTES_MENU_ID); + } } } @@ -892,6 +899,7 @@ public class MwmActivity extends BaseMwmFragmentActivity { closeBottomSheet(LAYERS_MENU_ID); closeBottomSheet(MAIN_MENU_ID); + closeBottomSheet(MYROUTES_MENU_ID); closePlacePage(); } @@ -1147,7 +1155,7 @@ public class MwmActivity extends BaseMwmFragmentActivity public void onBackPressed() { final RoutingController routingController = RoutingController.get(); - if (!closeBottomSheet(MAIN_MENU_ID) && !closeBottomSheet(LAYERS_MENU_ID) && + if (!closeBottomSheet(MAIN_MENU_ID) && !closeBottomSheet(LAYERS_MENU_ID) && !closeBottomSheet(MYROUTES_MENU_ID) && !collapseNavMenu() && !closePlacePage() && !closeSearchToolbar(true, true) && !closeSidePanel() && !closePositionChooser() && !routingController.resetToPlanningStateIfNavigating() && !routingController.cancel()) @@ -2164,6 +2172,8 @@ public class MwmActivity extends BaseMwmFragmentActivity { if (id.equals(LAYERS_MENU_ID)) return new ToggleMapLayerFragment(); + else if (id.equals(MYROUTES_MENU_ID)) + return new MyRoutesFragment(); return null; } diff --git a/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsController.java b/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsController.java index 94cbbc6f9c..791eca6762 100644 --- a/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsController.java +++ b/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsController.java @@ -44,6 +44,8 @@ public class MapButtonsController extends Fragment private View mBottomButtonsFrame; @Nullable private FloatingActionButton mToggleMapLayerButton; + @Nullable + private FloatingActionButton mMyRoutesButton; @Nullable private MyPositionButton mNavMyPosition; @@ -104,6 +106,11 @@ public class MapButtonsController extends Fragment final View myPosition = mFrame.findViewById(R.id.my_position); mNavMyPosition = new MyPositionButton(myPosition, (v) -> mMapButtonClickListener.onMapButtonClick(MapButtons.myPosition)); + mMyRoutesButton = mFrame.findViewById(R.id.btn_myroutes); + if (mMyRoutesButton != null) + { + mMyRoutesButton.setOnClickListener(view -> mMapButtonClickListener.onMapButtonClick(MapButtons.myRoutes)); + } // Some buttons do not exist in navigation mode mToggleMapLayerButton = mFrame.findViewById(R.id.layers_button); if (mToggleMapLayerButton != null) @@ -143,6 +150,8 @@ public class MapButtonsController extends Fragment mButtonsMap.put(MapButtons.bookmarks, bookmarksButton); mButtonsMap.put(MapButtons.search, searchButton); + if (mMyRoutesButton != null) + mButtonsMap.put(MapButtons.myRoutes, mMyRoutesButton); if (mToggleMapLayerButton != null) mButtonsMap.put(MapButtons.toggleMapLayer, mToggleMapLayerButton); if (menuButton != null) @@ -181,6 +190,11 @@ public class MapButtonsController extends Fragment case bookmarks: case menu: UiUtils.showIf(show, buttonView); + break; + case myRoutes: + UiUtils.showIf(show, mMyRoutesButton); + break; + } } @@ -350,7 +364,8 @@ public class MapButtonsController extends Fragment search, bookmarks, menu, - help + help, + myRoutes } public interface MapButtonClickListener diff --git a/android/app/src/main/java/app/organicmaps/maplayer/MyRoutesFragment.java b/android/app/src/main/java/app/organicmaps/maplayer/MyRoutesFragment.java new file mode 100644 index 0000000000..5ebde57c1c --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/maplayer/MyRoutesFragment.java @@ -0,0 +1,266 @@ +package app.organicmaps.maplayer; + +import android.app.AlertDialog; +import android.os.Bundle; +import android.os.SystemClock; +import android.text.InputFilter; +import android.text.InputType; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.button.MaterialButton; + +import java.util.ArrayList; +import java.util.List; + +import app.organicmaps.Framework; +import app.organicmaps.R; +import app.organicmaps.util.bottomsheet.MenuBottomSheetFragment; + +public class MyRoutesFragment extends Fragment +{ + private static final String MYROUTES_MENU_ID = "MYROUTES_MENU_BOTTOM_SHEET"; + @Nullable + private RoutesAdapter mAdapter; + private MapButtonsViewModel mMapButtonsViewModel; + private String mEditText = ""; + private String mDialogCaller = ""; + private RouteBottomSheetItem mCurrentItem = null; + private static final String SAVE_ID = "SAVE_ID"; + private static final String RENAME_ID = "RENAME_ID"; + private static final String DELETE_ID = "DELETE_ID"; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View mRoot = inflater.inflate(R.layout.fragment_myroutes, container, false); + + mMapButtonsViewModel = new ViewModelProvider(requireActivity()).get(MapButtonsViewModel.class); + MaterialButton mCloseButton = mRoot.findViewById(R.id.close_button); + mCloseButton.setOnClickListener(view -> closeMyRoutesBottomSheet()); + + Button mSaveButton = mRoot.findViewById(R.id.save_button); + if (Framework.nativeGetRoutePoints().length >= 2) + mSaveButton.setOnClickListener(view -> onSaveButtonClick()); + else + mSaveButton.setEnabled(false); + + initRecycler(mRoot); + return mRoot; + } + + private void initRecycler(@NonNull View root) + { + RecyclerView recycler = root.findViewById(R.id.recycler); + RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(requireContext(), + LinearLayoutManager.VERTICAL, + false); + recycler.setLayoutManager(layoutManager); + mAdapter = new RoutesAdapter(getRouteItems()); + recycler.setAdapter(mAdapter); + recycler.setNestedScrollingEnabled(false); + } + + private List getRouteItems() + { + String[] savedRouteNames = Framework.nativeGetUserRouteNames(); + List items = new ArrayList<>(); + for (String routeName : savedRouteNames) + items.add(createItem(routeName)); + return items; + } + + private RouteBottomSheetItem createItem(String routeName) + { + return RouteBottomSheetItem.create(routeName, this::onItemTitleClick, this::onItemRenameClick, this::onItemDeleteClick); + } + + private void onSaveButtonClick() + { + mEditText = ""; + mDialogCaller = SAVE_ID; + showTextInputDialog(""); + } + + private void checkSave() + { + String newRouteName = mEditText; + + if (newRouteName.isEmpty()) + return; + if (Framework.nativeHasSavedUserRoute(newRouteName)) + { + showConfirmationDialog(getString(R.string.user_route_overwrite_title, newRouteName), + getString(R.string.user_route_overwrite_body, newRouteName), + R.string.overwrite); + return; + } + save(false); + } + + private void save(boolean isOverwrite) + { + String newRouteName = mEditText; + + if (isOverwrite) + Framework.nativeDeleteUserRoute(newRouteName); + + Framework.nativeSaveUserRoutePoints(newRouteName); + + if (!isOverwrite) + mAdapter.addRoute(createItem(newRouteName)); + } + + private void onItemTitleClick(@NonNull View v, @NonNull RouteBottomSheetItem item) + { + Framework.nativeLoadUserRoutePoints(item.getRouteName()); + } + + private void onItemRenameClick(@NonNull View v, @NonNull RouteBottomSheetItem item) + { + mEditText = ""; + mDialogCaller = RENAME_ID; + mCurrentItem = item; + showTextInputDialog(item.getRouteName()); + } + + private void checkRename() + { + String newRouteName = mEditText; + + if (newRouteName.isEmpty()) + return; + + if (newRouteName.equals(mCurrentItem.getRouteName())) + return; + if (Framework.nativeHasSavedUserRoute(newRouteName)) + { + showConfirmationDialog(getString(R.string.user_route_overwrite_title, newRouteName), + getString(R.string.user_route_overwrite_body, newRouteName), + R.string.overwrite); + return; + } + rename(false); + } + + private void rename(boolean isOverwrite) + { + String newRouteName = mEditText; + + if (isOverwrite) + { + Framework.nativeDeleteUserRoute(newRouteName); + // Sometimes delete takes too long and renaming cannot happen; thus, sleep + SystemClock.sleep(250); // TODO(Fábio Gomes) find a better solution + } + + Framework.nativeRenameUserRoute(mCurrentItem.getRouteName(), newRouteName); + + mAdapter.removeRoute(mCurrentItem); + if (!isOverwrite) + mAdapter.addRoute(createItem(newRouteName)); + } + + private void onItemDeleteClick(@NonNull View v, @NonNull RouteBottomSheetItem item) + { + mDialogCaller = DELETE_ID; + mCurrentItem = item; + showConfirmationDialog(getString(R.string.user_route_delete_title, item.getRouteName()), + getString(R.string.user_route_delete_body), + R.string.delete); + } + + private void delete() + { + Framework.nativeDeleteUserRoute(mCurrentItem.getRouteName()); + mAdapter.removeRoute(mCurrentItem); + } + + private void closeMyRoutesBottomSheet() + { + MenuBottomSheetFragment bottomSheet = + (MenuBottomSheetFragment) requireActivity().getSupportFragmentManager().findFragmentByTag(MYROUTES_MENU_ID); + if (bottomSheet != null) + bottomSheet.dismiss(); + } + + private void showConfirmationDialog(String title, String message, @StringRes int buttonText) + { + AlertDialog dialog = new AlertDialog.Builder(this.getContext(), R.style.MwmTheme_AlertDialog) + .setTitle(title) + .setMessage(message) + .setPositiveButton(buttonText, (dialogInterface, i) -> { + switch (mDialogCaller) { + case SAVE_ID -> save(true); + case RENAME_ID -> rename(true); + case DELETE_ID -> delete(); + } + }) + .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { + switch (mDialogCaller) { + case SAVE_ID, RENAME_ID -> retryInput(); + } + }) + .create(); + + dialog.show(); + } + + private void showTextInputDialog(String defaultText) + { + EditText input = new EditText(this.getContext()); + input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); + input.setFilters(getInputFilters()); + input.setText(defaultText); + + AlertDialog dialog = new AlertDialog.Builder(this.getContext(), R.style.MwmTheme_AlertDialog) + .setTitle(R.string.user_route_input_title) + .setView(input) + .setPositiveButton(R.string.ok, (dialogInterface, i) -> { + mEditText = input.getText().toString(); + switch (mDialogCaller) { + case SAVE_ID -> checkSave(); + case RENAME_ID -> checkRename(); + } + }) + .setNegativeButton(R.string.cancel, null) + .create(); + + dialog.show(); + } + + private void retryInput() + { + showTextInputDialog(mEditText); + } + + private InputFilter[] getInputFilters() + { + InputFilter filter = (source, start, end, dest, dstart, dend) -> { + for (int i = start; i < end; i++) + { + char current = source.charAt(i); + if (!Character.isSpaceChar(current) && !Character.isLetterOrDigit(current)) + { + return ""; + } + } + return null; + }; + return new InputFilter[] {filter, new InputFilter.LengthFilter(32)}; + } +} diff --git a/android/app/src/main/java/app/organicmaps/maplayer/RouteBottomSheetItem.java b/android/app/src/main/java/app/organicmaps/maplayer/RouteBottomSheetItem.java new file mode 100644 index 0000000000..b1938f2aad --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/maplayer/RouteBottomSheetItem.java @@ -0,0 +1,58 @@ +package app.organicmaps.maplayer; + +import android.view.View; + +import androidx.annotation.NonNull; + +import app.organicmaps.adapter.OnItemClickListener; + +public class RouteBottomSheetItem +{ + @NonNull + private final String mRouteName; + @NonNull + private final OnItemClickListener mItemTitleClickListener; + @NonNull + private final OnItemClickListener mItemRenameClickListener; + @NonNull + private final OnItemClickListener mItemDeleteClickListener; + + RouteBottomSheetItem(@NonNull String routeName, + @NonNull OnItemClickListener itemTitleClickListener, + @NonNull OnItemClickListener itemRenameClickListener, + @NonNull OnItemClickListener itemDeleteClickListener) + { + mRouteName = routeName; + mItemTitleClickListener = itemTitleClickListener; + mItemRenameClickListener = itemRenameClickListener; + mItemDeleteClickListener = itemDeleteClickListener; + } + + public static RouteBottomSheetItem create(@NonNull String routeName, + @NonNull OnItemClickListener routeItemTitleClickListener, + @NonNull OnItemClickListener routeItemRenameClickListener, + @NonNull OnItemClickListener routeItemDeleteClickListener) + { + return new RouteBottomSheetItem(routeName, routeItemTitleClickListener, routeItemRenameClickListener, routeItemDeleteClickListener); + } + + public String getRouteName() + { + return mRouteName; + } + + public void onTitleClick(@NonNull View v, @NonNull RouteBottomSheetItem item) + { + mItemTitleClickListener.onItemClick(v, item); + } + + public void onRenameClick(@NonNull View v, @NonNull RouteBottomSheetItem item) + { + mItemRenameClickListener.onItemClick(v, item); + } + + public void onDeleteClick(@NonNull View v, @NonNull RouteBottomSheetItem item) + { + mItemDeleteClickListener.onItemClick(v, item); + } +} diff --git a/android/app/src/main/java/app/organicmaps/maplayer/RouteHolder.java b/android/app/src/main/java/app/organicmaps/maplayer/RouteHolder.java new file mode 100644 index 0000000000..1ba70c9fcb --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/maplayer/RouteHolder.java @@ -0,0 +1,59 @@ +package app.organicmaps.maplayer; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import app.organicmaps.R; +import app.organicmaps.adapter.OnItemClickListener; + +class RouteHolder extends RecyclerView.ViewHolder +{ + @NonNull + final TextView mTitle; + @NonNull + final ImageView mRenameButton; + @NonNull + final ImageView mDeleteButton; + @Nullable + RouteBottomSheetItem mItem; + @Nullable + OnItemClickListener mTitleListener; + @Nullable + OnItemClickListener mRenameListener; + @Nullable + OnItemClickListener mDeleteListener; + + RouteHolder(@NonNull View root) + { + super(root); + mTitle = root.findViewById(R.id.name); + mTitle.setOnClickListener(this::onItemTitleClicked); + mRenameButton = root.findViewById(R.id.rename); + mRenameButton.setOnClickListener(this::onItemRenameClicked); + mDeleteButton = root.findViewById(R.id.delete); + mDeleteButton.setOnClickListener(this::onItemDeleteClicked); + } + + public void onItemTitleClicked(@NonNull View v) + { + if (mTitleListener != null && mItem != null) + mTitleListener.onItemClick(v, mItem); + } + + public void onItemRenameClicked(@NonNull View v) + { + if (mRenameListener != null && mItem != null) + mRenameListener.onItemClick(v, mItem); + } + + public void onItemDeleteClicked(@NonNull View v) + { + if (mDeleteListener != null && mItem != null) + mDeleteListener.onItemClick(v, mItem); + } +} diff --git a/android/app/src/main/java/app/organicmaps/maplayer/RoutesAdapter.java b/android/app/src/main/java/app/organicmaps/maplayer/RoutesAdapter.java new file mode 100644 index 0000000000..34ae565ae5 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/maplayer/RoutesAdapter.java @@ -0,0 +1,83 @@ +package app.organicmaps.maplayer; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import app.organicmaps.R; +import app.organicmaps.util.SharedPropertiesUtils; +import app.organicmaps.util.UiUtils; +import app.organicmaps.util.log.Logger; + +public class RoutesAdapter extends RecyclerView.Adapter +{ + @NonNull + private final List mItems; + + public RoutesAdapter(@NonNull List items) + { + mItems = items; + } + + @NonNull + @Override + public RouteHolder onCreateViewHolder(ViewGroup parent, int viewType) + { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + View root = inflater.inflate(R.layout.item_myroute_button, parent, false); + return new RouteHolder(root); + } + + @Override + public void onBindViewHolder(RouteHolder holder, int position) + { + RouteBottomSheetItem item = mItems.get(position); + holder.mItem = item; + + holder.mTitle.setSelected(true); + holder.mTitle.setText(item.getRouteName()); + + holder.mTitleListener = item::onTitleClick; + holder.mRenameListener = item::onRenameClick; + holder.mDeleteListener = item::onDeleteClick; + } + + @Override + public int getItemCount() + { + return mItems.size(); + } + + public void addRoute(@NonNull RouteBottomSheetItem item) + { + // Compare strings toUpperCase to ignore case + String routeName = item.getRouteName().toUpperCase(); + String iName; + // Find index to add ordered + int pos = mItems.size(); + for (int i = 0; i < mItems.size(); i++) + { + iName = mItems.get(i).getRouteName().toUpperCase(); + if(routeName.compareTo(iName) < 0) + { + pos = i; + break; + } + } + mItems.add(pos, item); + notifyItemInserted(pos); + } + + public void removeRoute(@NonNull RouteBottomSheetItem item) + { + int pos = mItems.indexOf(item); + mItems.remove(item); + notifyItemRemoved(pos); + } +} diff --git a/android/app/src/main/res/layout-h400dp/map_buttons_layout_planning.xml b/android/app/src/main/res/layout-h400dp/map_buttons_layout_planning.xml index b80f476643..d643bceedb 100644 --- a/android/app/src/main/res/layout-h400dp/map_buttons_layout_planning.xml +++ b/android/app/src/main/res/layout-h400dp/map_buttons_layout_planning.xml @@ -33,6 +33,14 @@ android:layout_marginBottom="@dimen/margin_half" app:layout_constraintBottom_toTopOf="@+id/btn_bookmarks" app:layout_constraintStart_toStartOf="parent" /> + - \ No newline at end of file + + diff --git a/android/app/src/main/res/layout/fragment_myroutes.xml b/android/app/src/main/res/layout/fragment_myroutes.xml new file mode 100644 index 0000000000..0e2b9946de --- /dev/null +++ b/android/app/src/main/res/layout/fragment_myroutes.xml @@ -0,0 +1,65 @@ + + + + + + +