Merge branch 'main' into chore/upgrade-target-sdk

This commit is contained in:
Rohit Verma 2025-08-08 01:37:50 +05:30
commit 2d8a0034a7
No known key found for this signature in database
GPG key ID: D64E53E770687D0E
47 changed files with 3321 additions and 3546 deletions

View file

@ -1,5 +1,16 @@
# Wikimedia Commons for Android # 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 ## v5.5.0
### What's changed ### What's changed

View file

@ -33,7 +33,7 @@ class BookmarkFragment : CommonsDaggerSupportFragment() {
fun setScroll(canScroll: Boolean) { fun setScroll(canScroll: Boolean) {
binding?.let { binding?.let {
it.viewPagerBookmarks.isCanScroll = canScroll it.viewPagerBookmarks.canScroll = canScroll
} }
} }

View file

@ -7,7 +7,7 @@ import android.database.sqlite.SQLiteOpenHelper
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable
import fr.free.nrw.commons.category.CategoryDao 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 import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
@ -31,7 +31,7 @@ class DBOpenHelper(
CategoryDao.Table.onCreate(db) CategoryDao.Table.onCreate(db)
BookmarksTable.onCreate(db) BookmarksTable.onCreate(db)
BookmarkItemsTable.onCreate(db) BookmarkItemsTable.onCreate(db)
RecentSearchesDao.Table.onCreate(db) RecentSearchesTable.onCreate(db)
RecentLanguagesDao.Table.onCreate(db) RecentLanguagesDao.Table.onCreate(db)
} }
@ -39,7 +39,7 @@ class DBOpenHelper(
CategoryDao.Table.onUpdate(db, from, to) CategoryDao.Table.onUpdate(db, from, to)
BookmarksTable.onUpdate(db, from, to) BookmarksTable.onUpdate(db, from, to)
BookmarkItemsTable.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) RecentLanguagesDao.Table.onUpdate(db, from, to)
deleteTable(db, CONTRIBUTIONS_TABLE) deleteTable(db, CONTRIBUTIONS_TABLE)
deleteTable(db, BOOKMARKS_LOCATIONS) deleteTable(db, BOOKMARKS_LOCATIONS)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,6 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
abstract class PageableDepictionsFragment : BasePagingFragment<DepictedItem>() { abstract class PageableDepictionsFragment : BasePagingFragment<DepictedItem>() {
override val errorTextId: Int = R.string.error_loading_depictions override val errorTextId: Int = R.string.error_loading_depictions
override val pagedListAdapter by lazy { override val pagedListAdapter by lazy {
DepictionAdapter { WikidataItemDetailsActivity.startYourself(context, it) } DepictionAdapter { WikidataItemDetailsActivity.startYourself(requireContext(), it) }
} }
} }

View file

@ -1,304 +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.utils.EdgeToEdgeUtilsKt;
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());
final View view = binding.getRoot();
EdgeToEdgeUtilsKt.applyEdgeToEdgeAllInsets(view);
setContentView(view);
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);
}
}

View file

@ -0,0 +1,297 @@
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.applyEdgeToEdgeAllInsets
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)
applyEdgeToEdgeAllInsets(binding!!.root)
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<List<DepictedItem?>> { depictedItems: List<DepictedItem?> ->
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)
}
}
}

View file

@ -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<Media> callCommonsQuery(final LatLng currentLatLng) {
String coordinates = currentLatLng.getLatitude() + "|" + currentLatLng.getLongitude();
return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet();
}
}

View file

@ -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<Media> {
val coordinates = currentLatLng.latitude.toString() + "|" + currentLatLng.longitude
return mediaClient!!.getMediaListFromGeoSearch(coordinates).blockingGet()
}
}

View file

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

View file

@ -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<BaseMarker?>?)
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
}
}

View file

@ -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<Media> 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<Media, Double> 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<BaseMarker> loadAttractionsFromLocationToBaseMarkerOptions(
LatLng currentLatLng,
final List<Place> placeList,
Context context,
NearbyBaseMarkerThumbCallback callback,
ExplorePlacesInfo explorePlacesInfo) {
List<BaseMarker> 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<Bitmap>() {
// We add icons to markers when bitmaps are ready
@Override
public void onResourceReady(@NonNull Bitmap resource,
@Nullable Transition<? super Bitmap> 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<BaseMarker> baseMarkers,
ExplorePlacesInfo explorePlacesInfo);
}
}

View file

