Merge branch 'commons-app:main' into Fix-Nearby-labels

This commit is contained in:
Sonal Yadav 2025-08-01 07:51:21 +05:30 committed by GitHub
commit 88793d4070
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 2047 additions and 2042 deletions

View file

@ -15,9 +15,8 @@ import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig
import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable
import fr.free.nrw.commons.category.CategoryDao
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler
import fr.free.nrw.commons.concurrency.ThreadPoolService
@ -257,8 +256,8 @@ class CommonsApplication : MultiDexApplication() {
} catch (e: SQLiteException) {
Timber.e(e)
}
BookmarkPicturesDao.Table.onDelete(db)
BookmarkItemsDao.Table.onDelete(db)
BookmarksTable.onDelete(db)
BookmarkItemsTable.onDelete(db)
}

View file

@ -1,105 +0,0 @@
package fr.free.nrw.commons.bookmarks;
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.FragmentManager;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.databinding.FragmentBookmarksBinding;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.theme.BaseActivity;
import javax.inject.Inject;
import fr.free.nrw.commons.contributions.ContributionController;
import javax.inject.Named;
public class BookmarkFragment extends CommonsDaggerSupportFragment {
private FragmentManager supportFragmentManager;
private BookmarksPagerAdapter adapter;
FragmentBookmarksBinding binding;
@Inject
ContributionController controller;
/**
* To check if the user is loggedIn or not.
*/
@Inject
@Named("default_preferences")
public
JsonKvStore applicationKvStore;
@NonNull
public static BookmarkFragment newInstance() {
BookmarkFragment fragment = new BookmarkFragment();
fragment.setRetainInstance(true);
return fragment;
}
public void setScroll(boolean canScroll) {
if (binding!=null) {
binding.viewPagerBookmarks.setCanScroll(canScroll);
}
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = FragmentBookmarksBinding.inflate(inflater, container, false);
// Activity can call methods in the fragment by acquiring a
// reference to the Fragment from FragmentManager, using findFragmentById()
supportFragmentManager = getChildFragmentManager();
adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(),
applicationKvStore.getBoolean("login_skipped"));
binding.viewPagerBookmarks.setAdapter(adapter);
binding.tabLayout.setupWithViewPager(binding.viewPagerBookmarks);
((MainActivity) getActivity()).showTabs();
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
setupTabLayout();
return binding.getRoot();
}
/**
* This method sets up the tab layout. If the adapter has only one element it sets the
* visibility of tabLayout to gone.
*/
public void setupTabLayout() {
binding.tabLayout.setVisibility(View.VISIBLE);
if (adapter.getCount() == 1) {
binding.tabLayout.setVisibility(View.GONE);
}
}
public void onBackPressed() {
if (((BookmarkListRootFragment) (adapter.getItem(binding.tabLayout.getSelectedTabPosition())))
.backPressed()) {
// The event is handled internally by the adapter , no further action required.
return;
}
// Event is not handled by the adapter ( performed back action ) change action bar.
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,98 @@
package fr.free.nrw.commons.bookmarks
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import fr.free.nrw.commons.contributions.ContributionController
import fr.free.nrw.commons.contributions.MainActivity
import fr.free.nrw.commons.databinding.FragmentBookmarksBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.theme.BaseActivity
import javax.inject.Inject
import javax.inject.Named
class BookmarkFragment : CommonsDaggerSupportFragment() {
private var adapter: BookmarksPagerAdapter? = null
@JvmField
var binding: FragmentBookmarksBinding? = null
@JvmField
@Inject
var controller: ContributionController? = null
/**
* To check if the user is loggedIn or not.
*/
@JvmField
@Inject
@Named("default_preferences")
var applicationKvStore: JsonKvStore? = null
fun setScroll(canScroll: Boolean) {
binding?.let {
it.viewPagerBookmarks.isCanScroll = canScroll
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
binding = FragmentBookmarksBinding.inflate(inflater, container, false)
// Activity can call methods in the fragment by acquiring a
// reference to the Fragment from FragmentManager, using findFragmentById()
val supportFragmentManager = childFragmentManager
adapter = BookmarksPagerAdapter(
supportFragmentManager, requireContext(),
applicationKvStore!!.getBoolean("login_skipped")
)
binding!!.viewPagerBookmarks.adapter = adapter
binding!!.tabLayout.setupWithViewPager(binding!!.viewPagerBookmarks)
(requireActivity() as MainActivity).showTabs()
(requireActivity() as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
setupTabLayout()
return binding!!.root
}
/**
* This method sets up the tab layout. If the adapter has only one element it sets the
* visibility of tabLayout to gone.
*/
fun setupTabLayout() {
binding!!.tabLayout.visibility = View.VISIBLE
if (adapter!!.count == 1) {
binding!!.tabLayout.visibility = View.GONE
}
}
fun onBackPressed() {
if (((adapter!!.getItem(binding!!.tabLayout.selectedTabPosition)) as BookmarkListRootFragment).backPressed()) {
// The event is handled internally by the adapter , no further action required.
return
}
// Event is not handled by the adapter ( performed back action ) change action bar.
(requireActivity() as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
companion object {
fun newInstance(): BookmarkFragment = BookmarkFragment().apply {
retainInstance = true
}
}
}

View file

@ -1,267 +0,0 @@
package fr.free.nrw.commons.bookmarks;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesFragment;
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.category.GridViewAdapter;
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.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.media.MediaDetailProvider;
import fr.free.nrw.commons.navtab.NavTab;
import java.util.ArrayList;
import java.util.Iterator;
import timber.log.Timber;
public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements
FragmentManager.OnBackStackChangedListener,
MediaDetailProvider,
AdapterView.OnItemClickListener, CategoryImagesCallback {
private MediaDetailPagerFragment mediaDetails;
//private BookmarkPicturesFragment bookmarkPicturesFragment;
private BookmarkLocationsFragment bookmarkLocationsFragment;
public Fragment listFragment;
private BookmarksPagerAdapter bookmarksPagerAdapter;
FragmentFeaturedRootBinding binding;
public BookmarkListRootFragment() {
//empty constructor necessary otherwise crashes on recreate
}
public BookmarkListRootFragment(Bundle bundle, BookmarksPagerAdapter bookmarksPagerAdapter) {
String title = bundle.getString("categoryName");
int order = bundle.getInt("order");
final int orderItem = bundle.getInt("orderItem");
switch (order){
case 0: listFragment = new BookmarkPicturesFragment();
break;
case 1: listFragment = new BookmarkLocationsFragment();
break;
case 3: listFragment = new BookmarkCategoriesFragment();
break;
}
if(orderItem == 2) {
listFragment = new BookmarkItemsFragment();
}
Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", title);
listFragment.setArguments(featuredArguments);
this.bookmarksPagerAdapter = bookmarksPagerAdapter;
}
@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) {
Timber.d("on media clicked");
/*container.setVisibility(View.VISIBLE);
((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE);
mediaDetails = new MediaDetailPagerFragment(false, true, position);
setFragment(mediaDetails, bookmarkPicturesFragment);*/
}
/**
* 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 (bookmarksPagerAdapter.getMediaAdapter() == null) {
// not yet ready to return data
return null;
} else {
return (Media) bookmarksPagerAdapter.getMediaAdapter().getItem(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
public int getTotalMediaCount() {
if (bookmarksPagerAdapter.getMediaAdapter() == null) {
return 0;
}
return bookmarksPagerAdapter.getMediaAdapter().getCount();
}
@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);
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
((BookmarkFragment) getParentFragment()).setScroll(false);
setFragment(mediaDetails, listFragment);
mediaDetails.showImage(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();
}
}
public boolean backPressed() {
//check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException
if (mediaDetails != null) {
if (mediaDetails.isVisible()) {
// todo add get list fragment
((BookmarkFragment) getParentFragment()).setupTabLayout();
ArrayList<Integer> removed = mediaDetails.getRemovedItems();
removeFragment(mediaDetails);
((BookmarkFragment) getParentFragment()).setScroll(true);
setFragment(listFragment, mediaDetails);
((MainActivity) getActivity()).showTabs();
if (listFragment instanceof BookmarkPicturesFragment) {
GridViewAdapter adapter = ((GridViewAdapter) ((BookmarkPicturesFragment) listFragment)
.getAdapter());
Iterator i = removed.iterator();
while (i.hasNext()) {
adapter.remove(adapter.getItem((int) i.next()));
}
mediaDetails.clearRemoved();
}
} else {
moveToContributionsFragment();
}
} else {
moveToContributionsFragment();
}
// notify mediaDetails did not handled the backPressed further actions required.
return false;
}
void moveToContributionsFragment() {
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
((MainActivity) getActivity()).showTabs();
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Timber.d("on media clicked");
binding.exploreContainer.setVisibility(View.VISIBLE);
((BookmarkFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
((BookmarkFragment) getParentFragment()).setScroll(false);
setFragment(mediaDetails, listFragment);
mediaDetails.showImage(position);
}
@Override
public void onBackStackChanged() {
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,226 @@
package fr.free.nrw.commons.bookmarks
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 androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesFragment
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment
import fr.free.nrw.commons.category.CategoryImagesCallback
import fr.free.nrw.commons.category.GridViewAdapter
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.media.MediaDetailPagerFragment
import fr.free.nrw.commons.media.MediaDetailPagerFragment.Companion.newInstance
import fr.free.nrw.commons.media.MediaDetailProvider
import fr.free.nrw.commons.navtab.NavTab
import timber.log.Timber
class BookmarkListRootFragment : CommonsDaggerSupportFragment,
FragmentManager.OnBackStackChangedListener, MediaDetailProvider, OnItemClickListener,
CategoryImagesCallback {
private var mediaDetails: MediaDetailPagerFragment? = null
private val bookmarkLocationsFragment: BookmarkLocationsFragment? = null
var listFragment: Fragment? = null
private var bookmarksPagerAdapter: BookmarksPagerAdapter? = null
var binding: FragmentFeaturedRootBinding? = null
constructor()
constructor(bundle: Bundle, bookmarksPagerAdapter: BookmarksPagerAdapter) {
val title = bundle.getString("categoryName")
val order = bundle.getInt("order")
val orderItem = bundle.getInt("orderItem")
when (order) {
0 -> listFragment = BookmarkPicturesFragment()
1 -> listFragment = BookmarkLocationsFragment()
3 -> listFragment = BookmarkCategoriesFragment()
}
if (orderItem == 2) {
listFragment = BookmarkItemsFragment()
}
val featuredArguments = Bundle()
featuredArguments.putString("categoryName", title)
listFragment!!.setArguments(featuredArguments)
this.bookmarksPagerAdapter = bookmarksPagerAdapter
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreate(savedInstanceState)
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false)
return binding!!.getRoot()
}
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) {
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()
}
}
fun removeFragment(fragment: Fragment) {
getChildFragmentManager()
.beginTransaction()
.remove(fragment)
.commit()
getChildFragmentManager().executePendingTransactions()
}
override fun onMediaClicked(position: Int) {
Timber.d("on media clicked")
/*container.setVisibility(View.VISIBLE);
((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE);
mediaDetails = new MediaDetailPagerFragment(false, true, position);
setFragment(mediaDetails, bookmarkPicturesFragment);*/
}
/**
* 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? =
bookmarksPagerAdapter!!.mediaAdapter?.getItem(i) as Media?
/**
* 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 =
bookmarksPagerAdapter!!.mediaAdapter?.count ?: 0
override fun getContributionStateAt(position: Int): Int? {
return 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!!)
mediaDetails = newInstance(false, true)
(parentFragment as BookmarkFragment).setScroll(false)
setFragment(mediaDetails!!, listFragment)
mediaDetails!!.showImage(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() {
if (mediaDetails != null) {
mediaDetails!!.notifyDataSetChanged()
}
}
fun backPressed(): Boolean {
//check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException
if (mediaDetails != null) {
if (mediaDetails!!.isVisible()) {
// todo add get list fragment
(parentFragment as BookmarkFragment).setupTabLayout()
val removed: ArrayList<Int> = mediaDetails!!.removedItems
removeFragment(mediaDetails!!)
(parentFragment as BookmarkFragment).setScroll(true)
setFragment(listFragment!!, mediaDetails)
(requireActivity() as MainActivity).showTabs()
if (listFragment is BookmarkPicturesFragment) {
val adapter = ((listFragment as BookmarkPicturesFragment)
.getAdapter() as GridViewAdapter?)
val i: MutableIterator<*> = removed.iterator()
while (i.hasNext()) {
adapter!!.remove(adapter.getItem(i.next() as Int))
}
mediaDetails!!.clearRemoved()
}
} else {
moveToContributionsFragment()
}
} else {
moveToContributionsFragment()
}
// notify mediaDetails did not handled the backPressed further actions required.
return false
}
fun moveToContributionsFragment() {
(requireActivity() as MainActivity).setSelectedItemId(NavTab.CONTRIBUTIONS.code())
(requireActivity() as MainActivity).showTabs()
}
override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
Timber.d("on media clicked")
binding!!.exploreContainer.visibility = View.VISIBLE
(parentFragment as BookmarkFragment).binding!!.tabLayout.setVisibility(View.GONE)
mediaDetails = newInstance(false, true)
(parentFragment as BookmarkFragment).setScroll(false)
setFragment(mediaDetails!!, listFragment)
mediaDetails!!.showImage(position)
}
override fun onBackStackChanged() = Unit
override fun onDestroy() {
super.onDestroy()
binding = null
}
}

View file

@ -1,94 +0,0 @@
package fr.free.nrw.commons.bookmarks;
import android.content.Context;
import android.os.Bundle;
import android.widget.ListAdapter;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import java.util.ArrayList;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
public class BookmarksPagerAdapter extends FragmentPagerAdapter {
private ArrayList<BookmarkPages> pages;
/**
* Default Constructor
* @param fm
* @param context
* @param onlyPictures is true if the fragment requires only BookmarkPictureFragment
* (i.e. when no user is logged in).
*/
BookmarksPagerAdapter(FragmentManager fm, Context context,boolean onlyPictures) {
super(fm);
pages = new ArrayList<>();
Bundle picturesBundle = new Bundle();
picturesBundle.putString("categoryName", context.getString(R.string.title_page_bookmarks_pictures));
picturesBundle.putInt("order", 0);
pages.add(new BookmarkPages(
new BookmarkListRootFragment(picturesBundle, this),
context.getString(R.string.title_page_bookmarks_pictures)));
if (!onlyPictures) {
// if onlyPictures is false we also add the location fragment.
Bundle locationBundle = new Bundle();
locationBundle.putString("categoryName",
context.getString(R.string.title_page_bookmarks_locations));
locationBundle.putInt("order", 1);
pages.add(new BookmarkPages(
new BookmarkListRootFragment(locationBundle, this),
context.getString(R.string.title_page_bookmarks_locations)));
locationBundle.putInt("orderItem", 2);
pages.add(new BookmarkPages(
new BookmarkListRootFragment(locationBundle, this),
context.getString(R.string.title_page_bookmarks_items)));
}
final Bundle categoriesBundle = new Bundle();
categoriesBundle.putString("categoryName",
context.getString(R.string.title_page_bookmarks_categories));
categoriesBundle.putInt("order", 3);
pages.add(new BookmarkPages(
new BookmarkListRootFragment(categoriesBundle, this),
context.getString(R.string.title_page_bookmarks_categories)));
notifyDataSetChanged();
}
@Override
public Fragment getItem(int position) {
return pages.get(position).getPage();
}
@Override
public int getCount() {
return pages.size();
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return pages.get(position).getTitle();
}
/**
* Return the Adapter used to display the picture gridview
* @return adapter
*/
public ListAdapter getMediaAdapter() {
BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(((BookmarkListRootFragment)pages.get(0).getPage()).listFragment);
return fragment.getAdapter();
}
/**
* Update the pictures list for the bookmark fragment
*/
public void requestPictureListUpdate() {
BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(((BookmarkListRootFragment)pages.get(0).getPage()).listFragment);
fragment.onResume();
}
}

View file

@ -0,0 +1,82 @@
package fr.free.nrw.commons.bookmarks
import android.content.Context
import android.widget.ListAdapter
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import fr.free.nrw.commons.R
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment
class BookmarksPagerAdapter internal constructor(
fm: FragmentManager, context: Context, onlyPictures: Boolean
) : FragmentPagerAdapter(fm) {
private val pages = mutableListOf<BookmarkPages>()
/**
* Default Constructor
* @param fm
* @param context
* @param onlyPictures is true if the fragment requires only BookmarkPictureFragment
* (i.e. when no user is logged in).
*/
init {
pages.add(
BookmarkPages(
BookmarkListRootFragment(
bundleOf(
"categoryName" to context.getString(R.string.title_page_bookmarks_pictures),
"order" to 0
), this
), context.getString(R.string.title_page_bookmarks_pictures)
)
)
if (!onlyPictures) {
// if onlyPictures is false we also add the location fragment.
val locationBundle = bundleOf(
"categoryName" to context.getString(R.string.title_page_bookmarks_locations),
"order" to 1
)
pages.add(
BookmarkPages(
BookmarkListRootFragment(locationBundle, this),
context.getString(R.string.title_page_bookmarks_locations)
)
)
locationBundle.putInt("orderItem", 2)
pages.add(
BookmarkPages(
BookmarkListRootFragment(locationBundle, this),
context.getString(R.string.title_page_bookmarks_items)
)
)
}
pages.add(
BookmarkPages(
BookmarkListRootFragment(
bundleOf(
"categoryName" to context.getString(R.string.title_page_bookmarks_categories),
"order" to 3
), this),
context.getString(R.string.title_page_bookmarks_categories)
)
)
notifyDataSetChanged()
}
override fun getItem(position: Int): Fragment = pages[position].page!!
override fun getCount(): Int = pages.size
override fun getPageTitle(position: Int): CharSequence? = pages[position].title
/**
* Return the Adapter used to display the picture gridview
* @return adapter
*/
val mediaAdapter: ListAdapter?
get() = (((pages[0].page as BookmarkListRootFragment).listFragment) as BookmarkPicturesFragment).getAdapter()
}

View file

@ -1,129 +0,0 @@
package fr.free.nrw.commons.bookmarks.items;
import static fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_ID;
import static fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.TABLE_NAME;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
import javax.inject.Inject;
import timber.log.Timber;
/**
* Handles private storage for bookmarked items
*/
public class BookmarkItemsContentProvider extends CommonsDaggerContentProvider {
private static final String BASE_PATH = "bookmarksItems";
public static final Uri BASE_URI =
Uri.parse("content://" + BuildConfig.BOOKMARK_ITEMS_AUTHORITY + "/" + BASE_PATH);
/**
* Append bookmark items ID to the base uri
*/
public static Uri uriForName(final String id) {
return Uri.parse(BASE_URI + "/" + id);
}
@Inject
DBOpenHelper dbOpenHelper;
@Override
public String getType(@NonNull final Uri uri) {
return null;
}
/**
* Queries the SQLite database for the bookmark items
* @param uri : contains the uri for bookmark items
* @param projection : contains the all fields of the table
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
* @param sortOrder : ascending or descending
*/
@SuppressWarnings("ConstantConditions")
@Override
public Cursor query(@NonNull final Uri uri, final String[] projection, final String selection,
final String[] selectionArgs, final String sortOrder) {
final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(TABLE_NAME);
final SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
final Cursor cursor = queryBuilder.query(db, projection, selection,
selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
/**
* Handles the update query of local SQLite Database
* @param uri : contains the uri for bookmark items
* @param contentValues : new values to be entered to db
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
*/
@SuppressWarnings("ConstantConditions")
@Override
public int update(@NonNull final Uri uri, final ContentValues contentValues,
final String selection, final String[] selectionArgs) {
final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
final int rowsUpdated;
if (TextUtils.isEmpty(selection)) {
final int id = Integer.parseInt(uri.getLastPathSegment());
rowsUpdated = sqlDB.update(TABLE_NAME,
contentValues,
COLUMN_ID + " = ?",
new String[]{String.valueOf(id)});
} else {
throw new IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID");
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsUpdated;
}
/**
* Handles the insertion of new bookmark items record to local SQLite Database
* @param uri
* @param contentValues
* @return
*/
@SuppressWarnings("ConstantConditions")
@Override
public Uri insert(@NonNull final Uri uri, final ContentValues contentValues) {
final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
final long id = sqlDB.insert(TABLE_NAME, null, contentValues);
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(BASE_URI + "/" + id);
}
/**
* Handles the deletion of new bookmark items record to local SQLite Database
* @param uri
* @param s
* @param strings
* @return
*/
@SuppressWarnings("ConstantConditions")
@Override
public int delete(@NonNull final Uri uri, final String s, final String[] strings) {
final int rows;
final SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Timber.d("Deleting bookmark name %s", uri.getLastPathSegment());
rows = db.delete(
TABLE_NAME,
"item_id = ?",
new String[]{uri.getLastPathSegment()}
);
getContext().getContentResolver().notifyChange(uri, null);
return rows;
}
}

View file

@ -0,0 +1,101 @@
package fr.free.nrw.commons.bookmarks.items
import android.content.ContentValues
import android.database.Cursor
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.TABLE_NAME
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
import androidx.core.net.toUri
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_ID
/**
* Handles private storage for bookmarked items
*/
class BookmarkItemsContentProvider : CommonsDaggerContentProvider() {
override fun getType(uri: Uri): String? = null
/**
* Queries the SQLite database for the bookmark items
* @param uri : contains the uri for bookmark items
* @param projection : contains the all fields of the table
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
* @param sortOrder : ascending or descending
*/
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor {
val queryBuilder = SQLiteQueryBuilder().apply {
tables = TABLE_NAME
}
return queryBuilder.query(
requireDb(), projection, selection,
selectionArgs, null, null, sortOrder
).apply {
setNotificationUri(requireContext().contentResolver, uri)
}
}
/**
* Handles the update query of local SQLite Database
* @param uri : contains the uri for bookmark items
* @param contentValues : new values to be entered to db
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
*/
override fun update(
uri: Uri, contentValues: ContentValues?,
selection: String?, selectionArgs: Array<String>?
): Int {
val rowsUpdated: Int
if (selection.isNullOrEmpty()) {
val id = uri.lastPathSegment!!.toInt()
rowsUpdated = requireDb().update(
TABLE_NAME,
contentValues,
"$COLUMN_ID = ?",
arrayOf(id.toString())
)
} else {
throw IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID"
)
}
requireContext().contentResolver.notifyChange(uri, null)
return rowsUpdated
}
/**
* Handles the insertion of new bookmark items record to local SQLite Database
*/
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
val id = requireDb().insert(TABLE_NAME, null, contentValues)
requireContext().contentResolver.notifyChange(uri, null)
return "$BASE_URI/$id".toUri()
}
/**
* Handles the deletion of new bookmark items record to local SQLite Database
*/
override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int {
val rows: Int = requireDb().delete(
TABLE_NAME,
"$COLUMN_ID = ?",
arrayOf(uri.lastPathSegment)
)
requireContext().contentResolver.notifyChange(uri, null)
return rows
}
companion object {
private const val BASE_PATH = "bookmarksItems"
val BASE_URI: Uri = "content://${BuildConfig.BOOKMARK_ITEMS_AUTHORITY}/$BASE_PATH".toUri()
fun uriForName(id: String) = "$BASE_URI/$id".toUri()
}
}

View file

@ -1,27 +0,0 @@
package fr.free.nrw.commons.bookmarks.items;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Handles loading bookmarked items from Database
*/
@Singleton
public class BookmarkItemsController {
@Inject
BookmarkItemsDao bookmarkItemsDao;
@Inject
public BookmarkItemsController() {}
/**
* Load from DB the bookmarked items
* @return a list of DepictedItem objects.
*/
public List<DepictedItem> loadFavoritesItems() {
return bookmarkItemsDao.getAllBookmarksItems();
}
}

View file

@ -0,0 +1,23 @@
package fr.free.nrw.commons.bookmarks.items
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import javax.inject.Inject
import javax.inject.Singleton
/**
* Handles loading bookmarked items from Database
*/
@Singleton
class BookmarkItemsController @Inject constructor() {
@JvmField
@Inject
var bookmarkItemsDao: BookmarkItemsDao? = null
/**
* Load from DB the bookmarked items
* @return a list of DepictedItem objects.
*/
fun loadFavoritesItems(): List<DepictedItem> {
return bookmarkItemsDao?.getAllBookmarksItems() ?: emptyList()
}
}

View file

@ -1,329 +0,0 @@
package fr.free.nrw.commons.bookmarks.items;
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 fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
/**
* Handles database operations for bookmarked items
*/
@Singleton
public class BookmarkItemsDao {
private final Provider<ContentProviderClient> clientProvider;
@Inject
public BookmarkItemsDao(
@Named("bookmarksItem") final Provider<ContentProviderClient> clientProvider) {
this.clientProvider = clientProvider;
}
/**
* Find all persisted items bookmarks on database
* @return list of bookmarks
*/
public List<DepictedItem> getAllBookmarksItems() {
final List<DepictedItem> items = new ArrayList<>();
final ContentProviderClient db = clientProvider.get();
try (final Cursor cursor = db.query(
BookmarkItemsContentProvider.BASE_URI,
Table.ALL_FIELDS,
null,
new String[]{},
null)) {
while (cursor != null && cursor.moveToNext()) {
items.add(fromCursor(cursor));
}
} catch (final RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
return items;
}
/**
* Look for a bookmark in database and in order to insert or delete it
* @param depictedItem : Bookmark object
* @return boolean : is bookmark now favorite ?
*/
public boolean updateBookmarkItem(final DepictedItem depictedItem) {
final boolean bookmarkExists = findBookmarkItem(depictedItem.getId());
if (bookmarkExists) {
deleteBookmarkItem(depictedItem);
} else {
addBookmarkItem(depictedItem);
}
return !bookmarkExists;
}
/**
* Add a Bookmark to database
* @param depictedItem : Bookmark to add
*/
private void addBookmarkItem(final DepictedItem depictedItem) {
final ContentProviderClient db = clientProvider.get();
try {
db.insert(BookmarkItemsContentProvider.BASE_URI, toContentValues(depictedItem));
} catch (final RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Delete a bookmark from database
* @param depictedItem : Bookmark to delete
*/
private void deleteBookmarkItem(final DepictedItem depictedItem) {
final ContentProviderClient db = clientProvider.get();
try {
db.delete(BookmarkItemsContentProvider.uriForName(depictedItem.getId()), null, null);
} catch (final RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Find a bookmark from database based on its name
* @param depictedItemID : Bookmark to find
* @return boolean : is bookmark in database ?
*/
public boolean findBookmarkItem(final String depictedItemID) {
if (depictedItemID == null) { //Avoiding NPE's
return false;
}
final ContentProviderClient db = clientProvider.get();
try (final Cursor cursor = db.query(
BookmarkItemsContentProvider.BASE_URI,
Table.ALL_FIELDS,
Table.COLUMN_ID + "=?",
new String[]{depictedItemID},
null
)) {
if (cursor != null && cursor.moveToFirst()) {
return true;
}
} catch (final RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
return false;
}
/**
* Recives real data from cursor
* @param cursor : Object for storing database data
* @return DepictedItem
*/
@SuppressLint("Range")
DepictedItem fromCursor(final Cursor cursor) {
final String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME));
final String description
= cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION));
final String imageUrl = cursor.getString(cursor.getColumnIndex(Table.COLUMN_IMAGE));
final String instanceListString
= cursor.getString(cursor.getColumnIndex(Table.COLUMN_INSTANCE_LIST));
final List<String> instanceList = StringToArray(instanceListString);
final String categoryNameListString = cursor.getString(cursor
.getColumnIndex(Table.COLUMN_CATEGORIES_NAME_LIST));
final List<String> categoryNameList = StringToArray(categoryNameListString);
final String categoryDescriptionListString = cursor.getString(cursor
.getColumnIndex(Table.COLUMN_CATEGORIES_DESCRIPTION_LIST));
final List<String> categoryDescriptionList = StringToArray(categoryDescriptionListString);
final String categoryThumbnailListString = cursor.getString(cursor
.getColumnIndex(Table.COLUMN_CATEGORIES_THUMBNAIL_LIST));
final List<String> categoryThumbnailList = StringToArray(categoryThumbnailListString);
final List<CategoryItem> categoryList = convertToCategoryItems(categoryNameList,
categoryDescriptionList, categoryThumbnailList);
final boolean isSelected
= Boolean.parseBoolean(cursor.getString(cursor
.getColumnIndex(Table.COLUMN_IS_SELECTED)));
final String id = cursor.getString(cursor.getColumnIndex(Table.COLUMN_ID));
return new DepictedItem(
fileName,
description,
imageUrl,
instanceList,
categoryList,
isSelected,
id
);
}
private List<CategoryItem> convertToCategoryItems(List<String> categoryNameList,
List<String> categoryDescriptionList, List<String> categoryThumbnailList) {
List<CategoryItem> categoryItems = new ArrayList<>();
for(int i=0; i<categoryNameList.size(); i++){
categoryItems.add(new CategoryItem(categoryNameList.get(i),
categoryDescriptionList.get(i),
categoryThumbnailList.get(i), false));
}
return categoryItems;
}
/**
* Converts string to List
* @param listString comma separated single string from of list items
* @return List of string
*/
private List<String> StringToArray(final String listString) {
final String[] elements = listString.split(",");
return Arrays.asList(elements);
}
/**
* Converts string to List
* @param list list of items
* @return string comma separated single string of items
*/
private String ArrayToString(final List<String> list) {
if (list != null) {
return StringUtils.join(list, ',');
}
return null;
}
/**
* Takes data from DepictedItem and create a content value object
* @param depictedItem depicted item
* @return ContentValues
*/
private ContentValues toContentValues(final DepictedItem depictedItem) {
final List<String> namesOfCommonsCategories = new ArrayList<>();
for (final CategoryItem category :
depictedItem.getCommonsCategories()) {
namesOfCommonsCategories.add(category.getName());
}
final List<String> descriptionsOfCommonsCategories = new ArrayList<>();
for (final CategoryItem category :
depictedItem.getCommonsCategories()) {
descriptionsOfCommonsCategories.add(category.getDescription());
}
final List<String> thumbnailsOfCommonsCategories = new ArrayList<>();
for (final CategoryItem category :
depictedItem.getCommonsCategories()) {
thumbnailsOfCommonsCategories.add(category.getThumbnail());
}
final ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_NAME, depictedItem.getName());
cv.put(Table.COLUMN_DESCRIPTION, depictedItem.getDescription());
cv.put(Table.COLUMN_IMAGE, depictedItem.getImageUrl());
cv.put(Table.COLUMN_INSTANCE_LIST, ArrayToString(depictedItem.getInstanceOfs()));
cv.put(Table.COLUMN_CATEGORIES_NAME_LIST, ArrayToString(namesOfCommonsCategories));
cv.put(Table.COLUMN_CATEGORIES_DESCRIPTION_LIST,
ArrayToString(descriptionsOfCommonsCategories));
cv.put(Table.COLUMN_CATEGORIES_THUMBNAIL_LIST,
ArrayToString(thumbnailsOfCommonsCategories));
cv.put(Table.COLUMN_IS_SELECTED, depictedItem.isSelected());
cv.put(Table.COLUMN_ID, depictedItem.getId());
return cv;
}
/**
* Table of bookmarksItems data
*/
public static final class Table {
public static final String TABLE_NAME = "bookmarksItems";
public static final String COLUMN_NAME = "item_name";
public static final String COLUMN_DESCRIPTION = "item_description";
public static final String COLUMN_IMAGE = "item_image_url";
public static final String COLUMN_INSTANCE_LIST = "item_instance_of";
public static final String COLUMN_CATEGORIES_NAME_LIST = "item_name_categories";
public static final String COLUMN_CATEGORIES_DESCRIPTION_LIST = "item_description_categories";
public static final String COLUMN_CATEGORIES_THUMBNAIL_LIST = "item_thumbnail_categories";
public static final String COLUMN_IS_SELECTED = "item_is_selected";
public static final String COLUMN_ID = "item_id";
public static final String[] ALL_FIELDS = {
COLUMN_NAME,
COLUMN_DESCRIPTION,
COLUMN_IMAGE,
COLUMN_INSTANCE_LIST,
COLUMN_CATEGORIES_NAME_LIST,
COLUMN_CATEGORIES_DESCRIPTION_LIST,
COLUMN_CATEGORIES_THUMBNAIL_LIST,
COLUMN_IS_SELECTED,
COLUMN_ID
};
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_NAME + " STRING,"
+ COLUMN_DESCRIPTION + " STRING,"
+ COLUMN_IMAGE + " STRING,"
+ COLUMN_INSTANCE_LIST + " STRING,"
+ COLUMN_CATEGORIES_NAME_LIST + " STRING,"
+ COLUMN_CATEGORIES_DESCRIPTION_LIST + " STRING,"
+ COLUMN_CATEGORIES_THUMBNAIL_LIST + " STRING,"
+ COLUMN_IS_SELECTED + " STRING,"
+ COLUMN_ID + " STRING PRIMARY KEY"
+ ");";
/**
* Creates table
* @param db SQLiteDatabase
*/
public static void onCreate(final SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
}
/**
* Deletes database
* @param db SQLiteDatabase
*/
public static void onDelete(final SQLiteDatabase db) {
db.execSQL(DROP_TABLE_STATEMENT);
onCreate(db);
}
/**
* Updates database
* @param db SQLiteDatabase
* @param from starting
* @param to end
*/
public static void onUpdate(final SQLiteDatabase db, int from, final int to) {
if (from == to) {
return;
}
if (from < 18) {
// doesn't exist yet
from++;
onUpdate(db, from, to);
return;
}
if (from == 18) {
// table added in version 19
onCreate(db);
from++;
onUpdate(db, from, to);
}
}
}
}

View file

@ -0,0 +1,199 @@
package fr.free.nrw.commons.bookmarks.items
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.bookmarks.items.BookmarkItemsContentProvider.Companion.BASE_URI
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider.Companion.uriForName
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_DESCRIPTION_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_NAME_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_THUMBNAIL_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_DESCRIPTION
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_ID
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IMAGE
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_INSTANCE_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IS_SELECTED
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_NAME
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.utils.arrayToString
import fr.free.nrw.commons.utils.getString
import fr.free.nrw.commons.utils.getStringArray
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Provider
import javax.inject.Singleton
/**
* Handles database operations for bookmarked items
*/
@Singleton
class BookmarkItemsDao @Inject constructor(
@param:Named("bookmarksItem") private val clientProvider: Provider<ContentProviderClient>
) {
/**
* Find all persisted items bookmarks on database
* @return list of bookmarks
*/
fun getAllBookmarksItems(): List<DepictedItem> {
val items: MutableList<DepictedItem> = mutableListOf()
val db = clientProvider.get()
try {
db.query(
BASE_URI,
BookmarkItemsTable.ALL_FIELDS,
null,
arrayOf(),
null
).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
items.add(fromCursor(cursor))
}
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
return items
}
/**
* Look for a bookmark in database and in order to insert or delete it
* @param depictedItem : Bookmark object
* @return boolean : is bookmark now favorite ?
*/
fun updateBookmarkItem(depictedItem: DepictedItem): Boolean {
val bookmarkExists = findBookmarkItem(depictedItem.id)
if (bookmarkExists) {
deleteBookmarkItem(depictedItem)
} else {
addBookmarkItem(depictedItem)
}
return !bookmarkExists
}
/**
* Add a Bookmark to database
* @param depictedItem : Bookmark to add
*/
private fun addBookmarkItem(depictedItem: DepictedItem) {
val db = clientProvider.get()
try {
db.insert(BASE_URI, toContentValues(depictedItem))
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
}
/**
* Delete a bookmark from database
* @param depictedItem : Bookmark to delete
*/
private fun deleteBookmarkItem(depictedItem: DepictedItem) {
val db = clientProvider.get()
try {
db.delete(uriForName(depictedItem.id), null, null)
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
}
/**
* Find a bookmark from database based on its name
* @param depictedItemID : Bookmark to find
* @return boolean : is bookmark in database ?
*/
fun findBookmarkItem(depictedItemID: String?): Boolean {
if (depictedItemID == null) { //Avoiding NPE's
return false
}
val db = clientProvider.get()
try {
db.query(
BASE_URI,
BookmarkItemsTable.ALL_FIELDS,
COLUMN_ID + "=?",
arrayOf(depictedItemID),
null
).use { cursor ->
if (cursor != null && cursor.moveToFirst()) {
return true
}
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
return false
}
/**
* Recives real data from cursor
* @param cursor : Object for storing database data
* @return DepictedItem
*/
@SuppressLint("Range")
fun fromCursor(cursor: Cursor) = with(cursor) {
DepictedItem(
getString(COLUMN_NAME),
getString(COLUMN_DESCRIPTION),
getString(COLUMN_IMAGE),
getStringArray(COLUMN_INSTANCE_LIST),
convertToCategoryItems(
getStringArray(COLUMN_CATEGORIES_NAME_LIST),
getStringArray(COLUMN_CATEGORIES_DESCRIPTION_LIST),
getStringArray(COLUMN_CATEGORIES_THUMBNAIL_LIST)
),
getString(COLUMN_IS_SELECTED).toBoolean(),
getString(COLUMN_ID)
)
}
private fun convertToCategoryItems(
categoryNameList: List<String>,
categoryDescriptionList: List<String>,
categoryThumbnailList: List<String>
): List<CategoryItem> {
return buildList {
for (i in categoryNameList.indices) {
add(
CategoryItem(
categoryNameList[i],
categoryDescriptionList[i],
categoryThumbnailList[i],
false
)
)
}
}
}
/**
* Takes data from DepictedItem and create a content value object
* @param depictedItem depicted item
* @return ContentValues
*/
private fun toContentValues(depictedItem: DepictedItem): ContentValues {
return contentValuesOf(
COLUMN_NAME to depictedItem.name,
COLUMN_DESCRIPTION to depictedItem.description,
COLUMN_IMAGE to depictedItem.imageUrl,
COLUMN_INSTANCE_LIST to arrayToString(depictedItem.instanceOfs),
COLUMN_CATEGORIES_NAME_LIST to arrayToString(depictedItem.commonsCategories.map { it.name }),
COLUMN_CATEGORIES_DESCRIPTION_LIST to arrayToString(depictedItem.commonsCategories.map { it.description }),
COLUMN_CATEGORIES_THUMBNAIL_LIST to arrayToString(depictedItem.commonsCategories.map { it.thumbnail }),
COLUMN_IS_SELECTED to depictedItem.isSelected,
COLUMN_ID to depictedItem.id,
)
}
}

View file

@ -1,81 +0,0 @@
package fr.free.nrw.commons.bookmarks.items;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.databinding.FragmentBookmarksItemsBinding;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.List;
import javax.inject.Inject;
import org.jetbrains.annotations.NotNull;
/**
* Tab fragment to show list of bookmarked Wikidata Items
*/
public class BookmarkItemsFragment extends DaggerFragment {
private FragmentBookmarksItemsBinding binding;
@Inject
BookmarkItemsController controller;
public static BookmarkItemsFragment newInstance() {
return new BookmarkItemsFragment();
}
@Override
public View onCreateView(
@NonNull final LayoutInflater inflater,
final ViewGroup container,
final Bundle savedInstanceState
) {
binding = FragmentBookmarksItemsBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(final @NotNull View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initList(requireContext());
}
@Override
public void onResume() {
super.onResume();
initList(requireContext());
}
/**
* Get list of DepictedItem and sets to the adapter
* @param context context
*/
private void initList(final Context context) {
final List<DepictedItem> depictItems = controller.loadFavoritesItems();
final BookmarkItemsAdapter adapter = new BookmarkItemsAdapter(depictItems, context);
binding.listView.setAdapter(adapter);
binding.loadingImagesProgressBar.setVisibility(View.GONE);
if (depictItems.isEmpty()) {
binding.statusMessage.setText(R.string.bookmark_empty);
binding.statusMessage.setVisibility(View.VISIBLE);
} else {
binding.statusMessage.setVisibility(View.GONE);
}
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,62 @@
package fr.free.nrw.commons.bookmarks.items
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import dagger.android.support.DaggerFragment
import fr.free.nrw.commons.R
import fr.free.nrw.commons.databinding.FragmentBookmarksItemsBinding
import javax.inject.Inject
/**
* Tab fragment to show list of bookmarked Wikidata Items
*/
class BookmarkItemsFragment : DaggerFragment() {
private var binding: FragmentBookmarksItemsBinding? = null
@JvmField
@Inject
var controller: BookmarkItemsController? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentBookmarksItemsBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initList(requireContext())
}
override fun onResume() {
super.onResume()
initList(requireContext())
}
/**
* Get list of DepictedItem and sets to the adapter
* @param context context
*/
private fun initList(context: Context) {
val depictItems = controller!!.loadFavoritesItems()
binding!!.listView.adapter = BookmarkItemsAdapter(depictItems, context)
binding!!.loadingImagesProgressBar.visibility = View.GONE
if (depictItems.isEmpty()) {
binding!!.statusMessage.setText(R.string.bookmark_empty)
binding!!.statusMessage.visibility = View.VISIBLE
} else {
binding!!.statusMessage.visibility = View.GONE
}
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
}

View file

@ -0,0 +1,90 @@
package fr.free.nrw.commons.bookmarks.items
import android.database.sqlite.SQLiteDatabase
/**
* Table of bookmarksItems data
*/
object BookmarkItemsTable {
const val TABLE_NAME = "bookmarksItems"
const val COLUMN_NAME = "item_name"
const val COLUMN_DESCRIPTION = "item_description"
const val COLUMN_IMAGE = "item_image_url"
const val COLUMN_INSTANCE_LIST = "item_instance_of"
const val COLUMN_CATEGORIES_NAME_LIST = "item_name_categories"
const val COLUMN_CATEGORIES_DESCRIPTION_LIST = "item_description_categories"
const val COLUMN_CATEGORIES_THUMBNAIL_LIST = "item_thumbnail_categories"
const val COLUMN_IS_SELECTED = "item_is_selected"
const val COLUMN_ID = "item_id"
val ALL_FIELDS = arrayOf(
COLUMN_NAME,
COLUMN_DESCRIPTION,
COLUMN_IMAGE,
COLUMN_INSTANCE_LIST,
COLUMN_CATEGORIES_NAME_LIST,
COLUMN_CATEGORIES_DESCRIPTION_LIST,
COLUMN_CATEGORIES_THUMBNAIL_LIST,
COLUMN_IS_SELECTED,
COLUMN_ID
)
const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME"
val CREATE_TABLE_STATEMENT =
"""CREATE TABLE $TABLE_NAME (
$COLUMN_NAME STRING,
$COLUMN_DESCRIPTION STRING,
$COLUMN_IMAGE STRING,
$COLUMN_INSTANCE_LIST STRING,
$COLUMN_CATEGORIES_NAME_LIST STRING,
$COLUMN_CATEGORIES_DESCRIPTION_LIST STRING,
$COLUMN_CATEGORIES_THUMBNAIL_LIST STRING,
$COLUMN_IS_SELECTED STRING,
$COLUMN_ID STRING PRIMARY KEY
);""".trimIndent()
/**
* Creates table
*
* @param db SQLiteDatabase
*/
fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_TABLE_STATEMENT)
}
/**
* Deletes database
*
* @param db SQLiteDatabase
*/
fun onDelete(db: SQLiteDatabase) {
db.execSQL(DROP_TABLE_STATEMENT)
onCreate(db)
}
/**
* Updates database
*
* @param db SQLiteDatabase
* @param from starting
* @param to end
*/
fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) {
if (from == to) {
return
}
if (from < 18) {
// doesn't exist yet
onUpdate(db, from + 1, to)
return
}
if (from == 18) {
// table added in version 19
onCreate(db)
onUpdate(db, from + 1, to)
}
}
}

View file

@ -1,120 +0,0 @@
package fr.free.nrw.commons.bookmarks.pictures;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
// We can get uri using java.Net.Uri, but andoid implimentation is faster (but it's forgiving with handling exceptions though)
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import javax.inject.Inject;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
import timber.log.Timber;
import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.COLUMN_MEDIA_NAME;
import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.TABLE_NAME;
/**
* Handles private storage for Bookmark pictures
*/
public class BookmarkPicturesContentProvider extends CommonsDaggerContentProvider {
private static final String BASE_PATH = "bookmarks";
public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.BOOKMARK_AUTHORITY + "/" + BASE_PATH);
/**
* Append bookmark pictures name to the base uri
*/
public static Uri uriForName(String name) {
return Uri.parse(BASE_URI.toString() + "/" + name);
}
@Inject
DBOpenHelper dbOpenHelper;
@Override
public String getType(@NonNull Uri uri) {
return null;
}
/**
* Queries the SQLite database for the bookmark pictures
* @param uri : contains the uri for bookmark pictures
* @param projection
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
* @param sortOrder : ascending or descending
*/
@SuppressWarnings("ConstantConditions")
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(TABLE_NAME);
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
/**
* Handles the update query of local SQLite Database
* @param uri : contains the uri for bookmark pictures
* @param contentValues : new values to be entered to db
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
*/
@SuppressWarnings("ConstantConditions")
@Override
public int update(@NonNull Uri uri, ContentValues contentValues, String selection,
String[] selectionArgs) {
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated;
if (TextUtils.isEmpty(selection)) {
int id = Integer.valueOf(uri.getLastPathSegment());
rowsUpdated = sqlDB.update(TABLE_NAME,
contentValues,
COLUMN_MEDIA_NAME + " = ?",
new String[]{String.valueOf(id)});
} else {
throw new IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID");
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsUpdated;
}
/**
* Handles the insertion of new bookmark pictures record to local SQLite Database
*/
@SuppressWarnings("ConstantConditions")
@Override
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id = sqlDB.insert(BookmarkPicturesDao.Table.TABLE_NAME, null, contentValues);
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(BASE_URI + "/" + id);
}
@SuppressWarnings("ConstantConditions")
@Override
public int delete(@NonNull Uri uri, String s, String[] strings) {
int rows;
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Timber.d("Deleting bookmark name %s", uri.getLastPathSegment());
rows = db.delete(TABLE_NAME,
"media_name = ?",
new String[]{uri.getLastPathSegment()}
);
getContext().getContentResolver().notifyChange(uri, null);
return rows;
}
}

View file

@ -0,0 +1,100 @@
package fr.free.nrw.commons.bookmarks.pictures
import android.content.ContentValues
import android.database.Cursor
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
import androidx.core.net.toUri
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_MEDIA_NAME
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.TABLE_NAME
/**
* Handles private storage for Bookmark pictures
*/
class BookmarkPicturesContentProvider : CommonsDaggerContentProvider() {
override fun getType(uri: Uri): String? = null
/**
* Queries the SQLite database for the bookmark pictures
* @param uri : contains the uri for bookmark pictures
* @param projection
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
* @param sortOrder : ascending or descending
*/
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor {
val queryBuilder = SQLiteQueryBuilder().apply {
tables = TABLE_NAME
}
val cursor = queryBuilder.query(
requireDb(), projection, selection,
selectionArgs, null, null, sortOrder
)
cursor.setNotificationUri(requireContext().contentResolver, uri)
return cursor
}
/**
* Handles the update query of local SQLite Database
* @param uri : contains the uri for bookmark pictures
* @param contentValues : new values to be entered to db
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
*/
override fun update(
uri: Uri, contentValues: ContentValues?, selection: String?,
selectionArgs: Array<String>?
): Int {
val rowsUpdated: Int
if (selection.isNullOrEmpty()) {
val id = uri.lastPathSegment!!.toInt()
rowsUpdated = requireDb().update(
TABLE_NAME,
contentValues,
"$COLUMN_MEDIA_NAME = ?",
arrayOf(id.toString())
)
} else {
throw IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID"
)
}
requireContext().contentResolver.notifyChange(uri, null)
return rowsUpdated
}
/**
* Handles the insertion of new bookmark pictures record to local SQLite Database
*/
override fun insert(uri: Uri, contentValues: ContentValues?): Uri {
val id = requireDb().insert(TABLE_NAME, null, contentValues)
requireContext().contentResolver.notifyChange(uri, null)
return "$BASE_URI/$id".toUri()
}
override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int {
val rows: Int = requireDb().delete(
TABLE_NAME,
"media_name = ?",
arrayOf(uri.lastPathSegment)
)
requireContext().contentResolver.notifyChange(uri, null)
return rows
}
companion object {
private const val BASE_PATH = "bookmarks"
@JvmField
val BASE_URI: Uri = "content://${BuildConfig.BOOKMARK_AUTHORITY}/$BASE_PATH".toUri()
@JvmStatic
fun uriForName(name: String): Uri = "$BASE_URI/$name".toUri()
}
}

View file

@ -1,63 +0,0 @@
package fr.free.nrw.commons.bookmarks.pictures;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.bookmarks.models.Bookmark;
import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.Single;
import io.reactivex.functions.Function;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class BookmarkPicturesController {
private final MediaClient mediaClient;
private final BookmarkPicturesDao bookmarkDao;
private List<Bookmark> currentBookmarks;
@Inject
public BookmarkPicturesController(MediaClient mediaClient, BookmarkPicturesDao bookmarkDao) {
this.mediaClient = mediaClient;
this.bookmarkDao = bookmarkDao;
currentBookmarks = new ArrayList<>();
}
/**
* Loads the Media objects from the raw data stored in DB and the API.
* @return a list of bookmarked Media object
*/
Single<List<Media>> loadBookmarkedPictures() {
List<Bookmark> bookmarks = bookmarkDao.getAllBookmarks();
currentBookmarks = bookmarks;
return Observable.fromIterable(bookmarks)
.flatMap((Function<Bookmark, ObservableSource<Media>>) this::getMediaFromBookmark)
.toList();
}
private Observable<Media> getMediaFromBookmark(Bookmark bookmark) {
return mediaClient.getMedia(bookmark.getMediaName())
.toObservable()
.onErrorResumeNext(Observable.empty());
}
/**
* Loads the Media objects from the raw data stored in DB and the API.
* @return a list of bookmarked Media object
*/
boolean needRefreshBookmarkedPictures() {
List<Bookmark> bookmarks = bookmarkDao.getAllBookmarks();
return bookmarks.size() != currentBookmarks.size();
}
/**
* Cancels the requests to the API and the DB
*/
void stop() {
//noop
}
}

View file

@ -0,0 +1,38 @@
package fr.free.nrw.commons.bookmarks.pictures
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.bookmarks.models.Bookmark
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Observable
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BookmarkPicturesController @Inject constructor(
private val mediaClient: MediaClient,
private val bookmarkDao: BookmarkPicturesDao
) {
private var currentBookmarks: List<Bookmark> = listOf()
/**
* Loads the Media objects from the raw data stored in DB and the API.
* @return a list of bookmarked Media object
*/
fun loadBookmarkedPictures(): Single<List<Media>> {
val bookmarks = bookmarkDao.getAllBookmarks()
currentBookmarks = bookmarks
return Observable.fromIterable(bookmarks).flatMap {
mediaClient.getMedia(it.mediaName)
.toObservable()
.onErrorResumeNext(Observable.empty())
}.toList()
}
fun needRefreshBookmarkedPictures(): Boolean {
val bookmarks = bookmarkDao.getAllBookmarks()
return bookmarks.size != currentBookmarks.size
}
fun stop() = Unit
}

View file

@ -1,227 +0,0 @@
package fr.free.nrw.commons.bookmarks.pictures;
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 java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import fr.free.nrw.commons.bookmarks.models.Bookmark;
import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.BASE_URI;
@Singleton
public class BookmarkPicturesDao {
private final Provider<ContentProviderClient> clientProvider;
@Inject
public BookmarkPicturesDao(@Named("bookmarks") Provider<ContentProviderClient> clientProvider) {
this.clientProvider = clientProvider;
}
/**
* Find all persisted pictures bookmarks on database
*
* @return list of bookmarks
*/
@NonNull
public List<Bookmark> getAllBookmarks() {
List<Bookmark> items = new ArrayList<>();
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
BookmarkPicturesContentProvider.BASE_URI,
Table.ALL_FIELDS,
null,
new String[]{},
null);
while (cursor != null && cursor.moveToNext()) {
items.add(fromCursor(cursor));
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
db.release();
}
return items;
}
/**
* Look for a bookmark in database and in order to insert or delete it
*
* @param bookmark : Bookmark object
* @return boolean : is bookmark now fav ?
*/
public boolean updateBookmark(Bookmark bookmark) {
boolean bookmarkExists = findBookmark(bookmark);
if (bookmarkExists) {
deleteBookmark(bookmark);
} else {
addBookmark(bookmark);
}
return !bookmarkExists;
}
/**
* Add a Bookmark to database
*
* @param bookmark : Bookmark to add
*/
private void addBookmark(Bookmark bookmark) {
ContentProviderClient db = clientProvider.get();
try {
db.insert(BASE_URI, toContentValues(bookmark));
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Delete a bookmark from database
*
* @param bookmark : Bookmark to delete
*/
private void deleteBookmark(Bookmark bookmark) {
ContentProviderClient db = clientProvider.get();
try {
if (bookmark.getContentUri() == null) {
throw new RuntimeException("tried to delete item with no content URI");
} else {
db.delete(bookmark.getContentUri(), null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Find a bookmark from database based on its name
*
* @param bookmark : Bookmark to find
* @return boolean : is bookmark in database ?
*/
public boolean findBookmark(Bookmark bookmark) {
if (bookmark == null) {//Avoiding NPE's
return false;
}
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
BookmarkPicturesContentProvider.BASE_URI,
Table.ALL_FIELDS,
Table.COLUMN_MEDIA_NAME + "=?",
new String[]{bookmark.getMediaName()},
null);
if (cursor != null && cursor.moveToFirst()) {
return true;
}
} 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 false;
}
@SuppressLint("Range")
@NonNull
Bookmark fromCursor(Cursor cursor) {
String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_MEDIA_NAME));
return new Bookmark(
fileName,
cursor.getString(cursor.getColumnIndex(Table.COLUMN_CREATOR)),
BookmarkPicturesContentProvider.uriForName(fileName)
);
}
private ContentValues toContentValues(Bookmark bookmark) {
ContentValues cv = new ContentValues();
cv.put(BookmarkPicturesDao.Table.COLUMN_MEDIA_NAME, bookmark.getMediaName());
cv.put(BookmarkPicturesDao.Table.COLUMN_CREATOR, bookmark.getMediaCreator());
return cv;
}
public static class Table {
public static final String TABLE_NAME = "bookmarks";
public static final String COLUMN_MEDIA_NAME = "media_name";
public static final String COLUMN_CREATOR = "media_creator";
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = {
COLUMN_MEDIA_NAME,
COLUMN_CREATOR
};
public static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
public static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_MEDIA_NAME + " STRING PRIMARY KEY,"
+ COLUMN_CREATOR + " STRING"
+ ");";
public static void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
}
public static void onDelete(SQLiteDatabase db) {
db.execSQL(DROP_TABLE_STATEMENT);
onCreate(db);
}
public static void onUpdate(SQLiteDatabase db, int from, int to) {
if (from == to) {
return;
}
if (from < 7) {
// doesn't exist yet
from++;
onUpdate(db, from, to);
return;
}
if (from == 7) {
// table added in version 8
onCreate(db);
from++;
onUpdate(db, from, to);
return;
}
if (from == 8) {
from++;
onUpdate(db, from, to);
return;
}
}
}
}

View file

@ -0,0 +1,141 @@
package fr.free.nrw.commons.bookmarks.pictures
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.bookmarks.models.Bookmark
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.Companion.BASE_URI
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.Companion.uriForName
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.ALL_FIELDS
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_CREATOR
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_MEDIA_NAME
import fr.free.nrw.commons.utils.getString
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Provider
import javax.inject.Singleton
@Singleton
class BookmarkPicturesDao @Inject constructor(
@param:Named("bookmarks") private val clientProvider: Provider<ContentProviderClient>
) {
/**
* Find all persisted pictures bookmarks on database
*
* @return list of bookmarks
*/
fun getAllBookmarks(): List<Bookmark> {
val items: MutableList<Bookmark> = mutableListOf()
var cursor: Cursor? = null
val db = clientProvider.get()
try {
cursor = db.query(
BASE_URI, ALL_FIELDS, null, arrayOf(), null
)
while (cursor != null && cursor.moveToNext()) {
items.add(fromCursor(cursor))
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
cursor?.close()
db.release()
}
return items
}
/**
* Look for a bookmark in database and in order to insert or delete it
*
* @param bookmark : Bookmark object
* @return boolean : is bookmark now fav ?
*/
fun updateBookmark(bookmark: Bookmark): Boolean {
val bookmarkExists = findBookmark(bookmark)
if (bookmarkExists) {
deleteBookmark(bookmark)
} else {
addBookmark(bookmark)
}
return !bookmarkExists
}
/**
* Add a Bookmark to database
*
* @param bookmark : Bookmark to add
*/
private fun addBookmark(bookmark: Bookmark) {
val db = clientProvider.get()
try {
db.insert(BASE_URI, toContentValues(bookmark))
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
}
/**
* Delete a bookmark from database
*
* @param bookmark : Bookmark to delete
*/
private fun deleteBookmark(bookmark: Bookmark) {
val db = clientProvider.get()
try {
if (bookmark.contentUri == null) {
throw RuntimeException("tried to delete item with no content URI")
} else {
db.delete(bookmark.contentUri!!, null, null)
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
}
/**
* Find a bookmark from database based on its name
*
* @param bookmark : Bookmark to find
* @return boolean : is bookmark in database ?
*/
fun findBookmark(bookmark: Bookmark?): Boolean {
if (bookmark == null) {
return false
}
var cursor: Cursor? = null
val db = clientProvider.get()
try {
cursor = db.query(
BASE_URI, ALL_FIELDS, "$COLUMN_MEDIA_NAME=?", arrayOf(bookmark.mediaName), null
)
if (cursor != null && cursor.moveToFirst()) {
return true
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
cursor?.close()
db.release()
}
return false
}
fun fromCursor(cursor: Cursor): Bookmark {
val fileName = cursor.getString(COLUMN_MEDIA_NAME)
return Bookmark(
fileName, cursor.getString(COLUMN_CREATOR), uriForName(fileName)
)
}
private fun toContentValues(bookmark: Bookmark): ContentValues = contentValuesOf(
COLUMN_MEDIA_NAME to bookmark.mediaName,
COLUMN_CREATOR to bookmark.mediaCreator
)
}

View file

@ -1,218 +0,0 @@
package fr.free.nrw.commons.bookmarks.pictures;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment;
import fr.free.nrw.commons.category.GridViewAdapter;
import fr.free.nrw.commons.databinding.FragmentBookmarksPicturesBinding;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
import javax.inject.Inject;
import timber.log.Timber;
public class BookmarkPicturesFragment extends DaggerFragment {
private GridViewAdapter gridAdapter;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private FragmentBookmarksPicturesBinding binding;
@Inject
BookmarkPicturesController controller;
/**
* Create an instance of the fragment with the right bundle parameters
* @return an instance of the fragment
*/
public static BookmarkPicturesFragment newInstance() {
return new BookmarkPicturesFragment();
}
@Override
public View onCreateView(
@NonNull LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState
) {
binding = FragmentBookmarksPicturesBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
binding.bookmarkedPicturesList.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment());
initList();
}
@Override
public void onStop() {
super.onStop();
controller.stop();
}
@Override
public void onDestroy() {
super.onDestroy();
compositeDisposable.clear();
binding = null;
}
@Override
public void onResume() {
super.onResume();
if (controller.needRefreshBookmarkedPictures()) {
binding.bookmarkedPicturesList.setVisibility(GONE);
if (gridAdapter != null) {
gridAdapter.clear();
((BookmarkListRootFragment)getParentFragment()).viewPagerNotifyDataSetChanged();
}
initList();
}
}
/**
* Checks for internet connection and then initializes
* the recycler view with bookmarked pictures
*/
@SuppressLint("CheckResult")
private void initList() {
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
binding.loadingImagesProgressBar.setVisibility(VISIBLE);
binding.statusMessage.setVisibility(GONE);
compositeDisposable.add(controller.loadBookmarkedPictures()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::handleSuccess, this::handleError));
}
/**
* Handles the UI updates for no internet scenario
*/
private void handleNoInternet() {
binding.loadingImagesProgressBar.setVisibility(GONE);
if (gridAdapter == null || gridAdapter.isEmpty()) {
binding.statusMessage.setVisibility(VISIBLE);
binding.statusMessage.setText(getString(R.string.no_internet));
} else {
ViewUtil.showShortSnackbar(binding.parentLayout, R.string.no_internet);
}
}
/**
* Logs and handles API error scenario
* @param throwable
*/
private void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading images inside a category");
try{
ViewUtil.showShortSnackbar(binding.getRoot(), R.string.error_loading_images);
initErrorView();
}catch (Exception e){
e.printStackTrace();
}
}
/**
* Handles the UI updates for a error scenario
*/
private void initErrorView() {
binding.loadingImagesProgressBar.setVisibility(GONE);
if (gridAdapter == null || gridAdapter.isEmpty()) {
binding.statusMessage.setVisibility(VISIBLE);
binding.statusMessage.setText(getString(R.string.no_images_found));
} else {
binding.statusMessage.setVisibility(GONE);
}
}
/**
* Handles the UI updates when there is no bookmarks
*/
private void initEmptyBookmarkListView() {
binding.loadingImagesProgressBar.setVisibility(GONE);
if (gridAdapter == null || gridAdapter.isEmpty()) {
binding.statusMessage.setVisibility(VISIBLE);
binding.statusMessage.setText(getString(R.string.bookmark_empty));
} else {
binding.statusMessage.setVisibility(GONE);
}
}
/**
* Handles the success scenario
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
* @param collection List of new Media to be displayed
*/
private void handleSuccess(List<Media> collection) {
if (collection == null) {
initErrorView();
return;
}
if (collection.isEmpty()) {
initEmptyBookmarkListView();
return;
}
if (gridAdapter == null) {
setAdapter(collection);
} else {
if (gridAdapter.containsAll(collection)) {
binding.loadingImagesProgressBar.setVisibility(GONE);
binding.statusMessage.setVisibility(GONE);
binding.bookmarkedPicturesList.setVisibility(VISIBLE);
binding.bookmarkedPicturesList.setAdapter(gridAdapter);
return;
}
gridAdapter.addItems(collection);
((BookmarkListRootFragment) getParentFragment()).viewPagerNotifyDataSetChanged();
}
binding.loadingImagesProgressBar.setVisibility(GONE);
binding.statusMessage.setVisibility(GONE);
binding.bookmarkedPicturesList.setVisibility(VISIBLE);
}
/**
* Initializes the adapter with a list of Media objects
* @param mediaList List of new Media to be displayed
*/
private void setAdapter(List<Media> mediaList) {
gridAdapter = new GridViewAdapter(
this.getContext(),
R.layout.layout_category_images,
mediaList
);
binding.bookmarkedPicturesList.setAdapter(gridAdapter);
}
/**
* It return an instance of gridView adapter which helps in extracting media details
* used by the gridView
* @return GridView Adapter
*/
public ListAdapter getAdapter() {
return binding.bookmarkedPicturesList.getAdapter();
}
}

View file

@ -0,0 +1,201 @@
package fr.free.nrw.commons.bookmarks.pictures
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView.OnItemClickListener
import android.widget.ListAdapter
import dagger.android.support.DaggerFragment
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment
import fr.free.nrw.commons.category.GridViewAdapter
import fr.free.nrw.commons.databinding.FragmentBookmarksPicturesBinding
import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
import fr.free.nrw.commons.utils.ViewUtil.showShortSnackbar
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.functions.Consumer
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import javax.inject.Inject
class BookmarkPicturesFragment : DaggerFragment() {
private var gridAdapter: GridViewAdapter? = null
private val compositeDisposable = CompositeDisposable()
private var binding: FragmentBookmarksPicturesBinding? = null
@JvmField
@Inject
var controller: BookmarkPicturesController? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentBookmarksPicturesBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding!!.bookmarkedPicturesList.onItemClickListener =
parentFragment as OnItemClickListener?
initList()
}
override fun onStop() {
super.onStop()
controller!!.stop()
}
override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
binding = null
}
override fun onResume() {
super.onResume()
if (controller!!.needRefreshBookmarkedPictures()) {
binding!!.bookmarkedPicturesList.visibility = View.GONE
gridAdapter?.let {
it.clear()
(parentFragment as BookmarkListRootFragment).viewPagerNotifyDataSetChanged()
}
initList()
}
}
/**
* Checks for internet connection and then initializes
* the recycler view with bookmarked pictures
*/
@SuppressLint("CheckResult")
private fun initList() {
if (!isInternetConnectionEstablished(context)) {
handleNoInternet()
return
}
binding!!.loadingImagesProgressBar.visibility = View.VISIBLE
binding!!.statusMessage.visibility = View.GONE
compositeDisposable.add(
controller!!.loadBookmarkedPictures()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(::handleSuccess, ::handleError)
)
}
/**
* Handles the UI updates for no internet scenario
*/
private fun handleNoInternet() {
binding!!.loadingImagesProgressBar.visibility = View.GONE
if (gridAdapter == null || gridAdapter!!.isEmpty) {
binding!!.statusMessage.visibility = View.VISIBLE
binding!!.statusMessage.text = getString(R.string.no_internet)
} else {
showShortSnackbar(binding!!.parentLayout, R.string.no_internet)
}
}
/**
* Logs and handles API error scenario
* @param throwable
*/
private fun handleError(throwable: Throwable) {
Timber.e(throwable, "Error occurred while loading images inside a category")
try {
showShortSnackbar(binding!!.root, R.string.error_loading_images)
initErrorView()
} catch (e: Exception) {
Timber.e(e)
}
}
/**
* Handles the UI updates for a error scenario
*/
private fun initErrorView() {
binding!!.loadingImagesProgressBar.visibility = View.GONE
if (gridAdapter == null || gridAdapter!!.isEmpty) {
binding!!.statusMessage.visibility = View.VISIBLE
binding!!.statusMessage.text = getString(R.string.no_images_found)
} else {
binding!!.statusMessage.visibility = View.GONE
}
}
/**
* Handles the UI updates when there is no bookmarks
*/
private fun initEmptyBookmarkListView() {
binding!!.loadingImagesProgressBar.visibility = View.GONE
if (gridAdapter == null || gridAdapter!!.isEmpty) {
binding!!.statusMessage.visibility = View.VISIBLE
binding!!.statusMessage.text = getString(R.string.bookmark_empty)
} else {
binding!!.statusMessage.visibility = View.GONE
}
}
/**
* Handles the success scenario
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
* @param collection List of new Media to be displayed
*/
private fun handleSuccess(collection: List<Media>?) {
if (collection == null) {
initErrorView()
return
}
if (collection.isEmpty()) {
initEmptyBookmarkListView()
return
}
if (gridAdapter == null) {
setAdapter(collection)
} else {
if (gridAdapter!!.containsAll(collection)) {
binding!!.loadingImagesProgressBar.visibility = View.GONE
binding!!.statusMessage.visibility = View.GONE
binding!!.bookmarkedPicturesList.visibility = View.VISIBLE
binding!!.bookmarkedPicturesList.adapter = gridAdapter
return
}
gridAdapter!!.addItems(collection)
(parentFragment as BookmarkListRootFragment).viewPagerNotifyDataSetChanged()
}
binding!!.loadingImagesProgressBar.visibility = View.GONE
binding!!.statusMessage.visibility = View.GONE
binding!!.bookmarkedPicturesList.visibility = View.VISIBLE
}
/**
* Initializes the adapter with a list of Media objects
* @param mediaList List of new Media to be displayed
*/
private fun setAdapter(mediaList: List<Media>) {
gridAdapter = GridViewAdapter(
requireContext(),
R.layout.layout_category_images,
mediaList.toMutableList()
)
binding?.let { it.bookmarkedPicturesList.adapter = gridAdapter }
}
/**
* It return an instance of gridView adapter which helps in extracting media details
* used by the gridView
* @return GridView Adapter
*/
fun getAdapter(): ListAdapter? = binding?.bookmarkedPicturesList?.adapter
}

View file

@ -0,0 +1,54 @@
package fr.free.nrw.commons.bookmarks.pictures
import android.database.sqlite.SQLiteDatabase
object BookmarksTable {
const val TABLE_NAME: String = "bookmarks"
const val COLUMN_MEDIA_NAME: String = "media_name"
const val COLUMN_CREATOR: String = "media_creator"
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
val ALL_FIELDS = arrayOf(
COLUMN_MEDIA_NAME,
COLUMN_CREATOR
)
const val DROP_TABLE_STATEMENT: String = "DROP TABLE IF EXISTS $TABLE_NAME"
const val CREATE_TABLE_STATEMENT: String = ("CREATE TABLE $TABLE_NAME (" +
"$COLUMN_MEDIA_NAME STRING PRIMARY KEY, " +
"$COLUMN_CREATOR STRING" +
");")
fun onCreate(db: SQLiteDatabase) =
db.execSQL(CREATE_TABLE_STATEMENT)
fun onDelete(db: SQLiteDatabase) {
db.execSQL(DROP_TABLE_STATEMENT)
onCreate(db)
}
fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) {
if (from == to) {
return
}
if (from < 7) {
// doesn't exist yet
onUpdate(db, from+1, to)
return
}
if (from == 7) {
// table added in version 8
onCreate(db)
onUpdate(db, from+1, to)
return
}
if (from == 8) {
onUpdate(db, from+1, to)
return
}
}
}

View file

@ -9,12 +9,9 @@ import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
import android.text.TextUtils
import androidx.annotation.NonNull
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.data.DBOpenHelper
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
import timber.log.Timber
import javax.inject.Inject
import androidx.core.net.toUri
class CategoryContentProvider : CommonsDaggerContentProvider() {
@ -23,9 +20,6 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
addURI(BuildConfig.CATEGORY_AUTHORITY, "${BASE_PATH}/#", CATEGORIES_ID)
}
@Inject
lateinit var dbOpenHelper: DBOpenHelper
@SuppressWarnings("ConstantConditions")
override fun query(uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
@ -34,7 +28,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
}
val uriType = uriMatcher.match(uri)
val db = dbOpenHelper.readableDatabase
val db = requireDb()
val cursor: Cursor? = when (uriType) {
CATEGORIES -> queryBuilder.query(
@ -58,45 +52,37 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
else -> throw IllegalArgumentException("Unknown URI $uri")
}
cursor?.setNotificationUri(context?.contentResolver, uri)
cursor?.setNotificationUri(requireContext().contentResolver, uri)
return cursor
}
override fun getType(uri: Uri): String? {
return null
}
override fun getType(uri: Uri): String? = null
@SuppressWarnings("ConstantConditions")
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
override fun insert(uri: Uri, contentValues: ContentValues?): Uri {
val uriType = uriMatcher.match(uri)
val sqlDB = dbOpenHelper.writableDatabase
val id: Long
when (uriType) {
CATEGORIES -> {
id = sqlDB.insert(TABLE_NAME, null, contentValues)
id = requireDb().insert(TABLE_NAME, null, contentValues)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
context?.contentResolver?.notifyChange(uri, null)
return Uri.parse("${Companion.BASE_URI}/$id")
requireContext().contentResolver?.notifyChange(uri, null)
return "${BASE_URI}/$id".toUri()
}
@SuppressWarnings("ConstantConditions")
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
// Not implemented
return 0
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0
@SuppressWarnings("ConstantConditions")
override fun bulkInsert(uri: Uri, values: Array<ContentValues>): Int {
Timber.d("Hello, bulk insert! (CategoryContentProvider)")
val uriType = uriMatcher.match(uri)
val sqlDB = dbOpenHelper.writableDatabase
val sqlDB = requireDb()
sqlDB.beginTransaction()
when (uriType) {
CATEGORIES -> {
for (value in values) {
Timber.d("Inserting! %s", value)
sqlDB.insert(TABLE_NAME, null, value)
}
sqlDB.setTransactionSuccessful()
@ -104,7 +90,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
sqlDB.endTransaction()
context?.contentResolver?.notifyChange(uri, null)
requireContext().contentResolver?.notifyChange(uri, null)
return values.size
}
@ -112,17 +98,18 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
override fun update(uri: Uri, contentValues: ContentValues?, selection: String?,
selectionArgs: Array<String>?): Int {
val uriType = uriMatcher.match(uri)
val sqlDB = dbOpenHelper.writableDatabase
val rowsUpdated: Int
when (uriType) {
CATEGORIES_ID -> {
if (TextUtils.isEmpty(selection)) {
val id = uri.lastPathSegment?.toInt()
?: throw IllegalArgumentException("Invalid ID")
rowsUpdated = sqlDB.update(TABLE_NAME,
rowsUpdated = requireDb().update(
TABLE_NAME,
contentValues,
"$COLUMN_ID = ?",
arrayOf(id.toString()))
arrayOf(id.toString())
)
} else {
throw IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID")
@ -130,7 +117,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
}
else -> throw IllegalArgumentException("Unknown URI: $uri with type $uriType")
}
context?.contentResolver?.notifyChange(uri, null)
requireContext().contentResolver?.notifyChange(uri, null)
return rowsUpdated
}
@ -165,13 +152,9 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
"$COLUMN_TIMES_USED INTEGER" +
");"
fun uriForId(id: Int): Uri {
return Uri.parse("${BASE_URI}/$id")
}
fun uriForId(id: Int): Uri = Uri.parse("${BASE_URI}/$id")
fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_TABLE_STATEMENT)
}
fun onCreate(db: SQLiteDatabase) = db.execSQL(CREATE_TABLE_STATEMENT)
fun onDelete(db: SQLiteDatabase) {
db.execSQL(DROP_TABLE_STATEMENT)
@ -200,6 +183,6 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
private const val CATEGORIES = 1
private const val CATEGORIES_ID = 2
private const val BASE_PATH = "categories"
val BASE_URI: Uri = Uri.parse("content://${BuildConfig.CATEGORY_AUTHORITY}/${Companion.BASE_PATH}")
val BASE_URI: Uri = "content://${BuildConfig.CATEGORY_AUTHORITY}/${BASE_PATH}".toUri()
}
}

View file

@ -4,9 +4,8 @@ import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteException
import android.database.sqlite.SQLiteOpenHelper
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable
import fr.free.nrw.commons.category.CategoryDao
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
@ -30,16 +29,16 @@ class DBOpenHelper(
*/
override fun onCreate(db: SQLiteDatabase) {
CategoryDao.Table.onCreate(db)
BookmarkPicturesDao.Table.onCreate(db)
BookmarkItemsDao.Table.onCreate(db)
BookmarksTable.onCreate(db)
BookmarkItemsTable.onCreate(db)
RecentSearchesDao.Table.onCreate(db)
RecentLanguagesDao.Table.onCreate(db)
}
override fun onUpgrade(db: SQLiteDatabase, from: Int, to: Int) {
CategoryDao.Table.onUpdate(db, from, to)
BookmarkPicturesDao.Table.onUpdate(db, from, to)
BookmarkItemsDao.Table.onUpdate(db, from, to)
BookmarksTable.onUpdate(db, from, to)
BookmarkItemsTable.onUpdate(db, from, to)
RecentSearchesDao.Table.onUpdate(db, from, to)
RecentLanguagesDao.Table.onUpdate(db, from, to)
deleteTable(db, CONTRIBUTIONS_TABLE)

View file

@ -1,14 +1,25 @@
package fr.free.nrw.commons.di
import android.content.ContentProvider
import android.database.sqlite.SQLiteDatabase
import fr.free.nrw.commons.data.DBOpenHelper
import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance
import javax.inject.Inject
abstract class CommonsDaggerContentProvider : ContentProvider() {
@JvmField
@Inject
var dbOpenHelper: DBOpenHelper? = null
override fun onCreate(): Boolean {
inject()
return true
}
fun requireDbOpenHelper(): DBOpenHelper = dbOpenHelper!!
fun requireDb(): SQLiteDatabase = requireDbOpenHelper().writableDatabase!!
private fun inject() {
val injection = getInstance(context!!)

View file

@ -1,202 +0,0 @@
package fr.free.nrw.commons.explore.recentsearches;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import javax.inject.Inject;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
import timber.log.Timber;
import static android.content.UriMatcher.NO_MATCH;
import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS;
import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID;
import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.TABLE_NAME;
/**
* This class contains functions for executing queries for
* inserting, searching, deleting, editing recent searches in SqLite DB
**/
public class RecentSearchesContentProvider extends CommonsDaggerContentProvider {
// For URI matcher
private static final int RECENT_SEARCHES = 1;
private static final int RECENT_SEARCHES_ID = 2;
private static final String BASE_PATH = "recent_searches";
public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.RECENT_SEARCH_AUTHORITY + "/" + BASE_PATH);
private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH);
static {
uriMatcher.addURI(BuildConfig.RECENT_SEARCH_AUTHORITY, BASE_PATH, RECENT_SEARCHES);
uriMatcher.addURI(BuildConfig.RECENT_SEARCH_AUTHORITY, BASE_PATH + "/#", RECENT_SEARCHES_ID);
}
public static Uri uriForId(int id) {
return Uri.parse(BASE_URI.toString() + "/" + id);
}
@Inject DBOpenHelper dbOpenHelper;
/**
* This functions executes query for searching recent searches in SqLite DB
**/
@SuppressWarnings("ConstantConditions")
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(TABLE_NAME);
int uriType = uriMatcher.match(uri);
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor;
switch (uriType) {
case RECENT_SEARCHES:
cursor = queryBuilder.query(db, projection, selection, selectionArgs,
null, null, sortOrder);
break;
case RECENT_SEARCHES_ID:
cursor = queryBuilder.query(db,
ALL_FIELDS,
"_id = ?",
new String[]{uri.getLastPathSegment()},
null,
null,
sortOrder
);
break;
default:
throw new IllegalArgumentException("Unknown URI" + uri);
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public String getType(@NonNull Uri uri) {
return null;
}
/**
* This functions executes query for inserting a recentSearch object in SqLite DB
**/
@SuppressWarnings("ConstantConditions")
@Override
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id;
switch (uriType) {
case RECENT_SEARCHES:
id = sqlDB.insert(TABLE_NAME, null, contentValues);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(BASE_URI + "/" + id);
}
/**
* This functions executes query for deleting a recentSearch object in SqLite DB
**/
@Override
public int delete(@NonNull Uri uri, String s, String[] strings) {
int rows;
int uriType = uriMatcher.match(uri);
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
switch (uriType) {
case RECENT_SEARCHES_ID:
Timber.d("Deleting recent searches id %s", uri.getLastPathSegment());
rows = db.delete(RecentSearchesDao.Table.TABLE_NAME,
"_id = ?",
new String[]{uri.getLastPathSegment()}
);
break;
default:
throw new IllegalArgumentException("Unknown URI" + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return rows;
}
/**
* This functions executes query for inserting multiple recentSearch objects in SqLite DB
**/
@SuppressWarnings("ConstantConditions")
@Override
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (RecentSearchesContentProvider)");
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
sqlDB.beginTransaction();
switch (uriType) {
case RECENT_SEARCHES:
for (ContentValues value : values) {
Timber.d("Inserting! %s", value);
sqlDB.insert(TABLE_NAME, null, value);
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
sqlDB.setTransactionSuccessful();
sqlDB.endTransaction();
getContext().getContentResolver().notifyChange(uri, null);
return values.length;
}
/**
* This functions executes query for updating a particular recentSearch object in SqLite DB
**/
@SuppressWarnings("ConstantConditions")
@Override
public int update(@NonNull Uri uri, ContentValues contentValues, String selection,
String[] selectionArgs) {
/*
SQL Injection warnings: First, note that we're not exposing this to the
outside world (exported="false"). Even then, we should make sure to sanitize
all user input appropriately. Input that passes through ContentValues
should be fine. So only issues are those that pass in via concating.
In here, the only concat created argument is for id. It is cast to an int,
and will error out otherwise.
*/
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated;
switch (uriType) {
case RECENT_SEARCHES_ID:
if (TextUtils.isEmpty(selection)) {
int id = Integer.valueOf(uri.getLastPathSegment());
rowsUpdated = sqlDB.update(TABLE_NAME,
contentValues,
COLUMN_ID + " = ?",
new String[]{String.valueOf(id)});
} else {
throw new IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID");
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType);
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsUpdated;
}
}

View file

@ -0,0 +1,174 @@
package fr.free.nrw.commons.explore.recentsearches
import android.content.ContentValues
import android.content.UriMatcher
import android.database.Cursor
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
import androidx.core.net.toUri
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.TABLE_NAME
/**
* This class contains functions for executing queries for
* inserting, searching, deleting, editing recent searches in SqLite DB
*/
class RecentSearchesContentProvider : CommonsDaggerContentProvider() {
/**
* This functions executes query for searching recent searches in SqLite DB
*/
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor {
val queryBuilder = SQLiteQueryBuilder().apply {
tables = TABLE_NAME
}
val uriType = uriMatcher.match(uri)
val cursor = when (uriType) {
RECENT_SEARCHES -> queryBuilder.query(
requireDb(), projection, selection, selectionArgs,
null, null, sortOrder
)
RECENT_SEARCHES_ID -> queryBuilder.query(
requireDb(),
ALL_FIELDS,
"$COLUMN_ID = ?",
arrayOf(uri.lastPathSegment),
null,
null,
sortOrder
)
else -> throw IllegalArgumentException("Unknown URI$uri")
}
cursor.setNotificationUri(requireContext().contentResolver, uri)
return cursor
}
override fun getType(uri: Uri): String? = null
/**
* This functions executes query for inserting a recentSearch object in SqLite DB
*/
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
val uriType = uriMatcher.match(uri)
val id: Long = when (uriType) {
RECENT_SEARCHES -> requireDb().insert(TABLE_NAME, null, contentValues)
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
requireContext().contentResolver.notifyChange(uri, null)
return "$BASE_URI/$id".toUri()
}
/**
* This functions executes query for deleting a recentSearch object in SqLite DB
*/
override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int {
val rows: Int
val uriType = uriMatcher.match(uri)
when (uriType) {
RECENT_SEARCHES_ID -> {
rows = requireDb().delete(
TABLE_NAME,
"_id = ?",
arrayOf(uri.lastPathSegment)
)
}
else -> throw IllegalArgumentException("Unknown URI - $uri")
}
requireContext().contentResolver.notifyChange(uri, null)
return rows
}
/**
* This functions executes query for inserting multiple recentSearch objects in SqLite DB
*/
override fun bulkInsert(uri: Uri, values: Array<ContentValues>): Int {
val uriType = uriMatcher.match(uri)
val sqlDB = requireDb()
sqlDB.beginTransaction()
when (uriType) {
RECENT_SEARCHES -> for (value in values) {
sqlDB.insert(TABLE_NAME, null, value)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
sqlDB.setTransactionSuccessful()
sqlDB.endTransaction()
requireContext().contentResolver.notifyChange(uri, null)
return values.size
}
/**
* This functions executes query for updating a particular recentSearch object in SqLite DB
*/
override fun update(
uri: Uri, contentValues: ContentValues?, selection: String?,
selectionArgs: Array<String>?
): Int {
/*
SQL Injection warnings: First, note that we're not exposing this to the
outside world (exported="false"). Even then, we should make sure to sanitize
all user input appropriately. Input that passes through ContentValues
should be fine. So only issues are those that pass in via concating.
In here, the only concat created argument is for id. It is cast to an int,
and will error out otherwise.
*/
val uriType = uriMatcher.match(uri)
val rowsUpdated: Int
when (uriType) {
RECENT_SEARCHES_ID -> if (selection.isNullOrEmpty()) {
val id = uri.lastPathSegment!!.toInt()
rowsUpdated = requireDb().update(
TABLE_NAME,
contentValues,
"$COLUMN_ID = ?",
arrayOf(id.toString())
)
} else {
throw IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID"
)
}
else -> throw IllegalArgumentException("Unknown URI: $uri with type $uriType")
}
requireContext().contentResolver.notifyChange(uri, null)
return rowsUpdated
}
companion object {
// For URI matcher
private const val RECENT_SEARCHES = 1
private const val RECENT_SEARCHES_ID = 2
private const val BASE_PATH = "recent_searches"
@JvmField
val BASE_URI: Uri = "content://${BuildConfig.RECENT_SEARCH_AUTHORITY}/$BASE_PATH".toUri()
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
init {
uriMatcher.addURI(BuildConfig.RECENT_SEARCH_AUTHORITY, BASE_PATH, RECENT_SEARCHES)
uriMatcher.addURI(BuildConfig.RECENT_SEARCH_AUTHORITY, "$BASE_PATH/#", RECENT_SEARCHES_ID)
}
@JvmStatic
fun uriForId(id: Int): Uri = "$BASE_URI/$id".toUri()
}
}

View file

@ -166,7 +166,7 @@ class MediaDetailPagerFragment : CommonsDaggerSupportFragment(), OnPageChangeLis
val mediaDetailFragment = adapter!!.currentMediaDetailFragment
when (item.itemId) {
R.id.menu_bookmark_current_image -> {
val bookmarkExists = bookmarkDao!!.updateBookmark(bookmark)
val bookmarkExists = bookmarkDao!!.updateBookmark(bookmark!!)
val snackbar = if (bookmarkExists) Snackbar.make(
requireView(),
R.string.add_bookmark,
@ -436,7 +436,7 @@ ${m.pageTitle.canonicalUri}"""
bookmark = Bookmark(
m.filename,
m.getAuthorOrUser(),
BookmarkPicturesContentProvider.uriForName(m.filename)
BookmarkPicturesContentProvider.uriForName(m.filename!!)
)
updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image))
val contributionState = provider.getContributionStateAt(position)

View file

@ -3,17 +3,13 @@ package fr.free.nrw.commons.recentlanguages
import android.content.ContentValues
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
import android.text.TextUtils
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.data.DBOpenHelper
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.COLUMN_NAME
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.TABLE_NAME
import javax.inject.Inject
import timber.log.Timber
import androidx.core.net.toUri
/**
@ -23,27 +19,17 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
companion object {
private const val BASE_PATH = "recent_languages"
val BASE_URI: Uri =
Uri.parse(
"content://${BuildConfig.RECENT_LANGUAGE_AUTHORITY}/$BASE_PATH"
)
val BASE_URI: Uri = "content://${BuildConfig.RECENT_LANGUAGE_AUTHORITY}/$BASE_PATH".toUri()
/**
* Append language code to the base URI
* @param languageCode Code of a language
*/
@JvmStatic
fun uriForCode(languageCode: String): Uri {
return Uri.parse("$BASE_URI/$languageCode")
}
fun uriForCode(languageCode: String): Uri = "$BASE_URI/$languageCode".toUri()
}
@Inject
lateinit var dbOpenHelper: DBOpenHelper
override fun getType(uri: Uri): String? {
return null
}
override fun getType(uri: Uri): String? = null
/**
* Queries the SQLite database for the recently used languages
@ -60,11 +46,12 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
val queryBuilder = SQLiteQueryBuilder()
queryBuilder.tables = TABLE_NAME
val db = dbOpenHelper.readableDatabase
val queryBuilder = SQLiteQueryBuilder().apply {
tables = TABLE_NAME
}
val cursor = queryBuilder.query(
db,
requireDb(),
projection,
selection,
selectionArgs,
@ -72,7 +59,7 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
null,
sortOrder
)
cursor.setNotificationUri(context?.contentResolver, uri)
cursor.setNotificationUri(requireContext().contentResolver, uri)
return cursor
}
@ -89,12 +76,11 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
selection: String?,
selectionArgs: Array<String>?
): Int {
val sqlDB = dbOpenHelper.writableDatabase
val rowsUpdated: Int
if (selection.isNullOrEmpty()) {
val id = uri.lastPathSegment?.toInt()
?: throw IllegalArgumentException("Invalid URI: $uri")
rowsUpdated = sqlDB.update(
rowsUpdated = requireDb().update(
TABLE_NAME,
contentValues,
"$COLUMN_NAME = ?",
@ -104,7 +90,7 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
throw IllegalArgumentException("Parameter `selection` should be empty when updating an ID")
}
context?.contentResolver?.notifyChange(uri, null)
requireContext().contentResolver?.notifyChange(uri, null)
return rowsUpdated
}
@ -114,14 +100,13 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
* @param contentValues : new values to be entered to the database
*/
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
val sqlDB = dbOpenHelper.writableDatabase
val id = sqlDB.insert(
val id = requireDb().insert(
TABLE_NAME,
null,
contentValues
)
context?.contentResolver?.notifyChange(uri, null)
return Uri.parse("$BASE_URI/$id")
requireContext().contentResolver?.notifyChange(uri, null)
return "$BASE_URI/$id".toUri()
}
/**
@ -129,14 +114,12 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
* @param uri : contains the URI for recently used languages
*/
override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int {
val db = dbOpenHelper.readableDatabase
Timber.d("Deleting recently used language %s", uri.lastPathSegment)
val rows = db.delete(
val rows = requireDb().delete(
TABLE_NAME,
"language_code = ?",
arrayOf(uri.lastPathSegment)
)
context?.contentResolver?.notifyChange(uri, null)
requireContext().contentResolver?.notifyChange(uri, null)
return rows
}
}

View file

@ -0,0 +1,32 @@
package fr.free.nrw.commons.utils
import android.annotation.SuppressLint
import android.database.Cursor
fun Cursor.getStringArray(name: String): List<String> =
stringToArray(getString(name))
@SuppressLint("Range")
fun Cursor.getString(name: String): String =
getString(getColumnIndex(name))
/**
* Converts string to List
* @param listString comma separated single string from of list items
* @return List of string
*/
fun stringToArray(listString: String?): List<String> {
if (listString.isNullOrEmpty()) return emptyList();
val elements = listString.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
return listOf(*elements)
}
/**
* Converts string to List
* @param list list of items
* @return string comma separated single string of items
*/
fun arrayToString(list: List<String?>?): String? {
return list?.joinToString(",")
}

View file

@ -427,7 +427,6 @@
<string name="statistics_wikidata_edits">صور عبر \"الأماكن المجاورة\"</string>
<string name="level">المستوى %d</string>
<string name="profile_withLevel">%s (المستوى %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">الصور المرفوعة</string>
<string name="image_reverts">لم يتم إرجاع الصور</string>
<string name="images_used_by_wiki">الصور المستخدمة</string>

View file

@ -0,0 +1,233 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Authors:
* Blackisnewyellow
* Mansur
* Ерней
* Ильгиз
* Ильнар
-->
<resources>
<string name="commons_facebook">Викиҗыентыкның Facebook бите</string>
<string name="commons_github">Викиҗыентыкның гитхабтагы башлангыч кодлары</string>
<string name="commons_logo">Викиҗыентык логотипы</string>
<string name="commons_website">Викиҗыентыкның веб-сайты</string>
<string name="exit_location_picker">Урынны сайлау тәрәзәсеннән чыгарга</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="nearby_filter_search">Күренешне эзлә</string>
<string name="nearby_filter_state">Урын халәте</string>
<string name="appwidget_img">Көн сурәте</string>
<plurals name="uploads_pending_notification_indicator">
<item quantity="one">%1$d файл йөкләнә</item>
<item quantity="few">%1$d файл йөкләнә</item>
<item quantity="many">%1$d файл йөкләнә</item>
<item quantity="other">%1$d файл йөкләнә</item>
</plurals>
<plurals name="contributions_subtitle">
<item quantity="one">(%1$d)</item>
<item quantity="few">(%1$d)</item>
<item quantity="many">(%1$d)</item>
<item quantity="other">(%1$d)</item>
</plurals>
<string name="starting_uploads">Йөкләү башлана</string>
<plurals name="starting_multiple_uploads">
<item quantity="one">%d йөкләүне эшкәртү</item>
<item quantity="few">%d йөкләүне эшкәртү</item>
<item quantity="many">%d йөкләүне эшкәртү</item>
<item quantity="other">%d йөкләүне эшкәртү</item>
</plurals>
<plurals name="multiple_uploads_title">
<item quantity="one">%d йөкләү</item>
<item quantity="few">%d йөкләү</item>
<item quantity="many">%d йөкләү</item>
<item quantity="other">%d йөкләү</item>
</plurals>
<string name="navigation_item_explore">Тикшерергә</string>
<string name="preference_category_appearance">Күренеш</string>
<string name="preference_category_general">Гомуми</string>
<string name="preference_category_feedback">Кире элемтә</string>
<string name="preference_category_privacy">Шәхсилек</string>
<string name="app_name">Викиҗыентык</string>
<string name="menu_settings">Көйләнмәләр</string>
<string name="intent_share_upload_label">Викиҗыентыкка йөкләргә</string>
<string name="upload_in_progress">Йөкләү бара...</string>
<string name="username">Кулланучы исеме</string>
<string name="password">Серсүз</string>
<string name="login_credential">Commons Beta хисапъязмагызга керегез</string>
<string name="login">Керү</string>
<string name="forgot_password">Серсүзне оныттыгызмы?</string>
<string name="signup">Теркәлү</string>
<string name="logging_in_title">Керү бара…</string>
<string name="logging_in_message">Бераз көтегезче...</string>
<string name="updating_caption_title">Язмалар һәм тасвирламалар яңартыла</string>
<string name="updating_caption_message">Бераз көтегезче...</string>
<string name="login_success">Керү уңышлы башкарылды!</string>
<string name="login_failed">Системага кереп булмады!</string>
<string name="upload_failed">Файл табылмады. Башка файлны кулланып карагызчы.</string>
<string name="retry_limit_reached">Кабатлаулар саны нык артып китте! Йөкләүне кире кагыгыз яки яңадан кабатлагыз.</string>
<string name="unrestricted_battery_mode">Батәринең оптималь кулланылышын сүндерергәме?</string>
<string name="uploading_started">Йөкләү башланды!</string>
<string name="upload_completed_notification_title">%1$s төялде!</string>
<string name="upload_completed_notification_text">Төялгән файлыгызны карау өчен басыгыз</string>
<string name="upload_progress_notification_title_start">Файлны төяү: %s</string>
<string name="upload_progress_notification_title_in_progress">%1$s йөкләнә</string>
<string name="upload_progress_notification_title_finishing">%1$s йөкләве тәмамлана</string>
<string name="upload_failed_notification_title">%1$s йөкләнә алмады</string>
<string name="upload_paused_notification_title">%1$s йөкләнүе туктатылып калды</string>
<string name="upload_failed_notification_subtitle">Карау өчен басыгыз</string>
<string name="upload_paused_notification_subtitle">Карау өчен басыгыз</string>
<string name="title_activity_contributions">Минем соңгы төяүләрем</string>
<string name="contribution_state_failed">Төяү хатасы</string>
<string name="contribution_state_starting">Төяү бара</string>
<string name="menu_from_gallery">Галереядан</string>
<string name="menu_from_camera">Фото ясарга</string>
<string name="menu_nearby">Якында</string>
<string name="provider_contributions">Минем төяүләрем</string>
<string name="menu_copy_link">Сылтаманы күчереп ал</string>
<string name="menu_link_copied">Сылтама алмашу буферына күчереп алынды</string>
<string name="menu_share">Уртаклашырга</string>
<string name="menu_view_file_page">Файл битен күрсәтергә</string>
<string name="share_title_hint">Язма (Зарур)</string>
<string name="add_caption_toast">Бу файлның исемен билгеләгезче</string>
<string name="share_description_hint">Тасвирлама</string>
<string name="share_caption_hint">Язма</string>
<string name="login_failed_network">Кереп булмый - челтәр хатасы</string>
<string name="login_failed_blocked">Гафу итегез, мондый исемле кулланучы Викиҗыентыкта блокланган булган.</string>
<string name="login_failed_generic">Системага кереп булмады!</string>
<string name="share_upload_button">Төяү</string>
<string name="multiple_share_base_title">Бу файллар төркеме өчен исемне кертегез</string>
<string name="menu_upload_single">Төя</string>
<string name="categories_search_text_hint">Төркемнәрне сайла</string>
<string name="menu_save_categories">Сакла</string>
<string name="refresh_button">Яңарту</string>
<string name="display_list_button">Исемлек</string>
<string name="contributions_subtitle_zero">Төялгән файллар юк әле!</string>
<string name="categories_activity_title">Төркемнәр</string>
<string name="title_activity_settings">Көйләнмәләр</string>
<string name="title_activity_signup">Теркәл</string>
<string name="title_activity_featured_images">Сакланган сурәтләр</string>
<string name="title_activity_custom_selector">Кулланучы селекторы</string>
<string name="title_activity_category_details">Төркем</string>
<string name="title_activity_review">Тикшер</string>
<string name="menu_about">Кушымта турында</string>
<string name="about_privacy_policy">Яшеренлек сәясәте</string>
<string name="about_credits">Төзүчеләр</string>
<string name="title_activity_about">Кушымта турында</string>
<string name="no_email_client">Почта клиенты урнаштырылмаган</string>
<string name="provider_categories">Күптән түгел кулланылган төркемнәр</string>
<string name="waiting_first_sync">Беренче синхронлаштыруны көтү...</string>
<string name="no_uploads_yet">Сез әле бер сурәтне дә төямәдегез.</string>
<string name="menu_retry_upload">Кабатла</string>
<string name="menu_cancel_upload">Кире как</string>
<string name="menu_download">Иңләргә</string>
<string name="preference_license">Гадәттәге рөхсәтнамә килешүе</string>
<string name="use_previous">Алдагы исемне һәм тамвирламаны куллан</string>
<string name="preference_theme">Күренеш</string>
<string name="license_name_cc_by_sa_four"> Attribution-ShareAlike 4.0</string>
<string name="license_name_cc_by_four"> Attribution 4.0</string>
<string name="license_name_cc_by_sa"> Attribution-ShareAlike 3.0</string>
<string name="license_name_cc_by"> Attribution 3.0</string>
<string name="tutorial_1_text">Викиҗыентыктагы сурәтләр Википедиянең күпчелек күләмендә кулланыла.</string>
<string name="tutorial_1_subtext">Төягән сурәтләрегез бөтен дөньядагы кешеләргә белем алырга ярдәм итә ала!</string>
<string name="tutorial_2_text">Зинһар, бары тик үзегез ясаган яки төшергән сурәтләрне генә төягез:</string>
<string name="tutorial_2_subtext_1">Табигать объектлары (мәсәлән, чәчәкләр, хайваннар, таулар)</string>
<string name="tutorial_2_subtext_2">Файдалы җисемнәр (мәсәлән, велосипедлар, вокзаллар)</string>
<string name="tutorial_2_subtext_3">Билгеле кешеләр (мәсәлән, мэрыгыз, сез очраткан олимпияче-спортсменнар)</string>
<string name="tutorial_3_text">Зинһар, боларны ТӨЯМӘГЕЗ:</string>
<string name="tutorial_3_subtext_1">Селфилар яки дусларыгызның фотолары</string>
<string name="tutorial_3_subtext_2">Интернеттан иңләгән фотолар</string>
<string name="tutorial_3_subtext_3">Ирекле булмаган программаларның скриншотлары</string>
<string name="tutorial_4_text">Төяү мисалы:</string>
<string name="tutorial_4_subtext_1">Атамасы: Сидней опера театры</string>
<string name="welcome_wikipedia_text">Сурәтләрегезне төягез. Википедия мәкаләләрен кызыклырак ясарга булышыгыз!</string>
<string name="welcome_wikipedia_subtext">Википедиядә кулланылучы сурәтләр Викиҗыентыкта саклана.</string>
<string name="welcome_copyright_text">Төягән сурәтләрегез бөтен дөньядагы кешеләргә белем алырга ярдәм итә ала.</string>
<string name="welcome_copyright_subtext">Авторлык хокуклары белән сакланган материалларны, мәсәлән, Интернетта табылган плакатлар, китап тышлары һ. б. ш. сурәтләрне кулланмаска тырышыгыз.</string>
<string name="welcome_final_text">Бу сезгә аңлашыламы?</string>
<string name="welcome_final_button_text">Әйе!</string>
<string name="welcome_help_button_text">Тулырак мәгълүмат</string>
<string name="detail_panel_cats_label">Төркемнәр</string>
<string name="detail_panel_cats_loading">Йөкләнә...</string>
<string name="detail_panel_cats_none">Бернәрсә дә сайланмаган</string>
<string name="detail_caption_empty">Язмасыз</string>
<string name="detail_description_empty">Тасвирламасыз</string>
<string name="detail_discussion_empty">Фикерләшү юк</string>
<string name="detail_license_empty">Билгесез лицензия</string>
<string name="menu_refresh">Яңарту</string>
<string name="storage_permission_title">Тышкы саклагычны куллану рөхсәтен сорау</string>
<string name="location_permission_title">Локациягезне табуны сорау</string>
<string name="in_app_camera_location_permission_title">Кушымтада ясалган фотолар өчен локацияне яздыр</string>
<string name="ok">Ярар</string>
<string name="warning">Игътибар</string>
<string name="duplicate_file_name">Кабатлана торган файл исеме табылды</string>
<string name="upload">Төя</string>
<string name="yes">Әйе</string>
<string name="no">Юк</string>
<string name="media_detail_caption">Язма</string>
<string name="media_detail_title">Исем</string>
<string name="media_detail_depiction">Тасвирланган феномен</string>
<string name="media_detail_description">Тасвирлама</string>
<string name="media_detail_discussion">Фикер алышу</string>
<string name="media_detail_author">Автор</string>
<string name="media_detail_uploader">Төяп куючы</string>
<string name="media_detail_uploaded_date">Төяү вакыты</string>
<string name="media_detail_license">Лицензия</string>
<string name="media_detail_coordinates">Координатлар</string>
<string name="media_detail_coordinates_empty">Билгеләнмәгән</string>
<string name="become_a_tester_title">Бета-тестерга әйләнергә</string>
<string name="logout_verification">Сез чыннан да чыгарга телисезме?</string>
<string name="no_subcategory_found">Астөркемнәр табылмады.</string>
<string name="cancel">Баш тарту</string>
<string name="navigation_drawer_open">Ачарга</string>
<string name="navigation_drawer_close">Ябарга</string>
<string name="navigation_item_home">Баш бит</string>
<string name="navigation_item_upload">Төяргә</string>
<string name="navigation_item_nearby">Якын-тирәдә</string>
<string name="navigation_item_about">Кушымта турында</string>
<string name="navigation_item_settings">Көйләнмәләр</string>
<string name="navigation_item_feedback">Кире элемтә</string>
<string name="navigation_item_feedback_github">GitHub аркылы кире элемтә</string>
<string name="navigation_item_logout">Чыгарга</string>
<string name="navigation_item_info">Кулланма</string>
<string name="navigation_item_notification">Белдермәләр</string>
<string name="navigation_item_review">Тикшерү</string>
<string name="no_description_found">тасвирлама табылмады</string>
<string name="nearby_info_menu_commons_article">Файлның Викиҗыентыктагы бите</string>
<string name="nearby_info_menu_wikidata_article">Викимәгълүмат элементы</string>
<string name="nearby_info_menu_wikipedia_article">Википедия мәкаләсе</string>
<string name="upload_problem_image_dark">Сурәт артык караңгы.</string>
<string name="upload_problem_image_duplicate">Бу сурәт Викиҗыентыкта бар инде.</string>
<string name="upload_problem_different_geolocation">Бу сурәт башка урында ясалган булган.</string>
<string name="upload_problem_do_you_continue">Барыбер бу сурәтне төяргә телисезме?</string>
<string name="upload_connection_error_alert_title">Тоташтыру хатасы</string>
<string name="upload_problem_image">Сурәттә читенлекләр табылды</string>
<string name="use_external_storage">Кушымтада ясалган фотоларны сакла</string>
<string name="use_external_storage_summary">Җайланма камерасы ярдәмендә эшләнгән фотоларны җайланмада сакла</string>
<string name="null_url">Хата! Сылтама табылмады</string>
<string name="nominate_deletion">Бетерергә тәкъдим ит</string>
<string name="nominated_for_deletion">Бу сурәтне бетерергә тәкъдим ителде.</string>
<string name="nominated_see_more">Күбрәк мәгълүмат өчен битне карагыз</string>
<string name="skip_login">Калдырып үт</string>
<string name="navigation_item_login">Керү</string>
<string name="wikicode_copied">Викитекст алмашу буферына күчереп алынды</string>
<string name="nearby_directions">Юнәлешләр</string>
<string name="nearby_wikidata">Викимәгълүмат</string>
<string name="nearby_wikipedia">Википедия</string>
<string name="nearby_commons">Викиҗыентык</string>
<string name="about_rate_us">Безне бәяләгез</string>
<string name="about_faq">Еш бирелгән сораулар (ЕБС, ЧаВо)</string>
<string name="rotate">Бору</string>
<string name="storage_permissions_denied">Саклауга рөхсәтләр кире кагылды</string>
<string name="unable_to_share_upload_item">Бу объект белән уртаклашу мөмкин түгел</string>
</resources>

View file

@ -271,6 +271,7 @@
<string name="preference_author_name_toggle_summary">При качването използвайте персонализирано авторско име вместо потребителското си име</string>
<string name="preference_author_name">Персонализирано авторско име</string>
<string name="nearby_fragment">Наблизо</string>
<string name="notifications">Известия</string>
<string name="read_notifications">Известия (прочетени)</string>
<string name="list_sheet">Списък</string>
<string name="next">Следваща</string>

View file

@ -379,7 +379,7 @@
<string name="wikipedia_instructions_step_3">3. Хьайн суьртана догӀу йаззаман дакъа лаха</string>
<string name="wikipedia_instructions_step_4">4. «Хийца» иконкин тӀетаӀайе (къоламах тера йу) хӀокху декъана</string>
<string name="wikipedia_instructions_step_5">5. Вики-код йогӀучу метте дӀайазйе</string>
<string name="wikipedia_instructions_step_7">7. Йаззам дӀайазбе</string>
<string name="wikipedia_instructions_step_7">7. Йаззам дӀайазбан</string>
<string name="copy_wikicode_to_clipboard">Вики-код буфер чу копийе</string>
<string name="pause">пауза</string>
<string name="resume">кхидӀа</string>

View file

@ -408,7 +408,6 @@
<string name="statistics_wikidata_edits">Obrázky přes „Místa v okolí“</string>
<string name="level">Úroveň %d</string>
<string name="profile_withLevel">%s (úroveň %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Nahrané obrázky</string>
<string name="image_reverts">Nerevertované obrázky</string>
<string name="images_used_by_wiki">Použitých obrázků</string>

View file

@ -378,7 +378,6 @@
<string name="statistics_wikidata_edits">Billeder via \"Steder i nærheden\"</string>
<string name="level">Niveau %d</string>
<string name="profile_withLevel">%s (Niveau %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Uploadede billeder</string>
<string name="image_reverts">Billeder, som ikke er blevet trukket tilbage</string>
<string name="images_used_by_wiki">Billeder brugt</string>

View file

@ -412,7 +412,6 @@
<string name="statistics_wikidata_edits">Bilder über „Orte in der Nähe“</string>
<string name="level">Level %d</string>
<string name="profile_withLevel">%s (Level %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Hochgeladene Bilder</string>
<string name="image_reverts">Bilder nicht zurückgesetzt</string>
<string name="images_used_by_wiki">Verwendete Bilder</string>

View file

@ -391,7 +391,6 @@
<string name="statistics_wikidata_edits">Εικόνες μέσω «Κοντινά μέρη»</string>
<string name="level">Επίπεδο %d</string>
<string name="profile_withLevel">%s (Επίπεδο %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Εικόνες που μεταφορτώθηκαν</string>
<string name="image_reverts">Εικόνες που δεν ανεστράφησαν</string>
<string name="images_used_by_wiki">Εικόνες που χρησιμοποιήθηκαν</string>

View file

@ -423,7 +423,6 @@
<string name="statistics_wikidata_edits">Imágenes vía \"Sitios Cercanos\"</string>
<string name="level">Nivel %d</string>
<string name="profile_withLevel">%s (Nivel %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Imágenes subidas</string>
<string name="image_reverts">Imágenes no revertidas</string>
<string name="images_used_by_wiki">Imágenes utilizadas</string>

View file

@ -363,7 +363,6 @@
<string name="statistics_wikidata_edits">Kuvia läheltä</string>
<string name="level">Taso %d</string>
<string name="profile_withLevel">%s (taso %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Kuvia tallennettu</string>
<string name="image_reverts">Kuvia ei palautettu</string>
<string name="images_used_by_wiki">Kuvia käytetty</string>

View file

@ -418,7 +418,6 @@
<string name="statistics_wikidata_edits">Images par «Lieux à proximité»</string>
<string name="level">Niveau %d</string>
<string name="profile_withLevel">%s (niveau %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Images téléversées</string>
<string name="image_reverts">Images non annulées</string>
<string name="images_used_by_wiki">Images utilisées</string>

View file

@ -235,7 +235,7 @@
<string name="navigation_item_about">A proposito</string>
<string name="navigation_item_settings">Parametros</string>
<string name="navigation_item_feedback">Commentario</string>
<string name="navigation_item_feedback_github">Retroaction per GitHub</string>
<string name="navigation_item_feedback_github">Commentarios per GitHub</string>
<string name="navigation_item_logout">Clauder session</string>
<string name="navigation_item_info">Tutorial</string>
<string name="navigation_item_notification">Notificationes</string>
@ -366,7 +366,6 @@
<string name="statistics_wikidata_edits">Imagines via “Locos a proximitate”</string>
<string name="level">Nivello %d</string>
<string name="profile_withLevel">%s (Nivello %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Imagines incargate</string>
<string name="image_reverts">Imagines non revertite</string>
<string name="images_used_by_wiki">Imagines usate</string>
@ -719,8 +718,8 @@
<string name="device_model">Modello del apparato</string>
<string name="device_name">Nomine del apparato</string>
<string name="network_type">Typo de rete</string>
<string name="thanks_feedback">Gratias pro dar retroaction</string>
<string name="error_feedback">Error durante le invio del retroaction</string>
<string name="thanks_feedback">Gratias pro dar commentario</string>
<string name="error_feedback">Error durante le invio del commentario</string>
<string name="enter_description">Que es tu commentario?</string>
<string name="your_feedback">Tu commentario</string>
<string name="mark_as_not_for_upload">Marcar como non a incargar</string>
@ -784,7 +783,7 @@
<string name="is_at_a_different_place_wikidata">%1$s se trova in un altere loco.</string>
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">%1$s es in un altere loco. Per favor specifica le loco correcte hic infra, e si possibile, indica le latitude e longitude correcte.</string>
<string name="other_problem_or_information_please_explain_below">Altere problema o information (per favor explica hic infra).</string>
<string name="feedback_destination_note">Tu retroaction apparera sur le sequente pagina 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">Tu commentario apparera sur le sequente pagina wiki: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Mobile app/Feedback&lt;/a&gt;</string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">Es tu secur de voler cancellar tote le incargamentos?</string>
<string name="cancelling_all_the_uploads">Cancella tote le incargamentos…</string>
<string name="uploads">Incargamentos</string>

View file

@ -369,7 +369,6 @@
<string name="statistics_wikidata_edits">Imaji tra \"Loki Vicina\"</string>
<string name="level">Nivelo %d</string>
<string name="profile_withLevel">%s (Nivelo %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Imaji sendita</string>
<string name="image_reverts">Imaji ne reversionita</string>
<string name="images_used_by_wiki">Imaji uzita</string>

View file

@ -392,7 +392,6 @@
<string name="statistics_wikidata_edits">Immagini tramite \"Luoghi nelle vicinanze\"</string>
<string name="level">Livello %d</string>
<string name="profile_withLevel">%s (Livello %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Immagini caricate</string>
<string name="image_reverts">Immagini non ripristinate</string>
<string name="images_used_by_wiki">Immagini utilizzate</string>

View file

@ -403,7 +403,6 @@
<string name="statistics_wikidata_edits">תמונות דרך \"מקומות בסביבה\"</string>
<string name="level">רמה %d</string>
<string name="profile_withLevel">%s (רמה %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">תמונות שהועלו</string>
<string name="image_reverts">תמונות שלא שוחזרו</string>
<string name="images_used_by_wiki">תמונות בשימוש</string>

View file

@ -370,7 +370,6 @@
<string name="statistics_wikidata_edits">სურათები „ახლომდებარე ადგილები“ -დან</string>
<string name="level">დონე %d</string>
<string name="profile_withLevel">%s (დონე %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">სურათები ატვირთულია</string>
<string name="image_reverts">სურათები არ დაბრუნებულა</string>
<string name="images_used_by_wiki">სურათები გამოიყენება</string>

View file

@ -222,6 +222,7 @@
<string name="media_detail_description">설명</string>
<string name="media_detail_discussion">토론</string>
<string name="media_detail_author">저자</string>
<string name="media_detail_uploader">올린 사람</string>
<string name="media_detail_uploaded_date">올린 날짜</string>
<string name="media_detail_license">라이선스</string>
<string name="media_detail_coordinates">좌표</string>
@ -380,7 +381,6 @@
<string name="statistics_wikidata_edits">\"주변 장소\" 경유 이미지</string>
<string name="level">레벨 %d</string>
<string name="profile_withLevel">%s (레벨 %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">사진 업로드됨</string>
<string name="images_used_by_wiki">사용된 이미지</string>
<string name="achievements_share_message">친구와 성과를 공유하세요!</string>
@ -539,6 +539,8 @@
<string name="title_for_media">미디어</string>
<string name="title_for_child_classes">자식 클래스</string>
<string name="title_for_parent_classes">상위 클래스</string>
<string name="title_for_subcategories">하위 분류</string>
<string name="title_for_parent_categories">상위 분류</string>
<string name="upload_nearby_place_found_title">주변 장소 발견</string>
<string name="upload_nearby_place_found_description_plural">%1$s의 사진이 맞습니까?</string>
<string name="upload_nearby_place_found_description_singular">%1$s의 사진이 맞습니까?</string>
@ -583,8 +585,10 @@
<string name="menu_set_avatar">아바타로 설정</string>
<string name="leaderboard_yearly">매년</string>
<string name="leaderboard_weekly">매주</string>
<string name="leaderboard_all_time">항상</string>
<string name="leaderboard_upload">업로드</string>
<string name="leaderboard_nearby">근처</string>
<string name="leaderboard_used">사용됨</string>
<string name="leaderboard_my_rank_button_text">내 순위</string>
<string name="limited_connection_mode">제한된 연결 모드</string>
<string name="statistics_quality">고품질 사진</string>

View file

@ -370,7 +370,6 @@
<string name="statistics_wikidata_edits">\"Джууукъдагъы Джерле\" юсю бла суратла</string>
<string name="level">Дараджа %d</string>
<string name="profile_withLevel">%s (Дараджа %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Суратла Джюклендиле</string>
<string name="image_reverts">Суратла Кери Алынмадыла</string>
<string name="images_used_by_wiki">Суратла Хайырландыла</string>

View file

@ -308,7 +308,6 @@
<string name="statistics_featured">Bemierkenswäert Biller</string>
<string name="level">Niveau %d</string>
<string name="profile_withLevel">%s (Niveau %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Eropgeluede Biller</string>
<string name="image_reverts">Biller net zréckgesat</string>
<string name="images_used_by_wiki">Benotzte Biller</string>

View file

@ -354,7 +354,6 @@
<string name="statistics_wikidata_edits">Vaizdai per „Netoliese esančios vietos“</string>
<string name="level">Lygis %d</string>
<string name="profile_withLevel">%s (%s lygis)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Vaizdai įkelti</string>
<string name="image_reverts">Paveikslėliai negrąžinti</string>
<string name="images_used_by_wiki">Naudoti vaizdai</string>

View file

@ -372,7 +372,6 @@
<string name="statistics_wikidata_edits">Слики преку „Околни места“</string>
<string name="level">Степен %d</string>
<string name="profile_withLevel">%s (Степен %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Подигнати слики</string>
<string name="image_reverts">Неоткажани слики</string>
<string name="images_used_by_wiki">Употребени слики</string>

View file

@ -394,7 +394,6 @@
<string name="statistics_wikidata_edits">Afbeeldingen via \"Plaatsen in de buurt\"</string>
<string name="level">Niveau %d</string>
<string name="profile_withLevel">%s (Niveau %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Geüploade afbeeldingen</string>
<string name="image_reverts">Afbeeldingen niet teruggedraaid</string>
<string name="images_used_by_wiki">Gebruikte afbeeldingen</string>

View file

@ -410,7 +410,6 @@
<string name="statistics_wikidata_edits">Obrazy za pośrednictwem \"Pobliskie miejsca\"</string>
<string name="level">Poziom %d</string>
<string name="profile_withLevel">%s (Poziom %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Przesłane obrazy</string>
<string name="image_reverts">Nie wycofane obrazy</string>
<string name="images_used_by_wiki">Wykorzystane obrazy</string>

View file

@ -370,7 +370,6 @@
<string name="statistics_wikidata_edits">Plance për \"Pòst davzin\"</string>
<string name="level">Livel %d</string>
<string name="profile_withLevel">%s (Livel %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Plance carià</string>
<string name="image_reverts">Plance nen anulà</string>
<string name="images_used_by_wiki">Plance dovrà</string>

View file

@ -10,42 +10,42 @@
-->
<resources>
<string name="commons_facebook">د خونديځ فيسبوک پاڼه</string>
<string name="commons_github">خونديځ ګيټهوب سرچينه کوډ</string>
<string name="commons_github">خونديځ گيټ‌هاب سرچينه کوډ</string>
<string name="commons_logo">خونديځ نښان</string>
<string name="commons_website">خونديځ وېبپاڼه</string>
<string name="exit_location_picker">له ځای ټاکونکي وتل</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="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_language_description">ژبې څرگنداوی</string>
<string name="row_item_caption">نيونگ</string>
<string name="show_captions_description">سپيناوی</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="nearby_filter_search">لټون ليد</string>
<string name="nearby_filter_toggle">پورته بدلول</string>
<string name="nearby_filter_search">لټون کتنه</string>
<string name="nearby_filter_state">ځای حالت</string>
<string name="appwidget_img">ورځې انځور</string>
<plurals name="uploads_pending_notification_indicator">
<item quantity="one">%1$d دوتنه پورته کول</item>
<item quantity="other"> %1$d دوتنې پورته کول</item>
<item quantity="one">%1$d دوتنه راپورته‌کول</item>
<item quantity="other"> %1$d دوتنې راپورته‌کول</item>
</plurals>
<plurals name="contributions_subtitle">
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
</plurals>
<string name="starting_uploads">پورته کولو پيل</string>
<string name="starting_uploads">راپورته‌کول پيلول</string>
<plurals name="starting_multiple_uploads">
<item quantity="one">جريان %d پورته کول</item>
<item quantity="other">پورته کولو %d جريان</item>
<item quantity="one">راپورته‌کولو %d بهير</item>
<item quantity="other">راپورته‌کولو %d بهير</item>
</plurals>
<plurals name="multiple_uploads_title">
<item quantity="one">%d upload</item>
<item quantity="other">%d پورته کول</item>
<item quantity="other">%d راپورته‌کول</item>
</plurals>
<plurals name="share_license_summary">
<item quantity="one">دا انځور به د منښتليک %1$s لاندې وي</item>
@ -100,7 +100,7 @@
<string name="contribution_state_queued">لږ</string>
<string name="contribution_state_failed">نابريال شو</string>
<string name="contribution_state_in_progress">%1$d%% بشپړ</string>
<string name="contribution_state_starting">د برسېرېدلو په حال کې…</string>
<string name="contribution_state_starting">راپورته‌کېږي</string>
<string name="menu_from_gallery">له انځورتون څخه</string>
<string name="menu_from_camera">انځور اخيستل</string>
<string name="menu_nearby">نژدې</string>
@ -114,8 +114,8 @@
<string name="share_description_hint">څرگنداوی</string>
<string name="share_caption_hint">نيونگ</string>
<string name="login_failed_network">غونډال ته ننوتنه ناشونې ده - د جال پاتې راتلنه</string>
<string name="login_failed_throttled">ډیری ناکامه هڅې. لطفا څو دقیقې وروسته بیا هڅه وکړئ.</string>
<string name="login_failed_blocked">بخښنه غواړو، په دي کارن د کامنز لخوا بنديز ولګول شو</string>
<string name="login_failed_throttled">ډېرې ناکامه هڅې. لطفا څو دقیقې وروسته بیا هڅه وکړئ.</string>
<string name="login_failed_blocked">بخښنه غواړو، په دې کارن د کامنز لخوا بنديز ولگول شو</string>
<string name="login_failed_2fa_needed">تاسو بايد خپل دوه لامليز تاييد کوډ ورکړئ.</string>
<string name="login_failed_email_auth_needed">ستاسو برېښليک پتې ته د ننوتلو تاييد کوډ لېږل شوی دی. مهرباني وکړئ د ننوتلو لپاره کوډ ورکړئ.</string>
<string name="login_failed_generic">غونډال کې ننوتنه نابريالۍ شوه</string>
@ -187,9 +187,9 @@
<string name="detail_panel_cats_loading">رابرسېرېږي...</string>
<string name="detail_panel_cats_none">هېڅ هم نه دی ټاکل شوی</string>
<string name="detail_caption_empty">هيڅ نيونگ نشته</string>
<string name="detail_description_empty">څرگندونه نشته</string>
<string name="detail_description_empty">څرگنداوی نشته</string>
<string name="detail_discussion_empty">هيڅ شننه نشته</string>
<string name="detail_license_empty">نامعلوم جواز</string>
<string name="detail_license_empty">ناجوت منښتليک</string>
<string name="menu_refresh">تازه کول</string>
<string name="storage_permission_title">د زېرمه کولو د پرېښولي غوښتنه کول</string>
<string name="read_storage_permission_rationale">اړينه پرېښولی: بهرنۍ زېرمه ولولئ. کاريال ستاسو انځورتونه ته پرته له دې لاسرسی نشي موندلی.</string>
@ -211,7 +211,7 @@
<string name="media_detail_coordinates">کورډيناټونه</string>
<string name="media_detail_coordinates_empty">هېڅ نه دي چمتو شوي</string>
<string name="become_a_tester_title">ازمېښتي ازمايښتگر شئ</string>
<string name="welcome_image_welcome_wikipedia">ويکيپېډياښه راغلئ</string>
<string name="welcome_image_welcome_wikipedia">ويکيپېډيا ته ښه راغلئ</string>
<string name="welcome_image_welcome_copyright">لمېسل‌رښتې هرکلی</string>
<string name="cancel">ناگارل</string>
<string name="navigation_drawer_open">پرانېستل</string>
@ -237,6 +237,7 @@
<string name="nearby_wikidata">ويکي‌اومتوک</string>
<string name="nearby_wikipedia">ويکيپېډيا</string>
<string name="nearby_commons">خونديځ</string>
<string name="about_rate_us">و مو ارزوئ</string>
<string name="about_faq">ډ‌ځ‌پ</string>
<string name="user_guide">کارن لارښود</string>
<string name="welcome_skip_button">ښوونې پرېښودل</string>
@ -250,6 +251,7 @@
<string name="about_translate_proceed">پرمخ‌ځه</string>
<string name="about_translate_cancel">ناگارل</string>
<string name="retry">بيا هڅه‌کول</string>
<string name="showcase_view_whole_nearby_activity">دا تاسو ته نږدې ځايونه دي چې د ويکيپېډيا ليکنې يې د ښودلو لپاره انځورونو ته اړتيا لري.\n\n\'د دې ځای پلټل\' باندې کليک کولو سره نخچه تاله کوي او د هغه ځای شاوخوا نږدې سيمې لټون پيلول.</string>
<string name="showcase_view_needs_photo">داځای انځور ته اړتيا لري.</string>
<string name="showcase_view_has_photo">دا ځای لادمخه انځور لري.</string>
<string name="showcase_view_no_longer_exists">دا ځای نور شتون نه لري.</string>
@ -265,5 +267,39 @@
<string name="search_recent_header">وروستۍ پلټنې:</string>
<string name="provider_searches">وروستۍ پلټل شوې پوښتنې</string>
<string name="provider_recent_languages">وروستۍ ژبې پوښتنې</string>
<string name="search_tab_title_media">رسنۍ</string>
<string name="search_tab_title_categories">وېشنيزې</string>
<string name="search_tab_title_depictions">توکي</string>
<string name="explore_tab_title_featured">ټاکلې</string>
<string name="explore_tab_title_mobile">موبايل له لارې راپورته‌شوی</string>
<string name="explore_tab_title_map">نخشه</string>
<string name="successful_wikidata_edit">انځور په ویکي‌اومتوک کې %1$s ته ورگډ شو!</string>
<string name="ends_on">پای ته رسېږي په:</string>
<string name="display_campaigns">ټاکنيزې‌سيالۍ ښکاره‌کول</string>
<string name="display_campaigns_explanation">روانې ټاکنيزې‌سيالۍ وگورئ</string>
<string name="option_allow">پرېښول</string>
<string name="option_dismiss">تړل</string>
<string name="open_document_photo_picker_title">د لاسوند پربنسټ انځور راخيستونکی کارول</string>
<string name="error_processing_image">د انځور پروسس‌کولو پرمهال تېروتنه رامنځته شوه. مهرباني وکړئ بيا هڅه وکړئ!</string>
<string name="getting_edit_token">د سمون لپاره نښه ترلاسه کول</string>
<string name="check_category_adding_template">د وېشنيزې سم‌کتنې لپاره کينډۍ ورگډول</string>
<string name="check_category_notification_title">د %1$s وېشنيزې سم‌کتنې لپاره غوښتنه‌کول</string>
<string name="check_category_edit_summary">وېشنيزې سم‌کتنې غوښتنه‌کول</string>
<string name="check_category_success_title">وېشنيزې سم‌کتنې غوښتنه وشوه</string>
<string name="check_category_failure_title">د وېشنيزې سم‌کتنې غوښتنه کار نه کوي</string>
<string name="check_category_success_message">د %1$s وېشنيزې سم‌کتنې لپاره غوښتنه وشوه</string>
<string name="check_category_failure_message">د %1$s سم‌کتنې لپاره غوښتنه نشي کېدای</string>
<string name="check_category_toast">د %1$s وېشنيزې سم‌کتنې لپاره غوښتنه‌کول</string>
<string name="nominate_for_deletion_done">وشو</string>
<string name="send_thank_success_title">مننې لېږل: برياليتوب</string>
<string name="send_thank_success_message">%1$s ته په برياليتوب سره مننه ولېږل شوه</string>
<string name="send_thank_failure_message">%1$s ته د مننې لېږلو کې پاتې راغی</string>
<string name="send_thank_failure_title">مننې لېږل: ناکامي</string>
<string name="send_thank_toast">%1$s لپاره د مننې لېږل</string>
<string name="review_copyright">ايا دا ډ لمېسل‌رښتو سره سم دی؟</string>
<string name="review_category">ايا دا په سمه توگه ډلبندي شوي دي؟</string>
<string name="review_spam">ايا دا د منلو وړ دي؟</string>
<string name="review_thanks">ايا تاسو غواړئ له ونډه‌وال نه مننه وکړئ؟</string>
<string name="review_spam_explanation">که دا انځور ټولگټی نه وي؛ نو ړنگېدو ته د نوماندولو لپاره يې په نه کليک وکړئ.</string>
<string name="account">گڼون</string>
</resources>

View file

@ -173,6 +173,7 @@
<string name="delete">{{Identical|Delete}}</string>
<string name="statistics">Èstatistik</string>
<string name="statistics_featured">To see the correct translation for your language, please go to https://commons.wikimedia.org/wiki/Commons:Featured_pictures and select your language in \"This project page in other languages\".</string>
<string name="profile_withoutLevel">{{optional}}</string>
<string name="nearby_fragment">{{Identical|Nearby}}</string>
<string name="next">Refers to the next \'\'\'step\'\'\' in the uploading process.</string>
<string name="previous">Refers to the previous \'\'\'step\'\'\' in the uploading process.</string>

View file

@ -430,7 +430,6 @@
<string name="statistics_wikidata_edits">Изображения мест поблизости</string>
<string name="level">Уровень %d</string>
<string name="profile_withLevel">%s (Уровень %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Загружено изображений</string>
<string name="image_reverts">Изображения, которые не откатывались</string>
<string name="images_used_by_wiki">Использовано изображений</string>

View file

@ -387,7 +387,6 @@
<string name="statistics_wikidata_edits">Slike iz »Bližnji kraji«</string>
<string name="level">Raven %d</string>
<string name="profile_withLevel">%s (raven %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Naložene slike</string>
<string name="image_reverts">Nevrnjene slike</string>
<string name="images_used_by_wiki">Uporabljene slike</string>

View file

@ -219,6 +219,7 @@
<string name="media_detail_description">Beskrivning</string>
<string name="media_detail_discussion">Diskussion</string>
<string name="media_detail_author">Skapare</string>
<string name="media_detail_uploader">Uppladdare</string>
<string name="media_detail_uploaded_date">Uppladdningsdatum</string>
<string name="media_detail_license">Licens</string>
<string name="media_detail_coordinates">Koordinater</string>
@ -378,7 +379,6 @@
<string name="statistics_wikidata_edits">Bilder via \"Platser i närheten\"</string>
<string name="level">Nivå %d</string>
<string name="profile_withLevel">%s (Nivå %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Uppladdade bilder</string>
<string name="image_reverts">Bilder som inte har återställts</string>
<string name="images_used_by_wiki">Bilder som används</string>
@ -421,7 +421,7 @@
<string name="deletion_reason_bad_for_my_privacy">Jag insåg att den är dålig för mitt privatliv</string>
<string name="deletion_reason_no_longer_want_public">Jag ändrade mig, jag vill inte längre att den ska vara synlig offentligt</string>
<string name="deletion_reason_not_interesting">Tyvärr, denna bild är inte intressant för en encyklopedi</string>
<string name="uploaded_by_myself" fuzzy="true">Laddades upp av mig den %1$s och används i %2$d artiklar.</string>
<string name="uploaded_by_myself">Laddades upp av mig den %1$s och används i minst %2$d artiklar.</string>
<string name="no_uploads">Välkommen till Commons!\n\nLadda upp din första mediafil genom att trycka på knappen för att lägga till.</string>
<string name="no_categories_selected">Inga kategorier har valts</string>
<string name="no_categories_selected_warning_desc">Bilder utan kategorier används sällan. Är du säker på att du vill fortsätta utan att välja kategorier?</string>
@ -586,6 +586,8 @@
<string name="title_for_media">MEDIA</string>
<string name="title_for_child_classes">UNDERORDNADE KLASSER</string>
<string name="title_for_parent_classes">ÖVERORDNADE KLASSER</string>
<string name="title_for_subcategories">UNDERKATEGORIER</string>
<string name="title_for_parent_categories">ÖVERORDNADE KATEGORIER</string>
<string name="upload_nearby_place_found_title">Hittade platser i närheten</string>
<string name="upload_nearby_place_found_description_plural">Föreställer de här bilderna %1$s?</string>
<string name="upload_nearby_place_found_description_singular">Är detta en bild på %1$s?</string>
@ -761,7 +763,7 @@
<string name="permissions_are_required_for_functionality">Behörigheter krävs för funktionalitet</string>
<string name="learn_how_to_write_a_useful_description">Lär dig hur du skriver en användbar beskrivning</string>
<string name="learn_how_to_write_a_useful_caption">Lär dig hur du skriver en användbar bildtext</string>
<string name="see_your_achievements" fuzzy="true">Se dina prestationer</string>
<string name="see_your_achievements">Se dina prestationer</string>
<string name="edit_image">Redigera bild</string>
<string name="edit_location">Redigera plats</string>
<string name="location_updated">Plats uppdaterades!</string>
@ -830,4 +832,5 @@
<string name="show_in_nearby">Visa i \"I närheten\"</string>
<string name="image_tag_line_created_and_uploaded_by">Skapades och laddades upp av: %1$s</string>
<string name="image_tag_line_created_by_and_uploaded_by">Skapad av %1$s och laddades upp av %2$s</string>
<string name="nominated_for_deletion_btn">Nominerad för radering</string>
</resources>

View file

@ -401,7 +401,6 @@
<string name="statistics_wikidata_edits">\"Yakındaki Yerler\"den Resimler</string>
<string name="level">Seviye %d</string>
<string name="profile_withLevel">%s (Seviye %s)</string>
<string name="profile_withoutLevel">%s (%s)</string>
<string name="images_uploaded">Resimler Yüklendi</string>
<string name="image_reverts">Resimler Geri Alınmadı</string>
<string name="images_used_by_wiki">Resimler Kullanıldı</string>

View file

@ -288,7 +288,7 @@ class BookmarkListRootFragmentUnitTest {
@Test
@Throws(Exception::class)
fun testOnItemClick() {
fragment.onItemClick(null, null, 0, 0)
fragment.onItemClick(null, view, 0, 0)
verify(childFragmentManager).beginTransaction()
verify(childFragmentTransaction).commit()
verify(childFragmentManager).executePendingTransactions()

View file

@ -2,12 +2,20 @@ package fr.free.nrw.commons.bookmarks
import android.content.Context
import androidx.fragment.app.FragmentManager
import fr.free.nrw.commons.TestCommonsApplication
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [21], application = TestCommonsApplication::class)
@LooperMode(LooperMode.Mode.PAUSED)
class BookmarksPagerAdapterTests {
@Mock
private lateinit var bookmarksPagerAdapter: BookmarksPagerAdapter

View file

@ -2,15 +2,23 @@ package fr.free.nrw.commons.bookmarks
import android.content.Context
import androidx.fragment.app.FragmentManager
import fr.free.nrw.commons.TestCommonsApplication
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
/**
* BookmarksPagerAdapter when user is not loggedIn.
*/
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [21], application = TestCommonsApplication::class)
@LooperMode(LooperMode.Mode.PAUSED)
class LoggedOutBookmarksPagerAdapterTests {
@Mock
private lateinit var bookmarksPagerAdapter: BookmarksPagerAdapter

View file

@ -21,7 +21,7 @@ class BookmarkItemsControllerTest {
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
whenever(bookmarkDao!!.allBookmarksItems)
whenever(bookmarkDao!!.getAllBookmarksItems())
.thenReturn(mockBookmarkList)
}

View file

@ -18,20 +18,20 @@ import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_CATEGORIES_DESCRIPTION_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_CATEGORIES_NAME_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_CATEGORIES_THUMBNAIL_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_DESCRIPTION
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_ID
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_IMAGE
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_INSTANCE_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_IS_SELECTED
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_NAME
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.CREATE_TABLE_STATEMENT
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.DROP_TABLE_STATEMENT
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.onCreate
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.onDelete
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.onUpdate
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_DESCRIPTION_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_NAME_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_THUMBNAIL_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_DESCRIPTION
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_ID
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IMAGE
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_INSTANCE_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IS_SELECTED
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_NAME
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.CREATE_TABLE_STATEMENT
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.DROP_TABLE_STATEMENT
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.onCreate
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.onDelete
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.onUpdate
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import org.junit.Assert
@ -135,7 +135,7 @@ class BookmarkItemsDaoTest {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull()))
.thenReturn(createCursor(14))
val result = testObject.allBookmarksItems
val result = testObject.getAllBookmarksItems()
Assert.assertEquals(14, (result.size))
}
@ -145,20 +145,20 @@ class BookmarkItemsDaoTest {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow(
RemoteException(""),
)
testObject.allBookmarksItems
testObject.getAllBookmarksItems()
}
@Test
fun getAllItemsBookmarksReturnsEmptyList_emptyCursor() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull()))
.thenReturn(createCursor(0))
Assert.assertTrue(testObject.allBookmarksItems.isEmpty())
Assert.assertTrue(testObject.getAllBookmarksItems().isEmpty())
}
@Test
fun getAllItemsBookmarksReturnsEmptyList_nullCursor() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null)
Assert.assertTrue(testObject.allBookmarksItems.isEmpty())
Assert.assertTrue(testObject.getAllBookmarksItems().isEmpty())
}
@Test
@ -167,7 +167,7 @@ class BookmarkItemsDaoTest {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor)
whenever(mockCursor.moveToFirst()).thenReturn(false)
testObject.allBookmarksItems
testObject.getAllBookmarksItems()
verify(mockCursor).close()
}

View file

@ -88,7 +88,7 @@ class BookmarkItemsFragmentUnitTest {
context = ApplicationProvider.getApplicationContext()
OkHttpConnectionFactory.CLIENT = createTestClient()
val activity = Robolectric.buildActivity(ProfileActivity::class.java).create().get()
fragment = BookmarkItemsFragment.newInstance()
fragment = BookmarkItemsFragment()
val fragmentManager: FragmentManager = activity.supportFragmentManager
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
fragmentTransaction.add(fragment, null)

View file

@ -19,14 +19,14 @@ import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.bookmarks.models.Bookmark
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.BASE_URI
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.COLUMN_CREATOR
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.COLUMN_MEDIA_NAME
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.CREATE_TABLE_STATEMENT
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.DROP_TABLE_STATEMENT
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.onCreate
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.onDelete
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.onUpdate
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.Companion.BASE_URI
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_CREATOR
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_MEDIA_NAME
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.CREATE_TABLE_STATEMENT
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.DROP_TABLE_STATEMENT
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.onCreate
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.onDelete
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.onUpdate
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@ -84,7 +84,7 @@ class BookmarkPictureDaoTest {
fun getAllBookmarks() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(14))
var result = testObject.allBookmarks
var result = testObject.getAllBookmarks()
assertEquals(14, (result.size))
}
@ -92,19 +92,19 @@ class BookmarkPictureDaoTest {
@Test(expected = RuntimeException::class)
fun getAllBookmarksTranslatesExceptions() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow(RemoteException(""))
testObject.allBookmarks
testObject.getAllBookmarks()
}
@Test
fun getAllBookmarksReturnsEmptyList_emptyCursor() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(0))
assertTrue(testObject.allBookmarks.isEmpty())
assertTrue(testObject.getAllBookmarks().isEmpty())
}
@Test
fun getAllBookmarksReturnsEmptyList_nullCursor() {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null)
assertTrue(testObject.allBookmarks.isEmpty())
assertTrue(testObject.getAllBookmarks().isEmpty())
}
@Test
@ -113,7 +113,7 @@ class BookmarkPictureDaoTest {
whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor)
whenever(mockCursor.moveToFirst()).thenReturn(false)
testObject.allBookmarks
testObject.getAllBookmarks()
verify(mockCursor).close()
}

View file

@ -35,7 +35,7 @@ class BookmarkPicturesControllerTest {
fun setup() {
MockitoAnnotations.initMocks(this)
val mockMedia = mockMedia
whenever(bookmarkDao!!.allBookmarks)
whenever(bookmarkDao!!.getAllBookmarks())
.thenReturn(mockBookmarkList)
whenever(
mediaClient!!.getMedia(

View file

@ -88,7 +88,7 @@ class BookmarkPicturesFragmentUnitTests {
context = ApplicationProvider.getApplicationContext()
OkHttpConnectionFactory.CLIENT = createTestClient()
val activity = Robolectric.buildActivity(ProfileActivity::class.java).create().get()
fragment = BookmarkPicturesFragment.newInstance()
fragment = BookmarkPicturesFragment()
val fragmentManager: FragmentManager = activity.supportFragmentManager
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
fragmentTransaction.add(fragment, null)
@ -156,13 +156,13 @@ class BookmarkPicturesFragmentUnitTests {
val method: Method =
BookmarkPicturesFragment::class.java.getDeclaredMethod("setAdapter", List::class.java)
method.isAccessible = true
method.invoke(fragment, mediaList)
method.invoke(fragment, emptyList<Media>())
}
@Test
@Throws(Exception::class)
fun testGetAdapter() {
fragment.adapter
fragment.getAdapter()
}
@Test

View file

@ -29,7 +29,7 @@ import fr.free.nrw.commons.category.CategoryDao.Table.DROP_TABLE_STATEMENT
import fr.free.nrw.commons.category.CategoryDao.Table.onCreate
import fr.free.nrw.commons.category.CategoryDao.Table.onDelete
import fr.free.nrw.commons.category.CategoryDao.Table.onUpdate
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.uriForId
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull

View file

@ -18,8 +18,8 @@ import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.explore.models.RecentSearch
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.BASE_URI
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.uriForId
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.BASE_URI
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_LAST_USED