From 516039c91d4d53c37db50aaba97bff050756e78d Mon Sep 17 00:00:00 2001 From: Ritika Pahwa <83745993+RitikaPahwa4444@users.noreply.github.com> Date: Sat, 2 Aug 2025 12:34:34 +0530 Subject: [PATCH 1/3] Add v5.6.1 to CHANGELOG.md --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72fda2f5d..fc22a2b99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Wikimedia Commons for Android +## v5.6.1 + +### What's changed +* The app no longer uploads images to Wikidata if one exists already for a given item +* File usage displays correctly now +* No more infinite circular progress bar on nominating an image for deletion +* Enhanced location updates while using GPS +* Author/uploader names are now available in Media Details for Commons licensing compliance +* Improved usage of popups in Nearby +* Bug fixes and stability improvements + ## v5.5.0 ### What's changed From 6f36cae767328ecafbd9b10eefa926ef96329c64 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Sun, 3 Aug 2025 21:44:00 -0500 Subject: [PATCH 2/3] Convert explore package to kotlin (#6389) * Convert WikidataItemDetailsActivity to kotlin * Convert RecentSearchesDao to kotlin * Convert RecentSearchesFragment to kotlin * Convert ExploreListRootFragment to kotlin * Convert the ParentViewPager to kotlin * Convert ExploreMapRootFragment to kotlin * Convert SearchActivity to kotlin * Convert ExploreFragment to kotlin * Convert ExploreMapCalls and ExploreMapContract to kotlin * Convert ExploreMapController to kotlin * Convert the map presenter to kotlin * Convert the ExploreMapFragment to kotlin * Fix import issue --- .../nrw/commons/bookmarks/BookmarkFragment.kt | 2 +- .../fr/free/nrw/commons/data/DBOpenHelper.kt | 6 +- .../nrw/commons/explore/ExploreFragment.java | 260 ---- .../nrw/commons/explore/ExploreFragment.kt | 229 ++++ .../explore/ExploreListRootFragment.java | 215 ---- .../explore/ExploreListRootFragment.kt | 182 +++ .../explore/ExploreMapRootFragment.java | 239 ---- .../commons/explore/ExploreMapRootFragment.kt | 201 +++ .../nrw/commons/explore/ParentViewPager.java | 66 - .../nrw/commons/explore/ParentViewPager.kt | 25 + .../nrw/commons/explore/SearchActivity.java | 285 ----- .../nrw/commons/explore/SearchActivity.kt | 252 ++++ .../depictions/PageableDepictionsFragment.kt | 2 +- .../WikidataItemDetailsActivity.java | 302 ----- .../depictions/WikidataItemDetailsActivity.kt | 295 +++++ .../commons/explore/map/ExploreMapCalls.java | 34 - .../commons/explore/map/ExploreMapCalls.kt | 25 + .../explore/map/ExploreMapContract.java | 45 - .../commons/explore/map/ExploreMapContract.kt | 43 + .../explore/map/ExploreMapController.java | 213 --- .../explore/map/ExploreMapController.kt | 219 ++++ .../explore/map/ExploreMapFragment.java | 1140 ----------------- .../commons/explore/map/ExploreMapFragment.kt | 1091 ++++++++++++++++ .../explore/map/ExploreMapPresenter.java | 237 ---- .../explore/map/ExploreMapPresenter.kt | 223 ++++ .../RecentSearchesContentProvider.kt | 6 +- .../recentsearches/RecentSearchesDao.java | 275 ---- .../recentsearches/RecentSearchesDao.kt | 180 +++ .../RecentSearchesFragment.java | 149 --- .../recentsearches/RecentSearchesFragment.kt | 153 +++ .../recentsearches/RecentSearchesTable.kt | 71 + .../nrw/commons/profile/ProfileActivity.kt | 2 +- .../commons/upload/UploadProgressActivity.kt | 6 +- .../free/nrw/commons/utils/DatabaseUtils.kt | 8 + .../explore/ExploreFragmentUnitTest.kt | 4 +- .../recentsearches/RecentSearchesDaoTest.kt | 18 +- .../RecentSearchesFragmentUnitTest.kt | 14 - 37 files changed, 3218 insertions(+), 3499 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesTable.kt diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.kt index 48db40ad0..51f15b23c 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.kt @@ -33,7 +33,7 @@ class BookmarkFragment : CommonsDaggerSupportFragment() { fun setScroll(canScroll: Boolean) { binding?.let { - it.viewPagerBookmarks.isCanScroll = canScroll + it.viewPagerBookmarks.canScroll = canScroll } } diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt index 7bbe5de06..55ddec5bc 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt @@ -7,7 +7,7 @@ import android.database.sqlite.SQLiteOpenHelper import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable import fr.free.nrw.commons.category.CategoryDao -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao @@ -31,7 +31,7 @@ class DBOpenHelper( CategoryDao.Table.onCreate(db) BookmarksTable.onCreate(db) BookmarkItemsTable.onCreate(db) - RecentSearchesDao.Table.onCreate(db) + RecentSearchesTable.onCreate(db) RecentLanguagesDao.Table.onCreate(db) } @@ -39,7 +39,7 @@ class DBOpenHelper( CategoryDao.Table.onUpdate(db, from, to) BookmarksTable.onUpdate(db, from, to) BookmarkItemsTable.onUpdate(db, from, to) - RecentSearchesDao.Table.onUpdate(db, from, to) + RecentSearchesTable.onUpdate(db, from, to) RecentLanguagesDao.Table.onUpdate(db, from, to) deleteTable(db, CONTRIBUTIONS_TABLE) deleteTable(db, BOOKMARKS_LOCATIONS) diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java deleted file mode 100644 index 475d14287..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java +++ /dev/null @@ -1,260 +0,0 @@ -package fr.free.nrw.commons.explore; - -import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE; -import static fr.free.nrw.commons.ViewPagerAdapter.pairOf; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentPagerAdapter; -import androidx.viewpager.widget.ViewPager.OnPageChangeListener; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.ViewPagerAdapter; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.databinding.FragmentExploreBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.ActivityUtils; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import javax.inject.Inject; -import javax.inject.Named; -import kotlin.Pair; - -public class ExploreFragment extends CommonsDaggerSupportFragment { - - private static final String FEATURED_IMAGES_CATEGORY = "Featured_pictures_on_Wikimedia_Commons"; - private static final String MOBILE_UPLOADS_CATEGORY = "Uploaded_with_Mobile/Android"; - private static final String EXPLORE_MAP = "Map"; - private static final String MEDIA_DETAILS_FRAGMENT_TAG = "MediaDetailsFragment"; - - - public FragmentExploreBinding binding; - ViewPagerAdapter viewPagerAdapter; - private ExploreListRootFragment featuredRootFragment; - private ExploreListRootFragment mobileRootFragment; - private ExploreMapRootFragment mapRootFragment; - @Inject - @Named("default_preferences") - public JsonKvStore applicationKvStore; - - // Nearby map state (for if we came from Nearby fragment) - private double prevZoom; - private double prevLatitude; - private double prevLongitude; - - public void setScroll(boolean canScroll) { - if (binding != null) { - binding.viewPager.setCanScroll(canScroll); - } - } - - @NonNull - public static ExploreFragment newInstance() { - ExploreFragment fragment = new ExploreFragment(); - fragment.setRetainInstance(true); - return fragment; - } - - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - loadNearbyMapData(); - binding = FragmentExploreBinding.inflate(inflater, container, false); - - viewPagerAdapter = new ViewPagerAdapter(requireContext(), getChildFragmentManager(), - FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); - - binding.viewPager.setAdapter(viewPagerAdapter); - binding.viewPager.setId(R.id.viewPager); - binding.tabLayout.setupWithViewPager(binding.viewPager); - binding.viewPager.addOnPageChangeListener(new OnPageChangeListener() { - @Override - public void onPageScrolled(int position, float positionOffset, - int positionOffsetPixels) { - - } - - @Override - public void onPageSelected(int position) { - if (position == 2) { - binding.viewPager.setCanScroll(false); - } else { - binding.viewPager.setCanScroll(true); - } - } - - @Override - public void onPageScrollStateChanged(int state) { - - } - }); - setTabs(); - setHasOptionsMenu(true); - - // if we came from 'Show in Explore' in Nearby, jump to Map tab - if (isCameFromNearbyMap()) { - binding.viewPager.setCurrentItem(2); - } - return binding.getRoot(); - } - - /** - * Sets the titles in the tabLayout and fragments in the viewPager - */ - public void setTabs() { - Bundle featuredArguments = new Bundle(); - featuredArguments.putString("categoryName", FEATURED_IMAGES_CATEGORY); - - Bundle mobileArguments = new Bundle(); - mobileArguments.putString("categoryName", MOBILE_UPLOADS_CATEGORY); - - Bundle mapArguments = new Bundle(); - mapArguments.putString("categoryName", EXPLORE_MAP); - - // if we came from 'Show in Explore' in Nearby, pass on zoom and center to Explore map root - if (isCameFromNearbyMap()) { - mapArguments.putDouble("prev_zoom", prevZoom); - mapArguments.putDouble("prev_latitude", prevLatitude); - mapArguments.putDouble("prev_longitude", prevLongitude); - } - - featuredRootFragment = new ExploreListRootFragment(featuredArguments); - mobileRootFragment = new ExploreListRootFragment(mobileArguments); - mapRootFragment = new ExploreMapRootFragment(mapArguments); - - ((MainActivity) getActivity()).showTabs(); - ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); - - viewPagerAdapter.setTabs( - pairOf(R.string.explore_tab_title_featured, featuredRootFragment), - pairOf(R.string.explore_tab_title_mobile, mobileRootFragment), - pairOf(R.string.explore_tab_title_map, mapRootFragment) - ); - viewPagerAdapter.notifyDataSetChanged(); - } - - /** - * Fetch Nearby map camera data from fragment arguments if any. - */ - public void loadNearbyMapData() { - // get fragment arguments - if (getArguments() != null) { - prevZoom = getArguments().getDouble("prev_zoom"); - prevLatitude = getArguments().getDouble("prev_latitude"); - prevLongitude = getArguments().getDouble("prev_longitude"); - } - } - - /** - * Checks if fragment arguments contain data from Nearby map. if present, then the user - * navigated from Nearby using 'Show in Explore'. - * - * @return true if user navigated from Nearby map - **/ - public boolean isCameFromNearbyMap() { - return prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0; - } - - public boolean onBackPressed() { - if (binding.tabLayout.getSelectedTabPosition() == 0) { - if (featuredRootFragment.backPressed()) { - ((BaseActivity) getActivity()).getSupportActionBar() - .setDisplayHomeAsUpEnabled(false); - return true; - } - } else if (binding.tabLayout.getSelectedTabPosition() == 1) { //Mobile root fragment - if (mobileRootFragment.backPressed()) { - ((BaseActivity) getActivity()).getSupportActionBar() - .setDisplayHomeAsUpEnabled(false); - return true; - } - } else { //explore map fragment - if (mapRootFragment.backPressed()) { - ((BaseActivity) getActivity()).getSupportActionBar() - .setDisplayHomeAsUpEnabled(false); - return true; - } - } - return false; - } - - /** - * This method inflates the menu in the toolbar - */ - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - // if logged in 'Show in Nearby' menu item is visible - if (applicationKvStore.getBoolean("login_skipped") == false) { - inflater.inflate(R.menu.explore_fragment_menu, menu); - - MenuItem others = menu.findItem(R.id.list_item_show_in_nearby); - - if (binding.viewPager.getCurrentItem() == 2) { - others.setVisible(true); - } - - // if on Map tab, show all menu options, else only show search - binding.viewPager.addOnPageChangeListener(new OnPageChangeListener() { - @Override - public void onPageScrolled(int position, float positionOffset, - int positionOffsetPixels) { - } - - @Override - public void onPageSelected(int position) { - others.setVisible((position == 2)); - } - - @Override - public void onPageScrollStateChanged(int state) { - if (state == SCROLL_STATE_IDLE && binding.viewPager.getCurrentItem() == 2) { - onPageSelected(2); - } - } - }); - } else { - inflater.inflate(R.menu.menu_search, menu); - } - super.onCreateOptionsMenu(menu, inflater); - } - - /** - * This method handles the logic on ItemSelect in toolbar menu Currently only 1 choice is - * available to open search page of the app - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - - // Handle item selection - switch (item.getItemId()) { - case R.id.action_search: - ActivityUtils.startActivityWithFlags(getActivity(), SearchActivity.class); - return true; - case R.id.list_item_show_in_nearby: - mapRootFragment.loadNearbyMapFromExplore(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - binding = null; - } -} - - diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.kt new file mode 100644 index 000000000..ea96b50a3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.kt @@ -0,0 +1,229 @@ +package fr.free.nrw.commons.explore + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentPagerAdapter +import androidx.viewpager.widget.ViewPager +import androidx.viewpager.widget.ViewPager.OnPageChangeListener +import fr.free.nrw.commons.R +import fr.free.nrw.commons.ViewPagerAdapter +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.FragmentExploreBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags +import javax.inject.Inject +import javax.inject.Named + +class ExploreFragment : CommonsDaggerSupportFragment() { + + @JvmField + @Inject + @Named("default_preferences") + var applicationKvStore: JsonKvStore? = null + + private var featuredRootFragment: ExploreListRootFragment? = null + private var mobileRootFragment: ExploreListRootFragment? = null + private var mapRootFragment: ExploreMapRootFragment? = null + private var prevZoom = 0.0 + private var prevLatitude = 0.0 + private var prevLongitude = 0.0 + private var viewPagerAdapter: ViewPagerAdapter? = null + var binding: FragmentExploreBinding? = null + + fun setScroll(canScroll: Boolean) { + if (binding != null) { + binding!!.viewPager.canScroll = canScroll + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + super.onCreate(savedInstanceState) + loadNearbyMapData() + binding = FragmentExploreBinding.inflate(inflater, container, false) + + viewPagerAdapter = ViewPagerAdapter( + requireContext(), childFragmentManager, + FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT + ) + + binding!!.viewPager.adapter = viewPagerAdapter + binding!!.viewPager.id = R.id.viewPager + binding!!.tabLayout.setupWithViewPager(binding!!.viewPager) + binding!!.viewPager.addOnPageChangeListener(object : OnPageChangeListener { + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) = Unit + override fun onPageScrollStateChanged(state: Int) = Unit + override fun onPageSelected(position: Int) { + binding!!.viewPager.canScroll = position != 2 + } + }) + setTabs() + setHasOptionsMenu(true) + + // if we came from 'Show in Explore' in Nearby, jump to Map tab + if (isCameFromNearbyMap) { + binding!!.viewPager.currentItem = 2 + } + return binding!!.root + } + + /** + * Sets the titles in the tabLayout and fragments in the viewPager + */ + fun setTabs() { + val featuredArguments = Bundle() + featuredArguments.putString("categoryName", FEATURED_IMAGES_CATEGORY) + + val mobileArguments = Bundle() + mobileArguments.putString("categoryName", MOBILE_UPLOADS_CATEGORY) + + val mapArguments = Bundle() + mapArguments.putString("categoryName", EXPLORE_MAP) + + // if we came from 'Show in Explore' in Nearby, pass on zoom and center to Explore map root + if (isCameFromNearbyMap) { + mapArguments.putDouble("prev_zoom", prevZoom) + mapArguments.putDouble("prev_latitude", prevLatitude) + mapArguments.putDouble("prev_longitude", prevLongitude) + } + + featuredRootFragment = ExploreListRootFragment(featuredArguments) + mobileRootFragment = ExploreListRootFragment(mobileArguments) + mapRootFragment = ExploreMapRootFragment(mapArguments) + + (activity as MainActivity).showTabs() + (activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false) + + viewPagerAdapter!!.setTabs( + R.string.explore_tab_title_featured to featuredRootFragment!!, + R.string.explore_tab_title_mobile to mobileRootFragment!!, + R.string.explore_tab_title_map to mapRootFragment!! + ) + viewPagerAdapter!!.notifyDataSetChanged() + } + + /** + * Fetch Nearby map camera data from fragment arguments if any. + */ + private fun loadNearbyMapData() { + // get fragment arguments + if (arguments != null) { + with (requireArguments()) { + prevZoom = getDouble("prev_zoom") + prevLatitude = getDouble("prev_latitude") + prevLongitude = getDouble("prev_longitude") + } + } + } + + /** + * Checks if fragment arguments contain data from Nearby map. if present, then the user + * navigated from Nearby using 'Show in Explore'. + * + * @return true if user navigated from Nearby map + */ + private val isCameFromNearbyMap: Boolean + get() = prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0 + + fun onBackPressed(): Boolean { + if (binding!!.tabLayout.selectedTabPosition == 0) { + if (featuredRootFragment!!.backPressed()) { + (activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false) + return true + } + } else if (binding!!.tabLayout.selectedTabPosition == 1) { //Mobile root fragment + if (mobileRootFragment!!.backPressed()) { + (activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false) + return true + } + } else { //explore map fragment + if (mapRootFragment!!.backPressed()) { + (activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false) + return true + } + } + return false + } + + /** + * This method inflates the menu in the toolbar + */ + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + // if logged in 'Show in Nearby' menu item is visible + if (applicationKvStore!!.getBoolean("login_skipped") == false) { + inflater.inflate(R.menu.explore_fragment_menu, menu) + + val others = menu.findItem(R.id.list_item_show_in_nearby) + + if (binding!!.viewPager.currentItem == 2) { + others.setVisible(true) + } + + // if on Map tab, show all menu options, else only show search + binding!!.viewPager.addOnPageChangeListener(object : OnPageChangeListener { + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) = Unit + + override fun onPageSelected(position: Int) { + others.setVisible((position == 2)) + } + + override fun onPageScrollStateChanged(state: Int) { + if (state == ViewPager.SCROLL_STATE_IDLE && binding!!.viewPager.currentItem == 2) { + onPageSelected(2) + } + } + }) + } else { + inflater.inflate(R.menu.menu_search, menu) + } + super.onCreateOptionsMenu(menu, inflater) + } + + /** + * This method handles the logic on ItemSelect in toolbar menu Currently only 1 choice is + * available to open search page of the app + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle item selection + + when (item.itemId) { + R.id.action_search -> { + startActivityWithFlags(requireActivity(), SearchActivity::class.java) + return true + } + + R.id.list_item_show_in_nearby -> { + mapRootFragment!!.loadNearbyMapFromExplore() + return true + } + + else -> return super.onOptionsItemSelected(item) + } + } + + override fun onDestroy() { + super.onDestroy() + binding = null + } + + companion object { + private const val FEATURED_IMAGES_CATEGORY = "Featured_pictures_on_Wikimedia_Commons" + private const val MOBILE_UPLOADS_CATEGORY = "Uploaded_with_Mobile/Android" + private const val EXPLORE_MAP = "Map" + + fun newInstance(): ExploreFragment = ExploreFragment().apply { + retainInstance = true + } + } +} + + diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java deleted file mode 100644 index e3ad90119..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java +++ /dev/null @@ -1,215 +0,0 @@ -package fr.free.nrw.commons.explore; - -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.category.CategoryImagesCallback; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.media.MediaDetailProvider; -import fr.free.nrw.commons.navtab.NavTab; - -public class ExploreListRootFragment extends CommonsDaggerSupportFragment implements - MediaDetailProvider, CategoryImagesCallback { - - private MediaDetailPagerFragment mediaDetails; - private CategoriesMediaFragment listFragment; - - private FragmentFeaturedRootBinding binding; - - public ExploreListRootFragment() { - //empty constructor necessary otherwise crashes on recreate - } - - public ExploreListRootFragment(Bundle bundle) { - String title = bundle.getString("categoryName"); - listFragment = new CategoriesMediaFragment(); - Bundle featuredArguments = new Bundle(); - featuredArguments.putString("categoryName", title); - listFragment.setArguments(featuredArguments); - } - - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = FragmentFeaturedRootBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - if (savedInstanceState == null) { - setFragment(listFragment, mediaDetails); - } - } - - public void setFragment(Fragment fragment, Fragment otherFragment) { - if (fragment.isAdded() && otherFragment != null) { - getChildFragmentManager() - .beginTransaction() - .hide(otherFragment) - .show(fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (fragment.isAdded() && otherFragment == null) { - getChildFragmentManager() - .beginTransaction() - .show(fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (!fragment.isAdded() && otherFragment != null) { - getChildFragmentManager() - .beginTransaction() - .hide(otherFragment) - .add(R.id.explore_container, fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (!fragment.isAdded()) { - getChildFragmentManager() - .beginTransaction() - .replace(R.id.explore_container, fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } - } - - public void removeFragment(Fragment fragment) { - getChildFragmentManager() - .beginTransaction() - .remove(fragment) - .commit(); - getChildFragmentManager().executePendingTransactions(); - } - - @Override - public void onAttach(final Context context) { - super.onAttach(context); - } - - @Override - public void onMediaClicked(int position) { - if (binding!=null) { - binding.exploreContainer.setVisibility(View.VISIBLE); - } - if (((ExploreFragment) getParentFragment()).binding!=null) { - ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE); - } - mediaDetails = MediaDetailPagerFragment.newInstance(false, true); - ((ExploreFragment) getParentFragment()).setScroll(false); - setFragment(mediaDetails, listFragment); - mediaDetails.showImage(position); - } - - /** - * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index - * - * @param i It is the index of which media object is to be returned which is same as current - * index of viewPager. - * @return Media Object - */ - @Override - public Media getMediaAtPosition(int i) { - if (listFragment != null) { - return listFragment.getMediaAtPosition(i); - } else { - return null; - } - } - - /** - * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain - * same number of media items as that of media elements in adapter. - * - * @return Total Media count in the adapter - */ - @Override - public int getTotalMediaCount() { - if (listFragment != null) { - return listFragment.getTotalMediaCount(); - } else { - return 0; - } - } - - @Override - public Integer getContributionStateAt(int position) { - return null; - } - - /** - * Reload media detail fragment once media is nominated - * - * @param index item position that has been nominated - */ - @Override - public void refreshNominatedMedia(int index) { - if (mediaDetails != null && !listFragment.isVisible()) { - removeFragment(mediaDetails); - onMediaClicked(index); - } - } - - /** - * This method is called on success of API call for featured images or mobile uploads. The - * viewpager will notified that number of items have changed. - */ - @Override - public void viewPagerNotifyDataSetChanged() { - if (mediaDetails != null) { - mediaDetails.notifyDataSetChanged(); - } - } - - /** - * Performs back pressed action on the fragment. Return true if the event was handled by the - * mediaDetails otherwise returns false. - * - * @return - */ - public boolean backPressed() { - if (null != mediaDetails && mediaDetails.isVisible()) { - if (((ExploreFragment) getParentFragment()).binding != null) { - ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE); - } - removeFragment(mediaDetails); - ((ExploreFragment) getParentFragment()).setScroll(true); - setFragment(listFragment, mediaDetails); - ((MainActivity) getActivity()).showTabs(); - return true; - } else { - if (((MainActivity) getActivity()) != null) { - ((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code()); - } - } - if (((MainActivity) getActivity()) != null) { - ((MainActivity) getActivity()).showTabs(); - } - return false; - } - - @Override - public void onDestroy() { - super.onDestroy(); - - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.kt new file mode 100644 index 000000000..32acebbb1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.kt @@ -0,0 +1,182 @@ +package fr.free.nrw.commons.explore + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.category.CategoryImagesCallback +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.media.MediaDetailProvider +import fr.free.nrw.commons.navtab.NavTab + +class ExploreListRootFragment : CommonsDaggerSupportFragment, MediaDetailProvider, + CategoryImagesCallback { + private var mediaDetails: MediaDetailPagerFragment? = null + private var listFragment: CategoriesMediaFragment? = null + private var binding: FragmentFeaturedRootBinding? = null + + constructor() + + constructor(bundle: Bundle) { + listFragment = CategoriesMediaFragment().apply { + arguments = bundleOf( + "categoryName" to bundle.getString("categoryName") + ) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + super.onCreate(savedInstanceState) + + binding = FragmentFeaturedRootBinding.inflate(inflater, container, false) + return binding!!.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (savedInstanceState == null) { + setFragment(listFragment!!, mediaDetails) + } + } + + fun setFragment(fragment: Fragment, otherFragment: Fragment?) { + if (fragment.isAdded && otherFragment != null) { + childFragmentManager + .beginTransaction() + .hide(otherFragment) + .show(fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } else if (fragment.isAdded && otherFragment == null) { + childFragmentManager + .beginTransaction() + .show(fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } else if (!fragment.isAdded && otherFragment != null) { + childFragmentManager + .beginTransaction() + .hide(otherFragment) + .add(R.id.explore_container, fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } else if (!fragment.isAdded) { + childFragmentManager + .beginTransaction() + .replace(R.id.explore_container, fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } + } + + private fun removeFragment(fragment: Fragment) { + childFragmentManager + .beginTransaction() + .remove(fragment) + .commit() + childFragmentManager.executePendingTransactions() + } + + override fun onMediaClicked(position: Int) { + if (binding != null) { + binding!!.exploreContainer.visibility = View.VISIBLE + } + if ((parentFragment as ExploreFragment).binding != null) { + (parentFragment as ExploreFragment).binding!!.tabLayout.visibility = + View.GONE + } + mediaDetails = MediaDetailPagerFragment.newInstance(false, true) + (parentFragment as ExploreFragment).setScroll(false) + setFragment(mediaDetails!!, listFragment) + mediaDetails!!.showImage(position) + } + + /** + * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index + * + * @param i It is the index of which media object is to be returned which is same as current + * index of viewPager. + * @return Media Object + */ + override fun getMediaAtPosition(i: Int): Media? = listFragment?.getMediaAtPosition(i) + + /** + * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain + * same number of media items as that of media elements in adapter. + * + * @return Total Media count in the adapter + */ + override fun getTotalMediaCount(): Int = listFragment?.getTotalMediaCount() ?: 0 + + override fun getContributionStateAt(position: Int): Int? = null + + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + override fun refreshNominatedMedia(index: Int) { + if (mediaDetails != null && !listFragment!!.isVisible) { + removeFragment(mediaDetails!!) + onMediaClicked(index) + } + } + + /** + * This method is called on success of API call for featured images or mobile uploads. The + * viewpager will notified that number of items have changed. + */ + override fun viewPagerNotifyDataSetChanged() { + mediaDetails?.notifyDataSetChanged() + } + + /** + * Performs back pressed action on the fragment. Return true if the event was handled by the + * mediaDetails otherwise returns false. + * + * @return + */ + fun backPressed(): Boolean { + if (null != mediaDetails && mediaDetails!!.isVisible) { + if ((parentFragment as ExploreFragment).binding != null) { + (parentFragment as ExploreFragment).binding!!.tabLayout.visibility = + View.VISIBLE + } + removeFragment(mediaDetails!!) + (parentFragment as ExploreFragment).setScroll(true) + setFragment(listFragment!!, mediaDetails) + (activity as MainActivity).showTabs() + return true + } else { + if ((activity as MainActivity?) != null) { + (activity as MainActivity).setSelectedItemId(NavTab.CONTRIBUTIONS.code()) + } + } + if ((activity as MainActivity?) != null) { + (activity as MainActivity).showTabs() + } + return false + } + + override fun onDestroy() { + super.onDestroy() + + binding = null + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java deleted file mode 100644 index 31a8e11ba..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java +++ /dev/null @@ -1,239 +0,0 @@ -package fr.free.nrw.commons.explore; - -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.category.CategoryImagesCallback; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.explore.map.ExploreMapFragment; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.media.MediaDetailProvider; -import fr.free.nrw.commons.navtab.NavTab; - -public class ExploreMapRootFragment extends CommonsDaggerSupportFragment implements - MediaDetailProvider, CategoryImagesCallback { - - private MediaDetailPagerFragment mediaDetails; - private ExploreMapFragment mapFragment; - - private FragmentFeaturedRootBinding binding; - - public ExploreMapRootFragment() { - //empty constructor necessary otherwise crashes on recreate - } - - @NonNull - public static ExploreMapRootFragment newInstance() { - ExploreMapRootFragment fragment = new ExploreMapRootFragment(); - fragment.setRetainInstance(true); - return fragment; - } - - public ExploreMapRootFragment(Bundle bundle) { - // get fragment arguments - String title = bundle.getString("categoryName"); - double zoom = bundle.getDouble("prev_zoom"); - double latitude = bundle.getDouble("prev_latitude"); - double longitude = bundle.getDouble("prev_longitude"); - - mapFragment = new ExploreMapFragment(); - Bundle featuredArguments = new Bundle(); - featuredArguments.putString("categoryName", title); - - // if we came from 'Show in Explore' in Nearby, pass on zoom and center - if (zoom != 0.0 || latitude != 0.0 || longitude != 0.0) { - featuredArguments.putDouble("prev_zoom", zoom); - featuredArguments.putDouble("prev_latitude", latitude); - featuredArguments.putDouble("prev_longitude", longitude); - } - mapFragment.setArguments(featuredArguments); - } - - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = FragmentFeaturedRootBinding.inflate(inflater, container, false); - - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - if (savedInstanceState == null) { - setFragment(mapFragment, mediaDetails); - } - } - - public void setFragment(Fragment fragment, Fragment otherFragment) { - if (fragment.isAdded() && otherFragment != null) { - getChildFragmentManager() - .beginTransaction() - .hide(otherFragment) - .show(fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (fragment.isAdded() && otherFragment == null) { - getChildFragmentManager() - .beginTransaction() - .show(fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (!fragment.isAdded() && otherFragment != null) { - getChildFragmentManager() - .beginTransaction() - .hide(otherFragment) - .add(R.id.explore_container, fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } else if (!fragment.isAdded()) { - getChildFragmentManager() - .beginTransaction() - .replace(R.id.explore_container, fragment) - .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") - .commit(); - getChildFragmentManager().executePendingTransactions(); - } - } - - public void removeFragment(Fragment fragment) { - getChildFragmentManager() - .beginTransaction() - .remove(fragment) - .commit(); - getChildFragmentManager().executePendingTransactions(); - } - - @Override - public void onAttach(final Context context) { - super.onAttach(context); - } - - @Override - public void onMediaClicked(int position) { - binding.exploreContainer.setVisibility(View.VISIBLE); - ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE); - mediaDetails = MediaDetailPagerFragment.newInstance(false, true); - ((ExploreFragment) getParentFragment()).setScroll(false); - setFragment(mediaDetails, mapFragment); - mediaDetails.showImage(position); - } - - /** - * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index - * - * @param i It is the index of which media object is to be returned which is same as current - * index of viewPager. - * @return Media Object - */ - @Override - public Media getMediaAtPosition(int i) { - if (mapFragment != null && mapFragment.mediaList != null) { - return mapFragment.mediaList.get(i); - } else { - return null; - } - } - - /** - * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain - * same number of media items as that of media elements in adapter. - * - * @return Total Media count in the adapter - */ - @Override - public int getTotalMediaCount() { - if (mapFragment != null && mapFragment.mediaList != null) { - return mapFragment.mediaList.size(); - } else { - return 0; - } - } - - @Override - public Integer getContributionStateAt(int position) { - return null; - } - - /** - * Reload media detail fragment once media is nominated - * - * @param index item position that has been nominated - */ - @Override - public void refreshNominatedMedia(int index) { - if (mediaDetails != null && !mapFragment.isVisible()) { - removeFragment(mediaDetails); - onMediaClicked(index); - } - } - - /** - * This method is called on success of API call for featured images or mobile uploads. The - * viewpager will notified that number of items have changed. - */ - @Override - public void viewPagerNotifyDataSetChanged() { - if (mediaDetails != null) { - mediaDetails.notifyDataSetChanged(); - } - } - - /** - * Performs back pressed action on the fragment. Return true if the event was handled by the - * mediaDetails otherwise returns false. - * - * @return - */ - public boolean backPressed() { - if (null != mediaDetails && mediaDetails.isVisible()) { - ((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE); - removeFragment(mediaDetails); - ((ExploreFragment) getParentFragment()).setScroll(true); - setFragment(mapFragment, mediaDetails); - ((MainActivity) getActivity()).showTabs(); - return true; - - } - if (mapFragment != null && mapFragment.isVisible()) { - if (mapFragment.backButtonClicked()) { - // Explore map fragment handled the event no further action required. - return true; - } else { - ((MainActivity) getActivity()).showTabs(); - return false; - } - } else { - ((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code()); - } - ((MainActivity) getActivity()).showTabs(); - return false; - } - - public void loadNearbyMapFromExplore() { - mapFragment.loadNearbyMapFromExplore(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.kt new file mode 100644 index 000000000..af65834eb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.kt @@ -0,0 +1,201 @@ +package fr.free.nrw.commons.explore + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.category.CategoryImagesCallback +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.explore.map.ExploreMapFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.media.MediaDetailProvider +import fr.free.nrw.commons.navtab.NavTab + +class ExploreMapRootFragment : CommonsDaggerSupportFragment, MediaDetailProvider, + CategoryImagesCallback { + private var mediaDetails: MediaDetailPagerFragment? = null + private var mapFragment: ExploreMapFragment? = null + private var binding: FragmentFeaturedRootBinding? = null + + constructor() + + constructor(bundle: Bundle) { + // get fragment arguments + val title = bundle.getString("categoryName") + val zoom = bundle.getDouble("prev_zoom") + val latitude = bundle.getDouble("prev_latitude") + val longitude = bundle.getDouble("prev_longitude") + + mapFragment = ExploreMapFragment() + val featuredArguments = bundleOf( + "categoryName" to title + ) + + // if we came from 'Show in Explore' in Nearby, pass on zoom and center + if (zoom != 0.0 || latitude != 0.0 || longitude != 0.0) { + featuredArguments.putDouble("prev_zoom", zoom) + featuredArguments.putDouble("prev_latitude", latitude) + featuredArguments.putDouble("prev_longitude", longitude) + } + mapFragment!!.arguments = featuredArguments + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + super.onCreate(savedInstanceState) + + binding = FragmentFeaturedRootBinding.inflate(inflater, container, false) + + return binding!!.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (savedInstanceState == null) { + setFragment(mapFragment!!, mediaDetails) + } + } + + fun setFragment(fragment: Fragment, otherFragment: Fragment?) { + if (fragment.isAdded && otherFragment != null) { + childFragmentManager + .beginTransaction() + .hide(otherFragment) + .show(fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } else if (fragment.isAdded && otherFragment == null) { + childFragmentManager + .beginTransaction() + .show(fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } else if (!fragment.isAdded && otherFragment != null) { + childFragmentManager + .beginTransaction() + .hide(otherFragment) + .add(R.id.explore_container, fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } else if (!fragment.isAdded) { + childFragmentManager + .beginTransaction() + .replace(R.id.explore_container, fragment) + .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") + .commit() + childFragmentManager.executePendingTransactions() + } + } + + private fun removeFragment(fragment: Fragment) { + childFragmentManager + .beginTransaction() + .remove(fragment) + .commit() + childFragmentManager.executePendingTransactions() + } + + override fun onMediaClicked(position: Int) { + binding!!.exploreContainer.visibility = View.VISIBLE + (parentFragment as ExploreFragment).binding!!.tabLayout.visibility = View.GONE + mediaDetails = MediaDetailPagerFragment.newInstance(false, true) + (parentFragment as ExploreFragment).setScroll(false) + setFragment(mediaDetails!!, mapFragment) + mediaDetails!!.showImage(position) + } + + /** + * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index + * + * @param i It is the index of which media object is to be returned which is same as current + * index of viewPager. + * @return Media Object + */ + override fun getMediaAtPosition(i: Int): Media? = mapFragment?.mediaList?.get(i) + + /** + * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain + * same number of media items as that of media elements in adapter. + * + * @return Total Media count in the adapter + */ + override fun getTotalMediaCount(): Int = mapFragment?.mediaList?.size ?: 0 + + override fun getContributionStateAt(position: Int): Int? = null + + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + override fun refreshNominatedMedia(index: Int) { + if (mediaDetails != null && !mapFragment!!.isVisible) { + removeFragment(mediaDetails!!) + onMediaClicked(index) + } + } + + /** + * This method is called on success of API call for featured images or mobile uploads. The + * viewpager will notified that number of items have changed. + */ + override fun viewPagerNotifyDataSetChanged() { + mediaDetails?.notifyDataSetChanged() + } + + /** + * Performs back pressed action on the fragment. Return true if the event was handled by the + * mediaDetails otherwise returns false. + * + * @return + */ + fun backPressed(): Boolean { + if (null != mediaDetails && mediaDetails!!.isVisible) { + (parentFragment as ExploreFragment).binding!!.tabLayout.visibility = View.VISIBLE + removeFragment(mediaDetails!!) + (parentFragment as ExploreFragment).setScroll(true) + setFragment(mapFragment!!, mediaDetails) + (activity as MainActivity).showTabs() + return true + } + if (mapFragment != null && mapFragment!!.isVisible) { + if (mapFragment!!.backButtonClicked()) { + // Explore map fragment handled the event no further action required. + return true + } else { + (activity as MainActivity).showTabs() + return false + } + } else { + (activity as MainActivity).setSelectedItemId(NavTab.CONTRIBUTIONS.code()) + } + (activity as MainActivity).showTabs() + return false + } + + fun loadNearbyMapFromExplore() = mapFragment?.loadNearbyMapFromExplore() + + override fun onDestroy() { + super.onDestroy() + + binding = null + } + + companion object { + fun newInstance(): ExploreMapRootFragment = ExploreMapRootFragment().apply { + retainInstance = true + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.java b/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.java deleted file mode 100644 index 4112cda95..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.java +++ /dev/null @@ -1,66 +0,0 @@ -package fr.free.nrw.commons.explore; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import androidx.viewpager.widget.ViewPager; - -/** - * ParentViewPager A custom viewPager whose scrolling can be enabled and disabled. - */ -public class ParentViewPager extends ViewPager { - - /** - * Boolean variable that stores the current state of pager scroll i.e(enabled or disabled) - */ - private boolean canScroll = true; - - - /** - * Default constructors - */ - public ParentViewPager(Context context) { - super(context); - } - - public ParentViewPager(Context context, AttributeSet attrs) { - super(context, attrs); - } - - - /** - * Setter method for canScroll. - */ - public void setCanScroll(boolean canScroll) { - this.canScroll = canScroll; - } - - - /** - * Getter method for canScroll. - */ - public boolean isCanScroll() { - return canScroll; - } - - - /** - * Method that prevents scrolling if canScroll is set to false. - */ - @Override - public boolean onTouchEvent(MotionEvent ev) { - return canScroll && super.onTouchEvent(ev); - } - - - /** - * A facilitator method that allows parent to intercept touch events before its children. thus - * making it possible to prevent swiping parent on child end. - */ - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - return canScroll && super.onInterceptTouchEvent(ev); - } - - -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.kt b/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.kt new file mode 100644 index 000000000..36e85e70a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.explore + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import androidx.viewpager.widget.ViewPager + +/** + * ParentViewPager A custom viewPager whose scrolling can be enabled and disabled. + */ +class ParentViewPager : ViewPager { + var canScroll: Boolean = true + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + override fun onTouchEvent(ev: MotionEvent): Boolean { + return canScroll && super.onTouchEvent(ev) + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + return canScroll && super.onInterceptTouchEvent(ev) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java deleted file mode 100644 index b27ffc338..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java +++ /dev/null @@ -1,285 +0,0 @@ -package fr.free.nrw.commons.explore; - -import static fr.free.nrw.commons.ViewPagerAdapter.pairOf; - -import android.os.Bundle; -import android.text.TextUtils; -import android.view.View; -import androidx.annotation.NonNull; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; -import com.jakewharton.rxbinding2.view.RxView; -import com.jakewharton.rxbinding2.widget.RxSearchView; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.ViewPagerAdapter; -import fr.free.nrw.commons.category.CategoryImagesCallback; -import fr.free.nrw.commons.databinding.ActivitySearchBinding; -import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment; -import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment; -import fr.free.nrw.commons.explore.media.SearchMediaFragment; -import fr.free.nrw.commons.explore.models.RecentSearch; -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao; -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.media.MediaDetailProvider; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.FragmentUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.android.schedulers.AndroidSchedulers; -import java.util.Date; -import java.util.concurrent.TimeUnit; -import javax.inject.Inject; -import timber.log.Timber; - -/** - * Represents search screen of this app - */ - -public class SearchActivity extends BaseActivity - implements MediaDetailProvider, CategoryImagesCallback { - - @Inject - RecentSearchesDao recentSearchesDao; - - private SearchMediaFragment searchMediaFragment; - private SearchCategoryFragment searchCategoryFragment; - private SearchDepictionsFragment searchDepictionsFragment; - private RecentSearchesFragment recentSearchesFragment; - private FragmentManager supportFragmentManager; - private MediaDetailPagerFragment mediaDetails; - ViewPagerAdapter viewPagerAdapter; - - private ActivitySearchBinding binding; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ActivitySearchBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - setTitle(getString(R.string.title_activity_search)); - setSupportActionBar(binding.toolbarSearch); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - binding.toolbarSearch.setNavigationOnClickListener(v->onBackPressed()); - supportFragmentManager = getSupportFragmentManager(); - setSearchHistoryFragment(); - viewPagerAdapter = new ViewPagerAdapter(this, getSupportFragmentManager()); - binding.viewPager.setAdapter(viewPagerAdapter); - binding.viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive - binding.tabLayout.setupWithViewPager(binding.viewPager); - setTabs(); - binding.searchBox.setQueryHint(getString(R.string.search_commons)); - binding.searchBox.onActionViewExpanded(); - binding.searchBox.clearFocus(); - - } - - /** - * This method sets the search history fragment. - * Search history fragment is displayed when query is empty. - */ - private void setSearchHistoryFragment() { - recentSearchesFragment = new RecentSearchesFragment(); - FragmentTransaction transaction = supportFragmentManager.beginTransaction(); - transaction.add(R.id.searchHistoryContainer, recentSearchesFragment).commit(); - } - - /** - * Sets the titles in the tabLayout and fragments in the viewPager - */ - public void setTabs() { - searchMediaFragment = new SearchMediaFragment(); - searchDepictionsFragment = new SearchDepictionsFragment(); - searchCategoryFragment= new SearchCategoryFragment(); - - viewPagerAdapter.setTabs( - pairOf(R.string.search_tab_title_media, searchMediaFragment), - pairOf(R.string.search_tab_title_categories, searchCategoryFragment), - pairOf(R.string.search_tab_title_depictions, searchDepictionsFragment) - ); - viewPagerAdapter.notifyDataSetChanged(); - getCompositeDisposable().add(RxSearchView.queryTextChanges(binding.searchBox) - .takeUntil(RxView.detaches(binding.searchBox)) - .debounce(500, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::handleSearch, Timber::e - )); - } - - private void handleSearch(final CharSequence query) { - if (!TextUtils.isEmpty(query)) { - saveRecentSearch(query.toString()); - binding.viewPager.setVisibility(View.VISIBLE); - binding.tabLayout.setVisibility(View.VISIBLE); - binding.searchHistoryContainer.setVisibility(View.GONE); - - if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) { - searchDepictionsFragment.onQueryUpdated(query.toString()); - } - - if (FragmentUtils.isFragmentUIActive(searchMediaFragment)) { - searchMediaFragment.onQueryUpdated(query.toString()); - } - - if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) { - searchCategoryFragment.onQueryUpdated(query.toString()); - } - - } - else { - //Open RecentSearchesFragment - recentSearchesFragment.updateRecentSearches(); - binding.viewPager.setVisibility(View.GONE); - binding.tabLayout.setVisibility(View.GONE); - setSearchHistoryFragment(); - binding.searchHistoryContainer.setVisibility(View.VISIBLE); - } - } - - private void saveRecentSearch(@NonNull final String query) { - final RecentSearch recentSearch = recentSearchesDao.find(query); - // Newly searched query... - if (recentSearch == null) { - recentSearchesDao.save(new RecentSearch(null, query, new Date())); - } else { - recentSearch.setLastSearched(new Date()); - recentSearchesDao.save(recentSearch); - } - } - - /** - * returns Media Object at position - * @param i position of Media in the imagesRecyclerView adapter. - */ - @Override - public Media getMediaAtPosition(int i) { - return searchMediaFragment.getMediaAtPosition(i); - } - - /** - * returns total number of images present in the imagesRecyclerView adapter. - */ - @Override - public int getTotalMediaCount() { - return searchMediaFragment.getTotalMediaCount(); - } - - @Override - public Integer getContributionStateAt(int position) { - return null; - } - - /** - * Reload media detail fragment once media is nominated - * - * @param index item position that has been nominated - */ - @Override - public void refreshNominatedMedia(int index) { - if (getSupportFragmentManager().getBackStackEntryCount() == 1) { - onBackPressed(); - onMediaClicked(index); - } - } - - /** - * This method is called on success of API call for image Search. - * The viewpager will notified that number of items have changed. - */ - @Override - public void viewPagerNotifyDataSetChanged() { - if (mediaDetails!=null){ - mediaDetails.notifyDataSetChanged(); - } - } - - /** - * Open media detail pager fragment on click of image in search results - * @param index item index that should be opened - */ - @Override - public void onMediaClicked(int index) { - ViewUtil.hideKeyboard(this.findViewById(R.id.searchBox)); - binding.tabLayout.setVisibility(View.GONE); - binding.viewPager.setVisibility(View.GONE); - binding.mediaContainer.setVisibility(View.VISIBLE); - binding.searchBox.setVisibility(View.GONE);// to remove searchview when mediaDetails fragment open - if (mediaDetails == null || !mediaDetails.isVisible()) { - // set isFeaturedImage true for featured images, to include author field on media detail - mediaDetails = MediaDetailPagerFragment.newInstance(false, true); - supportFragmentManager - .beginTransaction() - .hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount())) - .add(R.id.mediaContainer, mediaDetails) - .addToBackStack(null) - .commit(); - // Reason for using hide, add instead of replace is to maintain scroll position after - // coming back to the search activity. See https://github.com/commons-app/apps-android-commons/issues/1631 - // https://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack/19022550#19022550 - supportFragmentManager.executePendingTransactions(); - } - mediaDetails.showImage(index); - } - - /** - * This method is called on Screen Rotation - */ - @Override - protected void onResume() { - if (supportFragmentManager.getBackStackEntryCount()==1){ - //FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time. - //FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894 - // This is called on screen rotation when user is inside media details. Ideally it should show Media Details but since we are not saving the state now. We are throwing the user to search screen otherwise the app was crashing. - // - onBackPressed(); - } - super.onResume(); - } - - /** - * This method is called on backPressed of anyFragment in the activity. - * If condition is called when mediaDetailFragment is opened. - */ - @Override - public void onBackPressed() { - //Remove the backstack entry that gets added when share button is clicked - //fixing:https://github.com/commons-app/apps-android-commons/issues/2296 - if (getSupportFragmentManager().getBackStackEntryCount() == 2) { - supportFragmentManager - .beginTransaction() - .remove(mediaDetails) - .commit(); - supportFragmentManager.popBackStack(); - supportFragmentManager.executePendingTransactions(); - } - if (getSupportFragmentManager().getBackStackEntryCount() == 1) { - // back to search so show search toolbar and hide navigation toolbar - binding.searchBox.setVisibility(View.VISIBLE);//set the searchview - binding.tabLayout.setVisibility(View.VISIBLE); - binding.viewPager.setVisibility(View.VISIBLE); - binding.mediaContainer.setVisibility(View.GONE); - } else { - binding.toolbarSearch.setVisibility(View.GONE); - } - super.onBackPressed(); - } - - /** - * This method is called on click of a recent search to update query in SearchView. - * @param query Recent Search Query - */ - public void updateText(String query) { - binding.searchBox.setQuery(query, true); - // Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details. - // https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511 - binding.viewPager.requestFocus(); - } - - @Override protected void onDestroy() { - super.onDestroy(); - //Dispose the disposables when the activity is destroyed - getCompositeDisposable().dispose(); - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt new file mode 100644 index 000000000..7b7bb2cd5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt @@ -0,0 +1,252 @@ +package fr.free.nrw.commons.explore + +import android.os.Bundle +import android.text.TextUtils +import android.view.View +import androidx.fragment.app.FragmentManager +import com.jakewharton.rxbinding2.view.RxView +import com.jakewharton.rxbinding2.widget.RxSearchView +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.ViewPagerAdapter +import fr.free.nrw.commons.category.CategoryImagesCallback +import fr.free.nrw.commons.databinding.ActivitySearchBinding +import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment +import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment +import fr.free.nrw.commons.explore.media.SearchMediaFragment +import fr.free.nrw.commons.explore.models.RecentSearch +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.media.MediaDetailProvider +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.FragmentUtils.isFragmentUIActive +import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard +import io.reactivex.android.schedulers.AndroidSchedulers +import timber.log.Timber +import java.util.Date +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** + * Represents search screen of this app + */ +class SearchActivity : BaseActivity(), MediaDetailProvider, CategoryImagesCallback { + @JvmField + @Inject + var recentSearchesDao: RecentSearchesDao? = null + + private var searchMediaFragment: SearchMediaFragment? = null + private var searchCategoryFragment: SearchCategoryFragment? = null + private var searchDepictionsFragment: SearchDepictionsFragment? = null + private var recentSearchesFragment: RecentSearchesFragment? = null + private var supportFragmentManager: FragmentManager? = null + private var mediaDetails: MediaDetailPagerFragment? = null + private var viewPagerAdapter: ViewPagerAdapter? = null + private var binding: ActivitySearchBinding? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySearchBinding.inflate(layoutInflater) + setContentView(binding!!.root) + + title = getString(R.string.title_activity_search) + setSupportActionBar(binding!!.toolbarSearch) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + binding!!.toolbarSearch.setNavigationOnClickListener { onBackPressed() } + supportFragmentManager = getSupportFragmentManager() + setSearchHistoryFragment() + viewPagerAdapter = ViewPagerAdapter(this, getSupportFragmentManager()) + binding!!.viewPager.adapter = viewPagerAdapter + binding!!.viewPager.offscreenPageLimit = 2 // Because we want all the fragments to be alive + binding!!.tabLayout.setupWithViewPager(binding!!.viewPager) + setTabs() + binding!!.searchBox.queryHint = getString(R.string.search_commons) + binding!!.searchBox.onActionViewExpanded() + binding!!.searchBox.clearFocus() + } + + /** + * This method sets the search history fragment. + * Search history fragment is displayed when query is empty. + */ + private fun setSearchHistoryFragment() { + recentSearchesFragment = RecentSearchesFragment() + val transaction = supportFragmentManager!!.beginTransaction() + transaction.add(R.id.searchHistoryContainer, recentSearchesFragment!!).commit() + } + + /** + * Sets the titles in the tabLayout and fragments in the viewPager + */ + fun setTabs() { + searchMediaFragment = SearchMediaFragment() + searchDepictionsFragment = SearchDepictionsFragment() + searchCategoryFragment = SearchCategoryFragment() + + viewPagerAdapter!!.setTabs( + R.string.search_tab_title_media to searchMediaFragment!!, + R.string.search_tab_title_categories to searchCategoryFragment!!, + R.string.search_tab_title_depictions to searchDepictionsFragment!! + ) + viewPagerAdapter!!.notifyDataSetChanged() + compositeDisposable.add( + RxSearchView.queryTextChanges(binding!!.searchBox) + .takeUntil(RxView.detaches(binding!!.searchBox)) + .debounce(500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(::handleSearch, Timber::e) + ) + } + + private fun handleSearch(query: CharSequence) { + if (!TextUtils.isEmpty(query)) { + saveRecentSearch(query.toString()) + binding!!.viewPager.visibility = View.VISIBLE + binding!!.tabLayout.visibility = View.VISIBLE + binding!!.searchHistoryContainer.visibility = View.GONE + + if (isFragmentUIActive(searchDepictionsFragment)) { + searchDepictionsFragment!!.onQueryUpdated(query.toString()) + } + + if (isFragmentUIActive(searchMediaFragment)) { + searchMediaFragment!!.onQueryUpdated(query.toString()) + } + + if (isFragmentUIActive(searchCategoryFragment)) { + searchCategoryFragment!!.onQueryUpdated(query.toString()) + } + } else { + //Open RecentSearchesFragment + recentSearchesFragment!!.updateRecentSearches() + binding!!.viewPager.visibility = View.GONE + binding!!.tabLayout.visibility = View.GONE + setSearchHistoryFragment() + binding!!.searchHistoryContainer.visibility = View.VISIBLE + } + } + + private fun saveRecentSearch(query: String) { + val recentSearch = recentSearchesDao!!.find(query) + // Newly searched query... + if (recentSearch == null) { + recentSearchesDao!!.save(RecentSearch(null, query, Date())) + } else { + recentSearch.lastSearched = Date() + recentSearchesDao!!.save(recentSearch) + } + } + + override fun getMediaAtPosition(i: Int): Media? = searchMediaFragment!!.getMediaAtPosition(i) + + override fun getTotalMediaCount(): Int = searchMediaFragment!!.getTotalMediaCount() + + override fun getContributionStateAt(position: Int): Int? = null + + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + override fun refreshNominatedMedia(index: Int) { + if (getSupportFragmentManager().backStackEntryCount == 1) { + onBackPressed() + onMediaClicked(index) + } + } + + /** + * This method is called on success of API call for image Search. + * The viewpager will notified that number of items have changed. + */ + override fun viewPagerNotifyDataSetChanged() { + mediaDetails?.notifyDataSetChanged() + } + + /** + * Open media detail pager fragment on click of image in search results + * @param position item index that should be opened + */ + override fun onMediaClicked(position: Int) { + hideKeyboard(findViewById(R.id.searchBox)) + binding!!.tabLayout.visibility = View.GONE + binding!!.viewPager.visibility = View.GONE + binding!!.mediaContainer.visibility = View.VISIBLE + binding!!.searchBox.visibility = + View.GONE // to remove searchview when mediaDetails fragment open + if (mediaDetails == null || !mediaDetails!!.isVisible) { + // set isFeaturedImage true for featured images, to include author field on media detail + mediaDetails = MediaDetailPagerFragment.newInstance(false, true) + supportFragmentManager!! + .beginTransaction() + .hide(supportFragmentManager!!.fragments[supportFragmentManager!!.backStackEntryCount]) + .add(R.id.mediaContainer, mediaDetails!!) + .addToBackStack(null) + .commit() + // Reason for using hide, add instead of replace is to maintain scroll position after + // coming back to the search activity. See https://github.com/commons-app/apps-android-commons/issues/1631 + // https://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack/19022550#19022550 + supportFragmentManager!!.executePendingTransactions() + } + mediaDetails!!.showImage(position) + } + + /** + * This method is called on Screen Rotation + */ + override fun onResume() { + if (supportFragmentManager!!.backStackEntryCount == 1) { + //FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time. + //FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894 + // This is called on screen rotation when user is inside media details. Ideally it should show Media Details but since we are not saving the state now. We are throwing the user to search screen otherwise the app was crashing. + onBackPressed() + } + super.onResume() + } + + /** + * This method is called on backPressed of anyFragment in the activity. + * If condition is called when mediaDetailFragment is opened. + */ + override fun onBackPressed() { + //Remove the backstack entry that gets added when share button is clicked + //fixing:https://github.com/commons-app/apps-android-commons/issues/2296 + if (getSupportFragmentManager().backStackEntryCount == 2) { + supportFragmentManager!! + .beginTransaction() + .remove(mediaDetails!!) + .commit() + supportFragmentManager!!.popBackStack() + supportFragmentManager!!.executePendingTransactions() + } + if (getSupportFragmentManager().backStackEntryCount == 1) { + // back to search so show search toolbar and hide navigation toolbar + binding!!.searchBox.visibility = View.VISIBLE //set the searchview + binding!!.tabLayout.visibility = View.VISIBLE + binding!!.viewPager.visibility = View.VISIBLE + binding!!.mediaContainer.visibility = View.GONE + } else { + binding!!.toolbarSearch.visibility = View.GONE + } + super.onBackPressed() + } + + /** + * This method is called on click of a recent search to update query in SearchView. + * @param query Recent Search Query + */ + fun updateText(query: String?) { + binding!!.searchBox.setQuery(query, true) + // Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details. + // https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511 + binding!!.viewPager.requestFocus() + } + + override fun onDestroy() { + super.onDestroy() + //Dispose the disposables when the activity is destroyed + compositeDisposable.dispose() + binding = null + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/PageableDepictionsFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/PageableDepictionsFragment.kt index 9c41628a2..7c1d08bac 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/PageableDepictionsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/PageableDepictionsFragment.kt @@ -7,6 +7,6 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem abstract class PageableDepictionsFragment : BasePagingFragment() { override val errorTextId: Int = R.string.error_loading_depictions override val pagedListAdapter by lazy { - DepictionAdapter { WikidataItemDetailsActivity.startYourself(context, it) } + DepictionAdapter { WikidataItemDetailsActivity.startYourself(requireContext(), it) } } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java deleted file mode 100644 index ec5ea42a4..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java +++ /dev/null @@ -1,302 +0,0 @@ -package fr.free.nrw.commons.explore.depictions; - -import static fr.free.nrw.commons.ViewPagerAdapter.pairOf; -import static fr.free.nrw.commons.utils.UrlUtilsKt.handleWebUrl; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import androidx.fragment.app.FragmentManager; -import com.google.android.material.snackbar.Snackbar; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.ViewPagerAdapter; -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao; -import fr.free.nrw.commons.category.CategoryImagesCallback; -import fr.free.nrw.commons.databinding.ActivityWikidataItemDetailsBinding; -import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment; -import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment; -import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.media.MediaDetailProvider; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.upload.structure.depictions.DepictModel; -import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; -import fr.free.nrw.commons.wikidata.WikidataConstants; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import javax.inject.Inject; - -/** - * Activity to show depiction media, parent classes and child classes of depicted items in Explore - */ -public class WikidataItemDetailsActivity extends BaseActivity implements MediaDetailProvider, - CategoryImagesCallback { - private FragmentManager supportFragmentManager; - private DepictedImagesFragment depictionImagesListFragment; - private MediaDetailPagerFragment mediaDetailPagerFragment; - - /** - * Name of the depicted item - * Ex: Rabbit - */ - - @Inject BookmarkItemsDao bookmarkItemsDao; - private CompositeDisposable compositeDisposable; - @Inject - DepictModel depictModel; - private String wikidataItemName; - private ActivityWikidataItemDetailsBinding binding; - - ViewPagerAdapter viewPagerAdapter; - private DepictedItem wikidataItem; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = ActivityWikidataItemDetailsBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - compositeDisposable = new CompositeDisposable(); - supportFragmentManager = getSupportFragmentManager(); - viewPagerAdapter = new ViewPagerAdapter(this, getSupportFragmentManager()); - binding.viewPager.setAdapter(viewPagerAdapter); - binding.viewPager.setOffscreenPageLimit(2); - binding.tabLayout.setupWithViewPager(binding.viewPager); - - final DepictedItem depictedItem = getIntent().getParcelableExtra( - WikidataConstants.BOOKMARKS_ITEMS); - wikidataItem = depictedItem; - setSupportActionBar(binding.toolbarBinding.toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - setTabs(); - setPageTitle(); - } - - /** - * Gets the passed wikidataItemName from the intents and displays it as the page title - */ - private void setPageTitle() { - if (getIntent() != null && getIntent().getStringExtra("wikidataItemName") != null) { - setTitle(getIntent().getStringExtra("wikidataItemName")); - } - } - - /** - * This method is called on success of API call for featured Images. - * The viewpager will notified that number of items have changed. - */ - @Override - public void viewPagerNotifyDataSetChanged() { - if (mediaDetailPagerFragment !=null){ - mediaDetailPagerFragment.notifyDataSetChanged(); - } - } - - /** - * This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab, - * Set the fragments according to the tab selected in the viewPager. - */ - private void setTabs() { - depictionImagesListFragment = new DepictedImagesFragment(); - ChildDepictionsFragment childDepictionsFragment = new ChildDepictionsFragment(); - ParentDepictionsFragment parentDepictionsFragment = new ParentDepictionsFragment(); - wikidataItemName = getIntent().getStringExtra("wikidataItemName"); - String entityId = getIntent().getStringExtra("entityId"); - if (getIntent() != null && wikidataItemName != null) { - Bundle arguments = new Bundle(); - arguments.putString("wikidataItemName", wikidataItemName); - arguments.putString("entityId", entityId); - depictionImagesListFragment.setArguments(arguments); - parentDepictionsFragment.setArguments(arguments); - childDepictionsFragment.setArguments(arguments); - } - - viewPagerAdapter.setTabs( - pairOf(R.string.title_for_media, depictionImagesListFragment), - pairOf(R.string.title_for_subcategories, childDepictionsFragment), - pairOf(R.string.title_for_parent_categories, parentDepictionsFragment) - ); - binding.viewPager.setOffscreenPageLimit(2); - viewPagerAdapter.notifyDataSetChanged(); - - } - - - /** - * Shows media detail fragment when user clicks on any image in the list - */ - @Override - public void onMediaClicked(int position) { - binding.tabLayout.setVisibility(View.GONE); - binding.viewPager.setVisibility(View.GONE); - binding.mediaContainer.setVisibility(View.VISIBLE); - if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) { - // set isFeaturedImage true for featured images, to include author field on media detail - mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true); - FragmentManager supportFragmentManager = getSupportFragmentManager(); - supportFragmentManager - .beginTransaction() - .replace(R.id.mediaContainer, mediaDetailPagerFragment) - .addToBackStack(null) - .commit(); - supportFragmentManager.executePendingTransactions(); - } - mediaDetailPagerFragment.showImage(position); - } - - /** - * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index - * @param i It is the index of which media object is to be returned which is same as - * current index of viewPager. - * @return Media Object - */ - @Override - public Media getMediaAtPosition(int i) { - return depictionImagesListFragment.getMediaAtPosition(i); - } - - /** - * This method is called on backPressed of anyFragment in the activity. - * If condition is called when mediaDetailFragment is opened. - */ - @Override - public void onBackPressed() { - if (supportFragmentManager.getBackStackEntryCount() == 1){ - binding.tabLayout.setVisibility(View.VISIBLE); - binding.viewPager.setVisibility(View.VISIBLE); - binding.mediaContainer.setVisibility(View.GONE); - } - super.onBackPressed(); - } - - /** - * This method is called on from getCount of MediaDetailPagerFragment - * The viewpager will contain same number of media items as that of media elements in adapter. - * @return Total Media count in the adapter - */ - @Override - public int getTotalMediaCount() { - return depictionImagesListFragment.getTotalMediaCount(); - } - - @Override - public Integer getContributionStateAt(int position) { - return null; - } - - /** - * Reload media detail fragment once media is nominated - * - * @param index item position that has been nominated - */ - @Override - public void refreshNominatedMedia(int index) { - if (getSupportFragmentManager().getBackStackEntryCount() == 1) { - onBackPressed(); - onMediaClicked(index); - } - } - - /** - * Consumers should be simply using this method to use this activity. - * - * @param context A Context of the application package implementing this class. - * @param depictedItem Name of the depicts for displaying its details - */ - public static void startYourself(Context context, DepictedItem depictedItem) { - Intent intent = new Intent(context, WikidataItemDetailsActivity.class); - intent.putExtra("wikidataItemName", depictedItem.getName()); - intent.putExtra("entityId", depictedItem.getId()); - intent.putExtra(WikidataConstants.BOOKMARKS_ITEMS, depictedItem); - context.startActivity(intent); - } - - /** - * This function inflates the menu - */ - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater menuInflater=getMenuInflater(); - menuInflater.inflate(R.menu.menu_wikidata_item,menu); - - updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_item)); - - return super.onCreateOptionsMenu(menu); - } - - /** - * This method handles the logic on item select in toolbar menu - * Currently only 1 choice is available to open Wikidata item details page in browser - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - - switch (item.getItemId()){ - case R.id.browser_actions_menu_items: - String entityId=getIntent().getStringExtra("entityId"); - Uri uri = Uri.parse("https://www.wikidata.org/wiki/" + entityId); - handleWebUrl(this, uri); - return true; - case R.id.menu_bookmark_current_item: - - if(getIntent().getStringExtra("fragment") != null) { - compositeDisposable.add(depictModel.getDepictions( - getIntent().getStringExtra("entityId") - ).subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(depictedItems -> { - final boolean bookmarkExists = bookmarkItemsDao.updateBookmarkItem( - depictedItems.get(0)); - final Snackbar snackbar - = bookmarkExists ? Snackbar.make(findViewById(R.id.toolbar_layout), - R.string.add_bookmark, Snackbar.LENGTH_LONG) - : Snackbar.make(findViewById(R.id.toolbar_layout), - R.string.remove_bookmark, - Snackbar.LENGTH_LONG); - - snackbar.show(); - updateBookmarkState(item); - })); - - } else { - final boolean bookmarkExists - = bookmarkItemsDao.updateBookmarkItem(wikidataItem); - final Snackbar snackbar - = bookmarkExists ? Snackbar.make(findViewById(R.id.toolbar_layout), - R.string.add_bookmark, Snackbar.LENGTH_LONG) - : Snackbar.make(findViewById(R.id.toolbar_layout), R.string.remove_bookmark, - Snackbar.LENGTH_LONG); - - snackbar.show(); - updateBookmarkState(item); - } - return true; - case android.R.id.home: - onBackPressed(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private void updateBookmarkState(final MenuItem item) { - final boolean isBookmarked; - if(getIntent().getStringExtra("fragment") != null) { - isBookmarked - = bookmarkItemsDao.findBookmarkItem(getIntent().getStringExtra("entityId")); - } else { - isBookmarked = bookmarkItemsDao.findBookmarkItem(wikidataItem.getId()); - } - final int icon - = isBookmarked ? R.drawable.menu_ic_round_star_filled_24px - : R.drawable.menu_ic_round_star_border_24px; - item.setIcon(icon); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt new file mode 100644 index 000000000..4696ae8d4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt @@ -0,0 +1,295 @@ +package fr.free.nrw.commons.explore.depictions + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.core.os.bundleOf +import androidx.fragment.app.FragmentManager +import com.google.android.material.snackbar.Snackbar +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.ViewPagerAdapter +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao +import fr.free.nrw.commons.category.CategoryImagesCallback +import fr.free.nrw.commons.databinding.ActivityWikidataItemDetailsBinding +import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment +import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment +import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.media.MediaDetailProvider +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.upload.structure.depictions.DepictModel +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem +import fr.free.nrw.commons.utils.handleWebUrl +import fr.free.nrw.commons.wikidata.WikidataConstants +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.functions.Consumer +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +/** + * Activity to show depiction media, parent classes and child classes of depicted items in Explore + */ +class WikidataItemDetailsActivity : BaseActivity(), MediaDetailProvider, CategoryImagesCallback { + @JvmField + @Inject + var bookmarkItemsDao: BookmarkItemsDao? = null + + @JvmField + @Inject + var depictModel: DepictModel? = null + + private var supportFragmentManager: FragmentManager? = null + private var depictionImagesListFragment: DepictedImagesFragment? = null + private var mediaDetailPagerFragment: MediaDetailPagerFragment? = null + private var binding: ActivityWikidataItemDetailsBinding? = null + + var viewPagerAdapter: ViewPagerAdapter? = null + private var wikidataItem: DepictedItem? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityWikidataItemDetailsBinding.inflate(layoutInflater) + setContentView(binding!!.root) + supportFragmentManager = getSupportFragmentManager() + viewPagerAdapter = ViewPagerAdapter(this, getSupportFragmentManager()) + binding!!.viewPager.adapter = viewPagerAdapter + binding!!.viewPager.offscreenPageLimit = 2 + binding!!.tabLayout.setupWithViewPager(binding!!.viewPager) + + wikidataItem = intent.getParcelableExtra(WikidataConstants.BOOKMARKS_ITEMS) + setSupportActionBar(binding!!.toolbarBinding.toolbar) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + setTabs() + setPageTitle() + } + + /** + * Gets the passed wikidataItemName from the intents and displays it as the page title + */ + private fun setPageTitle() { + if (intent != null && intent.getStringExtra("wikidataItemName") != null) { + title = intent.getStringExtra("wikidataItemName") + } + } + + /** + * This method is called on success of API call for featured Images. + * The viewpager will notified that number of items have changed. + */ + override fun viewPagerNotifyDataSetChanged() { + if (mediaDetailPagerFragment != null) { + mediaDetailPagerFragment!!.notifyDataSetChanged() + } + } + + /** + * This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab, + * Set the fragments according to the tab selected in the viewPager. + */ + private fun setTabs() { + depictionImagesListFragment = DepictedImagesFragment() + val childDepictionsFragment = ChildDepictionsFragment() + val parentDepictionsFragment = ParentDepictionsFragment() + val wikidataItemName = intent.getStringExtra("wikidataItemName") + val entityId = intent.getStringExtra("entityId") + if (intent != null && wikidataItemName != null) { + val arguments = bundleOf( + "wikidataItemName" to wikidataItemName, + "entityId" to entityId + ) + depictionImagesListFragment!!.arguments = arguments + parentDepictionsFragment.arguments = arguments + childDepictionsFragment.arguments = arguments + } + + viewPagerAdapter!!.setTabs( + R.string.title_for_media to depictionImagesListFragment!!, + R.string.title_for_subcategories to childDepictionsFragment, + R.string.title_for_parent_categories to parentDepictionsFragment + ) + binding!!.viewPager.offscreenPageLimit = 2 + viewPagerAdapter!!.notifyDataSetChanged() + } + + + /** + * Shows media detail fragment when user clicks on any image in the list + */ + override fun onMediaClicked(position: Int) { + binding!!.tabLayout.visibility = View.GONE + binding!!.viewPager.visibility = View.GONE + binding!!.mediaContainer.visibility = View.VISIBLE + if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment!!.isVisible) { + // set isFeaturedImage true for featured images, to include author field on media detail + mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true) + val supportFragmentManager = getSupportFragmentManager() + supportFragmentManager + .beginTransaction() + .replace(R.id.mediaContainer, mediaDetailPagerFragment!!) + .addToBackStack(null) + .commit() + supportFragmentManager.executePendingTransactions() + } + mediaDetailPagerFragment!!.showImage(position) + } + + /** + * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index + * @param i It is the index of which media object is to be returned which is same as + * current index of viewPager. + * @return Media Object + */ + override fun getMediaAtPosition(i: Int): Media? { + return depictionImagesListFragment!!.getMediaAtPosition(i) + } + + /** + * This method is called on backPressed of anyFragment in the activity. + * If condition is called when mediaDetailFragment is opened. + */ + override fun onBackPressed() { + if (supportFragmentManager!!.backStackEntryCount == 1) { + binding!!.tabLayout.visibility = View.VISIBLE + binding!!.viewPager.visibility = View.VISIBLE + binding!!.mediaContainer.visibility = View.GONE + } + super.onBackPressed() + } + + /** + * This method is called on from getCount of MediaDetailPagerFragment + * The viewpager will contain same number of media items as that of media elements in adapter. + * @return Total Media count in the adapter + */ + override fun getTotalMediaCount(): Int = depictionImagesListFragment!!.getTotalMediaCount() + + override fun getContributionStateAt(position: Int): Int? = null + + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + override fun refreshNominatedMedia(index: Int) { + if (getSupportFragmentManager().backStackEntryCount == 1) { + onBackPressed() + onMediaClicked(index) + } + } + + /** + * This function inflates the menu + */ + override fun onCreateOptionsMenu(menu: Menu): Boolean { + val menuInflater = menuInflater + menuInflater.inflate(R.menu.menu_wikidata_item, menu) + + updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_item)) + + return super.onCreateOptionsMenu(menu) + } + + /** + * This method handles the logic on item select in toolbar menu + * Currently only 1 choice is available to open Wikidata item details page in browser + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.browser_actions_menu_items -> { + val entityId = intent.getStringExtra("entityId") + val uri = Uri.parse("https://www.wikidata.org/wiki/$entityId") + handleWebUrl(this, uri) + return true + } + + R.id.menu_bookmark_current_item -> { + if (intent.getStringExtra("fragment") != null) { + compositeDisposable!!.add( + depictModel!!.getDepictions( + intent.getStringExtra("entityId")!! + ).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(Consumer> { depictedItems: List -> + val bookmarkExists = bookmarkItemsDao!!.updateBookmarkItem( + depictedItems[0]!! + ) + val snackbar = if (bookmarkExists) + Snackbar.make( + findViewById(R.id.toolbar_layout), + R.string.add_bookmark, Snackbar.LENGTH_LONG + ) + else + Snackbar.make( + findViewById(R.id.toolbar_layout), + R.string.remove_bookmark, + Snackbar.LENGTH_LONG + ) + + snackbar.show() + updateBookmarkState(item) + }) + ) + } else { + val bookmarkExists = bookmarkItemsDao!!.updateBookmarkItem(wikidataItem!!) + val snackbar = if (bookmarkExists) + Snackbar.make( + findViewById(R.id.toolbar_layout), + R.string.add_bookmark, Snackbar.LENGTH_LONG + ) + else + Snackbar.make( + findViewById(R.id.toolbar_layout), R.string.remove_bookmark, + Snackbar.LENGTH_LONG + ) + + snackbar.show() + updateBookmarkState(item) + } + return true + } + + android.R.id.home -> { + onBackPressed() + return true + } + + else -> return super.onOptionsItemSelected(item) + } + } + + private fun updateBookmarkState(item: MenuItem) { + val isBookmarked: Boolean = if (intent.getStringExtra("fragment") != null) { + bookmarkItemsDao!!.findBookmarkItem(intent.getStringExtra("entityId")) + } else { + bookmarkItemsDao!!.findBookmarkItem(wikidataItem!!.id) + } + item.setIcon(if (isBookmarked) { + R.drawable.menu_ic_round_star_filled_24px + } else { + R.drawable.menu_ic_round_star_border_24px + }) + } + + companion object { + /** + * Consumers should be simply using this method to use this activity. + * + * @param context A Context of the application package implementing this class. + * @param depictedItem Name of the depicts for displaying its details + */ + fun startYourself(context: Context, depictedItem: DepictedItem) { + val intent = Intent(context, WikidataItemDetailsActivity::class.java).apply { + putExtra("wikidataItemName", depictedItem.name) + putExtra("entityId", depictedItem.id) + putExtra(WikidataConstants.BOOKMARKS_ITEMS, depictedItem) + } + context.startActivity(intent) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.java deleted file mode 100644 index 5e674dceb..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.java +++ /dev/null @@ -1,34 +0,0 @@ -package fr.free.nrw.commons.explore.map; - -import androidx.annotation.NonNull; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.media.MediaClient; -import java.util.Collections; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -public class ExploreMapCalls { - - @Inject - MediaClient mediaClient; - - @Inject - public ExploreMapCalls() { - } - - /** - * Calls method to query Commons for uploads around a location - * - * @param currentLatLng coordinates of search location - * @return list of places obtained - */ - @NonNull - List callCommonsQuery(final LatLng currentLatLng) { - String coordinates = currentLatLng.getLatitude() + "|" + currentLatLng.getLongitude(); - return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet(); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.kt new file mode 100644 index 000000000..6c62d3667 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapCalls.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.explore.map + +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.media.MediaClient +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ExploreMapCalls @Inject constructor() { + @Inject + @JvmField + var mediaClient: MediaClient? = null + + /** + * Calls method to query Commons for uploads around a location + * + * @param currentLatLng coordinates of search location + * @return list of places obtained + */ + fun callCommonsQuery(currentLatLng: LatLng): List { + val coordinates = currentLatLng.latitude.toString() + "|" + currentLatLng.longitude + return mediaClient!!.getMediaListFromGeoSearch(coordinates).blockingGet() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.java deleted file mode 100644 index feb66bf55..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.java +++ /dev/null @@ -1,45 +0,0 @@ -package fr.free.nrw.commons.explore.map; - -import android.content.Context; -import fr.free.nrw.commons.BaseMarker; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.location.LocationServiceManager; -import java.util.List; - -public class ExploreMapContract { - - interface View { - boolean isNetworkConnectionEstablished(); - void populatePlaces(LatLng curlatLng); - void askForLocationPermission(); - void recenterMap(LatLng curLatLng); - void hideBottomDetailsSheet(); - LatLng getMapCenter(); - LatLng getMapFocus(); - LatLng getLastMapFocus(); - void addMarkersToMap(final List nearbyBaseMarkers); - void clearAllMarkers(); - void addSearchThisAreaButtonAction(); - void setSearchThisAreaButtonVisibility(boolean isVisible); - void setProgressBarVisibility(boolean isVisible); - boolean isDetailsBottomSheetVisible(); - boolean isSearchThisAreaButtonVisible(); - Context getContext(); - LatLng getLastLocation(); - void disableFABRecenter(); - void enableFABRecenter(); - void setFABRecenterAction(android.view.View.OnClickListener onClickListener); - boolean backButtonClicked(); - } - - interface UserActions { - void updateMap(LocationServiceManager.LocationChangeType locationChangeType); - void lockUnlockNearby(boolean isNearbyLocked); - void attachView(View view); - void detachView(); - void setActionListeners(JsonKvStore applicationKvStore); - boolean backButtonClicked(); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.kt new file mode 100644 index 000000000..306446a43 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapContract.kt @@ -0,0 +1,43 @@ +package fr.free.nrw.commons.explore.map + +import android.content.Context +import android.view.View +import fr.free.nrw.commons.BaseMarker +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType + +class ExploreMapContract { + interface View { + fun isNetworkConnectionEstablished(): Boolean + fun populatePlaces(curlatLng: LatLng?) + fun askForLocationPermission() + fun recenterMap(curLatLng: LatLng?) + fun hideBottomDetailsSheet() + fun getMapCenter(): LatLng? + fun getMapFocus(): LatLng? + fun getLastMapFocus(): LatLng? + fun addMarkersToMap(nearbyBaseMarkers: List?) + fun clearAllMarkers() + fun addSearchThisAreaButtonAction() + fun setSearchThisAreaButtonVisibility(isVisible: Boolean) + fun setProgressBarVisibility(isVisible: Boolean) + fun isDetailsBottomSheetVisible(): Boolean + fun isSearchThisAreaButtonVisible(): Boolean + fun getContext(): Context? + fun getLastLocation(): LatLng? + fun disableFABRecenter() + fun enableFABRecenter() + fun setFABRecenterAction(onClickListener: android.view.View.OnClickListener?) + fun backButtonClicked(): Boolean + } + + interface UserActions { + fun updateMap(locationChangeType: LocationChangeType) + fun lockUnlockNearby(isNearbyLocked: Boolean) + fun attachView(view: View?) + fun detachView() + fun setActionListeners(applicationKvStore: JsonKvStore?) + fun backButtonClicked(): Boolean + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.java deleted file mode 100644 index c944f75a1..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.java +++ /dev/null @@ -1,213 +0,0 @@ -package fr.free.nrw.commons.explore.map; - -import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween; -import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; -import com.bumptech.glide.request.target.CustomTarget; -import com.bumptech.glide.request.transition.Transition; -import fr.free.nrw.commons.BaseMarker; -import fr.free.nrw.commons.MapController; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.utils.ImageUtils; -import fr.free.nrw.commons.utils.LocationUtils; -import fr.free.nrw.commons.utils.PlaceUtils; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.inject.Inject; -import timber.log.Timber; - -public class ExploreMapController extends MapController { - - private final ExploreMapCalls exploreMapCalls; - public LatLng latestSearchLocation; // Can be current and camera target on search this area button is used - public LatLng currentLocation; // current location of user - public double latestSearchRadius = 0; // Any last search radius - public double currentLocationSearchRadius = 0; // Search radius of only searches around current location - - - @Inject - public ExploreMapController(ExploreMapCalls explorePlaces) { - this.exploreMapCalls = explorePlaces; - } - - /** - * Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, mediaList, - * explorePlaceList and boundaryCoordinates - * - * @param currentLatLng is current geolocation - * @param searchLatLng is the location that we want to search around - * @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around - * current location, false if another location - * @return explorePlacesInfo info that holds currentLatLng, mediaList, explorePlaceList and - * boundaryCoordinates - */ - public ExplorePlacesInfo loadAttractionsFromLocation(LatLng currentLatLng, LatLng searchLatLng, - boolean checkingAroundCurrentLocation) { - - if (searchLatLng == null) { - Timber.d("Loading attractions explore map, but search is null"); - return null; - } - - ExplorePlacesInfo explorePlacesInfo = new ExplorePlacesInfo(); - try { - explorePlacesInfo.currentLatLng = currentLatLng; - latestSearchLocation = searchLatLng; - - List mediaList = exploreMapCalls.callCommonsQuery(searchLatLng); - LatLng[] boundaryCoordinates = {mediaList.get(0).getCoordinates(), // south - mediaList.get(0).getCoordinates(), // north - mediaList.get(0).getCoordinates(), // west - mediaList.get(0).getCoordinates()};// east, init with a random location - - if (searchLatLng != null) { - Timber.d("Sorting places by distance..."); - final Map distances = new HashMap<>(); - for (Media media : mediaList) { - distances.put(media, - computeDistanceBetween(media.getCoordinates(), searchLatLng)); - // Find boundaries with basic find max approach - if (media.getCoordinates().getLatitude() - < boundaryCoordinates[0].getLatitude()) { - boundaryCoordinates[0] = media.getCoordinates(); - } - if (media.getCoordinates().getLatitude() - > boundaryCoordinates[1].getLatitude()) { - boundaryCoordinates[1] = media.getCoordinates(); - } - if (media.getCoordinates().getLongitude() - < boundaryCoordinates[2].getLongitude()) { - boundaryCoordinates[2] = media.getCoordinates(); - } - if (media.getCoordinates().getLongitude() - > boundaryCoordinates[3].getLongitude()) { - boundaryCoordinates[3] = media.getCoordinates(); - } - } - } - explorePlacesInfo.mediaList = mediaList; - explorePlacesInfo.explorePlaceList = PlaceUtils.mediaToExplorePlace(mediaList); - explorePlacesInfo.boundaryCoordinates = boundaryCoordinates; - - // Sets latestSearchRadius to maximum distance among boundaries and search location - for (LatLng bound : boundaryCoordinates) { - double distance = LocationUtils.calculateDistance(bound.getLatitude(), - bound.getLongitude(), searchLatLng.getLatitude(), searchLatLng.getLongitude()); - if (distance > latestSearchRadius) { - latestSearchRadius = distance; - } - } - - // Our radius searched around us, will be used to understand when user search their own location, we will follow them - if (checkingAroundCurrentLocation) { - currentLocationSearchRadius = latestSearchRadius; - currentLocation = currentLatLng; - } - } catch (Exception e) { - e.printStackTrace(); - } - return explorePlacesInfo; - } - - /** - * Loads attractions from location for map view, we need to return places in Place data type - * - * @return baseMarkerOptions list that holds nearby places with their icons - */ - public static List loadAttractionsFromLocationToBaseMarkerOptions( - LatLng currentLatLng, - final List placeList, - Context context, - NearbyBaseMarkerThumbCallback callback, - ExplorePlacesInfo explorePlacesInfo) { - List baseMarkerList = new ArrayList<>(); - - if (placeList == null) { - return baseMarkerList; - } - - VectorDrawableCompat vectorDrawable = null; - try { - vectorDrawable = VectorDrawableCompat.create( - context.getResources(), R.drawable.ic_custom_map_marker_dark, context.getTheme()); - - } catch (Resources.NotFoundException e) { - // ignore when running tests. - } - if (vectorDrawable != null) { - for (Place explorePlace : placeList) { - final BaseMarker baseMarker = new BaseMarker(); - String distance = formatDistanceBetween(currentLatLng, explorePlace.location); - explorePlace.setDistance(distance); - - baseMarker.setTitle( - explorePlace.name.substring(5, explorePlace.name.lastIndexOf("."))); - baseMarker.setPosition( - new fr.free.nrw.commons.location.LatLng( - explorePlace.location.getLatitude(), - explorePlace.location.getLongitude(), 0)); - baseMarker.setPlace(explorePlace); - - Glide.with(context) - .asBitmap() - .load(explorePlace.getThumb()) - .placeholder(R.drawable.image_placeholder_96) - .apply(new RequestOptions().override(96, 96).centerCrop()) - .into(new CustomTarget() { - // We add icons to markers when bitmaps are ready - @Override - public void onResourceReady(@NonNull Bitmap resource, - @Nullable Transition transition) { - baseMarker.setIcon( - ImageUtils.addRedBorder(resource, 6, context)); - baseMarkerList.add(baseMarker); - if (baseMarkerList.size() - == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback - callback.onNearbyBaseMarkerThumbsReady(baseMarkerList, - explorePlacesInfo); - } - } - - @Override - public void onLoadCleared(@Nullable Drawable placeholder) { - } - - // We add thumbnail icon for images that couldn't be loaded - @Override - public void onLoadFailed(@Nullable final Drawable errorDrawable) { - super.onLoadFailed(errorDrawable); - baseMarker.fromResource(context, R.drawable.image_placeholder_96); - baseMarkerList.add(baseMarker); - if (baseMarkerList.size() - == placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback - callback.onNearbyBaseMarkerThumbsReady(baseMarkerList, - explorePlacesInfo); - } - } - }); - } - } - return baseMarkerList; - } - - interface NearbyBaseMarkerThumbCallback { - - // Callback to notify thumbnails of explore markers are added as icons and ready - void onNearbyBaseMarkerThumbsReady(List baseMarkers, - ExplorePlacesInfo explorePlacesInfo); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt new file mode 100644 index 000000000..0873572d1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapController.kt @@ -0,0 +1,219 @@ +package fr.free.nrw.commons.explore.map + +import android.content.Context +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat +import com.bumptech.glide.Glide +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import fr.free.nrw.commons.BaseMarker +import fr.free.nrw.commons.MapController +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.utils.ImageUtils.addRedBorder +import fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween +import fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween +import fr.free.nrw.commons.utils.LocationUtils.calculateDistance +import fr.free.nrw.commons.utils.PlaceUtils.mediaToExplorePlace +import timber.log.Timber +import javax.inject.Inject + +class ExploreMapController @Inject constructor( + private val exploreMapCalls: ExploreMapCalls +) : MapController() { + // Can be current and camera target on search this area button is used + private var latestSearchLocation: LatLng? = null + + // Any last search radius + private var latestSearchRadius: Double = 0.0 + + // Search radius of only searches around current location + private var currentLocationSearchRadius: Double = 0.0 + + @JvmField + // current location of user + var currentLocation: LatLng? = null + + /** + * Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, mediaList, + * explorePlaceList and boundaryCoordinates + * + * @param currentLatLng is current geolocation + * @param searchLatLng is the location that we want to search around + * @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around + * current location, false if another location + * @return explorePlacesInfo info that holds currentLatLng, mediaList, explorePlaceList and + * boundaryCoordinates + */ + fun loadAttractionsFromLocation( + currentLatLng: LatLng?, searchLatLng: LatLng?, + checkingAroundCurrentLocation: Boolean + ): ExplorePlacesInfo? { + if (searchLatLng == null) { + Timber.d("Loading attractions explore map, but search is null") + return null + } + + val explorePlacesInfo = ExplorePlacesInfo() + try { + explorePlacesInfo.currentLatLng = currentLatLng + latestSearchLocation = searchLatLng + + val mediaList = exploreMapCalls.callCommonsQuery(searchLatLng) + val boundaryCoordinates = arrayOf( + mediaList[0].coordinates!!, // south + mediaList[0].coordinates!!, // north + mediaList[0].coordinates!!, // west + mediaList[0].coordinates!! + ) // east, init with a random location + + Timber.d("Sorting places by distance...") + val distances: MutableMap = HashMap() + for (media in mediaList) { + distances[media] = computeDistanceBetween(media.coordinates!!, searchLatLng) + // Find boundaries with basic find max approach + if (media.coordinates!!.latitude + < boundaryCoordinates[0]!!.latitude + ) { + boundaryCoordinates[0] = media.coordinates!! + } + if (media.coordinates!!.latitude + > boundaryCoordinates[1]!!.latitude + ) { + boundaryCoordinates[1] = media.coordinates!! + } + if (media.coordinates!!.longitude + < boundaryCoordinates[2]!!.longitude + ) { + boundaryCoordinates[2] = media.coordinates!! + } + if (media.coordinates!!.longitude + > boundaryCoordinates[3]!!.longitude + ) { + boundaryCoordinates[3] = media.coordinates!! + } + } + explorePlacesInfo.mediaList = mediaList + explorePlacesInfo.explorePlaceList = mediaToExplorePlace(mediaList) + explorePlacesInfo.boundaryCoordinates = boundaryCoordinates + + // Sets latestSearchRadius to maximum distance among boundaries and search location + for ((latitude, longitude) in boundaryCoordinates) { + val distance = calculateDistance( + latitude, + longitude, searchLatLng.latitude, searchLatLng.longitude + ) + if (distance > latestSearchRadius) { + latestSearchRadius = distance + } + } + + // Our radius searched around us, will be used to understand when user search their own location, we will follow them + if (checkingAroundCurrentLocation) { + currentLocationSearchRadius = latestSearchRadius + currentLocation = currentLatLng + } + } catch (e: Exception) { + Timber.e(e) + } + return explorePlacesInfo + } + + interface NearbyBaseMarkerThumbCallback { + // Callback to notify thumbnails of explore markers are added as icons and ready + fun onNearbyBaseMarkerThumbsReady( + baseMarkers: List?, + explorePlacesInfo: ExplorePlacesInfo? + ) + } + + companion object { + /** + * Loads attractions from location for map view, we need to return places in Place data type + * + * @return baseMarkerOptions list that holds nearby places with their icons + */ + fun loadAttractionsFromLocationToBaseMarkerOptions( + currentLatLng: LatLng?, + placeList: List?, + context: Context, + callback: NearbyBaseMarkerThumbCallback, + explorePlacesInfo: ExplorePlacesInfo? + ): List { + val baseMarkerList: MutableList = ArrayList() + + if (placeList == null) { + return baseMarkerList + } + + var vectorDrawable: VectorDrawableCompat? = null + try { + vectorDrawable = VectorDrawableCompat.create( + context.resources, R.drawable.ic_custom_map_marker_dark, context.theme + ) + } catch (e: Resources.NotFoundException) { + // ignore when running tests. + } + if (vectorDrawable != null) { + for (explorePlace in placeList) { + val baseMarker = BaseMarker() + val distance = formatDistanceBetween(currentLatLng, explorePlace.location) + explorePlace.setDistance(distance) + + baseMarker.title = + explorePlace.name.substring(5, explorePlace.name.lastIndexOf(".")) + baseMarker.position = LatLng( + explorePlace.location.latitude, + explorePlace.location.longitude, 0f + ) + baseMarker.place = explorePlace + + Glide.with(context) + .asBitmap() + .load(explorePlace.thumb) + .placeholder(R.drawable.image_placeholder_96) + .apply(RequestOptions().override(96, 96).centerCrop()) + .into(object : CustomTarget() { + // We add icons to markers when bitmaps are ready + override fun onResourceReady( + resource: Bitmap, + transition: Transition? + ) { + baseMarker.icon = addRedBorder(resource, 6, context) + baseMarkerList.add(baseMarker) + if (baseMarkerList.size == placeList.size) { + // if true, we added all markers to list and can trigger thumbs ready callback + callback.onNearbyBaseMarkerThumbsReady( + baseMarkerList, + explorePlacesInfo + ) + } + } + + override fun onLoadCleared(placeholder: Drawable?) = Unit + + // We add thumbnail icon for images that couldn't be loaded + override fun onLoadFailed(errorDrawable: Drawable?) { + super.onLoadFailed(errorDrawable) + baseMarker.fromResource(context, R.drawable.image_placeholder_96) + baseMarkerList.add(baseMarker) + if (baseMarkerList.size == placeList.size) { + // if true, we added all markers to list and can trigger thumbs ready callback + callback.onNearbyBaseMarkerThumbsReady( + baseMarkerList, + explorePlacesInfo + ) + } + } + }) + } + } + return baseMarkerList + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java deleted file mode 100644 index 364f4d53a..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java +++ /dev/null @@ -1,1140 +0,0 @@ -package fr.free.nrw.commons.explore.map; - -import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; -import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED; -import static fr.free.nrw.commons.utils.GeoCoordinatesKt.handleGeoCoordinates; -import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL; -import static fr.free.nrw.commons.utils.UrlUtilsKt.handleWebUrl; - -import android.Manifest.permission; -import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.location.Location; -import android.location.LocationManager; -import android.os.Build; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.text.Html; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.snackbar.Snackbar; -import fr.free.nrw.commons.BaseMarker; -import fr.free.nrw.commons.MapController; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.databinding.FragmentExploreMapBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.explore.ExploreMapRootFragment; -import fr.free.nrw.commons.explore.paging.LiveDataConverter; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.location.LocationPermissionsHelper; -import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback; -import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.location.LocationUpdateListener; -import fr.free.nrw.commons.media.MediaClient; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.MapUtils; -import fr.free.nrw.commons.utils.NetworkUtils; -import fr.free.nrw.commons.utils.SystemThemeUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.inject.Inject; -import javax.inject.Named; -import org.osmdroid.events.MapEventsReceiver; -import org.osmdroid.events.MapListener; -import org.osmdroid.events.ScrollEvent; -import org.osmdroid.events.ZoomEvent; -import org.osmdroid.tileprovider.tilesource.TileSourceFactory; -import org.osmdroid.util.GeoPoint; -import org.osmdroid.util.constants.GeoConstants; -import org.osmdroid.views.CustomZoomButtonsController; -import org.osmdroid.views.overlay.ItemizedIconOverlay.OnItemGestureListener; -import org.osmdroid.views.overlay.ItemizedOverlayWithFocus; -import org.osmdroid.views.overlay.MapEventsOverlay; -import org.osmdroid.views.overlay.Overlay; -import org.osmdroid.views.overlay.OverlayItem; -import org.osmdroid.views.overlay.ScaleBarOverlay; -import org.osmdroid.views.overlay.ScaleDiskOverlay; -import org.osmdroid.views.overlay.TilesOverlay; -import timber.log.Timber; - -public class ExploreMapFragment extends CommonsDaggerSupportFragment - implements ExploreMapContract.View, LocationUpdateListener, LocationPermissionCallback { - - private BottomSheetBehavior bottomSheetDetailsBehavior; - private BroadcastReceiver broadcastReceiver; - private boolean isNetworkErrorOccurred; - private Snackbar snackbar; - private boolean isDarkTheme; - private boolean isPermissionDenied; - private fr.free.nrw.commons.location.LatLng lastKnownLocation; // last location of user - private fr.free.nrw.commons.location.LatLng lastFocusLocation; // last location that map is focused - public List mediaList; - private boolean recenterToUserLocation; // true is recenter is needed (ie. when current location is in visible map boundaries) - private BaseMarker clickedMarker; - private GeoPoint mapCenter; - private GeoPoint lastMapFocus; - IntentFilter intentFilter = new IntentFilter(MapUtils.NETWORK_INTENT_ACTION); - private Map baseMarkerOverlayMap; - - @Inject - LiveDataConverter liveDataConverter; - @Inject - MediaClient mediaClient; - @Inject - LocationServiceManager locationManager; - @Inject - ExploreMapController exploreMapController; - @Inject - @Named("default_preferences") - JsonKvStore applicationKvStore; - @Inject - BookmarkLocationsDao bookmarkLocationDao; // May be needed in future if we want to integrate bookmarking explore places - @Inject - SystemThemeUtils systemThemeUtils; - LocationPermissionsHelper locationPermissionsHelper; - - // Nearby map state (if we came from Nearby) - private double prevZoom; - private double prevLatitude; - private double prevLongitude; - private boolean recentlyCameFromNearbyMap; - - private ExploreMapPresenter presenter; - - public FragmentExploreMapBinding binding; - - private ActivityResultLauncher activityResultLauncher = registerForActivityResult( - new ActivityResultContracts.RequestPermission(), isGranted -> { - if (isGranted) { - locationPermissionGranted(); - } else { - if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { - DialogUtil.showAlertDialog(getActivity(), - getActivity().getString(R.string.location_permission_title), - getActivity().getString(R.string.location_permission_rationale_explore), - getActivity().getString(android.R.string.ok), - getActivity().getString(android.R.string.cancel), - () -> { - askForLocationPermission(); - }, - null, - null - ); - } else { - if (isPermissionDenied) { - locationPermissionsHelper.showAppSettingsDialog(getActivity(), - R.string.explore_map_needs_location); - } - Timber.d("The user checked 'Don't ask again' or denied the permission twice"); - isPermissionDenied = true; - } - } - }); - - @NonNull - public static ExploreMapFragment newInstance() { - ExploreMapFragment fragment = new ExploreMapFragment(); - fragment.setRetainInstance(true); - return fragment; - } - - @Override - public View onCreateView( - @NonNull LayoutInflater inflater, - ViewGroup container, - Bundle savedInstanceState - ) { - loadNearbyMapData(); - binding = FragmentExploreMapBinding.inflate(getLayoutInflater()); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - setSearchThisAreaButtonVisibility(false); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - binding.tvAttribution.setText( - Html.fromHtml(getString(R.string.map_attribution), Html.FROM_HTML_MODE_LEGACY)); - } else { - binding.tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution))); - } - initNetworkBroadCastReceiver(); - locationPermissionsHelper = new LocationPermissionsHelper(getActivity(), locationManager, - this); - if (presenter == null) { - presenter = new ExploreMapPresenter(bookmarkLocationDao); - } - setHasOptionsMenu(true); - - isDarkTheme = systemThemeUtils.isDeviceInNightMode(); - isPermissionDenied = false; - presenter.attachView(this); - - initViews(); - presenter.setActionListeners(applicationKvStore); - - org.osmdroid.config.Configuration.getInstance().load(this.getContext(), - PreferenceManager.getDefaultSharedPreferences(this.getContext())); - - binding.mapView.setTileSource(TileSourceFactory.WIKIMEDIA); - binding.mapView.setTilesScaledToDpi(true); - - org.osmdroid.config.Configuration.getInstance().getAdditionalHttpRequestProperties().put( - "Referer", "http://maps.wikimedia.org/" - ); - - ScaleBarOverlay scaleBarOverlay = new ScaleBarOverlay(binding.mapView); - scaleBarOverlay.setScaleBarOffset(15, 25); - Paint barPaint = new Paint(); - barPaint.setARGB(200, 255, 250, 250); - scaleBarOverlay.setBackgroundPaint(barPaint); - scaleBarOverlay.enableScaleBar(); - binding.mapView.getOverlays().add(scaleBarOverlay); - binding.mapView.getZoomController() - .setVisibility(CustomZoomButtonsController.Visibility.NEVER); - binding.mapView.setMultiTouchControls(true); - - if (!isCameFromNearbyMap()) { - binding.mapView.getController().setZoom(ZOOM_LEVEL); - } - - - binding.mapView.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() { - @Override - public boolean singleTapConfirmedHelper(GeoPoint p) { - if (clickedMarker != null) { - removeMarker(clickedMarker); - addMarkerToMap(clickedMarker); - binding.mapView.invalidate(); - } else { - Timber.e("CLICKED MARKER IS NULL"); - } - if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { - // Back should first hide the bottom sheet if it is expanded - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - } else if (isDetailsBottomSheetVisible()) { - hideBottomDetailsSheet(); - } - return true; - } - - @Override - public boolean longPressHelper(GeoPoint p) { - return false; - } - })); - - binding.mapView.addMapListener(new MapListener() { - @Override - public boolean onScroll(ScrollEvent event) { - if (getLastMapFocus() != null) { - Location mylocation = new Location(""); - Location dest_location = new Location(""); - dest_location.setLatitude(binding.mapView.getMapCenter().getLatitude()); - dest_location.setLongitude(binding.mapView.getMapCenter().getLongitude()); - mylocation.setLatitude(getLastMapFocus().getLatitude()); - mylocation.setLongitude(getLastMapFocus().getLongitude()); - Float distance = mylocation.distanceTo(dest_location);//in meters - if (getLastMapFocus() != null) { - if (isNetworkConnectionEstablished() && (event.getX() > 0 - || event.getY() > 0)) { - if (distance > 2000.0) { - setSearchThisAreaButtonVisibility(true); - } else { - setSearchThisAreaButtonVisibility(false); - } - } - } else { - setSearchThisAreaButtonVisibility(false); - } - } - - return true; - } - - @Override - public boolean onZoom(ZoomEvent event) { - return false; - } - - }); - if (!locationPermissionsHelper.checkLocationPermission(getActivity())) { - askForLocationPermission(); - } - } - - @Override - public void onResume() { - super.onResume(); - binding.mapView.onResume(); - presenter.attachView(this); - registerNetworkReceiver(); - if (isResumed()) { - if (locationPermissionsHelper.checkLocationPermission(getActivity())) { - performMapReadyActions(); - } else { - startMapWithoutPermission(); - } - } - } - - @Override - public void onPause() { - super.onPause(); - // unregistering the broadcastReceiver, as it was causing an exception and a potential crash - unregisterNetworkReceiver(); - } - - - /** - * Unregisters the networkReceiver - */ - private void unregisterNetworkReceiver() { - if (getActivity() != null) { - getActivity().unregisterReceiver(broadcastReceiver); - } - } - - private void startMapWithoutPermission() { - lastKnownLocation = MapUtils.getDefaultLatLng(); - moveCameraToPosition( - new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude())); - presenter.onMapReady(exploreMapController); - } - - private void registerNetworkReceiver() { - if (getActivity() != null) { - getActivity().registerReceiver(broadcastReceiver, intentFilter); - } - } - - private void performMapReadyActions() { - if (isDarkTheme) { - binding.mapView.getOverlayManager().getTilesOverlay() - .setColorFilter(TilesOverlay.INVERT_COLORS); - } - if (applicationKvStore.getBoolean("doNotAskForLocationPermission", false) && - !locationPermissionsHelper.checkLocationPermission(getActivity())) { - isPermissionDenied = true; - } - - lastKnownLocation = getLastLocation(); - - if (lastKnownLocation == null) { - lastKnownLocation = MapUtils.getDefaultLatLng(); - } - - // if we came from 'Show in Explore' in Nearby, load Nearby map center and zoom - if (isCameFromNearbyMap()) { - moveCameraToPosition( - new GeoPoint(prevLatitude, prevLongitude), - prevZoom, - 1L - ); - } else { - moveCameraToPosition( - new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude())); - } - presenter.onMapReady(exploreMapController); - } - - /** - * Fetch Nearby map camera data from fragment arguments if any. - */ - public void loadNearbyMapData() { - // get fragment arguments - if (getArguments() != null) { - prevZoom = getArguments().getDouble("prev_zoom"); - prevLatitude = getArguments().getDouble("prev_latitude"); - prevLongitude = getArguments().getDouble("prev_longitude"); - } - - setRecentlyCameFromNearbyMap(isCameFromNearbyMap()); - } - - /** - * @return The LatLng from the previous Fragment's map center or (0,0,0) coordinates - * if that information is not available/applicable. - */ - public LatLng getPreviousLatLng() { - return new LatLng(prevLatitude, prevLongitude, (float)prevZoom); - } - - /** - * Checks if fragment arguments contain data from Nearby map, indicating that the user navigated - * from Nearby using 'Show in Explore'. - * - * @return true if user navigated from Nearby map - **/ - public boolean isCameFromNearbyMap() { - return prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0; - } - - /** - * Gets the value that indicates if the user navigated from "Show in Explore" in Nearby and - * that the LatLng from Nearby has yet to be searched for map markers. - */ - public boolean recentlyCameFromNearbyMap() { - return recentlyCameFromNearbyMap; - } - - /** - * Sets the value that indicates if the user navigated from "Show in Explore" in Nearby and - * that the LatLng from Nearby has yet to be searched for map markers. - * @param newValue The value to set. - */ - public void setRecentlyCameFromNearbyMap(boolean newValue) { - recentlyCameFromNearbyMap = newValue; - } - - public void loadNearbyMapFromExplore() { - ((MainActivity) getContext()).loadNearbyMapFromExplore( - binding.mapView.getZoomLevelDouble(), - binding.mapView.getMapCenter().getLatitude(), - binding.mapView.getMapCenter().getLongitude() - ); - } - - private void initViews() { - Timber.d("init views called"); - initBottomSheets(); - setBottomSheetCallbacks(); - } - - /** - * a) Creates bottom sheet behaviours from bottom sheet, sets initial states and visibility - * b) Gets the touch event on the map to perform following actions: - * if bottom sheet details are expanded or collapsed hide the bottom sheet details. - */ - @SuppressLint("ClickableViewAccessibility") - private void initBottomSheets() { - bottomSheetDetailsBehavior = BottomSheetBehavior.from( - binding.bottomSheetDetailsBinding.getRoot()); - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - binding.bottomSheetDetailsBinding.getRoot().setVisibility(View.VISIBLE); - } - - /** - * Defines how bottom sheets will act on click - */ - private void setBottomSheetCallbacks() { - binding.bottomSheetDetailsBinding.getRoot().setOnClickListener(v -> { - if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) { - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); - } else if (bottomSheetDetailsBehavior.getState() - == BottomSheetBehavior.STATE_EXPANDED) { - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - } - }); - } - - @Override - public void onLocationChangedSignificantly(LatLng latLng) { - Timber.d("Location significantly changed"); - if (latLng != null) { - handleLocationUpdate(latLng, LOCATION_SIGNIFICANTLY_CHANGED); - } - } - - @Override - public void onLocationChangedSlightly(LatLng latLng) { - Timber.d("Location slightly changed"); - if (latLng != null) {//If the map has never ever shown the current location, lets do it know - handleLocationUpdate(latLng, LOCATION_SLIGHTLY_CHANGED); - } - } - - private void handleLocationUpdate(final fr.free.nrw.commons.location.LatLng latLng, - final LocationServiceManager.LocationChangeType locationChangeType) { - lastKnownLocation = latLng; - exploreMapController.currentLocation = lastKnownLocation; - presenter.updateMap(locationChangeType); - } - - @Override - public void onLocationChangedMedium(LatLng latLng) { - - } - - @Override - public boolean isNetworkConnectionEstablished() { - return NetworkUtils.isInternetConnectionEstablished(getActivity()); - } - - @Override - public void populatePlaces(LatLng currentLatLng) { - final Observable nearbyPlacesInfoObservable; - if (currentLatLng == null) { - return; - } - if (currentLatLng.equals( - getLastMapFocus())) { // Means we are checking around current location - nearbyPlacesInfoObservable = presenter.loadAttractionsFromLocation(currentLatLng, - getLastMapFocus(), true); - } else { - nearbyPlacesInfoObservable = presenter.loadAttractionsFromLocation(getLastMapFocus(), - currentLatLng, false); - } - getCompositeDisposable().add(nearbyPlacesInfoObservable - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(explorePlacesInfo -> { - mediaList = explorePlacesInfo.mediaList; - if (mediaList == null) { - showResponseMessage(getString(R.string.no_pictures_in_this_area)); - } - updateMapMarkers(explorePlacesInfo); - lastMapFocus = new GeoPoint(currentLatLng.getLatitude(), - currentLatLng.getLongitude()); - }, - throwable -> { - Timber.d(throwable); - // Not showing the user, throwable localizedErrorMessage - showErrorMessage(getString(R.string.error_fetching_nearby_places)); - - setProgressBarVisibility(false); - presenter.lockUnlockNearby(false); - })); - if (recenterToUserLocation) { - recenterToUserLocation = false; - } - } - - /** - * Updates map markers according to latest situation - * - * @param explorePlacesInfo holds several information as current location, marker list etc. - */ - private void updateMapMarkers(final MapController.ExplorePlacesInfo explorePlacesInfo) { - presenter.updateMapMarkers(explorePlacesInfo); - } - - private void showErrorMessage(final String message) { - ViewUtil.showLongToast(getActivity(), message); - } - - private void showResponseMessage(final String message) { - ViewUtil.showLongSnackbar(getView(), message); - } - - @Override - public void askForLocationPermission() { - Timber.d("Asking for location permission"); - activityResultLauncher.launch(permission.ACCESS_FINE_LOCATION); - } - - private void locationPermissionGranted() { - isPermissionDenied = false; - applicationKvStore.putBoolean("doNotAskForLocationPermission", false); - lastKnownLocation = locationManager.getLastLocation(); - fr.free.nrw.commons.location.LatLng target = lastKnownLocation; - if (lastKnownLocation != null) { - GeoPoint targetP = new GeoPoint(target.getLatitude(), target.getLongitude()); - mapCenter = targetP; - binding.mapView.getController().setCenter(targetP); - recenterMarkerToPosition(targetP); - moveCameraToPosition(targetP); - } else if (locationManager.isGPSProviderEnabled() - || locationManager.isNetworkProviderEnabled()) { - locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER); - locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); - setProgressBarVisibility(true); - } else { - locationPermissionsHelper.showLocationOffDialog(getActivity(), - R.string.ask_to_turn_location_on_text); - } - presenter.onMapReady(exploreMapController); - registerUnregisterLocationListener(false); - } - - public void registerUnregisterLocationListener(final boolean removeLocationListener) { - MapUtils.registerUnregisterLocationListener(removeLocationListener, locationManager, this); - } - - @Override - public void recenterMap(LatLng currentLatLng) { - // if user has denied permission twice, then show dialog - if (isPermissionDenied) { - if (locationPermissionsHelper.checkLocationPermission(getActivity())) { - // this will run when user has given permission by opening app's settings - isPermissionDenied = false; - recenterMap(currentLatLng); - } else { - askForLocationPermission(); - } - } else { - if (!locationPermissionsHelper.checkLocationPermission(getActivity())) { - askForLocationPermission(); - } else { - locationPermissionGranted(); - } - } - if (currentLatLng == null) { - recenterToUserLocation = true; - return; - } - recenterMarkerToPosition( - new GeoPoint(currentLatLng.getLatitude(), currentLatLng.getLongitude())); - binding.mapView.getController() - .animateTo(new GeoPoint(currentLatLng.getLatitude(), currentLatLng.getLongitude())); - if (lastMapFocus != null) { - Location mylocation = new Location(""); - Location dest_location = new Location(""); - dest_location.setLatitude(binding.mapView.getMapCenter().getLatitude()); - dest_location.setLongitude(binding.mapView.getMapCenter().getLongitude()); - mylocation.setLatitude(lastMapFocus.getLatitude()); - mylocation.setLongitude(lastMapFocus.getLongitude()); - Float distance = mylocation.distanceTo(dest_location);//in meters - if (lastMapFocus != null) { - if (isNetworkConnectionEstablished()) { - if (distance > 2000.0) { - setSearchThisAreaButtonVisibility(true); - } else { - setSearchThisAreaButtonVisibility(false); - } - } - } else { - setSearchThisAreaButtonVisibility(false); - } - } - } - - @Override - public void hideBottomDetailsSheet() { - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - } - - /** - * Same bottom sheet carries information for all nearby places, so we need to pass information - * (title, description, distance and links) to view on nearby marker click - * - * @param place Place of clicked nearby marker - */ - private void passInfoToSheet(final Place place) { - binding.bottomSheetDetailsBinding.directionsButton.setOnClickListener( - view -> handleGeoCoordinates(requireActivity(), - place.getLocation(), binding.mapView.getZoomLevelDouble())); - - binding.bottomSheetDetailsBinding.commonsButton.setVisibility( - place.hasCommonsLink() ? View.VISIBLE : View.GONE); - binding.bottomSheetDetailsBinding.commonsButton.setOnClickListener( - view -> handleWebUrl(getContext(), place.siteLinks.getCommonsLink())); - - int index = 0; - for (Media media : mediaList) { - if (media.getFilename().equals(place.name)) { - int finalIndex = index; - binding.bottomSheetDetailsBinding.mediaDetailsButton.setOnClickListener(view -> { - ((ExploreMapRootFragment) getParentFragment()).onMediaClicked(finalIndex); - }); - } - index++; - } - binding.bottomSheetDetailsBinding.title.setText( - place.name.substring(5, place.name.lastIndexOf("."))); - binding.bottomSheetDetailsBinding.category.setText(place.distance); - // Remove label since it is double information - String descriptionText = place.getLongDescription() - .replace(place.getName() + " (", ""); - descriptionText = (descriptionText.equals(place.getLongDescription()) ? descriptionText - : descriptionText.replaceFirst(".$", "")); - // Set the short description after we remove place name from long description - binding.bottomSheetDetailsBinding.description.setText(descriptionText); - } - - @Override - public void addSearchThisAreaButtonAction() { - binding.searchThisAreaButton.setOnClickListener(presenter.onSearchThisAreaClicked()); - } - - @Override - public void setSearchThisAreaButtonVisibility(boolean isVisible) { - binding.searchThisAreaButton.setVisibility(isVisible ? View.VISIBLE : View.GONE); - } - - @Override - public void setProgressBarVisibility(boolean isVisible) { - binding.mapProgressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE); - } - - @Override - public boolean isDetailsBottomSheetVisible() { - if (binding.bottomSheetDetailsBinding.getRoot().getVisibility() == View.VISIBLE) { - return true; - } else { - return false; - } - } - - @Override - public boolean isSearchThisAreaButtonVisible() { - return binding.bottomSheetDetailsBinding.getRoot().getVisibility() == View.VISIBLE; - } - - @Override - public LatLng getLastLocation() { - if (lastKnownLocation == null) { - lastKnownLocation = locationManager.getLastLocation(); - } - return lastKnownLocation; - } - - @Override - public void disableFABRecenter() { - binding.fabRecenter.setEnabled(false); - } - - @Override - public void enableFABRecenter() { - binding.fabRecenter.setEnabled(true); - } - - /** - * Adds a markers to the map based on the list of NearbyBaseMarker. - * - * @param nearbyBaseMarkers The NearbyBaseMarker object representing the markers to be added. - */ - @Override - public void addMarkersToMap(List nearbyBaseMarkers) { - clearAllMarkers(); - for (int i = 0; i < nearbyBaseMarkers.size(); i++) { - addMarkerToMap(nearbyBaseMarkers.get(i)); - } - binding.mapView.invalidate(); - } - - /** - * Adds a marker to the map based on the specified NearbyBaseMarker. - * - * @param nearbyBaseMarker The NearbyBaseMarker object representing the marker to be added. - */ - private void addMarkerToMap(BaseMarker nearbyBaseMarker) { - if (isAttachedToActivity()) { - ArrayList items = new ArrayList<>(); - Bitmap icon = nearbyBaseMarker.getIcon(); - Drawable d = new BitmapDrawable(getResources(), icon); - GeoPoint point = new GeoPoint( - nearbyBaseMarker.getPlace().location.getLatitude(), - nearbyBaseMarker.getPlace().location.getLongitude()); - - Media markerMedia = this.getMediaFromImageURL(nearbyBaseMarker.getPlace().pic); - String authorUser = null; - if (markerMedia != null) { - authorUser = markerMedia.getAuthorOrUser(); - // HTML text is sometimes part of the author string and needs to be removed - authorUser = Html.fromHtml(authorUser, Html.FROM_HTML_MODE_LEGACY).toString(); - } - - String title = nearbyBaseMarker.getPlace().name; - // Remove "File:" if present at start - if (title.startsWith("File:")) { - title = title.substring(5); - } - // Remove extensions like .jpg, .jpeg, .png, .svg (case insensitive) - title = title.replaceAll("(?i)\\.(jpg|jpeg|png|svg)$", ""); - title = title.replace("_", " "); - //Truncate if too long because it doesn't fit the screen - if (title.length() > 43) { - title = title.substring(0, 40) + "…"; - } - - OverlayItem item = new OverlayItem(title, authorUser, point); - item.setMarker(d); - items.add(item); - ItemizedOverlayWithFocus overlay = new ItemizedOverlayWithFocus(items, - new OnItemGestureListener() { - @Override - public boolean onItemSingleTapUp(int index, OverlayItem item) { - final Place place = nearbyBaseMarker.getPlace(); - if (clickedMarker != null) { - removeMarker(clickedMarker); - addMarkerToMap(clickedMarker); - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - bottomSheetDetailsBehavior.setState( - BottomSheetBehavior.STATE_COLLAPSED); - } - clickedMarker = nearbyBaseMarker; - passInfoToSheet(place); - - //Move the overlay to the top so it can be fully seen. - moveOverlayToTop(getOverlay(item)); - return true; - } - - @Override - public boolean onItemLongPress(int index, OverlayItem item) { - return false; - } - }, getContext()); - - if (this.baseMarkerOverlayMap == null) { - this.baseMarkerOverlayMap = new HashMap<>(); - } - this.baseMarkerOverlayMap.put(nearbyBaseMarker, overlay); - - overlay.setFocusItemsOnTap(true); - binding.mapView.getOverlays().add(overlay); // Add the overlay to the map - } - } - - /** - * Moves the specified Overlay above all other Overlays. This prevents other Overlays from - * obstructing it. Upon failure, this method returns early. - * @param overlay The Overlay to move. - */ - private void moveOverlayToTop (Overlay overlay) { - if (overlay == null || binding == null || binding.mapView.getOverlays() == null) { - return; - } - - boolean successfulRemoval = binding.mapView.getOverlays().remove(overlay); - if (!successfulRemoval) { - return; - } - - binding.mapView.getOverlays().add(overlay); - } - - /** - * Performs a linear search for the first Overlay which contains the specified OverlayItem. - * - * @param item The OverlayItem contained within the first target Overlay. - * @return The first Overlay which contains the specified OverlayItem or null if the Overlay - * could not be found. - */ - private Overlay getOverlay (OverlayItem item) { - if (item == null || binding == null || binding.mapView.getOverlays() == null) { - return null; - } - - for (int i = 0; i < binding.mapView.getOverlays().size(); i++) { - if (binding.mapView.getOverlays().get(i) instanceof ItemizedOverlayWithFocus) { - ItemizedOverlayWithFocus overlay = - (ItemizedOverlayWithFocus)binding.mapView.getOverlays().get(i); - - for (int j = 0; j < overlay.size(); j++) { - if (overlay.getItem(j) == item) { - return overlay; - } - } - } - } - - return null; - } - - /** - * Retrieves the specific Media object from the mediaList field. - * @param url The specific Media's image URL. - * @return The Media object that matches the URL or null if it could not be found. - */ - private Media getMediaFromImageURL(String url) { - if (mediaList == null || url == null) { - return null; - } - - for (int i = 0; i < mediaList.size(); i++) { - if (mediaList.get(i) != null && mediaList.get(i).getImageUrl() != null - && mediaList.get(i).getImageUrl().equals(url)) { - return mediaList.get(i); - } - } - - return null; - } - - /** - * Removes a marker from the map based on the specified NearbyBaseMarker. - * - * @param nearbyBaseMarker The NearbyBaseMarker object representing the marker to be removed. - */ - private void removeMarker(BaseMarker nearbyBaseMarker) { - if (nearbyBaseMarker == null || nearbyBaseMarker.getPlace().getName() == null || - baseMarkerOverlayMap == null || !baseMarkerOverlayMap.containsKey(nearbyBaseMarker)) { - return; - } - - Overlay target = baseMarkerOverlayMap.get(nearbyBaseMarker); - List overlays = binding.mapView.getOverlays(); - - for (int i = 0; i < overlays.size(); i++) { - Overlay overlay = overlays.get(i); - - if (overlay.equals(target)) { - binding.mapView.getOverlays().remove(i); - binding.mapView.invalidate(); - baseMarkerOverlayMap.remove(nearbyBaseMarker); - break; - } - } - } - - /** - * Clears all markers from the map and resets certain map overlays and gestures. After clearing - * markers, it re-adds a scale bar overlay and rotation gesture overlay to the map. - */ - @Override - public void clearAllMarkers() { - if (isAttachedToActivity()) { - binding.mapView.getOverlayManager().clear(); - GeoPoint geoPoint = mapCenter; - if (geoPoint != null) { - List overlays = binding.mapView.getOverlays(); - ScaleDiskOverlay diskOverlay = - new ScaleDiskOverlay(this.getContext(), - geoPoint, 2000, GeoConstants.UnitOfMeasure.foot); - Paint circlePaint = new Paint(); - circlePaint.setColor(Color.rgb(128, 128, 128)); - circlePaint.setStyle(Paint.Style.STROKE); - circlePaint.setStrokeWidth(2f); - diskOverlay.setCirclePaint2(circlePaint); - Paint diskPaint = new Paint(); - diskPaint.setColor(Color.argb(40, 128, 128, 128)); - diskPaint.setStyle(Paint.Style.FILL_AND_STROKE); - diskOverlay.setCirclePaint1(diskPaint); - diskOverlay.setDisplaySizeMin(900); - diskOverlay.setDisplaySizeMax(1700); - binding.mapView.getOverlays().add(diskOverlay); - org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker( - binding.mapView); - startMarker.setPosition(geoPoint); - startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER, - org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM); - startMarker.setIcon( - ContextCompat.getDrawable(this.getContext(), - R.drawable.current_location_marker)); - startMarker.setTitle("Your Location"); - startMarker.setTextLabelFontSize(24); - binding.mapView.getOverlays().add(startMarker); - } - ScaleBarOverlay scaleBarOverlay = new ScaleBarOverlay(binding.mapView); - scaleBarOverlay.setScaleBarOffset(15, 25); - Paint barPaint = new Paint(); - barPaint.setARGB(200, 255, 250, 250); - scaleBarOverlay.setBackgroundPaint(barPaint); - scaleBarOverlay.enableScaleBar(); - binding.mapView.getOverlays().add(scaleBarOverlay); - binding.mapView.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() { - @Override - public boolean singleTapConfirmedHelper(GeoPoint p) { - if (clickedMarker != null) { - removeMarker(clickedMarker); - addMarkerToMap(clickedMarker); - binding.mapView.invalidate(); - } else { - Timber.e("CLICKED MARKER IS NULL"); - } - if (bottomSheetDetailsBehavior.getState() - == BottomSheetBehavior.STATE_EXPANDED) { - // Back should first hide the bottom sheet if it is expanded - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - } else if (isDetailsBottomSheetVisible()) { - hideBottomDetailsSheet(); - } - return true; - } - - @Override - public boolean longPressHelper(GeoPoint p) { - return false; - } - })); - binding.mapView.setMultiTouchControls(true); - } - } - - /** - * Recenters the map view to the specified GeoPoint and updates the marker to indicate the new - * position. - * - * @param geoPoint The GeoPoint representing the new center position for the map. - */ - private void recenterMarkerToPosition(GeoPoint geoPoint) { - if (geoPoint != null) { - binding.mapView.getController().setCenter(geoPoint); - List overlays = binding.mapView.getOverlays(); - for (int i = 0; i < overlays.size(); i++) { - if (overlays.get(i) instanceof org.osmdroid.views.overlay.Marker) { - binding.mapView.getOverlays().remove(i); - } else if (overlays.get(i) instanceof ScaleDiskOverlay) { - binding.mapView.getOverlays().remove(i); - } - } - ScaleDiskOverlay diskOverlay = - new ScaleDiskOverlay(this.getContext(), - geoPoint, 2000, GeoConstants.UnitOfMeasure.foot); - Paint circlePaint = new Paint(); - circlePaint.setColor(Color.rgb(128, 128, 128)); - circlePaint.setStyle(Paint.Style.STROKE); - circlePaint.setStrokeWidth(2f); - diskOverlay.setCirclePaint2(circlePaint); - Paint diskPaint = new Paint(); - diskPaint.setColor(Color.argb(40, 128, 128, 128)); - diskPaint.setStyle(Paint.Style.FILL_AND_STROKE); - diskOverlay.setCirclePaint1(diskPaint); - diskOverlay.setDisplaySizeMin(900); - diskOverlay.setDisplaySizeMax(1700); - binding.mapView.getOverlays().add(diskOverlay); - org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker( - binding.mapView); - startMarker.setPosition(geoPoint); - startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER, - org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM); - startMarker.setIcon( - ContextCompat.getDrawable(this.getContext(), R.drawable.current_location_marker)); - startMarker.setTitle("Your Location"); - startMarker.setTextLabelFontSize(24); - binding.mapView.getOverlays().add(startMarker); - } - } - - /** - * Moves the camera of the map view to the specified GeoPoint using an animation. - * - * @param geoPoint The GeoPoint representing the new camera position for the map. - */ - private void moveCameraToPosition(GeoPoint geoPoint) { - binding.mapView.getController().animateTo(geoPoint); - } - - /** - * Moves the camera of the map view to the specified GeoPoint at specified zoom level and speed - * using an animation. - * - * @param geoPoint The GeoPoint representing the new camera position for the map. - * @param zoom Zoom level of the map camera - * @param speed Speed of animation - */ - private void moveCameraToPosition(GeoPoint geoPoint, double zoom, long speed) { - binding.mapView.getController().animateTo(geoPoint, zoom, speed); - } - - @Override - public fr.free.nrw.commons.location.LatLng getLastMapFocus() { - return lastMapFocus == null ? getMapCenter() : new fr.free.nrw.commons.location.LatLng( - lastMapFocus.getLatitude(), lastMapFocus.getLongitude(), 100); - } - - @Override - public fr.free.nrw.commons.location.LatLng getMapCenter() { - fr.free.nrw.commons.location.LatLng latLnge = null; - if (mapCenter != null) { - latLnge = new fr.free.nrw.commons.location.LatLng( - mapCenter.getLatitude(), mapCenter.getLongitude(), 100); - } else { - if (applicationKvStore.getString("LastLocation") != null) { - final String[] locationLatLng - = applicationKvStore.getString("LastLocation").split(","); - lastKnownLocation - = new fr.free.nrw.commons.location.LatLng(Double.parseDouble(locationLatLng[0]), - Double.parseDouble(locationLatLng[1]), 1f); - latLnge = lastKnownLocation; - } else { - latLnge = new fr.free.nrw.commons.location.LatLng(51.506255446947776, - -0.07483536015053005, 1f); - } - } - return latLnge; - } - - @Override - public fr.free.nrw.commons.location.LatLng getMapFocus() { - fr.free.nrw.commons.location.LatLng mapFocusedLatLng = new fr.free.nrw.commons.location.LatLng( - binding.mapView.getMapCenter().getLatitude(), - binding.mapView.getMapCenter().getLongitude(), 100); - return mapFocusedLatLng; - } - - @Override - public void setFABRecenterAction(OnClickListener onClickListener) { - binding.fabRecenter.setOnClickListener(onClickListener); - } - - @Override - public boolean backButtonClicked() { - if (!(bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN)) { - bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); - return true; - } else { - return false; - } - } - - /** - * Adds network broadcast receiver to recognize connection established - */ - private void initNetworkBroadCastReceiver() { - broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - if (getActivity() != null) { - if (NetworkUtils.isInternetConnectionEstablished(getActivity())) { - if (isNetworkErrorOccurred) { - presenter.updateMap(LOCATION_SIGNIFICANTLY_CHANGED); - isNetworkErrorOccurred = false; - } - - if (snackbar != null) { - snackbar.dismiss(); - snackbar = null; - } - } else { - if (snackbar == null) { - snackbar = Snackbar.make(getView(), R.string.no_internet, - Snackbar.LENGTH_INDEFINITE); - setSearchThisAreaButtonVisibility(false); - setProgressBarVisibility(false); - } - - isNetworkErrorOccurred = true; - snackbar.show(); - } - } - } - }; - } - - /** - * helper function to confirm that this fragment has been attached. - **/ - public boolean isAttachedToActivity() { - boolean attached = isVisible() && getActivity() != null; - return attached; - } - - @Override - public void onLocationPermissionDenied(String toastMessage) { - } - - @Override - public void onLocationPermissionGranted() { - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt new file mode 100644 index 000000000..e64f12db3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.kt @@ -0,0 +1,1091 @@ +@file:Suppress("DEPRECATION") + +package fr.free.nrw.commons.explore.map + +import android.Manifest.permission +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Color +import android.graphics.Paint +import android.graphics.drawable.Drawable +import android.location.Location +import android.location.LocationManager +import android.os.Build +import android.os.Bundle +import android.preference.PreferenceManager +import android.text.Html +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toDrawable +import androidx.core.view.isVisible +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.snackbar.Snackbar +import fr.free.nrw.commons.BaseMarker +import fr.free.nrw.commons.MapController.ExplorePlacesInfo +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.FragmentExploreMapBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.explore.ExploreMapRootFragment +import fr.free.nrw.commons.explore.paging.LiveDataConverter +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.location.LocationPermissionsHelper +import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback +import fr.free.nrw.commons.location.LocationServiceManager +import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType +import fr.free.nrw.commons.location.LocationUpdateListener +import fr.free.nrw.commons.media.MediaClient +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog +import fr.free.nrw.commons.utils.MapUtils +import fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL +import fr.free.nrw.commons.utils.MapUtils.defaultLatLng +import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished +import fr.free.nrw.commons.utils.SystemThemeUtils +import fr.free.nrw.commons.utils.ViewUtil.showLongSnackbar +import fr.free.nrw.commons.utils.ViewUtil.showLongToast +import fr.free.nrw.commons.utils.handleGeoCoordinates +import fr.free.nrw.commons.utils.handleWebUrl +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.functions.Consumer +import io.reactivex.schedulers.Schedulers +import org.osmdroid.config.Configuration +import org.osmdroid.events.MapEventsReceiver +import org.osmdroid.events.MapListener +import org.osmdroid.events.ScrollEvent +import org.osmdroid.events.ZoomEvent +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.util.constants.GeoConstants +import org.osmdroid.views.CustomZoomButtonsController +import org.osmdroid.views.overlay.ItemizedIconOverlay.OnItemGestureListener +import org.osmdroid.views.overlay.ItemizedOverlayWithFocus +import org.osmdroid.views.overlay.MapEventsOverlay +import org.osmdroid.views.overlay.Marker +import org.osmdroid.views.overlay.Overlay +import org.osmdroid.views.overlay.OverlayItem +import org.osmdroid.views.overlay.ScaleBarOverlay +import org.osmdroid.views.overlay.ScaleDiskOverlay +import org.osmdroid.views.overlay.TilesOverlay +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Named + +class ExploreMapFragment : CommonsDaggerSupportFragment(), ExploreMapContract.View, + LocationUpdateListener, LocationPermissionCallback { + private var bottomSheetDetailsBehavior: BottomSheetBehavior<*>? = null + private var broadcastReceiver: BroadcastReceiver? = null + private var isNetworkErrorOccurred = false + private var snackbar: Snackbar? = null + private var isDarkTheme = false + private var isPermissionDenied = false + private var lastKnownLocation: LatLng? = null // last location of user + private var recenterToUserLocation = false // true is recenter is needed (ie. when current location is in visible map boundaries) + private var clickedMarker: BaseMarker? = null + private var mapCenter: GeoPoint? = null + private var lastMapFocus: GeoPoint? = null + private var intentFilter: IntentFilter = IntentFilter(MapUtils.NETWORK_INTENT_ACTION) + private var baseMarkerOverlayMap: MutableMap? = null + private var locationPermissionsHelper: LocationPermissionsHelper? = null + private var prevZoom = 0.0 + private var prevLatitude = 0.0 + private var prevLongitude = 0.0 + private var recentlyCameFromNearbyMap = false + private var presenter: ExploreMapPresenter? = null + private var binding: FragmentExploreMapBinding? = null + var mediaList: MutableList? = null + private set + + @Inject + lateinit var liveDataConverter: LiveDataConverter + + @Inject + lateinit var mediaClient: MediaClient + + @Inject + lateinit var locationManager: LocationServiceManager + + @Inject + lateinit var exploreMapController: ExploreMapController + + @Inject + @Named("default_preferences") + lateinit var applicationKvStore: JsonKvStore + + @Inject + lateinit var bookmarkLocationDao: BookmarkLocationsDao // May be needed in future if we want to integrate bookmarking explore places + + @Inject + lateinit var systemThemeUtils: SystemThemeUtils + + private val activityResultLauncher = registerForActivityResult( + RequestPermission() + ) { isGranted: Boolean? -> + if (isGranted == true) { + locationPermissionGranted() + } else { + if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { + showAlertDialog( + requireActivity(), + requireActivity().getString(R.string.location_permission_title), + requireActivity().getString(R.string.location_permission_rationale_explore), + requireActivity().getString(android.R.string.ok), + requireActivity().getString(android.R.string.cancel), + { askForLocationPermission() }, + null, + null + ) + } else { + if (isPermissionDenied) { + locationPermissionsHelper!!.showAppSettingsDialog( + requireActivity(), + R.string.explore_map_needs_location + ) + } + Timber.d("The user checked 'Don't ask again' or denied the permission twice") + isPermissionDenied = true + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + loadNearbyMapData() + binding = FragmentExploreMapBinding.inflate(getLayoutInflater()) + return binding!!.getRoot() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setSearchThisAreaButtonVisibility(false) + binding!!.tvAttribution.text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Html.fromHtml(getString(R.string.map_attribution), Html.FROM_HTML_MODE_LEGACY) + } else { + Html.fromHtml(getString(R.string.map_attribution)) + } + initNetworkBroadCastReceiver() + locationPermissionsHelper = LocationPermissionsHelper( + requireActivity(), locationManager, + this + ) + if (presenter == null) { + presenter = ExploreMapPresenter(bookmarkLocationDao) + } + setHasOptionsMenu(true) + + isDarkTheme = systemThemeUtils.isDeviceInNightMode() + isPermissionDenied = false + presenter!!.attachView(this) + + initViews() + presenter!!.setActionListeners(applicationKvStore) + + Configuration.getInstance().load( + requireContext(), + PreferenceManager.getDefaultSharedPreferences(requireContext()) + ) + + binding!!.mapView.setTileSource(TileSourceFactory.WIKIMEDIA) + binding!!.mapView.setTilesScaledToDpi(true) + + Configuration.getInstance().additionalHttpRequestProperties.put( + "Referer", "http://maps.wikimedia.org/" + ) + + val scaleBarOverlay = ScaleBarOverlay(binding!!.mapView) + scaleBarOverlay.setScaleBarOffset(15, 25) + val barPaint = Paint() + barPaint.setARGB(200, 255, 250, 250) + scaleBarOverlay.setBackgroundPaint(barPaint) + scaleBarOverlay.enableScaleBar() + binding!!.mapView.overlays.add(scaleBarOverlay) + binding!!.mapView.zoomController + .setVisibility(CustomZoomButtonsController.Visibility.NEVER) + binding!!.mapView.setMultiTouchControls(true) + + if (!isCameFromNearbyMap) { + binding!!.mapView.controller.setZoom(ZOOM_LEVEL.toDouble()) + } + + + binding!!.mapView.overlays.add(MapEventsOverlay(object : MapEventsReceiver { + override fun singleTapConfirmedHelper(p: GeoPoint?): Boolean { + if (clickedMarker != null) { + removeMarker(clickedMarker) + addMarkerToMap(clickedMarker!!) + binding!!.mapView.invalidate() + } else { + Timber.e("CLICKED MARKER IS NULL") + } + if (bottomSheetDetailsBehavior!!.getState() == BottomSheetBehavior.STATE_EXPANDED) { + // Back should first hide the bottom sheet if it is expanded + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_HIDDEN) + } else if (isDetailsBottomSheetVisible()) { + hideBottomDetailsSheet() + } + return true + } + + override fun longPressHelper(p: GeoPoint?): Boolean = false + })) + + binding!!.mapView.addMapListener(object : MapListener { + override fun onScroll(event: ScrollEvent): Boolean { + if (getLastMapFocus() != null) { + val mylocation = Location("") + val dest_location = Location("") + dest_location.latitude = binding!!.mapView.mapCenter.latitude + dest_location.longitude = binding!!.mapView.mapCenter.longitude + mylocation.latitude = getLastMapFocus()!!.latitude + mylocation.longitude = getLastMapFocus()!!.longitude + val distance = mylocation.distanceTo(dest_location) //in meters + if (getLastMapFocus() != null) { + if (isNetworkConnectionEstablished() && (event.getX() > 0 + || event.getY() > 0) + ) { + setSearchThisAreaButtonVisibility(distance > 2000.0) + } + } else { + setSearchThisAreaButtonVisibility(false) + } + } + + return true + } + + override fun onZoom(event: ZoomEvent?): Boolean = false + }) + if (!locationPermissionsHelper!!.checkLocationPermission(requireActivity())) { + askForLocationPermission() + } + } + + override fun onResume() { + super.onResume() + binding!!.mapView.onResume() + presenter!!.attachView(this) + registerNetworkReceiver() + if (isResumed) { + if (locationPermissionsHelper!!.checkLocationPermission(requireActivity())) { + performMapReadyActions() + } else { + startMapWithoutPermission() + } + } + } + + override fun onPause() { + super.onPause() + // unregistering the broadcastReceiver, as it was causing an exception and a potential crash + unregisterNetworkReceiver() + } + + + /** + * Unregisters the networkReceiver + */ + private fun unregisterNetworkReceiver() = + activity?.unregisterReceiver(broadcastReceiver) + + private fun startMapWithoutPermission() { + lastKnownLocation = defaultLatLng + moveCameraToPosition( + GeoPoint(lastKnownLocation!!.latitude, lastKnownLocation!!.longitude) + ) + presenter!!.onMapReady(exploreMapController) + } + + private fun registerNetworkReceiver() = + activity?.registerReceiver(broadcastReceiver, intentFilter) + + private fun performMapReadyActions() { + if (isDarkTheme) { + binding!!.mapView.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS) + } + + if (applicationKvStore.getBoolean("doNotAskForLocationPermission", false) && + !locationPermissionsHelper!!.checkLocationPermission(requireActivity())) { + isPermissionDenied = true + } + + lastKnownLocation = getLastLocation() + + if (lastKnownLocation == null) { + lastKnownLocation = defaultLatLng + } + + // if we came from 'Show in Explore' in Nearby, load Nearby map center and zoom + if (isCameFromNearbyMap) { + moveCameraToPosition( + GeoPoint(prevLatitude, prevLongitude), + prevZoom, + 1L + ) + } else { + moveCameraToPosition( + GeoPoint(lastKnownLocation!!.latitude, lastKnownLocation!!.longitude) + ) + } + presenter!!.onMapReady(exploreMapController) + } + + /** + * Fetch Nearby map camera data from fragment arguments if any. + */ + fun loadNearbyMapData() { + // get fragment arguments + if (arguments != null) { + with (requireArguments()) { + prevZoom = getDouble("prev_zoom") + prevLatitude = getDouble("prev_latitude") + prevLongitude = getDouble("prev_longitude") + } + } + + setRecentlyCameFromNearbyMap(isCameFromNearbyMap) + } + + /** + * @return The LatLng from the previous Fragment's map center or (0,0,0) coordinates + * if that information is not available/applicable. + */ + val previousLatLng: LatLng + get() = LatLng(prevLatitude, prevLongitude, prevZoom.toFloat()) + + /** + * Checks if fragment arguments contain data from Nearby map, indicating that the user navigated + * from Nearby using 'Show in Explore'. + * + * @return true if user navigated from Nearby map + */ + val isCameFromNearbyMap: Boolean + get() = prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0 + + /** + * Gets the value that indicates if the user navigated from "Show in Explore" in Nearby and + * that the LatLng from Nearby has yet to be searched for map markers. + */ + fun recentlyCameFromNearbyMap(): Boolean = + recentlyCameFromNearbyMap + + /** + * Sets the value that indicates if the user navigated from "Show in Explore" in Nearby and + * that the LatLng from Nearby has yet to be searched for map markers. + * @param newValue The value to set. + */ + fun setRecentlyCameFromNearbyMap(newValue: Boolean) { + recentlyCameFromNearbyMap = newValue + } + + fun loadNearbyMapFromExplore() { + (requireContext() as MainActivity).loadNearbyMapFromExplore( + binding!!.mapView.zoomLevelDouble, + binding!!.mapView.mapCenter.latitude, + binding!!.mapView.mapCenter.longitude + ) + } + + private fun initViews() { + initBottomSheets() + setBottomSheetCallbacks() + } + + /** + * a) Creates bottom sheet behaviours from bottom sheet, sets initial states and visibility + * b) Gets the touch event on the map to perform following actions: + * if bottom sheet details are expanded or collapsed hide the bottom sheet details. + */ + @SuppressLint("ClickableViewAccessibility") + private fun initBottomSheets() { + bottomSheetDetailsBehavior = BottomSheetBehavior.from( + binding!!.bottomSheetDetailsBinding.getRoot() + ) + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_HIDDEN) + binding!!.bottomSheetDetailsBinding.getRoot().visibility = View.VISIBLE + } + + /** + * Defines how bottom sheets will act on click + */ + private fun setBottomSheetCallbacks() { + binding!!.bottomSheetDetailsBinding.getRoot() + .setOnClickListener { v: View? -> + if (bottomSheetDetailsBehavior!!.getState() == BottomSheetBehavior.STATE_COLLAPSED) { + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_EXPANDED) + } else if (bottomSheetDetailsBehavior!!.getState() + == BottomSheetBehavior.STATE_EXPANDED + ) { + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_COLLAPSED) + } + } + } + + override fun onLocationChangedSignificantly(latLng: LatLng) = + handleLocationUpdate(latLng, LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) + + override fun onLocationChangedSlightly(latLng: LatLng) = + handleLocationUpdate(latLng, LocationChangeType.LOCATION_SLIGHTLY_CHANGED) + + private fun handleLocationUpdate( + latLng: LatLng?, + locationChangeType: LocationChangeType + ) { + lastKnownLocation = latLng + exploreMapController.currentLocation = lastKnownLocation + presenter!!.updateMap(locationChangeType) + } + + override fun onLocationChangedMedium(latLng: LatLng) = Unit + + override fun isNetworkConnectionEstablished(): Boolean = + isInternetConnectionEstablished(requireActivity()) + + override fun populatePlaces(curlatLng: LatLng?) { + val nearbyPlacesInfoObservable: Observable + if (curlatLng == null) { + return + } + if (curlatLng.equals( + getLastMapFocus() + ) + ) { // Means we are checking around current location + nearbyPlacesInfoObservable = presenter!!.loadAttractionsFromLocation( + curlatLng, + getLastMapFocus(), true + ) + } else { + nearbyPlacesInfoObservable = presenter!!.loadAttractionsFromLocation( + getLastMapFocus(), + curlatLng, false + ) + } + compositeDisposable.add( + nearbyPlacesInfoObservable + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + Consumer { explorePlacesInfo: ExplorePlacesInfo? -> + mediaList = explorePlacesInfo!!.mediaList.toMutableList() + if (mediaList!!.isEmpty()) { + showResponseMessage(getString(R.string.no_pictures_in_this_area)) + } + updateMapMarkers(explorePlacesInfo) + lastMapFocus = GeoPoint( + curlatLng.latitude, + curlatLng.longitude + ) + }, + Consumer { throwable: Throwable? -> + Timber.d(throwable) + // Not showing the user, throwable localizedErrorMessage + showErrorMessage(getString(R.string.error_fetching_nearby_places)) + + setProgressBarVisibility(false) + presenter!!.lockUnlockNearby(false) + }) + ) + if (recenterToUserLocation) { + recenterToUserLocation = false + } + } + + /** + * Updates map markers according to latest situation + * + * @param explorePlacesInfo holds several information as current location, marker list etc. + */ + private fun updateMapMarkers(explorePlacesInfo: ExplorePlacesInfo) = + presenter!!.updateMapMarkers(explorePlacesInfo) + + private fun showErrorMessage(message: String) = + showLongToast(requireActivity(), message) + + private fun showResponseMessage(message: String) = + showLongSnackbar(requireView(), message) + + override fun askForLocationPermission() { + Timber.d("Asking for location permission") + activityResultLauncher.launch(permission.ACCESS_FINE_LOCATION) + } + + private fun locationPermissionGranted() { + isPermissionDenied = false + applicationKvStore.putBoolean("doNotAskForLocationPermission", false) + lastKnownLocation = locationManager.getLastLocation() + val target = lastKnownLocation + if (lastKnownLocation != null) { + val targetP = GeoPoint(target!!.latitude, target.longitude) + mapCenter = targetP + binding!!.mapView.controller.setCenter(targetP) + recenterMarkerToPosition(targetP) + moveCameraToPosition(targetP) + } else if (locationManager.isGPSProviderEnabled() + || locationManager.isNetworkProviderEnabled() + ) { + locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) + locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER) + setProgressBarVisibility(true) + } else { + locationPermissionsHelper!!.showLocationOffDialog( + requireActivity(), + R.string.ask_to_turn_location_on_text + ) + } + presenter!!.onMapReady(exploreMapController) + registerUnregisterLocationListener(false) + } + + fun registerUnregisterLocationListener(removeLocationListener: Boolean) { + MapUtils.registerUnregisterLocationListener(removeLocationListener, locationManager, this) + } + + override fun recenterMap(curLatLng: LatLng?) { + // if user has denied permission twice, then show dialog + if (isPermissionDenied) { + if (locationPermissionsHelper!!.checkLocationPermission(requireActivity())) { + // this will run when user has given permission by opening app's settings + isPermissionDenied = false + recenterMap(curLatLng) + } else { + askForLocationPermission() + } + } else { + if (!locationPermissionsHelper!!.checkLocationPermission(requireActivity())) { + askForLocationPermission() + } else { + locationPermissionGranted() + } + } + if (curLatLng == null) { + recenterToUserLocation = true + return + } + recenterMarkerToPosition( + GeoPoint(curLatLng.latitude, curLatLng.longitude) + ) + binding!!.mapView.controller.animateTo( + GeoPoint(curLatLng.latitude, curLatLng.longitude) + ) + if (lastMapFocus != null) { + val mylocation = Location("") + val dest_location = Location("") + dest_location.latitude = binding!!.mapView.mapCenter.latitude + dest_location.longitude = binding!!.mapView.mapCenter.longitude + mylocation.latitude = lastMapFocus!!.latitude + mylocation.longitude = lastMapFocus!!.longitude + val distance = mylocation.distanceTo(dest_location) //in meters + if (lastMapFocus != null) { + if (isNetworkConnectionEstablished()) { + setSearchThisAreaButtonVisibility(distance > 2000.0) + } + } else { + setSearchThisAreaButtonVisibility(false) + } + } + } + + override fun hideBottomDetailsSheet() { + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_HIDDEN) + } + + /** + * Same bottom sheet carries information for all nearby places, so we need to pass information + * (title, description, distance and links) to view on nearby marker click + * + * @param place Place of clicked nearby marker + */ + private fun passInfoToSheet(place: Place) { + binding!!.bottomSheetDetailsBinding.directionsButton.setOnClickListener { + handleGeoCoordinates(requireActivity(), place.getLocation(), binding!!.mapView.zoomLevelDouble) + } + + binding!!.bottomSheetDetailsBinding.commonsButton.visibility = if (place.hasCommonsLink()) View.VISIBLE else View.GONE + binding!!.bottomSheetDetailsBinding.commonsButton.setOnClickListener { + handleWebUrl(requireContext(), place.siteLinks.commonsLink) + } + + var index = 0 + for (media in mediaList!!) { + if (media.filename == place.name) { + val finalIndex = index + binding!!.bottomSheetDetailsBinding.mediaDetailsButton.setOnClickListener { + (parentFragment as ExploreMapRootFragment).onMediaClicked(finalIndex) + } + } + index++ + } + binding!!.bottomSheetDetailsBinding.title.text = place.name.substring(5, place.name.lastIndexOf(".")) + binding!!.bottomSheetDetailsBinding.category.text = place.distance + // Remove label since it is double information + var descriptionText = place.longDescription + .replace(place.getName() + " (", "") + descriptionText = (if (descriptionText == place.longDescription) + descriptionText + else + descriptionText.replaceFirst(".$".toRegex(), "")) + // Set the short description after we remove place name from long description + binding!!.bottomSheetDetailsBinding.description.text = descriptionText + } + + override fun addSearchThisAreaButtonAction() { + binding!!.searchThisAreaButton.setOnClickListener(presenter!!.onSearchThisAreaClicked()) + } + + override fun setSearchThisAreaButtonVisibility(isVisible: Boolean) { + binding!!.searchThisAreaButton.visibility = if (isVisible) View.VISIBLE else View.GONE + } + + override fun setProgressBarVisibility(isVisible: Boolean) { + binding!!.mapProgressBar.visibility = if (isVisible) View.VISIBLE else View.GONE + } + + override fun isDetailsBottomSheetVisible(): Boolean = + binding!!.bottomSheetDetailsBinding.getRoot().isVisible + + override fun isSearchThisAreaButtonVisible(): Boolean = + binding!!.bottomSheetDetailsBinding.getRoot().isVisible + + override fun getLastLocation(): LatLng? { + if (lastKnownLocation == null) { + lastKnownLocation = locationManager.getLastLocation() + } + return lastKnownLocation + } + + override fun disableFABRecenter() { + binding!!.fabRecenter.setEnabled(false) + } + + override fun enableFABRecenter() { + binding!!.fabRecenter.setEnabled(true) + } + + /** + * Adds a markers to the map based on the list of NearbyBaseMarker. + * + * @param nearbyBaseMarkers The NearbyBaseMarker object representing the markers to be added. + */ + override fun addMarkersToMap(nearbyBaseMarkers: List?) { + clearAllMarkers() + nearbyBaseMarkers?.forEach { + addMarkerToMap(it!!) + } + binding!!.mapView.invalidate() + } + + /** + * Adds a marker to the map based on the specified NearbyBaseMarker. + * + * @param nearbyBaseMarker The NearbyBaseMarker object representing the marker to be added. + */ + private fun addMarkerToMap(nearbyBaseMarker: BaseMarker) { + if (isAttachedToActivity) { + val items = mutableListOf() + val d: Drawable = nearbyBaseMarker.icon!!.toDrawable(resources) + val point = GeoPoint( + nearbyBaseMarker.place.location.latitude, + nearbyBaseMarker.place.location.longitude + ) + + val markerMedia = getMediaFromImageURL(nearbyBaseMarker.place.pic) + var authorUser: String? = null + if (markerMedia != null) { + // HTML text is sometimes part of the author string and needs to be removed + authorUser = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Html.fromHtml(markerMedia.getAuthorOrUser(), Html.FROM_HTML_MODE_LEGACY) + } else { + Html.fromHtml(markerMedia.getAuthorOrUser()) + }.toString() + } + + var title = nearbyBaseMarker.place.name + // Remove "File:" if present at start + if (title.startsWith("File:")) { + title = title.substring(5) + } + // Remove extensions like .jpg, .jpeg, .png, .svg (case insensitive) + title = title.replace("(?i)\\.(jpg|jpeg|png|svg)$".toRegex(), "") + title = title.replace("_", " ") + //Truncate if too long because it doesn't fit the screen + if (title.length > 43) { + title = title.substring(0, 40) + "…" + } + + val item = OverlayItem(title, authorUser, point) + item.setMarker(d) + items.add(item) + val overlay = ItemizedOverlayWithFocus( + items, + object : OnItemGestureListener { + override fun onItemSingleTapUp(index: Int, item: OverlayItem?): Boolean { + val place = nearbyBaseMarker.place + if (clickedMarker != null) { + removeMarker(clickedMarker) + addMarkerToMap(clickedMarker!!) + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_HIDDEN) + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_COLLAPSED) + } + clickedMarker = nearbyBaseMarker + passInfoToSheet(place) + + //Move the overlay to the top so it can be fully seen. + moveOverlayToTop(getOverlay(item)) + return true + } + + override fun onItemLongPress(index: Int, item: OverlayItem?): Boolean = false + }, requireContext() + ) + + if (baseMarkerOverlayMap == null) { + baseMarkerOverlayMap = HashMap() + } + baseMarkerOverlayMap!!.put(nearbyBaseMarker, overlay) + + overlay.setFocusItemsOnTap(true) + binding!!.mapView.overlays.add(overlay) // Add the overlay to the map + } + } + + /** + * Moves the specified Overlay above all other Overlays. This prevents other Overlays from + * obstructing it. Upon failure, this method returns early. + * @param overlay The Overlay to move. + */ + private fun moveOverlayToTop(overlay: Overlay?) { + if (overlay == null || binding == null || binding!!.mapView.overlays == null) { + return + } + + val successfulRemoval = binding!!.mapView.overlays.remove(overlay) + if (!successfulRemoval) { + return + } + + binding!!.mapView.overlays.add(overlay) + } + + /** + * Performs a linear search for the first Overlay which contains the specified OverlayItem. + * + * @param item The OverlayItem contained within the first target Overlay. + * @return The first Overlay which contains the specified OverlayItem or null if the Overlay + * could not be found. + */ + private fun getOverlay(item: OverlayItem?): Overlay? { + if (item == null || binding == null || binding!!.mapView.overlays == null) { + return null + } + + for (i in binding!!.mapView.overlays.indices) { + if (binding!!.mapView.overlays[i] is ItemizedOverlayWithFocus<*>) { + val overlay = binding!!.mapView.overlays[i] as ItemizedOverlayWithFocus<*> + + for (j in 0.. { + setARGB(200, 255, 250, 250) + }) + enableScaleBar() + }) + + binding!!.mapView.overlays.add(MapEventsOverlay(object : MapEventsReceiver { + override fun singleTapConfirmedHelper(p: GeoPoint?): Boolean { + if (clickedMarker != null) { + removeMarker(clickedMarker) + addMarkerToMap(clickedMarker!!) + binding!!.mapView.invalidate() + } else { + Timber.e("CLICKED MARKER IS NULL") + } + if (bottomSheetDetailsBehavior!!.getState() == BottomSheetBehavior.STATE_EXPANDED) { + // Back should first hide the bottom sheet if it is expanded + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_HIDDEN) + } else if (isDetailsBottomSheetVisible()) { + hideBottomDetailsSheet() + } + return true + } + + override fun longPressHelper(p: GeoPoint?): Boolean = false + })) + binding!!.mapView.setMultiTouchControls(true) + } + } + + /** + * Recenters the map view to the specified GeoPoint and updates the marker to indicate the new + * position. + * + * @param geoPoint The GeoPoint representing the new center position for the map. + */ + private fun recenterMarkerToPosition(geoPoint: GeoPoint?) { + if (geoPoint != null) { + binding!!.mapView.controller.setCenter(geoPoint) + val overlays = binding!!.mapView.overlays + for (i in overlays.indices) { + if (overlays[i] is Marker) { + binding!!.mapView.overlays.removeAt(i) + } else if (overlays[i] is ScaleDiskOverlay) { + binding!!.mapView.overlays.removeAt(i) + } + } + val diskOverlay = ScaleDiskOverlay( + requireContext(), + geoPoint, 2000, GeoConstants.UnitOfMeasure.foot + ).apply { + setCirclePaint2(Paint().apply { + setColor(Color.rgb(128, 128, 128)) + this.style = Paint.Style.STROKE + this.strokeWidth = 2f + }) + + setCirclePaint1(Paint().apply { + setColor(Color.argb(40, 128, 128, 128)) + this.style = Paint.Style.FILL_AND_STROKE + }) + setDisplaySizeMin(900) + setDisplaySizeMax(1700) + } + binding!!.mapView.overlays.add(diskOverlay) + + val startMarker = Marker( + binding!!.mapView + ).apply { + setPosition(geoPoint) + setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) + icon = ContextCompat.getDrawable(requireContext(), R.drawable.current_location_marker) + title = "Your Location" + textLabelFontSize = 24 + } + binding!!.mapView.overlays.add(startMarker) + } + } + + /** + * Moves the camera of the map view to the specified GeoPoint using an animation. + * + * @param geoPoint The GeoPoint representing the new camera position for the map. + */ + private fun moveCameraToPosition(geoPoint: GeoPoint?) { + binding!!.mapView.controller.animateTo(geoPoint) + } + + /** + * Moves the camera of the map view to the specified GeoPoint at specified zoom level and speed + * using an animation. + * + * @param geoPoint The GeoPoint representing the new camera position for the map. + * @param zoom Zoom level of the map camera + * @param speed Speed of animation + */ + private fun moveCameraToPosition(geoPoint: GeoPoint?, zoom: Double, speed: Long) { + binding!!.mapView.controller.animateTo(geoPoint, zoom, speed) + } + + override fun getLastMapFocus(): LatLng? = if (lastMapFocus == null) { + getMapCenter() + } else { + LatLng(lastMapFocus!!.latitude, lastMapFocus!!.longitude, 100f) + } + + override fun getMapCenter(): LatLng? = if (mapCenter != null) { + LatLng(mapCenter!!.latitude, mapCenter!!.longitude, 100f) + } else { + if (applicationKvStore.getString("LastLocation") != null) { + val locationLatLng: Array = + applicationKvStore.getString("LastLocation")!! + .split(",".toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray() + lastKnownLocation = LatLng( + locationLatLng[0]!!.toDouble(), + locationLatLng[1]!!.toDouble(), 1f + ) + lastKnownLocation + } else { + LatLng(51.506255446947776, -0.07483536015053005, 1f) + } + } + + override fun getMapFocus(): LatLng? = LatLng( + binding!!.mapView.mapCenter.latitude, + binding!!.mapView.mapCenter.longitude, 100f + ) + + override fun setFABRecenterAction(onClickListener: View.OnClickListener?) { + binding!!.fabRecenter.setOnClickListener(onClickListener) + } + + override fun backButtonClicked(): Boolean { + if (bottomSheetDetailsBehavior!!.getState() != BottomSheetBehavior.STATE_HIDDEN) { + bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_HIDDEN) + return true + } else { + return false + } + } + + /** + * Adds network broadcast receiver to recognize connection established + */ + private fun initNetworkBroadCastReceiver() { + broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (activity != null) { + if (isInternetConnectionEstablished(requireActivity())) { + if (isNetworkErrorOccurred) { + presenter!!.updateMap(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) + isNetworkErrorOccurred = false + } + + if (snackbar != null) { + snackbar!!.dismiss() + snackbar = null + } + } else { + if (snackbar == null) { + snackbar = Snackbar.make( + requireView(), R.string.no_internet, + Snackbar.LENGTH_INDEFINITE + ) + setSearchThisAreaButtonVisibility(false) + setProgressBarVisibility(false) + } + + isNetworkErrorOccurred = true + snackbar!!.show() + } + } + } + } + } + + val isAttachedToActivity: Boolean + get() = isVisible && activity != null + + override fun onLocationPermissionDenied(toastMessage: String) = Unit + + override fun onLocationPermissionGranted() = Unit + + companion object { + fun newInstance(): ExploreMapFragment { + val fragment = ExploreMapFragment() + fragment.setRetainInstance(true) + return fragment + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java deleted file mode 100644 index 70f785b40..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java +++ /dev/null @@ -1,237 +0,0 @@ -package fr.free.nrw.commons.explore.map; - -import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; -import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.SEARCH_CUSTOM_AREA; - - -import android.location.Location; -import android.view.View; -import fr.free.nrw.commons.BaseMarker; -import fr.free.nrw.commons.MapController; -import fr.free.nrw.commons.MapController.ExplorePlacesInfo; -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; -import fr.free.nrw.commons.explore.map.ExploreMapController.NearbyBaseMarkerThumbCallback; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType; -import fr.free.nrw.commons.nearby.Place; -import io.reactivex.Observable; -import java.lang.reflect.Proxy; -import java.util.List; -import timber.log.Timber; - -public class ExploreMapPresenter - implements ExploreMapContract.UserActions, - NearbyBaseMarkerThumbCallback { - - BookmarkLocationsDao bookmarkLocationDao; - private boolean isNearbyLocked; - private LatLng currentLatLng; - private ExploreMapController exploreMapController; - - private static final ExploreMapContract.View DUMMY = (ExploreMapContract.View) Proxy - .newProxyInstance( - ExploreMapContract.View.class.getClassLoader(), - new Class[]{ExploreMapContract.View.class}, (proxy, method, args) -> { - if (method.getName().equals("onMyEvent")) { - return null; - } else if (String.class == method.getReturnType()) { - return ""; - } else if (Integer.class == method.getReturnType()) { - return Integer.valueOf(0); - } else if (int.class == method.getReturnType()) { - return 0; - } else if (Boolean.class == method.getReturnType()) { - return Boolean.FALSE; - } else if (boolean.class == method.getReturnType()) { - return false; - } else { - return null; - } - } - ); - private ExploreMapContract.View exploreMapFragmentView = DUMMY; - - public ExploreMapPresenter(BookmarkLocationsDao bookmarkLocationDao) { - this.bookmarkLocationDao = bookmarkLocationDao; - } - - @Override - public void updateMap(LocationChangeType locationChangeType) { - Timber.d("Presenter updates map and list" + locationChangeType.toString()); - if (isNearbyLocked) { - Timber.d("Nearby is locked, so updateMapAndList returns"); - return; - } - - if (!exploreMapFragmentView.isNetworkConnectionEstablished()) { - Timber.d("Network connection is not established"); - return; - } - - /** - * Significant changed - Markers and current location will be updated together - * Slightly changed - Only current position marker will be updated - */ - if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)) { - Timber.d("LOCATION_SIGNIFICANTLY_CHANGED"); - LatLng populateLatLng = exploreMapFragmentView.getMapCenter(); - - //If "Show in Explore" was selected in Nearby, use the previous LatLng - if (exploreMapFragmentView instanceof ExploreMapFragment) { - ExploreMapFragment exploreMapFragment = (ExploreMapFragment)exploreMapFragmentView; - if (exploreMapFragment.recentlyCameFromNearbyMap()) { - //Ensure this LatLng will not be used again if user searches their GPS location - exploreMapFragment.setRecentlyCameFromNearbyMap(false); - - populateLatLng = exploreMapFragment.getPreviousLatLng(); - } - } - - lockUnlockNearby(true); - exploreMapFragmentView.setProgressBarVisibility(true); - exploreMapFragmentView.populatePlaces(populateLatLng); - } else if (locationChangeType.equals(SEARCH_CUSTOM_AREA)) { - Timber.d("SEARCH_CUSTOM_AREA"); - lockUnlockNearby(true); - exploreMapFragmentView.setProgressBarVisibility(true); - exploreMapFragmentView.populatePlaces(exploreMapFragmentView.getMapFocus()); - } else { // Means location changed slightly, ie user is walking or driving. - Timber.d("Means location changed slightly"); - } - } - - /** - * Nearby updates takes time, since they are network operations. During update time, we don't - * want to get any other calls from user. So locking nearby. - * - * @param isNearbyLocked true means lock, false means unlock - */ - @Override - public void lockUnlockNearby(boolean isNearbyLocked) { - this.isNearbyLocked = isNearbyLocked; - if (isNearbyLocked) { - exploreMapFragmentView.disableFABRecenter(); - } else { - exploreMapFragmentView.enableFABRecenter(); - } - } - - @Override - public void attachView(ExploreMapContract.View view) { - exploreMapFragmentView = view; - } - - @Override - public void detachView() { - exploreMapFragmentView = DUMMY; - } - - /** - * Sets click listener of FAB - */ - @Override - public void setActionListeners(JsonKvStore applicationKvStore) { - exploreMapFragmentView.setFABRecenterAction(v -> { - exploreMapFragmentView.recenterMap(currentLatLng); - }); - - } - - @Override - public boolean backButtonClicked() { - return exploreMapFragmentView.backButtonClicked(); - } - - public void onMapReady(ExploreMapController exploreMapController) { - this.exploreMapController = exploreMapController; - if (null != exploreMapFragmentView) { - exploreMapFragmentView.addSearchThisAreaButtonAction(); - initializeMapOperations(); - } - } - - public void initializeMapOperations() { - lockUnlockNearby(false); - updateMap(LOCATION_SIGNIFICANTLY_CHANGED); - } - - public Observable loadAttractionsFromLocation(LatLng currentLatLng, - LatLng searchLatLng, boolean checkingAroundCurrent) { - return Observable - .fromCallable(() -> exploreMapController - .loadAttractionsFromLocation(currentLatLng, searchLatLng, checkingAroundCurrent)); - } - - /** - * Populates places for custom location, should be used for finding nearby places around a - * location where you are not at. - * - * @param explorePlacesInfo This variable has placeToCenter list information and distances. - */ - public void updateMapMarkers( - MapController.ExplorePlacesInfo explorePlacesInfo) { - if (explorePlacesInfo.mediaList != null) { - prepareNearbyBaseMarkers(explorePlacesInfo); - } else { - lockUnlockNearby(false); // So that new location updates wont come - exploreMapFragmentView.setProgressBarVisibility(false); - } - } - - void prepareNearbyBaseMarkers(MapController.ExplorePlacesInfo explorePlacesInfo) { - exploreMapController - .loadAttractionsFromLocationToBaseMarkerOptions(explorePlacesInfo.currentLatLng, - // Curlatlang will be used to calculate distances - (List) explorePlacesInfo.explorePlaceList, - exploreMapFragmentView.getContext(), - this, - explorePlacesInfo); - } - - @Override - public void onNearbyBaseMarkerThumbsReady(List baseMarkers, - ExplorePlacesInfo explorePlacesInfo) { - if (null != exploreMapFragmentView) { - exploreMapFragmentView.addMarkersToMap(baseMarkers); - lockUnlockNearby(false); // So that new location updates wont come - exploreMapFragmentView.setProgressBarVisibility(false); - } - } - - public View.OnClickListener onSearchThisAreaClicked() { - return v -> { - // Lock map operations during search this area operation - exploreMapFragmentView.setSearchThisAreaButtonVisibility(false); - - if (searchCloseToCurrentLocation()) { - updateMap(LOCATION_SIGNIFICANTLY_CHANGED); - } else { - updateMap(SEARCH_CUSTOM_AREA); - } - }; - } - - /** - * Returns true if search this area button is used around our current location, so that we can - * continue following our current location again - * - * @return Returns true if search this area button is used around our current location - */ - public boolean searchCloseToCurrentLocation() { - if (null == exploreMapFragmentView.getLastMapFocus()) { - return true; - } - - Location mylocation = new Location(""); - Location dest_location = new Location(""); - dest_location.setLatitude(exploreMapFragmentView.getMapFocus().getLatitude()); - dest_location.setLongitude(exploreMapFragmentView.getMapFocus().getLongitude()); - mylocation.setLatitude(exploreMapFragmentView.getLastMapFocus().getLatitude()); - mylocation.setLongitude(exploreMapFragmentView.getLastMapFocus().getLongitude()); - Float distance = mylocation.distanceTo(dest_location); - - return !(distance > 2000.0 * 3 / 4); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.kt b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.kt new file mode 100644 index 000000000..002ff6044 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.kt @@ -0,0 +1,223 @@ +package fr.free.nrw.commons.explore.map + +import android.location.Location +import android.view.View +import fr.free.nrw.commons.BaseMarker +import fr.free.nrw.commons.MapController.ExplorePlacesInfo +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao +import fr.free.nrw.commons.explore.map.ExploreMapController.Companion.loadAttractionsFromLocationToBaseMarkerOptions +import fr.free.nrw.commons.explore.map.ExploreMapController.NearbyBaseMarkerThumbCallback +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType +import fr.free.nrw.commons.nearby.Place +import io.reactivex.Observable +import timber.log.Timber +import java.lang.reflect.Method +import java.lang.reflect.Proxy +import java.util.concurrent.Callable + +class ExploreMapPresenter( + var bookmarkLocationDao: BookmarkLocationsDao +) : ExploreMapContract.UserActions, NearbyBaseMarkerThumbCallback { + + private var isNearbyLocked = false + private val currentLatLng: LatLng? = null + private var exploreMapController: ExploreMapController? = null + private var exploreMapFragmentView: ExploreMapContract.View? = DUMMY + + override fun updateMap(locationChangeType: LocationChangeType) { + Timber.d("Presenter updates map and list$locationChangeType") + if (isNearbyLocked) { + Timber.d("Nearby is locked, so updateMapAndList returns") + return + } + + if (!exploreMapFragmentView!!.isNetworkConnectionEstablished()) { + Timber.d("Network connection is not established") + return + } + + /** + * Significant changed - Markers and current location will be updated together + * Slightly changed - Only current position marker will be updated + */ + if (locationChangeType == LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) { + Timber.d("LOCATION_SIGNIFICANTLY_CHANGED") + var populateLatLng = exploreMapFragmentView!!.getMapCenter() + + //If "Show in Explore" was selected in Nearby, use the previous LatLng + if (exploreMapFragmentView is ExploreMapFragment) { + val exploreMapFragment = exploreMapFragmentView as ExploreMapFragment + if (exploreMapFragment.recentlyCameFromNearbyMap()) { + //Ensure this LatLng will not be used again if user searches their GPS location + exploreMapFragment.setRecentlyCameFromNearbyMap(false) + + populateLatLng = exploreMapFragment.previousLatLng + } + } + + lockUnlockNearby(true) + exploreMapFragmentView!!.setProgressBarVisibility(true) + exploreMapFragmentView!!.populatePlaces(populateLatLng) + } else if (locationChangeType == LocationChangeType.SEARCH_CUSTOM_AREA) { + Timber.d("SEARCH_CUSTOM_AREA") + lockUnlockNearby(true) + exploreMapFragmentView!!.setProgressBarVisibility(true) + exploreMapFragmentView!!.populatePlaces(exploreMapFragmentView!!.getMapFocus()) + } else { // Means location changed slightly, ie user is walking or driving. + Timber.d("Means location changed slightly") + } + } + + /** + * Nearby updates takes time, since they are network operations. During update time, we don't + * want to get any other calls from user. So locking nearby. + * + * @param isNearbyLocked true means lock, false means unlock + */ + override fun lockUnlockNearby(isNearbyLocked: Boolean) { + this.isNearbyLocked = isNearbyLocked + if (isNearbyLocked) { + exploreMapFragmentView!!.disableFABRecenter() + } else { + exploreMapFragmentView!!.enableFABRecenter() + } + } + + override fun attachView(view: ExploreMapContract.View?) { + exploreMapFragmentView = view + } + + override fun detachView() { + exploreMapFragmentView = DUMMY + } + + /** + * Sets click listener of FAB + */ + override fun setActionListeners(applicationKvStore: JsonKvStore?) { + exploreMapFragmentView!!.setFABRecenterAction { + exploreMapFragmentView!!.recenterMap(currentLatLng) + } + } + + override fun backButtonClicked(): Boolean = + exploreMapFragmentView!!.backButtonClicked() + + fun onMapReady(exploreMapController: ExploreMapController?) { + this.exploreMapController = exploreMapController + if (null != exploreMapFragmentView) { + exploreMapFragmentView!!.addSearchThisAreaButtonAction() + initializeMapOperations() + } + } + + fun initializeMapOperations() { + lockUnlockNearby(false) + updateMap(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) + } + + fun loadAttractionsFromLocation( + currentLatLng: LatLng?, + searchLatLng: LatLng?, checkingAroundCurrent: Boolean + ): Observable = Observable.fromCallable(Callable { + exploreMapController!!.loadAttractionsFromLocation( + currentLatLng, + searchLatLng, + checkingAroundCurrent + ) + }) + + /** + * Populates places for custom location, should be used for finding nearby places around a + * location where you are not at. + * + * @param explorePlacesInfo This variable has placeToCenter list information and distances. + */ + fun updateMapMarkers( + explorePlacesInfo: ExplorePlacesInfo + ) { + if (explorePlacesInfo.mediaList != null) { + prepareNearbyBaseMarkers(explorePlacesInfo) + } else { + lockUnlockNearby(false) // So that new location updates wont come + exploreMapFragmentView!!.setProgressBarVisibility(false) + } + } + + private fun prepareNearbyBaseMarkers(explorePlacesInfo: ExplorePlacesInfo) { + loadAttractionsFromLocationToBaseMarkerOptions( + explorePlacesInfo.currentLatLng, // Curlatlang will be used to calculate distances + explorePlacesInfo.explorePlaceList, + exploreMapFragmentView!!.getContext()!!, + this, + explorePlacesInfo + ) + } + + override fun onNearbyBaseMarkerThumbsReady( + baseMarkers: List?, + explorePlacesInfo: ExplorePlacesInfo? + ) { + if (null != exploreMapFragmentView) { + exploreMapFragmentView!!.addMarkersToMap(baseMarkers) + lockUnlockNearby(false) // So that new location updates wont come + exploreMapFragmentView!!.setProgressBarVisibility(false) + } + } + + fun onSearchThisAreaClicked(): View.OnClickListener { + return View.OnClickListener { + // Lock map operations during search this area operation + exploreMapFragmentView!!.setSearchThisAreaButtonVisibility(false) + updateMap(if (searchCloseToCurrentLocation()) { + LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED + } else { + LocationChangeType.SEARCH_CUSTOM_AREA + }) + } + } + + /** + * Returns true if search this area button is used around our current location, so that we can + * continue following our current location again + * + * @return Returns true if search this area button is used around our current location + */ + private fun searchCloseToCurrentLocation(): Boolean { + if (null == exploreMapFragmentView!!.getLastMapFocus()) { + return true + } + + val mylocation = Location("").apply { + latitude = exploreMapFragmentView!!.getLastMapFocus()!!.latitude + longitude = exploreMapFragmentView!!.getLastMapFocus()!!.longitude + } + val dest_location = Location("").apply { + latitude = exploreMapFragmentView!!.getMapFocus()!!.latitude + longitude = exploreMapFragmentView!!.getMapFocus()!!.longitude + } + + val distance = mylocation.distanceTo(dest_location) + + return !(distance > 2000.0 * 3 / 4) + } + + companion object { + private val DUMMY = Proxy.newProxyInstance( + ExploreMapContract.View::class.java.classLoader, + arrayOf>(ExploreMapContract.View::class.java) + ) { _: Any?, method: Method, _: Array? -> + when { + method.name == "onMyEvent" -> null + String::class.java == method.returnType -> "" + Int::class.java == method.returnType -> 0 + Int::class.javaPrimitiveType == method.returnType -> 0 + Boolean::class.java == method.returnType -> java.lang.Boolean.FALSE + Boolean::class.javaPrimitiveType == method.returnType -> false + else -> null + } + } as ExploreMapContract.View + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt index f30636db7..21f7a1a22 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.kt @@ -8,9 +8,9 @@ import android.net.Uri import androidx.core.net.toUri import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.di.CommonsDaggerContentProvider -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.TABLE_NAME +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.TABLE_NAME /** * This class contains functions for executing queries for diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java deleted file mode 100644 index cee8a25ae..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java +++ /dev/null @@ -1,275 +0,0 @@ -package fr.free.nrw.commons.explore.recentsearches; - -import android.annotation.SuppressLint; -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.os.RemoteException; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.explore.models.RecentSearch; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Provider; - -import timber.log.Timber; - -/** - * This class doesn't execute queries in database directly instead it contains the logic behind - * inserting, deleting, searching data from recent searches database. - **/ -public class RecentSearchesDao { - - private final Provider clientProvider; - - @Inject - public RecentSearchesDao(@Named("recentsearch") Provider clientProvider) { - this.clientProvider = clientProvider; - } - - /** - * This method is called on click of media/ categories for storing them in recent searches - * @param recentSearch a recent searches object that is to be added in SqLite DB - */ - public void save(RecentSearch recentSearch) { - ContentProviderClient db = clientProvider.get(); - try { - if (recentSearch.getContentUri() == null) { - recentSearch.setContentUri(db.insert(RecentSearchesContentProvider.BASE_URI, toContentValues(recentSearch))); - } else { - db.update(recentSearch.getContentUri(), toContentValues(recentSearch), null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - /** - * This method is called on confirmation of delete recent searches. - * It deletes all recent searches from the database - */ - public void deleteAll() { - Cursor cursor = null; - ContentProviderClient db = clientProvider.get(); - try { - cursor = db.query( - RecentSearchesContentProvider.BASE_URI, - Table.ALL_FIELDS, - null, - new String[]{}, - Table.COLUMN_LAST_USED + " DESC" - ); - while (cursor != null && cursor.moveToNext()) { - try { - RecentSearch recentSearch = find(fromCursor(cursor).getQuery()); - if (recentSearch.getContentUri() == null) { - throw new RuntimeException("tried to delete item with no content URI"); - } else { - Timber.d("QUERY_NAME %s - delete tried", recentSearch.getContentUri()); - db.delete(recentSearch.getContentUri(), null, null); - Timber.d("QUERY_NAME %s - query deleted", recentSearch.getQuery()); - } - } catch (RemoteException e) { - Timber.e(e, "query deleted"); - throw new RuntimeException(e); - } finally { - db.release(); - } - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - /** - * Deletes a recent search from the database - */ - public void delete(RecentSearch recentSearch) { - - ContentProviderClient db = clientProvider.get(); - try { - if (recentSearch.getContentUri() == null) { - throw new RuntimeException("tried to delete item with no content URI"); - } else { - db.delete(recentSearch.getContentUri(), null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - - /** - * Find persisted search query in database, based on its name. - * @param name Search query Ex- "butterfly" - * @return recently searched query from database, or null if not found - */ - @Nullable - public RecentSearch find(String name) { - Cursor cursor = null; - ContentProviderClient db = clientProvider.get(); - try { - cursor = db.query( - RecentSearchesContentProvider.BASE_URI, - Table.ALL_FIELDS, - Table.COLUMN_NAME + "=?", - new String[]{name}, - null); - if (cursor != null && cursor.moveToFirst()) { - return fromCursor(cursor); - } - } catch (RemoteException e) { - // This feels lazy, but to hell with checked exceptions. :) - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - db.release(); - } - return null; - } - - /** - * Retrieve recently-searched queries, ordered by descending date. - * @return a list containing recent searches - */ - @NonNull - public List recentSearches(int limit) { - List items = new ArrayList<>(); - Cursor cursor = null; - ContentProviderClient db = clientProvider.get(); - try { - cursor = db.query( RecentSearchesContentProvider.BASE_URI, Table.ALL_FIELDS, - null, new String[]{}, Table.COLUMN_LAST_USED + " DESC"); - // fixme add a limit on the original query instead of falling out of the loop? - while (cursor != null && cursor.moveToNext() && cursor.getPosition() < limit) { - items.add(fromCursor(cursor).getQuery()); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - db.release(); - } - return items; - } - - - /** - * It creates an Recent Searches object from data stored in the SQLite DB by using cursor - * @param cursor - * @return RecentSearch object - */ - @NonNull - @SuppressLint("Range") - RecentSearch fromCursor(Cursor cursor) { - // Hardcoding column positions! - return new RecentSearch( - RecentSearchesContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))), - cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)), - new Date(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LAST_USED))) - ); - } - - /** - * This class contains the database table architechture for recent searches, - * It also contains queries and logic necessary to the create, update, delete this table. - */ - private ContentValues toContentValues(RecentSearch recentSearch) { - ContentValues cv = new ContentValues(); - cv.put(RecentSearchesDao.Table.COLUMN_NAME, recentSearch.getQuery()); - cv.put(RecentSearchesDao.Table.COLUMN_LAST_USED, recentSearch.getLastSearched().getTime()); - return cv; - } - - /** - * This class contains the database table architechture for recent searches, - * It also contains queries and logic necessary to the create, update, delete this table. - */ - public static class Table { - public static final String TABLE_NAME = "recent_searches"; - public static final String COLUMN_ID = "_id"; - static final String COLUMN_NAME = "name"; - static final String COLUMN_LAST_USED = "last_used"; - - // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. - public static final String[] ALL_FIELDS = { - COLUMN_ID, - COLUMN_NAME, - COLUMN_LAST_USED, - }; - - static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; - - static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + COLUMN_ID + " INTEGER PRIMARY KEY," - + COLUMN_NAME + " STRING," - + COLUMN_LAST_USED + " INTEGER" - + ");"; - - /** - * This method creates a RecentSearchesTable in SQLiteDatabase - * @param db SQLiteDatabase - */ - public static void onCreate(SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); - } - - /** - * This method deletes RecentSearchesTable from SQLiteDatabase - * @param db SQLiteDatabase - */ - public static void onDelete(SQLiteDatabase db) { - db.execSQL(DROP_TABLE_STATEMENT); - onCreate(db); - } - - /** - * This method is called on migrating from a older version to a newer version - * @param db SQLiteDatabase - * @param from Version from which we are migrating - * @param to Version to which we are migrating - */ - public static void onUpdate(SQLiteDatabase db, int from, int to) { - if (from == to) { - return; - } - if (from < 6) { - // doesn't exist yet - from++; - onUpdate(db, from, to); - return; - } - if (from == 6) { - // table added in version 7 - onCreate(db); - from++; - onUpdate(db, from, to); - return; - } - if (from == 7) { - from++; - onUpdate(db, from, to); - return; - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt new file mode 100644 index 000000000..e1d0740de --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.kt @@ -0,0 +1,180 @@ +package fr.free.nrw.commons.explore.recentsearches + +import android.annotation.SuppressLint +import android.content.ContentProviderClient +import android.content.ContentValues +import android.database.Cursor +import android.os.RemoteException +import androidx.core.content.contentValuesOf +import fr.free.nrw.commons.explore.models.RecentSearch +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.BASE_URI +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_LAST_USED +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_NAME +import fr.free.nrw.commons.utils.getInt +import fr.free.nrw.commons.utils.getLong +import fr.free.nrw.commons.utils.getString +import java.util.Date +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Provider + +/** + * This class doesn't execute queries in database directly instead it contains the logic behind + * inserting, deleting, searching data from recent searches database. + */ +class RecentSearchesDao @Inject constructor( + @param:Named("recentsearch") private val clientProvider: Provider +) { + /** + * This method is called on click of media/ categories for storing them in recent searches + * @param recentSearch a recent searches object that is to be added in SqLite DB + */ + fun save(recentSearch: RecentSearch) { + val db = clientProvider.get() + try { + val contentValues = toContentValues(recentSearch) + if (recentSearch.contentUri == null) { + recentSearch.contentUri = db.insert(BASE_URI, contentValues) + } else { + db.update(recentSearch.contentUri!!, contentValues, null, null) + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + + /** + * This method is called on confirmation of delete recent searches. + * It deletes all recent searches from the database + */ + fun deleteAll() { + var cursor: Cursor? = null + val db = clientProvider.get() + try { + cursor = db.query( + BASE_URI, + ALL_FIELDS, + null, + arrayOf(), + "$COLUMN_LAST_USED DESC" + ) + while (cursor != null && cursor.moveToNext()) { + try { + val recentSearch = find(fromCursor(cursor).query) + if (recentSearch!!.contentUri == null) { + throw RuntimeException("tried to delete item with no content URI") + } else { + db.delete(recentSearch.contentUri!!, null, null) + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + cursor?.close() + } + } + + /** + * Deletes a recent search from the database + */ + fun delete(recentSearch: RecentSearch) { + val db = clientProvider.get() + try { + if (recentSearch.contentUri == null) { + throw RuntimeException("tried to delete item with no content URI") + } else { + db.delete(recentSearch.contentUri!!, null, null) + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + + + /** + * Find persisted search query in database, based on its name. + * @param name Search query Ex- "butterfly" + * @return recently searched query from database, or null if not found + */ + fun find(name: String): RecentSearch? { + var cursor: Cursor? = null + val db = clientProvider.get() + try { + cursor = db.query( + BASE_URI, + ALL_FIELDS, + "$COLUMN_NAME=?", + arrayOf(name), + null + ) + if (cursor != null && cursor.moveToFirst()) { + return fromCursor(cursor) + } + } catch (e: RemoteException) { + // This feels lazy, but to hell with checked exceptions. :) + throw RuntimeException(e) + } finally { + cursor?.close() + db.release() + } + return null + } + + /** + * Retrieve recently-searched queries, ordered by descending date. + * @return a list containing recent searches + */ + fun recentSearches(limit: Int): List { + val items: MutableList = mutableListOf() + var cursor: Cursor? = null + val db = clientProvider.get() + try { + cursor = db.query( + BASE_URI, ALL_FIELDS, + null, arrayOf(), "$COLUMN_LAST_USED DESC" + ) + // fixme add a limit on the original query instead of falling out of the loop? + while (cursor != null && cursor.moveToNext() && cursor.position < limit) { + items.add(fromCursor(cursor).query) + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + cursor?.close() + db.release() + } + return items + } + + /** + * It creates an Recent Searches object from data stored in the SQLite DB by using cursor + * @param cursor + * @return RecentSearch object + */ + fun fromCursor(cursor: Cursor): RecentSearch = RecentSearch( + uriForId(cursor.getInt(COLUMN_ID)), + cursor.getString(COLUMN_NAME), + Date(cursor.getLong(COLUMN_LAST_USED)) + ) + + /** + * This class contains the database table architechture for recent searches, + * It also contains queries and logic necessary to the create, update, delete this table. + */ + private fun toContentValues(recentSearch: RecentSearch): ContentValues = contentValuesOf( + COLUMN_NAME to recentSearch.query, + COLUMN_LAST_USED to recentSearch.lastSearched.time + ) +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java deleted file mode 100644 index 588f3a25f..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java +++ /dev/null @@ -1,149 +0,0 @@ -package fr.free.nrw.commons.explore.recentsearches; - -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.databinding.FragmentSearchHistoryBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.explore.SearchActivity; -import java.util.List; -import java.util.Locale; -import javax.inject.Inject; - - -/** - * Displays the recent searches screen. - */ -public class RecentSearchesFragment extends CommonsDaggerSupportFragment { - - @Inject - RecentSearchesDao recentSearchesDao; - List recentSearches; - ArrayAdapter adapter; - - private FragmentSearchHistoryBinding binding; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - binding = FragmentSearchHistoryBinding.inflate(inflater, container, false); - - recentSearches = recentSearchesDao.recentSearches(10); - - if (recentSearches.isEmpty()) { - binding.recentSearchesDeleteButton.setVisibility(View.GONE); - binding.recentSearchesTextView.setText(R.string.no_recent_searches); - } - - binding.recentSearchesDeleteButton.setOnClickListener(v -> { - showDeleteRecentAlertDialog(requireContext()); - }); - - adapter = new ArrayAdapter<>(requireContext(), R.layout.item_recent_searches, - recentSearches); - binding.recentSearchesList.setAdapter(adapter); - binding.recentSearchesList.setOnItemClickListener((parent, view, position, id) -> ( - (SearchActivity) getContext()).updateText(recentSearches.get(position))); - binding.recentSearchesList.setOnItemLongClickListener((parent, view, position, id) -> { - showDeleteAlertDialog(requireContext(), position); - return true; - }); - updateRecentSearches(); - - return binding.getRoot(); - } - - private void showDeleteRecentAlertDialog(@NonNull final Context context) { - new AlertDialog.Builder(context) - .setMessage(getString(R.string.delete_recent_searches_dialog)) - .setPositiveButton(android.R.string.yes, - (dialog, which) -> setDeleteRecentPositiveButton(context, dialog)) - .setNegativeButton(android.R.string.no, null) - .setCancelable(false) - .create() - .show(); - } - - private void setDeleteRecentPositiveButton(@NonNull final Context context, - final DialogInterface dialog) { - recentSearchesDao.deleteAll(); - if (binding != null) { - binding.recentSearchesDeleteButton.setVisibility(View.GONE); - binding.recentSearchesTextView.setText(R.string.no_recent_searches); - Toast.makeText(getContext(), getString(R.string.search_history_deleted), - Toast.LENGTH_SHORT).show(); - recentSearches = recentSearchesDao.recentSearches(10); - adapter = new ArrayAdapter<>(context, R.layout.item_recent_searches, - recentSearches); - binding.recentSearchesList.setAdapter(adapter); - adapter.notifyDataSetChanged(); - } - dialog.dismiss(); - } - - private void showDeleteAlertDialog(@NonNull final Context context, final int position) { - new AlertDialog.Builder(context) - .setMessage(R.string.delete_search_dialog) - .setPositiveButton(getString(R.string.delete).toUpperCase(Locale.ROOT), - ((dialog, which) -> setDeletePositiveButton(context, dialog, position))) - .setNegativeButton(android.R.string.cancel, null) - .setCancelable(false) - .create() - .show(); - } - - private void setDeletePositiveButton(@NonNull final Context context, - final DialogInterface dialog, final int position) { - recentSearchesDao.delete(recentSearchesDao.find(recentSearches.get(position))); - recentSearches = recentSearchesDao.recentSearches(10); - adapter = new ArrayAdapter<>(context, R.layout.item_recent_searches, - recentSearches); - if (binding != null){ - binding.recentSearchesList.setAdapter(adapter); - adapter.notifyDataSetChanged(); - } - dialog.dismiss(); - } - - /** - * This method is called on back press of activity so we are updating the list from database to - * refresh the recent searches list. - */ - @Override - public void onResume() { - updateRecentSearches(); - super.onResume(); - } - - /** - * This method is called when search query is null to update Recent Searches - */ - public void updateRecentSearches() { - recentSearches = recentSearchesDao.recentSearches(10); - adapter.notifyDataSetChanged(); - - if (!recentSearches.isEmpty()) { - if (binding!= null) { - binding.recentSearchesDeleteButton.setVisibility(View.VISIBLE); - binding.recentSearchesTextView.setText(R.string.search_recent_header); - } - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - - if (binding != null) { - binding = null; - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt new file mode 100644 index 000000000..c0f1bd5db --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.kt @@ -0,0 +1,153 @@ +package fr.free.nrw.commons.explore.recentsearches + +import android.content.Context +import android.content.DialogInterface +import android.content.DialogInterface.OnClickListener +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.AdapterView.OnItemClickListener +import android.widget.AdapterView.OnItemLongClickListener +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import fr.free.nrw.commons.R +import fr.free.nrw.commons.databinding.FragmentSearchHistoryBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.explore.SearchActivity +import javax.inject.Inject + +/** + * Displays the recent searches screen. + */ +class RecentSearchesFragment : CommonsDaggerSupportFragment() { + @JvmField + @Inject + var recentSearchesDao: RecentSearchesDao? = null + + private var recentSearches: List = emptyList() + private lateinit var adapter: ArrayAdapter + private var binding: FragmentSearchHistoryBinding? = null + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentSearchHistoryBinding.inflate(inflater, container, false) + + recentSearches = recentSearchesDao!!.recentSearches(10) + + if (recentSearches.isEmpty()) { + binding!!.recentSearchesDeleteButton.visibility = View.GONE + binding!!.recentSearchesTextView.setText(R.string.no_recent_searches) + } + + binding!!.recentSearchesDeleteButton.setOnClickListener { v: View? -> + showDeleteRecentAlertDialog(requireContext()) + } + + adapter = ArrayAdapter(requireContext(), R.layout.item_recent_searches, recentSearches) + binding!!.recentSearchesList.adapter = adapter + binding!!.recentSearchesList.onItemClickListener = + OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long -> + (context as SearchActivity).updateText(recentSearches[position]) + } + binding!!.recentSearchesList.onItemLongClickListener = + OnItemLongClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long -> + showDeleteAlertDialog(requireContext(), position) + true + } + updateRecentSearches() + + return binding!!.root + } + + private fun showDeleteRecentAlertDialog(context: Context) { + AlertDialog.Builder(context) + .setMessage(getString(R.string.delete_recent_searches_dialog)) + .setPositiveButton(android.R.string.yes) { dialog: DialogInterface, _: Int -> + setDeleteRecentPositiveButton(context, dialog) + } + .setNegativeButton(android.R.string.no, null) + .setCancelable(false) + .create() + .show() + } + + private fun setDeleteRecentPositiveButton(context: Context, dialog: DialogInterface) { + recentSearchesDao!!.deleteAll() + if (binding != null) { + binding!!.recentSearchesDeleteButton.visibility = View.GONE + binding!!.recentSearchesTextView.setText(R.string.no_recent_searches) + Toast.makeText( + getContext(), getString(R.string.search_history_deleted), + Toast.LENGTH_SHORT + ).show() + recentSearches = recentSearchesDao!!.recentSearches(10) + adapter = ArrayAdapter(context, R.layout.item_recent_searches, recentSearches) + binding!!.recentSearchesList.adapter = adapter + adapter.notifyDataSetChanged() + } + dialog.dismiss() + } + + private fun showDeleteAlertDialog(context: Context, position: Int) { + AlertDialog.Builder(context) + .setMessage(R.string.delete_search_dialog) + .setPositiveButton( + getString(R.string.delete).uppercase(), + { dialog: DialogInterface, _: Int -> + setDeletePositiveButton(context, dialog, position) + } + ) + .setNegativeButton(android.R.string.cancel, null) + .setCancelable(false) + .create() + .show() + } + + private fun setDeletePositiveButton(context: Context, dialog: DialogInterface, position: Int) { + recentSearchesDao!!.delete(recentSearchesDao!!.find(recentSearches[position])!!) + recentSearches = recentSearchesDao!!.recentSearches(10) + adapter = ArrayAdapter( + context, R.layout.item_recent_searches, + recentSearches + ) + if (binding != null) { + binding!!.recentSearchesList.adapter = adapter + adapter.notifyDataSetChanged() + } + dialog.dismiss() + } + + /** + * This method is called on back press of activity so we are updating the list from database to + * refresh the recent searches list. + */ + override fun onResume() { + updateRecentSearches() + super.onResume() + } + + /** + * This method is called when search query is null to update Recent Searches + */ + fun updateRecentSearches() { + recentSearches = recentSearchesDao!!.recentSearches(10) + adapter.notifyDataSetChanged() + + if (recentSearches.isNotEmpty()) { + if (binding != null) { + binding!!.recentSearchesDeleteButton.visibility = View.VISIBLE + binding!!.recentSearchesTextView.setText(R.string.search_recent_header) + } + } + } + + override fun onDestroy() { + super.onDestroy() + binding = null + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesTable.kt b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesTable.kt new file mode 100644 index 000000000..e32fc9fa4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesTable.kt @@ -0,0 +1,71 @@ +package fr.free.nrw.commons.explore.recentsearches + +import android.database.sqlite.SQLiteDatabase + +/** + * This class contains the database table architechture for recent searches, It also contains + * queries and logic necessary to the create, update, delete this table. + */ +object RecentSearchesTable { + const val TABLE_NAME: String = "recent_searches" + const val COLUMN_ID: String = "_id" + const val COLUMN_NAME: String = "name" + const val COLUMN_LAST_USED: String = "last_used" + + // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. + @JvmField + val ALL_FIELDS = arrayOf( + COLUMN_ID, + COLUMN_NAME, + COLUMN_LAST_USED, + ) + + const val DROP_TABLE_STATEMENT: String = "DROP TABLE IF EXISTS $TABLE_NAME" + + const val CREATE_TABLE_STATEMENT: String = ("CREATE TABLE $TABLE_NAME ($COLUMN_ID INTEGER PRIMARY KEY,$COLUMN_NAME STRING,$COLUMN_LAST_USED INTEGER);") + + /** + * This method creates a RecentSearchesTable in SQLiteDatabase + * + * @param db SQLiteDatabase + */ + fun onCreate(db: SQLiteDatabase) = db.execSQL(CREATE_TABLE_STATEMENT) + + /** + * This method deletes RecentSearchesTable from SQLiteDatabase + * + * @param db SQLiteDatabase + */ + fun onDelete(db: SQLiteDatabase) { + db.execSQL(DROP_TABLE_STATEMENT) + onCreate(db) + } + + /** + * This method is called on migrating from a older version to a newer version + * + * @param db SQLiteDatabase + * @param from Version from which we are migrating + * @param to Version to which we are migrating + */ + fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) { + if (from == to) { + return + } + if (from < 6) { + // doesn't exist yet + onUpdate(db, from + 1, to) + return + } + if (from == 6) { + // table added in version 7 + onCreate(db) + onUpdate(db, from + 1, to) + return + } + if (from == 7) { + onUpdate(db, from + 1, to) + return + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt index 48e61051c..d80be9ea2 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt @@ -46,7 +46,7 @@ class ProfileActivity : BaseActivity() { private var contributionsFragment: ContributionsFragment? = null fun setScroll(canScroll: Boolean) { - binding.viewPager.setCanScroll(canScroll) + binding.viewPager.canScroll = canScroll } override fun onRestoreInstanceState(savedInstanceState: Bundle) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt index 5d98ebffb..3cf9d3a65 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadProgressActivity.kt @@ -56,11 +56,7 @@ class UploadProgressActivity : BaseActivity() { override fun onPageSelected(position: Int) { updateMenuItems(position) - if (position == 2) { - binding.uploadProgressViewPager.setCanScroll(false) - } else { - binding.uploadProgressViewPager.setCanScroll(true) - } + binding.uploadProgressViewPager.canScroll = (position != 2) } override fun onPageScrollStateChanged(state: Int) { diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt index 69560279b..1fd99bcee 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/DatabaseUtils.kt @@ -10,6 +10,14 @@ fun Cursor.getStringArray(name: String): List = fun Cursor.getString(name: String): String = getString(getColumnIndex(name)) +@SuppressLint("Range") +fun Cursor.getInt(name: String): Int = + getInt(getColumnIndex(name)) + +@SuppressLint("Range") +fun Cursor.getLong(name: String): Long = + getLong(getColumnIndex(name)) + /** * Converts string to List * @param listString comma separated single string from of list items diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/ExploreFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/ExploreFragmentUnitTest.kt index e2ef3bb92..85298d0a3 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/ExploreFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/ExploreFragmentUnitTest.kt @@ -88,14 +88,14 @@ class ExploreFragmentUnitTest { @Throws(Exception::class) fun testSetScrollCaseTrue() { fragment.setScroll(true) - Assert.assertEquals(viewPager.isCanScroll, true) + Assert.assertEquals(viewPager.canScroll, true) } @Test @Throws(Exception::class) fun testSetScrollCaseFalse() { fragment.setScroll(false) - Assert.assertEquals(viewPager.isCanScroll, false) + Assert.assertEquals(viewPager.canScroll, false) } @Test diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt index c772f796e..5e128d4ee 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt @@ -20,15 +20,15 @@ import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.explore.models.RecentSearch import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.BASE_URI import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_LAST_USED -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_NAME -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.CREATE_TABLE_STATEMENT -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.DROP_TABLE_STATEMENT -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onCreate -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onDelete -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onUpdate +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_LAST_USED +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_NAME +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.CREATE_TABLE_STATEMENT +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.DROP_TABLE_STATEMENT +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onCreate +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onDelete +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onUpdate import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragmentUnitTest.kt index 8173dfc44..cd1186fc8 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragmentUnitTest.kt @@ -123,18 +123,4 @@ class RecentSearchesFragmentUnitTest { method.isAccessible = true method.invoke(fragment, context, 0) } - - @Test - @Throws(Exception::class) - fun testSetDeletePositiveButton() { - val method: Method = - RecentSearchesFragment::class.java.getDeclaredMethod( - "setDeletePositiveButton", - Context::class.java, - DialogInterface::class.java, - Int::class.java, - ) - method.isAccessible = true - method.invoke(fragment, context, dialog, 0) - } } From 6dcce45c592f7a4d6d862d49a67239d21058f4ad Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 4 Aug 2025 14:02:18 +0200 Subject: [PATCH 3/3] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ar/strings.xml | 3 ++- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-ps/strings.xml | 10 +++++++++ app/src/main/res/values-pt/strings.xml | 28 +++++++++++++++++++++++++- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index c9c0323c8..2d704c0e3 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -268,6 +268,7 @@ الوصف نقاش المؤلف + الرافع تاريخ الرفع الترخيص الإحداثيات @@ -822,7 +823,7 @@ الإذن مطلوب لهذه الوظيفة تعلم كيفية كتابة وصف مفيد تعلم كيفية كتابة تعليق مفيد - شاهد إنجازاتك + عرض إنجازاتك تعديل الصورة تعديل الموقع تم تحديث الموقع! diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 0282d99ed..0c5befc26 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -241,7 +241,7 @@ Niet opgegeven Word bètatester Meld u aan voor ons bètakanaal op Google Play en krijg vroegtijdig toegang tot nieuwe functies en bugfixes - 2FA-code + Tweetrapsauthenticatie-code E-mailverificatiecode Wilt u zich echt afmelden? Media-afbeelding is mislukt diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 64a7762a7..4bfaea5ab 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -274,6 +274,15 @@ موبايل له لارې راپورته‌شوی نخشه انځور په ویکي‌اومتوک کې %1$s ته ورگډ شو! + پېژنيال + مټ‌تړوني + شمارنې + مننه ترلاسه‌شو + ټاکلی انځور + انځورنه د \"څېرمه ځايونو\" له لارې + انځورونه راپورته‌شول + انځورونه په څټ‌گرځول‌شوي نه دي + کارول‌شوي انځورونه پای ته رسېږي په: ټاکنيزې‌سيالۍ ښکاره‌کول روانې ټاکنيزې‌سيالۍ وگورئ @@ -301,5 +310,6 @@ ايا دا د منلو وړ دي؟ ايا تاسو غواړئ له ونډه‌وال نه مننه وکړئ؟ که دا انځور ټولگټی نه وي؛ نو ړنگېدو ته د نوماندولو لپاره يې په نه کليک وکړئ. + ځای اومتوکي له ويکي کارنانو سره مرسته کوي چې ستاسو انځور موندلو او لا ډېر گټور کولو کې مرسته کوي.\nستاسو وروستۍ راپورته‌کېدنې ځای نه لري.\nموږ تاسو ته سپارښته کوو چې خپل د ځای ښودنه د کامرې په کاريال په اوڼنو کې بل کړئ.\nله راپورته کولو مو مننه! گڼون diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 721528c7b..659c2705a 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -228,6 +228,7 @@ Descrição Discussão Autor + Carregador Data de carregamento Licença Coordenadas @@ -235,6 +236,7 @@ Torne-se um Testador Beta Opte pelo nosso canal beta no Google Play e obtenha acesso antecipado às novas funcionalidades e às correções de erros Código de autenticação de dois fatores + Código de verificação do e-mail Deseja realmente sair? Falha na imagem multimédia Não foi encontrada nenhuma subcategoria. @@ -255,6 +257,7 @@ Sobre Configurações Comentários + Comentários via GitHub Sair Explicação Notificações @@ -294,8 +297,10 @@ Copiar o texto wiki para a área de transferência O texto wiki foi copiado para a área de transferência A identificação de locais próximos pode não funcionar devidamente; o serviço de localização não está disponível. + Internet indisponível. A mostrar apenas os locais em cache. Acesso à localização negado. Para usar esta funcionalidade defina a sua localização manualmente, por favor. É necessária permissão para mostrar uma lista dos locais próximos + É necessária a permissão para exibir uma lista de imagens próximas Indicações Wikidata Wikipédia @@ -364,6 +369,7 @@ Partilhar aplicação Rodar Não foi possível carregar locais próximos + Não há fotografias nesta área Não existem locais próximos Erro ao procurar monumentos próximos. Não há pesquisas recentes @@ -764,11 +770,12 @@ São necessárias permissões para a funcionalidade Aprenda a escrever uma descrição útil Aprenda a escrever uma legenda útil - Ver as suas realizações + Ver as suas realizações Editar imagem Editar localização Localização actualizada! Remover Localização + Remover Aviso de Localização Localização removida! Agradecer ao autor Erro no envio de agradecimento ao autor. @@ -788,5 +795,24 @@ Discussão Outro problema ou informação (por favor, explique em baixo). O seu comentário é publicado na seguinte página da wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> + Envios + Pendente + Falhou + Não foi possível carregar os dados do local + Eliminar Pasta + Confirmar Eliminação + Eliminar + Cancelar + Commons + Outras wikis + Utilização de ficheiro + Conta + Legenda + Legenda copiada para a área de transferência + Parabéns, todas as fotografias neste álbum foram envidas ou marcadas como \'não para enviar\'. + Mostrar no Explorador + Mostrar nas Proximidades + Criada e enviada por: %1$s + Criada por %1$s e enviada por %2$s Nomeada para Eliminação