@ -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<Media, Double> = 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<BaseMarker>?,
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<Place>?,
context: Context,
callback: NearbyBaseMarkerThumbCallback,
explorePlacesInfo: ExplorePlacesInfo?
): List<BaseMarker> {
val baseMarkerList: MutableList<BaseMarker> = 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<Bitmap>() {
// We add icons to markers when bitmaps are ready
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
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
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<ExplorePlacesInfo> 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<Place>) explorePlacesInfo.explorePlaceList,
exploreMapFragmentView.getContext(),
this,
explorePlacesInfo);
}
@Override
public void onNearbyBaseMarkerThumbsReady(List<BaseMarker> 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);
}
}

View file

@ -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<ExplorePlacesInfo?> = 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<BaseMarker>?,
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<Class<*>>(ExploreMapContract.View::class.java)
) { _: Any?, method: Method, _: Array<Any?>? ->
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
}
}

View file

@ -8,9 +8,9 @@ import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.di.CommonsDaggerContentProvider 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.RecentSearchesTable.ALL_FIELDS
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.TABLE_NAME import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.TABLE_NAME
/** /**
* This class contains functions for executing queries for * This class contains functions for executing queries for

View file

@ -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<ContentProviderClient> clientProvider;
@Inject
public RecentSearchesDao(@Named("recentsearch") Provider<ContentProviderClient> 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<String> recentSearches(int limit) {
List<String> 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;
}
}
}
}

View file

@ -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<ContentProviderClient>
) {
/**
* 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<String> {
val items: MutableList<String> = 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
)
}

View file

@ -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<String> 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;
}
}
}

View file

@ -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<String> = emptyList()
private lateinit var adapter: ArrayAdapter<String>
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
}
}

View file

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

View file

@ -7,7 +7,8 @@ class NearbyResultItem(
private val wikipediaArticle: ResultTuple?, private val wikipediaArticle: ResultTuple?,
private val commonsArticle: ResultTuple?, private val commonsArticle: ResultTuple?,
private val location: ResultTuple?, private val location: ResultTuple?,
private val label: ResultTuple?, @field:SerializedName("label") private val label: ResultTuple?,
@field:SerializedName("itemLabel") private val itemLabel: ResultTuple?,
@field:SerializedName("streetAddress") private val address: ResultTuple?, @field:SerializedName("streetAddress") private val address: ResultTuple?,
private val icon: ResultTuple?, private val icon: ResultTuple?,
@field:SerializedName("class") private val className: ResultTuple?, @field:SerializedName("class") private val className: ResultTuple?,
@ -29,7 +30,15 @@ class NearbyResultItem(
fun getLocation(): ResultTuple = location ?: ResultTuple() fun getLocation(): ResultTuple = location ?: ResultTuple()
fun getLabel(): ResultTuple = label ?: ResultTuple() /**
* Returns label for display (pins, popup), using fallback to itemLabel if needed.
*/
fun getLabel(): ResultTuple = label ?: itemLabel ?: ResultTuple()
/**
* Returns only the original label field, for Wikidata edits.
*/
fun getOriginalLabel(): ResultTuple = label ?: ResultTuple()
fun getIcon(): ResultTuple = icon ?: ResultTuple() fun getIcon(): ResultTuple = icon ?: ResultTuple()

View file

