diff --git a/android/app/src/main/cpp/app/organicmaps/Framework.cpp b/android/app/src/main/cpp/app/organicmaps/Framework.cpp index d749e7a390..905a6b36fe 100644 --- a/android/app/src/main/cpp/app/organicmaps/Framework.cpp +++ b/android/app/src/main/cpp/app/organicmaps/Framework.cpp @@ -1993,4 +1993,74 @@ Java_app_organicmaps_Framework_nativeGetKayakHotelLink(JNIEnv * env, jclass, jst return url.empty() ? nullptr : jni::ToJavaString(env, url); } +JNIEXPORT jboolean JNICALL +Java_app_organicmaps_Framework_nativeShouldShowProducts(JNIEnv * env, jclass) +{ + return frm()->ShouldShowProducts(); +} + +JNIEXPORT jobject JNICALL +Java_app_organicmaps_Framework_nativeGetProductsConfiguration(JNIEnv * env, jclass) +{ + auto config = frm()->GetProductsConfiguration(); + if (!config) return nullptr; + + static jclass const productClass = jni::GetGlobalClassRef( + env, + "app/organicmaps/products/Product" + ); + static jmethodID const productConstructor = jni::GetConstructorID( + env, + productClass, + "(Ljava/lang/String;Ljava/lang/String;)V" + ); + + jobjectArray products = jni::ToJavaArray( + env, + productClass, + config->GetProducts(), + [](JNIEnv * env, products::ProductsConfig::Product const & product) + { + jni::TScopedLocalRef const title(env, jni::ToJavaString(env, product.GetTitle())); + jni::TScopedLocalRef const link(env, jni::ToJavaString(env, product.GetLink())); + + return env->NewObject( + productClass, + productConstructor, + title.get(), + link.get() + ); + }); + + static jclass const productsConfigClass = jni::GetGlobalClassRef( + env, + "app/organicmaps/products/ProductsConfig" + ); + static jmethodID const productsConfigConstructor = jni::GetConstructorID( + env, + productsConfigClass, + "(Ljava/lang/String;[Lapp/organicmaps/products/Product;)V" + ); + + jni::TScopedLocalRef const placePagePrompt(env, jni::ToJavaString(env, config->GetPlacePagePrompt())); + return env->NewObject(productsConfigClass, productsConfigConstructor, placePagePrompt.get(), products); +} + +JNIEXPORT void JNICALL +Java_app_organicmaps_Framework_nativeDidCloseProductsPopup(JNIEnv * env, jclass, jstring reason) +{ + frm()->DidCloseProductsPopup(frm()->FromString(jni::ToNativeString(env, reason))); +} + +JNIEXPORT void JNICALL +Java_app_organicmaps_Framework_nativeDidSelectProduct(JNIEnv * env, jclass, jstring title, jstring link) +{ + products::ProductsConfig::Product product( + jni::ToNativeString(env, title), + jni::ToNativeString(env, link) + ); + + frm()->DidSelectProduct(product); +} + } // extern "C" diff --git a/android/app/src/main/java/app/organicmaps/Framework.java b/android/app/src/main/java/app/organicmaps/Framework.java index 63d70abde3..349431f208 100644 --- a/android/app/src/main/java/app/organicmaps/Framework.java +++ b/android/app/src/main/java/app/organicmaps/Framework.java @@ -15,6 +15,8 @@ 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; @@ -461,4 +463,13 @@ public class Framework @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); } diff --git a/android/app/src/main/java/app/organicmaps/products/Product.java b/android/app/src/main/java/app/organicmaps/products/Product.java new file mode 100644 index 0000000000..18e354c84e --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/products/Product.java @@ -0,0 +1,20 @@ +package app.organicmaps.products; + +import androidx.annotation.Keep; +import androidx.annotation.Nullable; + +// Called from JNI. +@Keep +@SuppressWarnings("unused") +public class Product { + @Nullable + public String title; + + @Nullable + public String link; + + public Product(@Nullable String title, @Nullable String link) { + this.title = title; + this.link = link; + } +} diff --git a/android/app/src/main/java/app/organicmaps/products/ProductsConfig.java b/android/app/src/main/java/app/organicmaps/products/ProductsConfig.java new file mode 100644 index 0000000000..9fb8d50f6f --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/products/ProductsConfig.java @@ -0,0 +1,20 @@ +package app.organicmaps.products; + +import androidx.annotation.Keep; +import androidx.annotation.Nullable; + +// Called from JNI. +@Keep +@SuppressWarnings("unused") +public class ProductsConfig { + public ProductsConfig(@Nullable String placePagePrompt, @Nullable Product[] products) { + this.placePagePrompt = placePagePrompt; + this.products = products; + } + + @Nullable + public String placePagePrompt; + + @Nullable + public Product[] products; +} diff --git a/android/app/src/main/java/app/organicmaps/util/Constants.java b/android/app/src/main/java/app/organicmaps/util/Constants.java index a9d4f75e9a..b29d23da00 100644 --- a/android/app/src/main/java/app/organicmaps/util/Constants.java +++ b/android/app/src/main/java/app/organicmaps/util/Constants.java @@ -57,5 +57,13 @@ public final class Constants public static final String XIAOMI = "XIAOMI"; } + public static class ProductsPopupCloseReason + { + public static final String CLOSE = "close"; + public static final String REMIND_LATER = "remind_later"; + public static final String ALREADY_DONATED = "already_donated"; + public static final String SELECT_PRODUCT = "select_product"; + } + private Constants() {} } diff --git a/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageView.java b/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageView.java index 343a5bb475..e6489f8a05 100644 --- a/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageView.java +++ b/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageView.java @@ -50,6 +50,7 @@ import app.organicmaps.widget.placepage.sections.PlacePageBookmarkFragment; import app.organicmaps.widget.placepage.sections.PlacePageLinksFragment; import app.organicmaps.widget.placepage.sections.PlacePageOpeningHoursFragment; import app.organicmaps.widget.placepage.sections.PlacePagePhoneFragment; +import app.organicmaps.widget.placepage.sections.PlacePageProductsFragment; import app.organicmaps.widget.placepage.sections.PlacePageWikipediaFragment; import com.google.android.material.button.MaterialButton; @@ -69,6 +70,7 @@ public class PlacePageView extends Fragment implements View.OnClickListener, { private static final String PREF_COORDINATES_FORMAT = "coordinates_format"; private static final String BOOKMARK_FRAGMENT_TAG = "BOOKMARK_FRAGMENT_TAG"; + private static final String PRODUCTS_FRAGMENT_TAG = "PRODUCTS_FRAGMENT_TAG"; private static final String WIKIPEDIA_FRAGMENT_TAG = "WIKIPEDIA_FRAGMENT_TAG"; private static final String PHONE_FRAGMENT_TAG = "PHONE_FRAGMENT_TAG"; private static final String OPENING_HOURS_FRAGMENT_TAG = "OPENING_HOURS_FRAGMENT_TAG"; @@ -202,9 +204,11 @@ public class PlacePageView extends Fragment implements View.OnClickListener, mFrame.setOnClickListener((v) -> mPlacePageViewListener.onPlacePageRequestToggleState()); mPreview = mFrame.findViewById(R.id.pp__preview); + mFrame.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { final int oldHeight = oldBottom - oldTop; final int newHeight = bottom - top; + if (oldHeight != newHeight) mPlacePageViewListener.onPlacePageContentChanged(mPreview.getHeight(), newHeight); }); @@ -387,6 +391,18 @@ public class PlacePageView extends Fragment implements View.OnClickListener, updateViewFragment(PlacePageWikipediaFragment.class, WIKIPEDIA_FRAGMENT_TAG, R.id.place_page_wikipedia_fragment, hasWikipediaEntry()); } + private boolean hasProductsEntry() + { + return Framework.nativeShouldShowProducts(); + } + + private void updateProductsView() + { + var hasProductsEntry = hasProductsEntry(); + + updateViewFragment(PlacePageProductsFragment.class, PRODUCTS_FRAGMENT_TAG, R.id.place_page_products_fragment, hasProductsEntry); + } + private void setTextAndColorizeSubtitle() { String text = mMapObject.getSubtitle(); @@ -480,6 +496,7 @@ public class PlacePageView extends Fragment implements View.OnClickListener, } updateLinksView(); updateOpeningHoursView(); + updateProductsView(); updateWikipediaView(); updateBookmarkView(); updatePhoneView(); diff --git a/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageProductsFragment.java b/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageProductsFragment.java new file mode 100644 index 0000000000..9982f0cdf8 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageProductsFragment.java @@ -0,0 +1,107 @@ +package app.organicmaps.widget.placepage.sections; + +import static androidx.core.util.ObjectsCompat.requireNonNull; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import java.util.Objects; + +import app.organicmaps.Framework; +import app.organicmaps.R; +import app.organicmaps.products.Product; +import app.organicmaps.products.ProductsConfig; +import app.organicmaps.util.Constants; +import app.organicmaps.util.UiUtils; +import app.organicmaps.util.Utils; + +public class PlacePageProductsFragment extends Fragment +{ + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + return inflater.inflate(R.layout.place_page_products_fragment, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + + var config = Framework.nativeGetProductsConfiguration(); + if (config != null && isValidConfig(config)) + { + UiUtils.show(view); + + updateView(view, config); + } else + { + UiUtils.hide(view); + } + } + + private void updateView(@NonNull View view, @NonNull ProductsConfig config) + { + var layoutInflater = LayoutInflater.from(view.getContext()); + + TextView productsPrompt = requireNonNull(view.findViewById(R.id.products_prompt)); + LinearLayout productsButtons = requireNonNull(view.findViewById(R.id.products_buttons)); + View closeButton = requireNonNull(view.findViewById(R.id.products_close)); + View productsRemindLater = requireNonNull(view.findViewById(R.id.products_remind_later)); + View alreadyDonated = requireNonNull(view.findViewById(R.id.products_already_donated)); + + productsPrompt.setText(config.placePagePrompt); + + productsButtons.removeAllViews(); + + for (var product : Objects.requireNonNull(config.products)) + { + var button = (Button) layoutInflater.inflate(R.layout.item_product, productsButtons, false); + button.setText(product.title); + button.setOnClickListener((v) -> { + onProductSelected(product); + }); + + productsButtons.addView(button); + } + + closeButton.setOnClickListener((v) -> { + closeWithReason(view, Constants.ProductsPopupCloseReason.CLOSE); + }); + + productsRemindLater.setOnClickListener((v) -> { + closeWithReason(view, Constants.ProductsPopupCloseReason.REMIND_LATER); + }); + + alreadyDonated.setOnClickListener((v) -> { + closeWithReason(view, Constants.ProductsPopupCloseReason.ALREADY_DONATED); + }); + } + + private void closeWithReason(View view, String reason) + { + Framework.nativeDidCloseProductsPopup(reason); + UiUtils.hide(view); + } + + private void onProductSelected(Product product) + { + Utils.openUrl(requireActivity(), product.link); + Framework.nativeDidSelectProduct(product.title, product.link); + } + + private boolean isValidConfig(@NonNull ProductsConfig config) + { + return config.products != null && config.products.length > 0; + } +} diff --git a/android/app/src/main/res/layout/item_product.xml b/android/app/src/main/res/layout/item_product.xml new file mode 100644 index 0000000000..89bc1f430b --- /dev/null +++ b/android/app/src/main/res/layout/item_product.xml @@ -0,0 +1,11 @@ + +