From 109c02b88926a5dcf52091acbb8e49af9ab054ef Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Sat, 12 Jul 2025 22:30:28 -0500 Subject: [PATCH] Convert SearchActivity to kotlin --- .../nrw/commons/explore/SearchActivity.java | 285 ------------------ .../nrw/commons/explore/SearchActivity.kt | 252 ++++++++++++++++ 2 files changed, 252 insertions(+), 285 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java deleted file mode 100644 index b27ffc338..000000000 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java +++ /dev/null @@ -1,285 +0,0 @@ -package fr.free.nrw.commons.explore; - -import static fr.free.nrw.commons.ViewPagerAdapter.pairOf; - -import android.os.Bundle; -import android.text.TextUtils; -import android.view.View; -import androidx.annotation.NonNull; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; -import com.jakewharton.rxbinding2.view.RxView; -import com.jakewharton.rxbinding2.widget.RxSearchView; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.ViewPagerAdapter; -import fr.free.nrw.commons.category.CategoryImagesCallback; -import fr.free.nrw.commons.databinding.ActivitySearchBinding; -import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment; -import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment; -import fr.free.nrw.commons.explore.media.SearchMediaFragment; -import fr.free.nrw.commons.explore.models.RecentSearch; -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao; -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.media.MediaDetailProvider; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.FragmentUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.android.schedulers.AndroidSchedulers; -import java.util.Date; -import java.util.concurrent.TimeUnit; -import javax.inject.Inject; -import timber.log.Timber; - -/** - * Represents search screen of this app - */ - -public class SearchActivity extends BaseActivity - implements MediaDetailProvider, CategoryImagesCallback { - - @Inject - RecentSearchesDao recentSearchesDao; - - private SearchMediaFragment searchMediaFragment; - private SearchCategoryFragment searchCategoryFragment; - private SearchDepictionsFragment searchDepictionsFragment; - private RecentSearchesFragment recentSearchesFragment; - private FragmentManager supportFragmentManager; - private MediaDetailPagerFragment mediaDetails; - ViewPagerAdapter viewPagerAdapter; - - private ActivitySearchBinding binding; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ActivitySearchBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - setTitle(getString(R.string.title_activity_search)); - setSupportActionBar(binding.toolbarSearch); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - binding.toolbarSearch.setNavigationOnClickListener(v->onBackPressed()); - supportFragmentManager = getSupportFragmentManager(); - setSearchHistoryFragment(); - viewPagerAdapter = new ViewPagerAdapter(this, getSupportFragmentManager()); - binding.viewPager.setAdapter(viewPagerAdapter); - binding.viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive - binding.tabLayout.setupWithViewPager(binding.viewPager); - setTabs(); - binding.searchBox.setQueryHint(getString(R.string.search_commons)); - binding.searchBox.onActionViewExpanded(); - binding.searchBox.clearFocus(); - - } - - /** - * This method sets the search history fragment. - * Search history fragment is displayed when query is empty. - */ - private void setSearchHistoryFragment() { - recentSearchesFragment = new RecentSearchesFragment(); - FragmentTransaction transaction = supportFragmentManager.beginTransaction(); - transaction.add(R.id.searchHistoryContainer, recentSearchesFragment).commit(); - } - - /** - * Sets the titles in the tabLayout and fragments in the viewPager - */ - public void setTabs() { - searchMediaFragment = new SearchMediaFragment(); - searchDepictionsFragment = new SearchDepictionsFragment(); - searchCategoryFragment= new SearchCategoryFragment(); - - viewPagerAdapter.setTabs( - pairOf(R.string.search_tab_title_media, searchMediaFragment), - pairOf(R.string.search_tab_title_categories, searchCategoryFragment), - pairOf(R.string.search_tab_title_depictions, searchDepictionsFragment) - ); - viewPagerAdapter.notifyDataSetChanged(); - getCompositeDisposable().add(RxSearchView.queryTextChanges(binding.searchBox) - .takeUntil(RxView.detaches(binding.searchBox)) - .debounce(500, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::handleSearch, Timber::e - )); - } - - private void handleSearch(final CharSequence query) { - if (!TextUtils.isEmpty(query)) { - saveRecentSearch(query.toString()); - binding.viewPager.setVisibility(View.VISIBLE); - binding.tabLayout.setVisibility(View.VISIBLE); - binding.searchHistoryContainer.setVisibility(View.GONE); - - if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) { - searchDepictionsFragment.onQueryUpdated(query.toString()); - } - - if (FragmentUtils.isFragmentUIActive(searchMediaFragment)) { - searchMediaFragment.onQueryUpdated(query.toString()); - } - - if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) { - searchCategoryFragment.onQueryUpdated(query.toString()); - } - - } - else { - //Open RecentSearchesFragment - recentSearchesFragment.updateRecentSearches(); - binding.viewPager.setVisibility(View.GONE); - binding.tabLayout.setVisibility(View.GONE); - setSearchHistoryFragment(); - binding.searchHistoryContainer.setVisibility(View.VISIBLE); - } - } - - private void saveRecentSearch(@NonNull final String query) { - final RecentSearch recentSearch = recentSearchesDao.find(query); - // Newly searched query... - if (recentSearch == null) { - recentSearchesDao.save(new RecentSearch(null, query, new Date())); - } else { - recentSearch.setLastSearched(new Date()); - recentSearchesDao.save(recentSearch); - } - } - - /** - * returns Media Object at position - * @param i position of Media in the imagesRecyclerView adapter. - */ - @Override - public Media getMediaAtPosition(int i) { - return searchMediaFragment.getMediaAtPosition(i); - } - - /** - * returns total number of images present in the imagesRecyclerView adapter. - */ - @Override - public int getTotalMediaCount() { - return searchMediaFragment.getTotalMediaCount(); - } - - @Override - public Integer getContributionStateAt(int position) { - return null; - } - - /** - * Reload media detail fragment once media is nominated - * - * @param index item position that has been nominated - */ - @Override - public void refreshNominatedMedia(int index) { - if (getSupportFragmentManager().getBackStackEntryCount() == 1) { - onBackPressed(); - onMediaClicked(index); - } - } - - /** - * This method is called on success of API call for image Search. - * The viewpager will notified that number of items have changed. - */ - @Override - public void viewPagerNotifyDataSetChanged() { - if (mediaDetails!=null){ - mediaDetails.notifyDataSetChanged(); - } - } - - /** - * Open media detail pager fragment on click of image in search results - * @param index item index that should be opened - */ - @Override - public void onMediaClicked(int index) { - ViewUtil.hideKeyboard(this.findViewById(R.id.searchBox)); - binding.tabLayout.setVisibility(View.GONE); - binding.viewPager.setVisibility(View.GONE); - binding.mediaContainer.setVisibility(View.VISIBLE); - binding.searchBox.setVisibility(View.GONE);// to remove searchview when mediaDetails fragment open - if (mediaDetails == null || !mediaDetails.isVisible()) { - // set isFeaturedImage true for featured images, to include author field on media detail - mediaDetails = MediaDetailPagerFragment.newInstance(false, true); - supportFragmentManager - .beginTransaction() - .hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount())) - .add(R.id.mediaContainer, mediaDetails) - .addToBackStack(null) - .commit(); - // Reason for using hide, add instead of replace is to maintain scroll position after - // coming back to the search activity. See https://github.com/commons-app/apps-android-commons/issues/1631 - // https://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack/19022550#19022550 - supportFragmentManager.executePendingTransactions(); - } - mediaDetails.showImage(index); - } - - /** - * This method is called on Screen Rotation - */ - @Override - protected void onResume() { - if (supportFragmentManager.getBackStackEntryCount()==1){ - //FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time. - //FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894 - // This is called on screen rotation when user is inside media details. Ideally it should show Media Details but since we are not saving the state now. We are throwing the user to search screen otherwise the app was crashing. - // - onBackPressed(); - } - super.onResume(); - } - - /** - * This method is called on backPressed of anyFragment in the activity. - * If condition is called when mediaDetailFragment is opened. - */ - @Override - public void onBackPressed() { - //Remove the backstack entry that gets added when share button is clicked - //fixing:https://github.com/commons-app/apps-android-commons/issues/2296 - if (getSupportFragmentManager().getBackStackEntryCount() == 2) { - supportFragmentManager - .beginTransaction() - .remove(mediaDetails) - .commit(); - supportFragmentManager.popBackStack(); - supportFragmentManager.executePendingTransactions(); - } - if (getSupportFragmentManager().getBackStackEntryCount() == 1) { - // back to search so show search toolbar and hide navigation toolbar - binding.searchBox.setVisibility(View.VISIBLE);//set the searchview - binding.tabLayout.setVisibility(View.VISIBLE); - binding.viewPager.setVisibility(View.VISIBLE); - binding.mediaContainer.setVisibility(View.GONE); - } else { - binding.toolbarSearch.setVisibility(View.GONE); - } - super.onBackPressed(); - } - - /** - * This method is called on click of a recent search to update query in SearchView. - * @param query Recent Search Query - */ - public void updateText(String query) { - binding.searchBox.setQuery(query, true); - // Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details. - // https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511 - binding.viewPager.requestFocus(); - } - - @Override protected void onDestroy() { - super.onDestroy(); - //Dispose the disposables when the activity is destroyed - getCompositeDisposable().dispose(); - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt new file mode 100644 index 000000000..1ab3e349b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.kt @@ -0,0 +1,252 @@ +package fr.free.nrw.commons.explore + +import android.os.Bundle +import android.text.TextUtils +import android.view.View +import androidx.fragment.app.FragmentManager +import com.jakewharton.rxbinding2.view.RxView +import com.jakewharton.rxbinding2.widget.RxSearchView +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.ViewPagerAdapter +import fr.free.nrw.commons.category.CategoryImagesCallback +import fr.free.nrw.commons.databinding.ActivitySearchBinding +import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment +import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment +import fr.free.nrw.commons.explore.media.SearchMediaFragment +import fr.free.nrw.commons.explore.models.RecentSearch +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment.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!!.totalMediaCount + + 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 + } +}