@ -47,7 +47,7 @@ class ProfileActivity : BaseActivity() {
private var contributionsFragment: ContributionsFragment? = null private var contributionsFragment: ContributionsFragment? = null
fun setScroll(canScroll: Boolean) { fun setScroll(canScroll: Boolean) {
binding.viewPager.setCanScroll(canScroll) binding.viewPager.canScroll = canScroll
} }
override fun onRestoreInstanceState(savedInstanceState: Bundle) { override fun onRestoreInstanceState(savedInstanceState: Bundle) {

View file

@ -58,11 +58,7 @@ class UploadProgressActivity : BaseActivity() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
updateMenuItems(position) updateMenuItems(position)
if (position == 2) { binding.uploadProgressViewPager.canScroll = (position != 2)
binding.uploadProgressViewPager.setCanScroll(false)
} else {
binding.uploadProgressViewPager.setCanScroll(true)
}
} }
override fun onPageScrollStateChanged(state: Int) { override fun onPageScrollStateChanged(state: Int) {

View file

@ -821,6 +821,7 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra
{ {
showProgress(false) showProgress(false)
uploadItem.imageQuality = IMAGE_OK uploadItem.imageQuality = IMAGE_OK
uploadItem.hasInvalidLocation = false // Reset invalid location flag when user confirms upload
}, },
{ {
presenterCallback!!.deletePictureAtIndex(index) presenterCallback!!.deletePictureAtIndex(index)

View file

@ -10,6 +10,14 @@ fun Cursor.getStringArray(name: String): List<String> =
fun Cursor.getString(name: String): String = fun Cursor.getString(name: String): String =
getString(getColumnIndex(name)) 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 * Converts string to List
* @param listString comma separated single string from of list items * @param listString comma separated single string from of list items

View file

@ -268,6 +268,7 @@
<string name="media_detail_description">الوصف</string> <string name="media_detail_description">الوصف</string>
<string name="media_detail_discussion">نقاش</string> <string name="media_detail_discussion">نقاش</string>
<string name="media_detail_author">المؤلف</string> <string name="media_detail_author">المؤلف</string>
<string name="media_detail_uploader">الرافع</string>
<string name="media_detail_uploaded_date">تاريخ الرفع</string> <string name="media_detail_uploaded_date">تاريخ الرفع</string>
<string name="media_detail_license">الترخيص</string> <string name="media_detail_license">الترخيص</string>
<string name="media_detail_coordinates">الإحداثيات</string> <string name="media_detail_coordinates">الإحداثيات</string>
@ -822,7 +823,7 @@
<string name="permissions_are_required_for_functionality">الإذن مطلوب لهذه الوظيفة</string> <string name="permissions_are_required_for_functionality">الإذن مطلوب لهذه الوظيفة</string>
<string name="learn_how_to_write_a_useful_description">تعلم كيفية كتابة وصف مفيد</string> <string name="learn_how_to_write_a_useful_description">تعلم كيفية كتابة وصف مفيد</string>
<string name="learn_how_to_write_a_useful_caption">تعلم كيفية كتابة تعليق مفيد</string> <string name="learn_how_to_write_a_useful_caption">تعلم كيفية كتابة تعليق مفيد</string>
<string name="see_your_achievements" fuzzy="true">شاهد إنجازاتك</string> <string name="see_your_achievements">عرض إنجازاتك</string>
<string name="edit_image">تعديل الصورة</string> <string name="edit_image">تعديل الصورة</string>
<string name="edit_location">تعديل الموقع</string> <string name="edit_location">تعديل الموقع</string>
<string name="location_updated">تم تحديث الموقع!</string> <string name="location_updated">تم تحديث الموقع!</string>

View file

@ -2,6 +2,7 @@
<!-- Authors: <!-- Authors:
* Anoop rao * Anoop rao
* Anzx * Anzx
* Gopala Krishna A
* Mahadevaiah Siddaiah * Mahadevaiah Siddaiah
* Omshivaprakash * Omshivaprakash
* VASANTH S.N. * VASANTH S.N.
@ -10,8 +11,23 @@
--> -->
<resources> <resources>
<string name="commons_facebook">ಕಾಮನ್ಸ್ ಫೇಸ್ಬುಕ್ ಪುಟ</string> <string name="commons_facebook">ಕಾಮನ್ಸ್ ಫೇಸ್ಬುಕ್ ಪುಟ</string>
<string name="commons_github">ಕಾಮನ್ಸ್‌ನ ಗಿಟ್‍ಹಬ್ ಮೂಲ ಕೋಡ್</string>
<string name="commons_logo">ಕಾಮನ್ಸ್‌ ಲಾಂಛನ</string>
<string name="commons_website">ಕಾಮನ್ಸ್ ಜಾಲತಾಣ</string> <string name="commons_website">ಕಾಮನ್ಸ್ ಜಾಲತಾಣ</string>
<string name="exit_location_picker">ಸ್ಥಳ ಆಯ್ಕೆಯಿಂದ ನಿರ್ಗಮಿಸಿ</string>
<string name="submit">ಸಲ್ಲಿಸಿ</string> <string name="submit">ಸಲ್ಲಿಸಿ</string>
<string name="add_another_description">ಇನ್ನೊಂದು ವಿವರಣೆಯನ್ನು ಸೇರಿಸಿ</string>
<string name="add_new_contribution">ಹೊಸ ಕೊಡುಗೆಗಳನ್ನು ಸೇರಿಸಿ</string>
<string name="add_contribution_from_camera">ಕ್ಯಾಮೆರಾದಿಂದ ಕೊಡುಗೆಯನ್ನು ಸೇರಿಸಿ</string>
<string name="add_contribution_from_photos">ಫೋಟೋಗಳಿಂದ ಕೊಡುಗೆಯನ್ನು ಸೇರಿಸಿ</string>
<string name="add_contribution_from_contributions_gallery">ಹಿಂದಿನ ಕೊಡುಗೆಗಳ ಗ್ಯಾಲರಿಯಿಂದ ಕೊಡುಗೆಯನ್ನು ಸೇರಿಸಿ</string>
<string name="show_captions">ತಲೆಬರಹ</string>
<string name="row_item_language_description">ಭಾಷಾ ವಿವರಣೆ</string>
<string name="row_item_caption">ತಲೆಬರಹ</string>
<string name="show_captions_description">ವಿವರಣೆ</string>
<string name="nearby_row_image">ಚಿತ್ರ</string>
<string name="nearby_all">ಎಲ್ಲಾ</string>
<string name="nearby_filter_toggle">ಮೇಲಕ್ಕೆ ಟಾಗಲ್ ಮಾಡಿ</string>
<string name="appwidget_img">ದಿನದ ಚಿತ್ರ</string> <string name="appwidget_img">ದಿನದ ಚಿತ್ರ</string>
<plurals name="uploads_pending_notification_indicator"> <plurals name="uploads_pending_notification_indicator">
<item quantity="one">%1$d ಕಡತ ಅಪ್ಲೋಡ್ ಅಗುತ್ತಿದೆ</item> <item quantity="one">%1$d ಕಡತ ಅಪ್ಲೋಡ್ ಅಗುತ್ತಿದೆ</item>

View file

@ -241,7 +241,7 @@
<string name="media_detail_coordinates_empty">Niet opgegeven</string> <string name="media_detail_coordinates_empty">Niet opgegeven</string>
<string name="become_a_tester_title">Word bètatester</string> <string name="become_a_tester_title">Word bètatester</string>
<string name="become_a_tester_description">Meld u aan voor ons bètakanaal op Google Play en krijg vroegtijdig toegang tot nieuwe functies en bugfixes</string> <string name="become_a_tester_description">Meld u aan voor ons bètakanaal op Google Play en krijg vroegtijdig toegang tot nieuwe functies en bugfixes</string>
<string name="_2fa_code">2FA-code</string> <string name="_2fa_code">Tweetrapsauthenticatie-code</string>
<string name="email_auth_code">E-mailverificatiecode</string> <string name="email_auth_code">E-mailverificatiecode</string>
<string name="logout_verification">Wilt u zich echt afmelden?</string> <string name="logout_verification">Wilt u zich echt afmelden?</string>
<string name="mediaimage_failed">Media-afbeelding is mislukt</string> <string name="mediaimage_failed">Media-afbeelding is mislukt</string>

View file

@ -274,6 +274,15 @@
<string name="explore_tab_title_mobile">موبايل له لارې راپورته‌شوی</string> <string name="explore_tab_title_mobile">موبايل له لارې راپورته‌شوی</string>
<string name="explore_tab_title_map">نخشه</string> <string name="explore_tab_title_map">نخشه</string>
<string name="successful_wikidata_edit">انځور په ویکي‌اومتوک کې %1$s ته ورگډ شو!</string> <string name="successful_wikidata_edit">انځور په ویکي‌اومتوک کې %1$s ته ورگډ شو!</string>
<string name="Profile">پېژنيال</string>
<string name="badges">مټ‌تړوني</string>
<string name="statistics">شمارنې</string>
<string name="statistics_thanks">مننه ترلاسه‌شو</string>
<string name="statistics_featured">ټاکلی انځور</string>
<string name="statistics_wikidata_edits">انځورنه د \"څېرمه ځايونو\" له لارې</string>
<string name="images_uploaded">انځورونه راپورته‌شول</string>
<string name="image_reverts">انځورونه په څټ‌گرځول‌شوي نه دي</string>
<string name="images_used_by_wiki">کارول‌شوي انځورونه</string>
<string name="ends_on">پای ته رسېږي په:</string> <string name="ends_on">پای ته رسېږي په:</string>
<string name="display_campaigns">ټاکنيزې‌سيالۍ ښکاره‌کول</string> <string name="display_campaigns">ټاکنيزې‌سيالۍ ښکاره‌کول</string>
<string name="display_campaigns_explanation">روانې ټاکنيزې‌سيالۍ وگورئ</string> <string name="display_campaigns_explanation">روانې ټاکنيزې‌سيالۍ وگورئ</string>
@ -301,5 +310,6 @@
<string name="review_spam">ايا دا د منلو وړ دي؟</string> <string name="review_spam">ايا دا د منلو وړ دي؟</string>
<string name="review_thanks">ايا تاسو غواړئ له ونډه‌وال نه مننه وکړئ؟</string> <string name="review_thanks">ايا تاسو غواړئ له ونډه‌وال نه مننه وکړئ؟</string>
<string name="review_spam_explanation">که دا انځور ټولگټی نه وي؛ نو ړنگېدو ته د نوماندولو لپاره يې په نه کليک وکړئ.</string> <string name="review_spam_explanation">که دا انځور ټولگټی نه وي؛ نو ړنگېدو ته د نوماندولو لپاره يې په نه کليک وکړئ.</string>
<string name="location_message">ځای اومتوکي له ويکي کارنانو سره مرسته کوي چې ستاسو انځور موندلو او لا ډېر گټور کولو کې مرسته کوي.\nستاسو وروستۍ راپورته‌کېدنې ځای نه لري.\nموږ تاسو ته سپارښته کوو چې خپل د ځای ښودنه د کامرې په کاريال په اوڼنو کې بل کړئ.\nله راپورته کولو مو مننه!</string>
<string name="account">گڼون</string> <string name="account">گڼون</string>
</resources> </resources>

View file

@ -228,6 +228,7 @@
<string name="media_detail_description">Descrição</string> <string name="media_detail_description">Descrição</string>
<string name="media_detail_discussion">Discussão</string> <string name="media_detail_discussion">Discussão</string>
<string name="media_detail_author">Autor</string> <string name="media_detail_author">Autor</string>
<string name="media_detail_uploader">Carregador</string>
<string name="media_detail_uploaded_date">Data de carregamento</string> <string name="media_detail_uploaded_date">Data de carregamento</string>
<string name="media_detail_license">Licença</string> <string name="media_detail_license">Licença</string>
<string name="media_detail_coordinates">Coordenadas</string> <string name="media_detail_coordinates">Coordenadas</string>
@ -235,6 +236,7 @@
<string name="become_a_tester_title">Torne-se um Testador Beta</string> <string name="become_a_tester_title">Torne-se um Testador Beta</string>
<string name="become_a_tester_description">Opte pelo nosso canal beta no Google Play e obtenha acesso antecipado às novas funcionalidades e às correções de erros</string> <string name="become_a_tester_description">Opte pelo nosso canal beta no Google Play e obtenha acesso antecipado às novas funcionalidades e às correções de erros</string>
<string name="_2fa_code">Código de autenticação de dois fatores</string> <string name="_2fa_code">Código de autenticação de dois fatores</string>
<string name="email_auth_code">Código de verificação do e-mail</string>
<string name="logout_verification">Deseja realmente sair?</string> <string name="logout_verification">Deseja realmente sair?</string>
<string name="mediaimage_failed">Falha na imagem multimédia</string> <string name="mediaimage_failed">Falha na imagem multimédia</string>
<string name="no_subcategory_found">Não foi encontrada nenhuma subcategoria.</string> <string name="no_subcategory_found">Não foi encontrada nenhuma subcategoria.</string>
@ -255,6 +257,7 @@
<string name="navigation_item_about">Sobre</string> <string name="navigation_item_about">Sobre</string>
<string name="navigation_item_settings">Configurações</string> <string name="navigation_item_settings">Configurações</string>
<string name="navigation_item_feedback">Comentários</string> <string name="navigation_item_feedback">Comentários</string>
<string name="navigation_item_feedback_github">Comentários via GitHub</string>
<string name="navigation_item_logout">Sair</string> <string name="navigation_item_logout">Sair</string>
<string name="navigation_item_info">Explicação</string> <string name="navigation_item_info">Explicação</string>
<string name="navigation_item_notification">Notificações</string> <string name="navigation_item_notification">Notificações</string>
@ -294,8 +297,10 @@
<string name="copy_wikicode">Copiar o texto wiki para a área de transferência</string> <string name="copy_wikicode">Copiar o texto wiki para a área de transferência</string>
<string name="wikicode_copied">O texto wiki foi copiado para a área de transferência</string> <string name="wikicode_copied">O texto wiki foi copiado para a área de transferência</string>
<string name="nearby_location_not_available">A identificação de locais próximos pode não funcionar devidamente; o serviço de localização não está disponível.</string> <string name="nearby_location_not_available">A identificação de locais próximos pode não funcionar devidamente; o serviço de localização não está disponível.</string>
<string name="nearby_showing_pins_offline">Internet indisponível. A mostrar apenas os locais em cache.</string>
<string name="upload_location_access_denied">Acesso à localização negado. Para usar esta funcionalidade defina a sua localização manualmente, por favor.</string> <string name="upload_location_access_denied">Acesso à localização negado. Para usar esta funcionalidade defina a sua localização manualmente, por favor.</string>
<string name="location_permission_rationale_nearby">É necessária permissão para mostrar uma lista dos locais próximos</string> <string name="location_permission_rationale_nearby">É necessária permissão para mostrar uma lista dos locais próximos</string>
<string name="location_permission_rationale_explore">É necessária a permissão para exibir uma lista de imagens próximas</string>
<string name="nearby_directions">Indicações</string> <string name="nearby_directions">Indicações</string>
<string name="nearby_wikidata">Wikidata</string> <string name="nearby_wikidata">Wikidata</string>
<string name="nearby_wikipedia">Wikipédia</string> <string name="nearby_wikipedia">Wikipédia</string>
@ -364,6 +369,7 @@
<string name="share_app_title">Partilhar aplicação</string> <string name="share_app_title">Partilhar aplicação</string>
<string name="rotate">Rodar</string> <string name="rotate">Rodar</string>
<string name="error_fetching_nearby_places">Não foi possível carregar locais próximos</string> <string name="error_fetching_nearby_places">Não foi possível carregar locais próximos</string>
<string name="no_pictures_in_this_area">Não há fotografias nesta área</string>
<string name="no_nearby_places_around">Não existem locais próximos</string> <string name="no_nearby_places_around">Não existem locais próximos</string>
<string name="error_fetching_nearby_monuments">Erro ao procurar monumentos próximos.</string> <string name="error_fetching_nearby_monuments">Erro ao procurar monumentos próximos.</string>
<string name="no_recent_searches">Não há pesquisas recentes</string> <string name="no_recent_searches">Não há pesquisas recentes</string>
@ -764,11 +770,12 @@
<string name="permissions_are_required_for_functionality">São necessárias permissões para a funcionalidade</string> <string name="permissions_are_required_for_functionality">São necessárias permissões para a funcionalidade</string>
<string name="learn_how_to_write_a_useful_description">Aprenda a escrever uma descrição útil</string> <string name="learn_how_to_write_a_useful_description">Aprenda a escrever uma descrição útil</string>
<string name="learn_how_to_write_a_useful_caption">Aprenda a escrever uma legenda útil</string> <string name="learn_how_to_write_a_useful_caption">Aprenda a escrever uma legenda útil</string>
<string name="see_your_achievements" fuzzy="true">Ver as suas realizações</string> <string name="see_your_achievements">Ver as suas realizações</string>
<string name="edit_image">Editar imagem</string> <string name="edit_image">Editar imagem</string>
<string name="edit_location">Editar localização</string> <string name="edit_location">Editar localização</string>
<string name="location_updated">Localização actualizada!</string> <string name="location_updated">Localização actualizada!</string>
<string name="remove_location">Remover Localização</string> <string name="remove_location">Remover Localização</string>
<string name="remove_location_warning_title">Remover Aviso de Localização</string>
<string name="location_removed">Localização removida!</string> <string name="location_removed">Localização removida!</string>
<string name="send_thanks_to_author">Agradecer ao autor</string> <string name="send_thanks_to_author">Agradecer ao autor</string>
<string name="error_sending_thanks">Erro no envio de agradecimento ao autor.</string> <string name="error_sending_thanks">Erro no envio de agradecimento ao autor.</string>
@ -788,5 +795,24 @@
<string name="talk">Discussão</string> <string name="talk">Discussão</string>
<string name="other_problem_or_information_please_explain_below">Outro problema ou informação (por favor, explique em baixo).</string> <string name="other_problem_or_information_please_explain_below">Outro problema ou informação (por favor, explique em baixo).</string>
<string name="feedback_destination_note">O seu comentário é publicado na seguinte página da wiki: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Mobile app/Feedback&lt;/a&gt;</string> <string name="feedback_destination_note">O seu comentário é publicado na seguinte página da wiki: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Mobile app/Feedback&lt;/a&gt;</string>
<string name="uploads">Envios</string>
<string name="pending">Pendente</string>
<string name="failed">Falhou</string>
<string name="could_not_load_place_data">Não foi possível carregar os dados do local</string>
<string name="custom_selector_delete_folder">Eliminar Pasta</string>
<string name="custom_selector_confirm_deletion_title">Confirmar Eliminação</string>
<string name="custom_selector_delete">Eliminar</string>
<string name="custom_selector_cancel">Cancelar</string>
<string name="usages_on_commons_heading">Commons</string>
<string name="usages_on_other_wikis_heading">Outras wikis</string>
<string name="file_usages_container_heading">Utilização de ficheiro</string>
<string name="account">Conta</string>
<string name="caption">Legenda</string>
<string name="caption_copied_to_clipboard">Legenda copiada para a área de transferência</string>
<string name="congratulations_all_pictures_in_this_album_have_been_either_uploaded_or_marked_as_not_for_upload">Parabéns, todas as fotografias neste álbum foram envidas ou marcadas como \'não para enviar\'.</string>
<string name="show_in_explore">Mostrar no Explorador</string>
<string name="show_in_nearby">Mostrar nas Proximidades</string>
<string name="image_tag_line_created_and_uploaded_by">Criada e enviada por: %1$s</string>
<string name="image_tag_line_created_by_and_uploaded_by">Criada por %1$s e enviada por %2$s</string>
<string name="nominated_for_deletion_btn">Nomeada para Eliminação</string> <string name="nominated_for_deletion_btn">Nomeada para Eliminação</string>
</resources> </resources>

View file

@ -6,6 +6,7 @@
* Annick green * Annick green
* Cabal * Cabal
* Googology * Googology
* H78c67c
* LeGuyanaisPure * LeGuyanaisPure
* Liuxinyu970226 * Liuxinyu970226
* Madhurgupta10 * Madhurgupta10
@ -28,7 +29,7 @@
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="all"> <resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="all">
<string name="submit">{{Identical|Submit}}</string> <string name="submit">{{Identical|Submit}}</string>
<string name="nearby_all">{{identical|All}}</string> <string name="nearby_all">{{identical|All}}</string>
<string name="nearby_filter_search">Reba ishakiro</string> <string name="nearby_filter_search"/>
<string name="uploads_pending_notification_indicator">Status text about number of uploads left.\n* %1$d represents number of uploads left, including current one</string> <string name="uploads_pending_notification_indicator">Status text about number of uploads left.\n* %1$d represents number of uploads left, including current one</string>
<string name="contributions_subtitle">See the current issue [https://phabricator.wikimedia.org/T267142 T267142] tracked in Phabricator about the &lt;code&gt;&lt;nowiki&gt;|zero=&lt;/nowiki&gt;&lt;/code&gt; option currently not supported on Translatewiki.net with the custom &lt;code&gt;&lt;nowiki&gt;{{PLURAL}}&lt;/nowiki&gt;&lt;/code&gt; rules used by this project for Android, using a non-MediaWiki syntax.</string> <string name="contributions_subtitle">See the current issue [https://phabricator.wikimedia.org/T267142 T267142] tracked in Phabricator about the &lt;code&gt;&lt;nowiki&gt;|zero=&lt;/nowiki&gt;&lt;/code&gt; option currently not supported on Translatewiki.net with the custom &lt;code&gt;&lt;nowiki&gt;{{PLURAL}}&lt;/nowiki&gt;&lt;/code&gt; rules used by this project for Android, using a non-MediaWiki syntax.</string>
<string name="multiple_uploads_title">{{Identical|Upload}}</string> <string name="multiple_uploads_title">{{Identical|Upload}}</string>

View file

@ -1,50 +1,32 @@
SELECT SELECT
?item ?item
(SAMPLE(?label) AS ?label) ?itemLabel
(SAMPLE(?class) AS ?class) ?itemDescription
(SAMPLE(?description) AS ?description) ?class
(SAMPLE(?classLabel) AS ?classLabel) ?classLabel
(SAMPLE(?pic) AS ?pic) ?pic
(SAMPLE(?destroyed) AS ?destroyed) ?destroyed
(SAMPLE(?endTime) AS ?endTime) ?endTime
(SAMPLE(?wikipediaArticle) AS ?wikipediaArticle) ?wikipediaArticle
(SAMPLE(?commonsArticle) AS ?commonsArticle) ?commonsArticle
(SAMPLE(?commonsCategory) AS ?commonsCategory) ?commonsCategory
(SAMPLE(?dateOfOfficialClosure) AS ?dateOfOfficialClosure) ?dateOfOfficialClosure
(SAMPLE(?pointInTime) AS ?pointInTime) ?pointInTime
WHERE { WHERE {
SERVICE <https://query.wikidata.org/sparql> { SERVICE <https://query.wikidata.org/sparql> {
values ?item { VALUES ?item {${ENTITY}}
${ENTITY}
}
} }
# Get the label in the preferred language of the user, or any other language if no label is available in that language. # Get item label/class label/description in the preferred language of the user, or fallback.
OPTIONAL {?item rdfs:label ?itemLabelPreferredLanguage. FILTER (lang(?itemLabelPreferredLanguage) = "${LANG}")} SERVICE wikibase:label { bd:serviceParam wikibase:language "${LANG},en,aa,ab,ae,af,ak,am,an,ar,as,av,ay,az,ba,be,bg,bh,bi,bm,bn,bo,br,bs,ca,ce,ch,co,cr,cs,cu,cv,cy,da,de,dv,dz,ee,el,eo,es,et,eu,fa,ff,fi,fj,fo,fr,fy,ga,gd,gl,gn,gu,gv,ha,he,hi,ho,hr,ht,hu,hy,hz,ia,id,ie,ig,ii,ik,io,is,it,iu,ja,jv,ka,kg,ki,kj,kk,kl,km,kn,ko,kr,ks,ku,kv,kw,ky,la,lb,lg,li,ln,lo,lt,lu,lv,mg,mh,mi,mk,ml,mn,mo,mr,ms,mt,my,na,nb,nd,ne,ng,nl,nn,no,ny,oc,oj,om,or,os,pa,pi,pl,ps,pt,qu,rm,rn,ro,ru,rw,sa,sc,sd,se,sg,sh,si,sk,sl,sm,sn,so,sq,sr,ss,st,su,sv,sw,ta,te,tg,th,ti,tk,tl,tn,to,tr,ts,tt,tw,ty,ug,uk,ur,uz,ve,vi,vo,wa,wo,xh,yi,yo,za,zh,zu". }
OPTIONAL {?item rdfs:label ?itemLabelAnyLanguage}
BIND(COALESCE(?itemLabelPreferredLanguage, ?itemLabelAnyLanguage, "?") as ?label)
# Get the description in the preferred language of the user, or any other language if no description is available in that language. # Get class (such as forest or bridge)
OPTIONAL {?item schema:description ?itemDescriptionPreferredLanguage. FILTER (lang(?itemDescriptionPreferredLanguage) = "${LANG}")} OPTIONAL {?item p:P31/ps:P31 ?class}
OPTIONAL {?item schema:description ?itemDescriptionAnyLanguage}
BIND(COALESCE(?itemDescriptionPreferredLanguage, ?itemDescriptionAnyLanguage, "?") as ?description)
# Get the class label in the preferred language of the user, or any other language if no label is available in that language. # Get picture (items without a picture will be shown in red on the Nearby map)
OPTIONAL {
?item p:P31/ps:P31 ?class.
OPTIONAL {?class rdfs:label ?classLabelPreferredLanguage. FILTER (lang(?classLabelPreferredLanguage) = "${LANG}")}
OPTIONAL {?class rdfs:label ?classLabelAnyLanguage}
BIND(COALESCE(?classLabelPreferredLanguage, ?classLabelAnyLanguage, "?") as ?classLabel)
}
OPTIONAL {
?item p:P31/ps:P31 ?class.
}
# Get picture
OPTIONAL {?item wdt:P18 ?pic} OPTIONAL {?item wdt:P18 ?pic}
# Get existence # Get existence (whether an item still exists or not)
OPTIONAL {?item wdt:P576 ?destroyed} OPTIONAL {?item wdt:P576 ?destroyed}
OPTIONAL {?item wdt:P582 ?endTime} OPTIONAL {?item wdt:P582 ?endTime}
OPTIONAL {?item wdt:P3999 ?dateOfOfficialClosure} OPTIONAL {?item wdt:P3999 ?dateOfOfficialClosure}
@ -64,5 +46,4 @@ WHERE {
?commonsArticle schema:about ?item. ?commonsArticle schema:about ?item.
?commonsArticle schema:isPartOf <https://commons.wikimedia.org/>. ?commonsArticle schema:isPartOf <https://commons.wikimedia.org/>.
} }
} }
GROUP BY ?item

View file

@ -88,14 +88,14 @@ class ExploreFragmentUnitTest {
@Throws(Exception::class) @Throws(Exception::class)
fun testSetScrollCaseTrue() { fun testSetScrollCaseTrue() {
fragment.setScroll(true) fragment.setScroll(true)
Assert.assertEquals(viewPager.isCanScroll, true) Assert.assertEquals(viewPager.canScroll, true)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testSetScrollCaseFalse() { fun testSetScrollCaseFalse() {
fragment.setScroll(false) fragment.setScroll(false)
Assert.assertEquals(viewPager.isCanScroll, false) Assert.assertEquals(viewPager.canScroll, false)
} }
@Test @Test

View file

@ -20,15 +20,15 @@ import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.explore.models.RecentSearch 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.BASE_URI
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId 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.RecentSearchesTable.ALL_FIELDS
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_LAST_USED import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_LAST_USED
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_NAME import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_NAME
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.CREATE_TABLE_STATEMENT import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.CREATE_TABLE_STATEMENT
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.DROP_TABLE_STATEMENT import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.DROP_TABLE_STATEMENT
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onCreate import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onCreate
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onDelete import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onDelete
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.onUpdate import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.onUpdate
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull import org.junit.Assert.assertNull

View file

@ -123,18 +123,4 @@ class RecentSearchesFragmentUnitTest {
method.isAccessible = true method.isAccessible = true
method.invoke(fragment, context, 0) 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)
}
} }