diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fb776920e..d56a874b5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -232,12 +232,6 @@ android:exported="false" android:label="@string/provider_bookmarks" android:syncable="false" /> - loadFavoritesLocations() { - return bookmarkLocationDao.getAllBookmarksLocations(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsController.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsController.kt new file mode 100644 index 000000000..81ec80214 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsController.kt @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.bookmarks.locations + +import fr.free.nrw.commons.nearby.Place +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class BookmarkLocationsController @Inject constructor( + private val bookmarkLocationDao: BookmarkLocationsDao +) { + + /** + * Load bookmarked locations from the database. + * @return a list of Place objects. + */ + suspend fun loadFavoritesLocations(): List = + bookmarkLocationDao.getAllBookmarksLocationsPlace() +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java deleted file mode 100644 index fe4f603f4..000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java +++ /dev/null @@ -1,313 +0,0 @@ -package fr.free.nrw.commons.bookmarks.locations; - -import android.annotation.SuppressLint; -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.os.RemoteException; - -import androidx.annotation.NonNull; - -import fr.free.nrw.commons.nearby.NearbyController; -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Provider; - -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.nearby.Label; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.nearby.Sitelinks; -import timber.log.Timber; - -import static fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider.BASE_URI; - -public class BookmarkLocationsDao { - - private final Provider clientProvider; - - @Inject - public BookmarkLocationsDao(@Named("bookmarksLocation") Provider clientProvider) { - this.clientProvider = clientProvider; - } - - /** - * Find all persisted locations bookmarks on database - * - * @return list of Place - */ - @NonNull - public List getAllBookmarksLocations() { - List items = new ArrayList<>(); - Cursor cursor = null; - ContentProviderClient db = clientProvider.get(); - try { - cursor = db.query( - BookmarkLocationsContentProvider.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 place in bookmarks table in order to insert or delete it - * - * @param bookmarkLocation : Place object - * @return is Place now fav ? - */ - public boolean updateBookmarkLocation(Place bookmarkLocation) { - boolean bookmarkExists = findBookmarkLocation(bookmarkLocation); - if (bookmarkExists) { - deleteBookmarkLocation(bookmarkLocation); - NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, false); - } else { - addBookmarkLocation(bookmarkLocation); - NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, true); - } - return !bookmarkExists; - } - - /** - * Add a Place to bookmarks table - * - * @param bookmarkLocation : Place to add - */ - private void addBookmarkLocation(Place bookmarkLocation) { - ContentProviderClient db = clientProvider.get(); - try { - db.insert(BASE_URI, toContentValues(bookmarkLocation)); - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - /** - * Delete a Place from bookmarks table - * - * @param bookmarkLocation : Place to delete - */ - private void deleteBookmarkLocation(Place bookmarkLocation) { - ContentProviderClient db = clientProvider.get(); - try { - db.delete(BookmarkLocationsContentProvider.uriForName(bookmarkLocation.name), null, null); - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - /** - * Find a Place from database based on its name - * - * @param bookmarkLocation : Place to find - * @return boolean : is Place in database ? - */ - public boolean findBookmarkLocation(Place bookmarkLocation) { - Cursor cursor = null; - ContentProviderClient db = clientProvider.get(); - try { - cursor = db.query( - BookmarkLocationsContentProvider.BASE_URI, - Table.ALL_FIELDS, - Table.COLUMN_NAME + "=?", - new String[]{bookmarkLocation.name}, - 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 - Place fromCursor(final Cursor cursor) { - final LatLng location = new LatLng(cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LAT)), - cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LONG)), 1F); - - final Sitelinks.Builder builder = new Sitelinks.Builder(); - builder.setWikipediaLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_WIKIPEDIA_LINK))); - builder.setWikidataLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_WIKIDATA_LINK))); - builder.setCommonsLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_COMMONS_LINK))); - - return new Place( - cursor.getString(cursor.getColumnIndex(Table.COLUMN_LANGUAGE)), - cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)), - Label.fromText((cursor.getString(cursor.getColumnIndex(Table.COLUMN_LABEL_TEXT)))), - cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)), - location, - cursor.getString(cursor.getColumnIndex(Table.COLUMN_CATEGORY)), - builder.build(), - cursor.getString(cursor.getColumnIndex(Table.COLUMN_PIC)), - Boolean.parseBoolean(cursor.getString(cursor.getColumnIndex(Table.COLUMN_EXISTS))) - ); - } - - private ContentValues toContentValues(Place bookmarkLocation) { - ContentValues cv = new ContentValues(); - cv.put(BookmarkLocationsDao.Table.COLUMN_NAME, bookmarkLocation.getName()); - cv.put(BookmarkLocationsDao.Table.COLUMN_LANGUAGE, bookmarkLocation.getLanguage()); - cv.put(BookmarkLocationsDao.Table.COLUMN_DESCRIPTION, bookmarkLocation.getLongDescription()); - cv.put(BookmarkLocationsDao.Table.COLUMN_CATEGORY, bookmarkLocation.getCategory()); - cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_TEXT, bookmarkLocation.getLabel()!=null ? bookmarkLocation.getLabel().getText() : ""); - cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_ICON, bookmarkLocation.getLabel()!=null ? bookmarkLocation.getLabel().getIcon() : null); - cv.put(BookmarkLocationsDao.Table.COLUMN_WIKIPEDIA_LINK, bookmarkLocation.siteLinks.getWikipediaLink().toString()); - cv.put(BookmarkLocationsDao.Table.COLUMN_WIKIDATA_LINK, bookmarkLocation.siteLinks.getWikidataLink().toString()); - cv.put(BookmarkLocationsDao.Table.COLUMN_COMMONS_LINK, bookmarkLocation.siteLinks.getCommonsLink().toString()); - cv.put(BookmarkLocationsDao.Table.COLUMN_LAT, bookmarkLocation.location.getLatitude()); - cv.put(BookmarkLocationsDao.Table.COLUMN_LONG, bookmarkLocation.location.getLongitude()); - cv.put(BookmarkLocationsDao.Table.COLUMN_PIC, bookmarkLocation.pic); - cv.put(BookmarkLocationsDao.Table.COLUMN_EXISTS, bookmarkLocation.exists.toString()); - return cv; - } - - public static class Table { - public static final String TABLE_NAME = "bookmarksLocations"; - - static final String COLUMN_NAME = "location_name"; - static final String COLUMN_LANGUAGE = "location_language"; - static final String COLUMN_DESCRIPTION = "location_description"; - static final String COLUMN_LAT = "location_lat"; - static final String COLUMN_LONG = "location_long"; - static final String COLUMN_CATEGORY = "location_category"; - static final String COLUMN_LABEL_TEXT = "location_label_text"; - static final String COLUMN_LABEL_ICON = "location_label_icon"; - static final String COLUMN_IMAGE_URL = "location_image_url"; - static final String COLUMN_WIKIPEDIA_LINK = "location_wikipedia_link"; - static final String COLUMN_WIKIDATA_LINK = "location_wikidata_link"; - static final String COLUMN_COMMONS_LINK = "location_commons_link"; - static final String COLUMN_PIC = "location_pic"; - static final String COLUMN_EXISTS = "location_exists"; - - // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. - public static final String[] ALL_FIELDS = { - COLUMN_NAME, - COLUMN_LANGUAGE, - COLUMN_DESCRIPTION, - COLUMN_CATEGORY, - COLUMN_LABEL_TEXT, - COLUMN_LABEL_ICON, - COLUMN_LAT, - COLUMN_LONG, - COLUMN_IMAGE_URL, - COLUMN_WIKIPEDIA_LINK, - COLUMN_WIKIDATA_LINK, - COLUMN_COMMONS_LINK, - COLUMN_PIC, - COLUMN_EXISTS, - }; - - 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 PRIMARY KEY," - + COLUMN_LANGUAGE + " STRING," - + COLUMN_DESCRIPTION + " STRING," - + COLUMN_CATEGORY + " STRING," - + COLUMN_LABEL_TEXT + " STRING," - + COLUMN_LABEL_ICON + " INTEGER," - + COLUMN_LAT + " DOUBLE," - + COLUMN_LONG + " DOUBLE," - + COLUMN_IMAGE_URL + " STRING," - + COLUMN_WIKIPEDIA_LINK + " STRING," - + COLUMN_WIKIDATA_LINK + " STRING," - + COLUMN_COMMONS_LINK + " STRING," - + COLUMN_PIC + " STRING," - + COLUMN_EXISTS + " 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(final SQLiteDatabase db, int from, final int to) { - Timber.d("bookmarksLocations db is updated from:"+from+", to:"+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 < 10) { - from++; - onUpdate(db, from, to); - return; - } - if (from == 10) { - //This is safe, and can be called clean, as we/I do not remember the appropriate version for this - //We are anyways switching to room, these things won't be necessary then - try { - db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_pic STRING;"); - }catch (SQLiteException exception){ - Timber.e(exception);// - } - return; - } - if (from >= 12) { - try { - db.execSQL( - "ALTER TABLE bookmarksLocations ADD COLUMN location_destroyed STRING;"); - } catch (SQLiteException exception) { - Timber.e(exception); - } - } - if (from >= 13){ - try { - db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_language STRING;"); - } catch (SQLiteException exception){ - Timber.e(exception); - } - } - if (from >= 14){ - try { - db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_exists STRING;"); - } catch (SQLiteException exception){ - Timber.e(exception); - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.kt new file mode 100644 index 000000000..2fa65b2d9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.kt @@ -0,0 +1,65 @@ +package fr.free.nrw.commons.bookmarks.locations + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import fr.free.nrw.commons.nearby.NearbyController +import fr.free.nrw.commons.nearby.Place + +/** + * DAO for managing bookmark locations in the database. + */ +@Dao +abstract class BookmarkLocationsDao { + + /** + * Adds or updates a bookmark location in the database. + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract suspend fun addBookmarkLocation(bookmarkLocation: BookmarksLocations) + + /** + * Fetches all bookmark locations from the database. + */ + @Query("SELECT * FROM bookmarks_locations") + abstract suspend fun getAllBookmarksLocations(): List + + /** + * Checks if a bookmark location exists by name. + */ + @Query("SELECT EXISTS (SELECT 1 FROM bookmarks_locations WHERE location_name = :name)") + abstract suspend fun findBookmarkLocation(name: String): Boolean + + /** + * Deletes a bookmark location from the database. + */ + @Delete + abstract suspend fun deleteBookmarkLocation(bookmarkLocation: BookmarksLocations) + + /** + * Adds or removes a bookmark location and updates markers. + * @return `true` if added, `false` if removed. + */ + suspend fun updateBookmarkLocation(bookmarkLocation: Place): Boolean { + val exists = findBookmarkLocation(bookmarkLocation.name) + + if (exists) { + deleteBookmarkLocation(bookmarkLocation.toBookmarksLocations()) + NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, false) + } else { + addBookmarkLocation(bookmarkLocation.toBookmarksLocations()) + NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, true) + } + + return !exists + } + + /** + * Fetches all bookmark locations as `Place` objects. + */ + suspend fun getAllBookmarksLocationsPlace(): List { + return getAllBookmarksLocations().map { it.toPlace() } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java deleted file mode 100644 index f5ce556c4..000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java +++ /dev/null @@ -1,137 +0,0 @@ -package fr.free.nrw.commons.bookmarks.locations; - -import android.Manifest.permission; -import android.content.Intent; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.activity.result.ActivityResultCallback; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearLayoutManager; -import dagger.android.support.DaggerFragment; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.ContributionController; -import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions; -import fr.free.nrw.commons.nearby.fragments.PlaceAdapter; -import java.util.List; -import java.util.Map; -import javax.inject.Inject; -import kotlin.Unit; - -public class BookmarkLocationsFragment extends DaggerFragment { - - public FragmentBookmarksLocationsBinding binding; - - @Inject BookmarkLocationsController controller; - @Inject ContributionController contributionController; - @Inject BookmarkLocationsDao bookmarkLocationDao; - @Inject CommonPlaceClickActions commonPlaceClickActions; - private PlaceAdapter adapter; - - private final ActivityResultLauncher cameraPickLauncherForResult = - registerForActivityResult(new StartActivityForResult(), - result -> { - contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> { - contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks); - }); - }); - - private final ActivityResultLauncher galleryPickLauncherForResult = - registerForActivityResult(new StartActivityForResult(), - result -> { - contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> { - contributionController.onPictureReturnedFromGallery(result, requireActivity(), callbacks); - }); - }); - - private ActivityResultLauncher inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { - @Override - public void onActivityResult(Map result) { - boolean areAllGranted = true; - for(final boolean b : result.values()) { - areAllGranted = areAllGranted && b; - } - - if (areAllGranted) { - contributionController.locationPermissionCallback.onLocationPermissionGranted(); - } else { - if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { - contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); - } else { - contributionController.locationPermissionCallback.onLocationPermissionDenied(getActivity().getString(R.string.in_app_camera_location_permission_denied)); - } - } - } - }); - - /** - * Create an instance of the fragment with the right bundle parameters - * @return an instance of the fragment - */ - public static BookmarkLocationsFragment newInstance() { - return new BookmarkLocationsFragment(); - } - - @Override - public View onCreateView( - @NonNull LayoutInflater inflater, - ViewGroup container, - Bundle savedInstanceState - ) { - binding = FragmentBookmarksLocationsBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - binding.loadingImagesProgressBar.setVisibility(View.VISIBLE); - binding.listView.setLayoutManager(new LinearLayoutManager(getContext())); - adapter = new PlaceAdapter(bookmarkLocationDao, - place -> Unit.INSTANCE, - (place, isBookmarked) -> { - adapter.remove(place); - return Unit.INSTANCE; - }, - commonPlaceClickActions, - inAppCameraLocationPermissionLauncher, - galleryPickLauncherForResult, - cameraPickLauncherForResult - ); - binding.listView.setAdapter(adapter); - } - - @Override - public void onResume() { - super.onResume(); - initList(); - } - - /** - * Initialize the recycler view with bookmarked locations - */ - private void initList() { - List places = controller.loadFavoritesLocations(); - adapter.setItems(places); - binding.loadingImagesProgressBar.setVisibility(View.GONE); - if (places.size() <= 0) { - 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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.kt new file mode 100644 index 000000000..4ab21462c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.kt @@ -0,0 +1,161 @@ +package fr.free.nrw.commons.bookmarks.locations + +import android.Manifest.permission +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import dagger.android.support.DaggerFragment +import fr.free.nrw.commons.R +import fr.free.nrw.commons.contributions.ContributionController +import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding +import fr.free.nrw.commons.filepicker.FilePicker +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions +import fr.free.nrw.commons.nearby.fragments.PlaceAdapter +import kotlinx.coroutines.launch +import javax.inject.Inject + + +class BookmarkLocationsFragment : DaggerFragment() { + + private var binding: FragmentBookmarksLocationsBinding? = null + + @Inject lateinit var controller: BookmarkLocationsController + @Inject lateinit var contributionController: ContributionController + @Inject lateinit var bookmarkLocationDao: BookmarkLocationsDao + @Inject lateinit var commonPlaceClickActions: CommonPlaceClickActions + + private lateinit var inAppCameraLocationPermissionLauncher: + ActivityResultLauncher> + private lateinit var adapter: PlaceAdapter + + private val cameraPickLauncherForResult = + registerForActivityResult(StartActivityForResult()) { result -> + contributionController.handleActivityResultWithCallback( + requireActivity(), + object: FilePicker.HandleActivityResult { + override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) { + contributionController.onPictureReturnedFromCamera( + result, + requireActivity(), + callbacks + ) + } + } + ) + } + + private val galleryPickLauncherForResult = + registerForActivityResult(StartActivityForResult()) { result -> + contributionController.handleActivityResultWithCallback( + requireActivity(), + object: FilePicker.HandleActivityResult { + override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) { + contributionController.onPictureReturnedFromGallery( + result, + requireActivity(), + callbacks + ) + } + } + ) + } + + companion object { + fun newInstance(): BookmarkLocationsFragment { + return BookmarkLocationsFragment() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentBookmarksLocationsBinding.inflate(inflater, container, false) + return binding?.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding?.loadingImagesProgressBar?.visibility = View.VISIBLE + binding?.listView?.layoutManager = LinearLayoutManager(context) + + inAppCameraLocationPermissionLauncher = + registerForActivityResult(RequestMultiplePermissions()) { result -> + val areAllGranted = result.values.all { it } + + if (areAllGranted) { + contributionController.locationPermissionCallback?.onLocationPermissionGranted() + } else { + if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { + contributionController.handleShowRationaleFlowCameraLocation( + requireActivity(), + inAppCameraLocationPermissionLauncher, + cameraPickLauncherForResult + ) + } else { + contributionController.locationPermissionCallback + ?.onLocationPermissionDenied( + getString(R.string.in_app_camera_location_permission_denied) + ) + } + } + } + + adapter = PlaceAdapter( + bookmarkLocationDao, + lifecycleScope, + { }, + { place, _ -> + adapter.remove(place) + }, + commonPlaceClickActions, + inAppCameraLocationPermissionLauncher, + galleryPickLauncherForResult, + cameraPickLauncherForResult + ) + binding?.listView?.adapter = adapter + } + + override fun onResume() { + super.onResume() + initList() + } + + fun initList() { + var places: List + if(view != null) { + viewLifecycleOwner.lifecycleScope.launch { + places = controller.loadFavoritesLocations() + updateUIList(places) + } + } + } + + private fun updateUIList(places: List) { + adapter.items = places + binding?.loadingImagesProgressBar?.visibility = View.GONE + if (places.isEmpty()) { + binding?.statusMessage?.text = getString(R.string.bookmark_empty) + binding?.statusMessage?.visibility = View.VISIBLE + } else { + binding?.statusMessage?.visibility = View.GONE + } + } + + override fun onDestroy() { + super.onDestroy() + // Make sure to null out the binding to avoid memory leaks + binding = null + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsViewModel.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsViewModel.kt new file mode 100644 index 000000000..b22723c0f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsViewModel.kt @@ -0,0 +1,15 @@ +package fr.free.nrw.commons.bookmarks.locations + +import androidx.lifecycle.ViewModel +import fr.free.nrw.commons.nearby.Place +import kotlinx.coroutines.flow.Flow + +class BookmarkLocationsViewModel( + private val bookmarkLocationsDao: BookmarkLocationsDao +): ViewModel() { + +// fun getAllBookmarkLocations(): List { +// return bookmarkLocationsDao.getAllBookmarksLocationsPlace() +// } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarksLocations.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarksLocations.kt new file mode 100644 index 000000000..66d670169 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarksLocations.kt @@ -0,0 +1,72 @@ +package fr.free.nrw.commons.bookmarks.locations + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.nearby.Label +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.nearby.Sitelinks + +@Entity(tableName = "bookmarks_locations") +data class BookmarksLocations( + @PrimaryKey @ColumnInfo(name = "location_name") val locationName: String, + @ColumnInfo(name = "location_language") val locationLanguage: String, + @ColumnInfo(name = "location_description") val locationDescription: String, + @ColumnInfo(name = "location_lat") val locationLat: Double, + @ColumnInfo(name = "location_long") val locationLong: Double, + @ColumnInfo(name = "location_category") val locationCategory: String, + @ColumnInfo(name = "location_label_text") val locationLabelText: String, + @ColumnInfo(name = "location_label_icon") val locationLabelIcon: Int?, + @ColumnInfo(name = "location_image_url") val locationImageUrl: String, + @ColumnInfo(name = "location_wikipedia_link") val locationWikipediaLink: String, + @ColumnInfo(name = "location_wikidata_link") val locationWikidataLink: String, + @ColumnInfo(name = "location_commons_link") val locationCommonsLink: String, + @ColumnInfo(name = "location_pic") val locationPic: String, + @ColumnInfo(name = "location_exists") val locationExists: Boolean +) + +fun BookmarksLocations.toPlace(): Place { + val location = LatLng( + locationLat, + locationLong, + 1F + ) + + val builder = Sitelinks.Builder().apply { + setWikipediaLink(locationWikipediaLink) + setWikidataLink(locationWikidataLink) + setCommonsLink(locationCommonsLink) + } + + return Place( + locationLanguage, + locationName, + Label.fromText(locationLabelText), + locationDescription, + location, + locationCategory, + builder.build(), + locationPic, + locationExists + ) +} + +fun Place.toBookmarksLocations(): BookmarksLocations { + return BookmarksLocations( + locationName = name, + locationLanguage = language, + locationDescription = longDescription, + locationCategory = category, + locationLat = location.latitude, + locationLong = location.longitude, + locationLabelText = label?.text ?: "", + locationLabelIcon = label?.icon, + locationImageUrl = pic, + locationWikipediaLink = siteLinks.wikipediaLink.toString(), + locationWikidataLink = siteLinks.wikidataLink.toString(), + locationCommonsLink = siteLinks.commonsLink.toString(), + locationPic = pic, + locationExists = exists + ) +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt index 1377ae281..7cb7f60f7 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt @@ -18,8 +18,9 @@ class DBOpenHelper( companion object { private const val DATABASE_NAME = "commons.db" - private const val DATABASE_VERSION = 21 + private const val DATABASE_VERSION = 22 const val CONTRIBUTIONS_TABLE = "contributions" + const val BOOKMARKS_LOCATIONS = "bookmarksLocations" private const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS %s" } @@ -30,7 +31,6 @@ class DBOpenHelper( override fun onCreate(db: SQLiteDatabase) { CategoryDao.Table.onCreate(db) BookmarkPicturesDao.Table.onCreate(db) - BookmarkLocationsDao.Table.onCreate(db) BookmarkItemsDao.Table.onCreate(db) RecentSearchesDao.Table.onCreate(db) RecentLanguagesDao.Table.onCreate(db) @@ -39,11 +39,11 @@ class DBOpenHelper( override fun onUpgrade(db: SQLiteDatabase, from: Int, to: Int) { CategoryDao.Table.onUpdate(db, from, to) BookmarkPicturesDao.Table.onUpdate(db, from, to) - BookmarkLocationsDao.Table.onUpdate(db, from, to) BookmarkItemsDao.Table.onUpdate(db, from, to) RecentSearchesDao.Table.onUpdate(db, from, to) RecentLanguagesDao.Table.onUpdate(db, from, to) deleteTable(db, CONTRIBUTIONS_TABLE) + deleteTable(db, BOOKMARKS_LOCATIONS) } /** diff --git a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt index 0c34bbdec..74ec9bc89 100644 --- a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt +++ b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt @@ -1,10 +1,16 @@ package fr.free.nrw.commons.db +import android.content.Context import androidx.room.Database +import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao import fr.free.nrw.commons.bookmarks.category.BookmarksCategoryModal +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao +import fr.free.nrw.commons.bookmarks.locations.BookmarksLocations import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.ContributionDao import fr.free.nrw.commons.customselector.database.NotForUploadStatus @@ -23,8 +29,8 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao * */ @Database( - entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class, BookmarksCategoryModal::class], - version = 19, + entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class, BookmarksCategoryModal::class, BookmarksLocations::class], + version = 20, exportSchema = false, ) @TypeConverters(Converters::class) @@ -42,4 +48,6 @@ abstract class AppDatabase : RoomDatabase() { abstract fun ReviewDao(): ReviewDao abstract fun bookmarkCategoriesDao(): BookmarkCategoriesDao + + abstract fun bookmarkLocationsDao(): BookmarkLocationsDao } diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt index 58d9039d5..85af7f6eb 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.ContentProviderClient import android.content.ContentResolver import android.content.Context +import android.database.sqlite.SQLiteDatabase import android.view.inputmethod.InputMethodManager import androidx.collection.LruCache import androidx.room.Room.databaseBuilder @@ -16,6 +17,7 @@ import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.R import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesDao +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao import fr.free.nrw.commons.contributions.ContributionDao import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao import fr.free.nrw.commons.customselector.database.UploadedStatusDao @@ -36,6 +38,7 @@ import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl import io.reactivex.Scheduler import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers +import timber.log.Timber import java.util.Objects import javax.inject.Named import javax.inject.Singleton @@ -49,6 +52,11 @@ import javax.inject.Singleton @Module @Suppress("unused") open class CommonsApplicationModule(private val applicationContext: Context) { + + init { + appContext = applicationContext + } + @Provides fun providesImageFileLoader(context: Context): ImageFileLoader = ImageFileLoader(context) @@ -110,11 +118,6 @@ open class CommonsApplicationModule(private val applicationContext: Context) { fun provideBookmarkContentProviderClient(context: Context): ContentProviderClient? = context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_AUTHORITY) - @Provides - @Named("bookmarksLocation") - fun provideBookmarkLocationContentProviderClient(context: Context): ContentProviderClient? = - context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY) - @Provides @Named("bookmarksItem") fun provideBookmarkItemContentProviderClient(context: Context): ContentProviderClient? = @@ -196,7 +199,10 @@ open class CommonsApplicationModule(private val applicationContext: Context) { applicationContext, AppDatabase::class.java, "commons_room.db" - ).addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build() + ).addMigrations( + MIGRATION_1_2, + MIGRATION_19_TO_20 + ).fallbackToDestructiveMigration().build() @Provides fun providesContributionsDao(appDatabase: AppDatabase): ContributionDao = @@ -206,6 +212,10 @@ open class CommonsApplicationModule(private val applicationContext: Context) { fun providesPlaceDao(appDatabase: AppDatabase): PlaceDao = appDatabase.PlaceDao() + @Provides + fun providesBookmarkLocationsDao(appDatabase: AppDatabase): BookmarkLocationsDao = + appDatabase.bookmarkLocationsDao() + @Provides fun providesDepictDao(appDatabase: AppDatabase): DepictsDao = appDatabase.DepictsDao() @@ -239,6 +249,9 @@ open class CommonsApplicationModule(private val applicationContext: Context) { const val IO_THREAD: String = "io_thread" const val MAIN_THREAD: String = "main_thread" + lateinit var appContext: Context + private set + val MIGRATION_1_2: Migration = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL( @@ -246,5 +259,101 @@ open class CommonsApplicationModule(private val applicationContext: Context) { ) } } + + private val MIGRATION_19_TO_20 = object : Migration(19, 20) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS bookmarks_locations ( + location_name TEXT NOT NULL PRIMARY KEY, + location_language TEXT NOT NULL, + location_description TEXT NOT NULL, + location_lat REAL NOT NULL, + location_long REAL NOT NULL, + location_category TEXT NOT NULL, + location_label_text TEXT NOT NULL, + location_label_icon INTEGER, + location_image_url TEXT NOT NULL DEFAULT '', + location_wikipedia_link TEXT NOT NULL, + location_wikidata_link TEXT NOT NULL, + location_commons_link TEXT NOT NULL, + location_pic TEXT NOT NULL, + location_exists INTEGER NOT NULL CHECK(location_exists IN (0, 1)) + ) + """ + ) + + val oldDbPath = appContext.getDatabasePath("commons.db").path + val oldDb = SQLiteDatabase + .openDatabase(oldDbPath, null, SQLiteDatabase.OPEN_READONLY) + + val cursor = oldDb.rawQuery("SELECT * FROM bookmarksLocations", null) + + while (cursor.moveToNext()) { + val locationName = + cursor.getString(cursor.getColumnIndexOrThrow("location_name")) + val locationLanguage = + cursor.getString(cursor.getColumnIndexOrThrow("location_language")) + val locationDescription = + cursor.getString(cursor.getColumnIndexOrThrow("location_description")) + val locationCategory = + cursor.getString(cursor.getColumnIndexOrThrow("location_category")) + val locationLabelText = + cursor.getString(cursor.getColumnIndexOrThrow("location_label_text")) + val locationLabelIcon = + cursor.getInt(cursor.getColumnIndexOrThrow("location_label_icon")) + val locationLat = + cursor.getDouble(cursor.getColumnIndexOrThrow("location_lat")) + val locationLong = + cursor.getDouble(cursor.getColumnIndexOrThrow("location_long")) + + // Handle NULL values safely + val locationImageUrl = + cursor.getString( + cursor.getColumnIndexOrThrow("location_image_url") + ) ?: "" + val locationWikipediaLink = + cursor.getString( + cursor.getColumnIndexOrThrow("location_wikipedia_link") + ) ?: "" + val locationWikidataLink = + cursor.getString( + cursor.getColumnIndexOrThrow("location_wikidata_link") + ) ?: "" + val locationCommonsLink = + cursor.getString( + cursor.getColumnIndexOrThrow("location_commons_link") + ) ?: "" + val locationPic = + cursor.getString( + cursor.getColumnIndexOrThrow("location_pic") + ) ?: "" + val locationExists = + cursor.getInt( + cursor.getColumnIndexOrThrow("location_exists") + ) + + db.execSQL( + """ + INSERT OR REPLACE INTO bookmarks_locations ( + location_name, location_language, location_description, location_category, + location_label_text, location_label_icon, location_lat, location_long, + location_image_url, location_wikipedia_link, location_wikidata_link, + location_commons_link, location_pic, location_exists + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + arrayOf( + locationName, locationLanguage, locationDescription, locationCategory, + locationLabelText, locationLabelIcon, locationLat, locationLong, + locationImageUrl, locationWikipediaLink, locationWikidataLink, + locationCommonsLink, locationPic, locationExists + ) + ) + } + + cursor.close() + oldDb.close() + } + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt index 1882f77a9..80a0626cc 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt @@ -3,7 +3,6 @@ package fr.free.nrw.commons.di import dagger.Module import dagger.android.ContributesAndroidInjector import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider import fr.free.nrw.commons.category.CategoryContentProvider import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider @@ -26,9 +25,6 @@ abstract class ContentProviderBuilderModule { @ContributesAndroidInjector abstract fun bindBookmarkContentProvider(): BookmarkPicturesContentProvider - @ContributesAndroidInjector - abstract fun bindBookmarkLocationContentProvider(): BookmarkLocationsContentProvider - @ContributesAndroidInjector abstract fun bindBookmarkItemContentProvider(): BookmarkItemsContentProvider diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/BottomSheetAdapter.kt b/app/src/main/java/fr/free/nrw/commons/nearby/BottomSheetAdapter.kt index 8bcc21e40..a83d49f75 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/BottomSheetAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/BottomSheetAdapter.kt @@ -68,7 +68,21 @@ class BottomSheetAdapter( item.imageResourceId == R.drawable.ic_round_star_border_24px ) { item.imageResourceId = icon - this.notifyItemChanged(index) + notifyItemChanged(index) + return + } + } + } + + fun toggleBookmarkIcon() { + itemList.forEachIndexed { index, item -> + if(item.imageResourceId == R.drawable.ic_round_star_filled_24px) { + item.imageResourceId = R.drawable.ic_round_star_border_24px + notifyItemChanged(index) + return + } else if(item.imageResourceId == R.drawable.ic_round_star_border_24px){ + item.imageResourceId = R.drawable.ic_round_star_filled_24px + notifyItemChanged(index) return } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyUtil.kt b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyUtil.kt new file mode 100644 index 000000000..236316fef --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyUtil.kt @@ -0,0 +1,28 @@ +package fr.free.nrw.commons.nearby + +import android.util.Log +import androidx.lifecycle.LifecycleCoroutineScope +import fr.free.nrw.commons.R +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao +import kotlinx.coroutines.launch +import timber.log.Timber + +object NearbyUtil { + + fun getBookmarkLocationExists( + bookmarksLocationsDao: BookmarkLocationsDao, + name: String, + scope: LifecycleCoroutineScope?, + bottomSheetAdapter: BottomSheetAdapter, + ) { + scope?.launch { + val isBookmarked = bookmarksLocationsDao.findBookmarkLocation(name) + Timber.i("isBookmarked: $isBookmarked") + if (isBookmarked) { + bottomSheetAdapter.updateBookmarkIcon(R.drawable.ic_round_star_filled_24px) + } else { + bottomSheetAdapter.updateBookmarkIcon(R.drawable.ic_round_star_border_24px) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceAdapterDelegate.kt b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceAdapterDelegate.kt index 7156568b6..d9a76c25d 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceAdapterDelegate.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceAdapterDelegate.kt @@ -7,6 +7,7 @@ import android.view.View.INVISIBLE import android.view.View.VISIBLE import android.widget.RelativeLayout import androidx.activity.result.ActivityResultLauncher +import androidx.lifecycle.LifecycleCoroutineScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionManager @@ -16,9 +17,11 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import fr.free.nrw.commons.R import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao import fr.free.nrw.commons.databinding.ItemPlaceBinding +import kotlinx.coroutines.launch fun placeAdapterDelegate( bookmarkLocationDao: BookmarkLocationsDao, + scope: LifecycleCoroutineScope?, onItemClick: ((Place) -> Unit)? = null, onCameraClicked: (Place, ActivityResultLauncher>, ActivityResultLauncher) -> Unit, onCameraLongPressed: () -> Boolean, @@ -61,7 +64,10 @@ fun placeAdapterDelegate( nearbyButtonLayout.galleryButton.setOnClickListener { onGalleryClicked(item, galleryPickLauncherForResult) } nearbyButtonLayout.galleryButton.setOnLongClickListener { onGalleryLongPressed() } bookmarkButtonImage.setOnClickListener { - val isBookmarked = bookmarkLocationDao.updateBookmarkLocation(item) + var isBookmarked = false + scope?.launch { + isBookmarked = bookmarkLocationDao.updateBookmarkLocation(item) + } bookmarkButtonImage.setImageResource( if (isBookmarked) R.drawable.ic_round_star_filled_24px else R.drawable.ic_round_star_border_24px, ) @@ -93,13 +99,15 @@ fun placeAdapterDelegate( GONE } - bookmarkButtonImage.setImageResource( - if (bookmarkLocationDao.findBookmarkLocation(item)) { - R.drawable.ic_round_star_filled_24px - } else { - R.drawable.ic_round_star_border_24px - }, - ) + scope?.launch { + bookmarkButtonImage.setImageResource( + if (bookmarkLocationDao.findBookmarkLocation(item.name)) { + R.drawable.ic_round_star_filled_24px + } else { + R.drawable.ic_round_star_border_24px + }, + ) + } } nearbyButtonLayout.directionsButton.setOnLongClickListener { onDirectionsLongPressed() } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java b/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java index e46e95353..1d59fcd34 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java @@ -134,7 +134,7 @@ public interface NearbyParentFragmentContract { void setAdvancedQuery(String query); - void toggleBookmarkedStatus(Place place); + void toggleBookmarkedStatus(Place place, LifecycleCoroutineScope scope); void handleMapScrolled(LifecycleCoroutineScope scope, boolean isNetworkAvailable); } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt index c25efa2da..500231642 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt @@ -85,6 +85,7 @@ import fr.free.nrw.commons.nearby.MarkerPlaceGroup import fr.free.nrw.commons.nearby.NearbyController import fr.free.nrw.commons.nearby.NearbyFilterSearchRecyclerViewAdapter import fr.free.nrw.commons.nearby.NearbyFilterState +import fr.free.nrw.commons.nearby.NearbyUtil import fr.free.nrw.commons.nearby.Place import fr.free.nrw.commons.nearby.PlacesRepository import fr.free.nrw.commons.nearby.WikidataFeedback @@ -664,21 +665,23 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), private fun initRvNearbyList() { binding!!.bottomSheetNearby.rvNearbyList.layoutManager = LinearLayoutManager(context) adapter = PlaceAdapter( - bookmarkLocationDao!!, - { place: Place -> + bookmarkLocationsDao = bookmarkLocationDao, + scope = scope, + onPlaceClicked = { place: Place -> moveCameraToPosition( - GeoPoint(place.location.latitude, place.location.longitude) + GeoPoint( + place.location.latitude, + place.location.longitude + ) ) - Unit }, - { place: Place?, isBookmarked: Boolean? -> - presenter!!.toggleBookmarkedStatus(place) - Unit + onBookmarkClicked = { place: Place?, _: Boolean? -> + presenter!!.toggleBookmarkedStatus(place, scope) }, - commonPlaceClickActions!!, - inAppCameraLocationPermissionLauncher, - galleryPickLauncherForResult, - cameraPickLauncherForResult + commonPlaceClickActions = commonPlaceClickActions, + inAppCameraLocationPermissionLauncher = inAppCameraLocationPermissionLauncher, + galleryPickLauncherForResult = galleryPickLauncherForResult, + cameraPickLauncherForResult = cameraPickLauncherForResult ) binding!!.bottomSheetNearby.rvNearbyList.adapter = adapter } @@ -2303,34 +2306,34 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), // TODO: Decide button text for fitting in the screen (dataList as ArrayList).add( BottomSheetItem( - fr.free.nrw.commons.R.drawable.ic_round_star_border_24px, + R.drawable.ic_round_star_border_24px, "" ) ) (dataList as ArrayList).add( BottomSheetItem( - fr.free.nrw.commons.R.drawable.ic_directions_black_24dp, + R.drawable.ic_directions_black_24dp, resources.getString(fr.free.nrw.commons.R.string.nearby_directions) ) ) if (place.hasWikidataLink()) { (dataList as ArrayList).add( BottomSheetItem( - fr.free.nrw.commons.R.drawable.ic_wikidata_logo_24dp, + R.drawable.ic_wikidata_logo_24dp, resources.getString(fr.free.nrw.commons.R.string.nearby_wikidata) ) ) } (dataList as ArrayList).add( BottomSheetItem( - fr.free.nrw.commons.R.drawable.ic_feedback_black_24dp, + R.drawable.ic_feedback_black_24dp, resources.getString(fr.free.nrw.commons.R.string.nearby_wikitalk) ) ) if (place.hasWikipediaLink()) { (dataList as ArrayList).add( BottomSheetItem( - fr.free.nrw.commons.R.drawable.ic_wikipedia_logo_24dp, + R.drawable.ic_wikipedia_logo_24dp, resources.getString(fr.free.nrw.commons.R.string.nearby_wikipedia) ) ) @@ -2338,7 +2341,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), if (selectedPlace!!.hasCommonsLink()) { (dataList as ArrayList).add( BottomSheetItem( - fr.free.nrw.commons.R.drawable.ic_commons_icon_vector, + R.drawable.ic_commons_icon_vector, resources.getString(fr.free.nrw.commons.R.string.nearby_commons) ) ) @@ -2566,12 +2569,16 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), } private fun updateBookmarkButtonImage(place: Place) { - val bookmarkIcon = if (bookmarkLocationDao!!.findBookmarkLocation(place)) { - fr.free.nrw.commons.R.drawable.ic_round_star_filled_24px - } else { - fr.free.nrw.commons.R.drawable.ic_round_star_border_24px - } - bottomSheetAdapter!!.updateBookmarkIcon(bookmarkIcon) + NearbyUtil.getBookmarkLocationExists( + bookmarkLocationDao, + place.getName(), + scope, + bottomSheetAdapter!! + ) + } + + private fun toggleBookmarkButtonImage() { + bottomSheetAdapter?.toggleBookmarkIcon() } override fun onAttach(context: Context) { @@ -2749,26 +2756,31 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), override fun onBottomSheetItemClick(view: View?, position: Int) { val item = dataList?.get(position) ?: return // Null check for dataList when (item.imageResourceId) { - fr.free.nrw.commons.R.drawable.ic_round_star_border_24px, - fr.free.nrw.commons.R.drawable.ic_round_star_filled_24px -> { - presenter?.toggleBookmarkedStatus(selectedPlace) + R.drawable.ic_round_star_border_24px -> { + presenter?.toggleBookmarkedStatus(selectedPlace, scope) + toggleBookmarkButtonImage() + } + + R.drawable.ic_round_star_filled_24px -> { + presenter?.toggleBookmarkedStatus(selectedPlace, scope) + toggleBookmarkButtonImage() selectedPlace?.let { updateBookmarkButtonImage(it) } } - fr.free.nrw.commons.R.drawable.ic_directions_black_24dp -> { + R.drawable.ic_directions_black_24dp -> { selectedPlace?.let { Utils.handleGeoCoordinates(this.context, it.getLocation()) binding?.map?.zoomLevelDouble ?: 0.0 } } - fr.free.nrw.commons.R.drawable.ic_wikidata_logo_24dp -> { + R.drawable.ic_wikidata_logo_24dp -> { selectedPlace?.siteLinks?.wikidataLink?.let { Utils.handleWebUrl(this.context, it) } } - fr.free.nrw.commons.R.drawable.ic_feedback_black_24dp -> { + R.drawable.ic_feedback_black_24dp -> { selectedPlace?.let { val intent = Intent(this.context, WikidataFeedback::class.java).apply { putExtra("lat", it.location.latitude) @@ -2780,13 +2792,13 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), } } - fr.free.nrw.commons.R.drawable.ic_wikipedia_logo_24dp -> { + R.drawable.ic_wikipedia_logo_24dp -> { selectedPlace?.siteLinks?.wikipediaLink?.let { Utils.handleWebUrl(this.context, it) } } - fr.free.nrw.commons.R.drawable.ic_commons_icon_vector -> { + R.drawable.ic_commons_icon_vector -> { selectedPlace?.siteLinks?.commonsLink?.let { Utils.handleWebUrl(this.context, it) } @@ -2800,13 +2812,13 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), override fun onBottomSheetItemLongClick(view: View?, position: Int) { val item = dataList!![position] val message = when (item.imageResourceId) { - fr.free.nrw.commons.R.drawable.ic_round_star_border_24px -> getString(fr.free.nrw.commons.R.string.menu_bookmark) - fr.free.nrw.commons.R.drawable.ic_round_star_filled_24px -> getString(fr.free.nrw.commons.R.string.menu_bookmark) - fr.free.nrw.commons.R.drawable.ic_directions_black_24dp -> getString(fr.free.nrw.commons.R.string.nearby_directions) - fr.free.nrw.commons.R.drawable.ic_wikidata_logo_24dp -> getString(fr.free.nrw.commons.R.string.nearby_wikidata) - fr.free.nrw.commons.R.drawable.ic_feedback_black_24dp -> getString(fr.free.nrw.commons.R.string.nearby_wikitalk) - fr.free.nrw.commons.R.drawable.ic_wikipedia_logo_24dp -> getString(fr.free.nrw.commons.R.string.nearby_wikipedia) - fr.free.nrw.commons.R.drawable.ic_commons_icon_vector -> getString(fr.free.nrw.commons.R.string.nearby_commons) + R.drawable.ic_round_star_border_24px -> getString(fr.free.nrw.commons.R.string.menu_bookmark) + R.drawable.ic_round_star_filled_24px -> getString(fr.free.nrw.commons.R.string.menu_bookmark) + R.drawable.ic_directions_black_24dp -> getString(fr.free.nrw.commons.R.string.nearby_directions) + R.drawable.ic_wikidata_logo_24dp -> getString(fr.free.nrw.commons.R.string.nearby_wikidata) + R.drawable.ic_feedback_black_24dp -> getString(fr.free.nrw.commons.R.string.nearby_wikitalk) + R.drawable.ic_wikipedia_logo_24dp -> getString(fr.free.nrw.commons.R.string.nearby_wikipedia) + R.drawable.ic_commons_icon_vector -> getString(fr.free.nrw.commons.R.string.nearby_commons) else -> "Long click" } Toast.makeText(this.context, message, Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/PlaceAdapter.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/PlaceAdapter.kt index e5cc92667..adb687014 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/PlaceAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/PlaceAdapter.kt @@ -2,6 +2,7 @@ package fr.free.nrw.commons.nearby.fragments import android.content.Intent import androidx.activity.result.ActivityResultLauncher +import androidx.lifecycle.LifecycleCoroutineScope import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao import fr.free.nrw.commons.nearby.Place import fr.free.nrw.commons.nearby.placeAdapterDelegate @@ -9,6 +10,7 @@ import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter class PlaceAdapter( bookmarkLocationsDao: BookmarkLocationsDao, + scope: LifecycleCoroutineScope? = null, onPlaceClicked: ((Place) -> Unit)? = null, onBookmarkClicked: (Place, Boolean) -> Unit, commonPlaceClickActions: CommonPlaceClickActions, @@ -18,6 +20,7 @@ class PlaceAdapter( ) : BaseDelegateAdapter( placeAdapterDelegate( bookmarkLocationsDao, + scope, onPlaceClicked, commonPlaceClickActions.onCameraClicked(), commonPlaceClickActions.onCameraLongPressed(), diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt index ed84751b0..b4639b14a 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive import kotlinx.coroutines.job import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import okhttp3.internal.wait import timber.log.Timber @@ -136,25 +137,31 @@ class NearbyParentFragmentPresenter * @param place The place whose bookmarked status is to be toggled. If the place is `null`, * the operation is skipped. */ - override fun toggleBookmarkedStatus(place: Place?) { + override fun toggleBookmarkedStatus( + place: Place?, + scope: LifecycleCoroutineScope? + ) { if (place == null) return - val nowBookmarked = bookmarkLocationDao.updateBookmarkLocation(place) - bookmarkChangedPlaces.add(place) - val placeIndex = - NearbyController.markerLabelList.indexOfFirst { it.place.location == place.location } - NearbyController.markerLabelList[placeIndex] = MarkerPlaceGroup( - nowBookmarked, - NearbyController.markerLabelList[placeIndex].place - ) - nearbyParentFragmentView.setFilterState() + var nowBookmarked: Boolean + scope?.launch { + nowBookmarked = bookmarkLocationDao.updateBookmarkLocation(place) + bookmarkChangedPlaces.add(place) + val placeIndex = + NearbyController.markerLabelList.indexOfFirst { it.place.location == place.location } + NearbyController.markerLabelList[placeIndex] = MarkerPlaceGroup( + nowBookmarked, + NearbyController.markerLabelList[placeIndex].place + ) + nearbyParentFragmentView.setFilterState() + } } override fun attachView(view: NearbyParentFragmentContract.View) { - this.nearbyParentFragmentView = view + nearbyParentFragmentView = view } override fun detachView() { - this.nearbyParentFragmentView = DUMMY + nearbyParentFragmentView = DUMMY } override fun removeNearbyPreferences(applicationKvStore: JsonKvStore) { @@ -337,7 +344,7 @@ class NearbyParentFragmentPresenter for (i in 0..updatedGroups.lastIndex) { val repoPlace = placesRepository.fetchPlace(updatedGroups[i].place.entityID) if (repoPlace != null && repoPlace.name != null && repoPlace.name != ""){ - updatedGroups[i].isBookmarked = bookmarkLocationDao.findBookmarkLocation(repoPlace) + updatedGroups[i].isBookmarked = bookmarkLocationDao.findBookmarkLocation(repoPlace.name) updatedGroups[i].place.apply { name = repoPlace.name isMonument = repoPlace.isMonument @@ -375,7 +382,7 @@ class NearbyParentFragmentPresenter collectResults.send( fetchedPlaces.mapIndexed { index, place -> Pair(indices[index], MarkerPlaceGroup( - bookmarkLocationDao.findBookmarkLocation(place), + bookmarkLocationDao.findBookmarkLocation(place.name), place )) } @@ -393,7 +400,10 @@ class NearbyParentFragmentPresenter onePlaceBatch.add(Pair(i, MarkerPlaceGroup( bookmarkLocationDao.findBookmarkLocation( - fetchedPlace[0]),fetchedPlace[0]))) + fetchedPlace[0].name + ), + fetchedPlace[0] + ))) } catch (e: Exception) { Timber.tag("NearbyPinDetails").e(e) onePlaceBatch.add(Pair(i, updatedGroups[i])) @@ -457,7 +467,7 @@ class NearbyParentFragmentPresenter if (bookmarkChangedPlacesBacklog.containsKey(group.place.location)) { updatedGroups[index] = MarkerPlaceGroup( bookmarkLocationDao - .findBookmarkLocation(updatedGroups[index].place), + .findBookmarkLocation(updatedGroups[index].place.name), updatedGroups[index].place ) } @@ -565,7 +575,7 @@ class NearbyParentFragmentPresenter ).sortedBy { it.getDistanceInDouble(mapFocus) }.take(NearbyController.MAX_RESULTS) .map { MarkerPlaceGroup( - bookmarkLocationDao.findBookmarkLocation(it), it + bookmarkLocationDao.findBookmarkLocation(it.name), it ) } ensureActive() diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookMarkLocationDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookMarkLocationDaoTest.kt index 391dd8d17..4808d8518 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookMarkLocationDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookMarkLocationDaoTest.kt @@ -7,6 +7,8 @@ import android.database.MatrixCursor import android.database.sqlite.SQLiteDatabase import android.net.Uri import android.os.RemoteException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.argumentCaptor @@ -18,36 +20,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.locations.BookmarkLocationsContentProvider.BASE_URI -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_CATEGORY -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_COMMONS_LINK -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_DESCRIPTION -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_EXISTS -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_IMAGE_URL -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_LABEL_ICON -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_LABEL_TEXT -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_LANGUAGE -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_LAT -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_LONG -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_NAME -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_PIC -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_WIKIDATA_LINK -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.COLUMN_WIKIPEDIA_LINK -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.CREATE_TABLE_STATEMENT -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.DROP_TABLE_STATEMENT -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.onCreate -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.onDelete -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.onUpdate +import fr.free.nrw.commons.db.AppDatabase import fr.free.nrw.commons.location.LatLng import fr.free.nrw.commons.nearby.Label import fr.free.nrw.commons.nearby.Place import fr.free.nrw.commons.nearby.Sitelinks +import kotlinx.coroutines.runBlocking +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock import org.mockito.Mockito.verifyNoInteractions import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @@ -55,28 +41,11 @@ import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(sdk = [21], application = TestCommonsApplication::class) class BookMarkLocationDaoTest { - private val columns = - arrayOf( - COLUMN_NAME, - COLUMN_LANGUAGE, - COLUMN_DESCRIPTION, - COLUMN_CATEGORY, - COLUMN_LABEL_TEXT, - COLUMN_LABEL_ICON, - COLUMN_IMAGE_URL, - COLUMN_WIKIPEDIA_LINK, - COLUMN_WIKIDATA_LINK, - COLUMN_COMMONS_LINK, - COLUMN_LAT, - COLUMN_LONG, - COLUMN_PIC, - COLUMN_EXISTS, - ) - private val client: ContentProviderClient = mock() - private val database: SQLiteDatabase = mock() - private val captor = argumentCaptor() - private lateinit var testObject: BookmarkLocationsDao + private lateinit var bookmarkLocationsDao: BookmarkLocationsDao + + private lateinit var database: AppDatabase + private lateinit var examplePlaceBookmark: Place private lateinit var exampleLabel: Label private lateinit var exampleUri: Uri @@ -89,10 +58,18 @@ class BookMarkLocationDaoTest { exampleUri = Uri.parse("wikimedia/uri") exampleLocation = LatLng(40.0, 51.4, 1f) - builder = Sitelinks.Builder() - builder.setWikipediaLink("wikipediaLink") - builder.setWikidataLink("wikidataLink") - builder.setCommonsLink("commonsLink") + database = Room.inMemoryDatabaseBuilder( + ApplicationProvider.getApplicationContext(), + AppDatabase::class.java + ).allowMainThreadQueries().build() + + bookmarkLocationsDao = database.bookmarkLocationsDao() + + builder = Sitelinks.Builder().apply { + setWikipediaLink("wikipediaLink") + setWikidataLink("wikidataLink") + setCommonsLink("commonsLink") + } examplePlaceBookmark = Place( @@ -106,236 +83,75 @@ class BookMarkLocationDaoTest { "picName", false, ) - testObject = BookmarkLocationsDao { client } + } + + @After + fun tearDown() { + database.close() } @Test - fun createTable() { - onCreate(database) - verify(database).execSQL(CREATE_TABLE_STATEMENT) + fun testForAddAndGetAllBookmarkLocations() = runBlocking { + bookmarkLocationsDao.addBookmarkLocation(examplePlaceBookmark.toBookmarksLocations()) + + val bookmarks = bookmarkLocationsDao.getAllBookmarksLocations() + + assertEquals(1, bookmarks.size) + val retrievedBookmark = bookmarks.first() + assertEquals(examplePlaceBookmark.name, retrievedBookmark.locationName) + assertEquals(examplePlaceBookmark.language, retrievedBookmark.locationLanguage) } @Test - fun deleteTable() { - onDelete(database) - inOrder(database) { - verify(database).execSQL(DROP_TABLE_STATEMENT) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } + fun testFindBookmarkByNameForTrue() = runBlocking { + bookmarkLocationsDao.addBookmarkLocation(examplePlaceBookmark.toBookmarksLocations()) + + val exists = bookmarkLocationsDao.findBookmarkLocation(examplePlaceBookmark.name) + assertTrue(exists) } @Test - fun createFromCursor() { - createCursor(1).let { cursor -> - cursor.moveToFirst() - testObject.fromCursor(cursor).let { - assertEquals("en", it.language) - assertEquals("placeName", it.name) - assertEquals(Label.FOREST, it.label) - assertEquals("placeDescription", it.longDescription) - assertEquals(40.0, it.location.latitude, 0.001) - assertEquals(51.4, it.location.longitude, 0.001) - assertEquals("placeCategory", it.category) - assertEquals(builder.build().wikipediaLink, it.siteLinks.wikipediaLink) - assertEquals(builder.build().wikidataLink, it.siteLinks.wikidataLink) - assertEquals(builder.build().commonsLink, it.siteLinks.commonsLink) - assertEquals("picName", it.pic) - assertEquals(false, it.exists) - } - } + fun testFindBookmarkByNameForFalse() = runBlocking { + bookmarkLocationsDao.addBookmarkLocation(examplePlaceBookmark.toBookmarksLocations()) + + val exists = bookmarkLocationsDao.findBookmarkLocation("xyz") + assertFalse(exists) } @Test - fun getAllLocationBookmarks() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(14)) + fun testDeleteBookmark() = runBlocking { + val bookmarkLocation = examplePlaceBookmark.toBookmarksLocations() + bookmarkLocationsDao.addBookmarkLocation(bookmarkLocation) - var result = testObject.allBookmarksLocations + bookmarkLocationsDao.deleteBookmarkLocation(bookmarkLocation) - assertEquals(14, result.size) - } - - @Test(expected = RuntimeException::class) - fun getAllLocationBookmarksTranslatesExceptions() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow(RemoteException("")) - testObject.allBookmarksLocations + val bookmarks = bookmarkLocationsDao.getAllBookmarksLocations() + assertTrue(bookmarks.isEmpty()) } @Test - fun getAllLocationBookmarksReturnsEmptyList_emptyCursor() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(0)) - assertTrue(testObject.allBookmarksLocations.isEmpty()) + fun testUpdateBookmarkForTrue() = runBlocking { + val exists = bookmarkLocationsDao.updateBookmarkLocation(examplePlaceBookmark) + + assertTrue(exists) } @Test - fun getAllLocationBookmarksReturnsEmptyList_nullCursor() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null) - assertTrue(testObject.allBookmarksLocations.isEmpty()) + fun testUpdateBookmarkForFalse() = runBlocking { + val newBookmark = examplePlaceBookmark.toBookmarksLocations() + bookmarkLocationsDao.addBookmarkLocation(newBookmark) + + val exists = bookmarkLocationsDao.updateBookmarkLocation(examplePlaceBookmark) + assertFalse(exists) } @Test - fun cursorsAreClosedAfterGetAllLocationBookmarksQuery() { - val mockCursor: Cursor = mock() - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor) - whenever(mockCursor.moveToFirst()).thenReturn(false) + fun testGetAllBookmarksLocationsPlace() = runBlocking { + val bookmarkLocation = examplePlaceBookmark.toBookmarksLocations() + bookmarkLocationsDao.addBookmarkLocation(bookmarkLocation) - testObject.allBookmarksLocations - - verify(mockCursor).close() + val bookmarks = bookmarkLocationsDao.getAllBookmarksLocationsPlace() + assertEquals(1, bookmarks.size) + assertEquals(examplePlaceBookmark.name, bookmarks.first().name) } - - @Test - fun updateNewLocationBookmark() { - whenever(client.insert(any(), any())).thenReturn(Uri.EMPTY) - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(null) - - assertTrue(testObject.updateBookmarkLocation(examplePlaceBookmark)) - verify(client).insert(eq(BASE_URI), captor.capture()) - captor.firstValue.let { cv -> - assertEquals(13, cv.size()) - assertEquals(examplePlaceBookmark.name, cv.getAsString(COLUMN_NAME)) - assertEquals(examplePlaceBookmark.language, cv.getAsString(COLUMN_LANGUAGE)) - assertEquals(examplePlaceBookmark.longDescription, cv.getAsString(COLUMN_DESCRIPTION)) - assertEquals(examplePlaceBookmark.label.text, cv.getAsString(COLUMN_LABEL_TEXT)) - assertEquals(examplePlaceBookmark.category, cv.getAsString(COLUMN_CATEGORY)) - assertEquals(examplePlaceBookmark.location.latitude, cv.getAsDouble(COLUMN_LAT), 0.001) - assertEquals(examplePlaceBookmark.location.longitude, cv.getAsDouble(COLUMN_LONG), 0.001) - assertEquals(examplePlaceBookmark.siteLinks.wikipediaLink.toString(), cv.getAsString(COLUMN_WIKIPEDIA_LINK)) - assertEquals(examplePlaceBookmark.siteLinks.wikidataLink.toString(), cv.getAsString(COLUMN_WIKIDATA_LINK)) - assertEquals(examplePlaceBookmark.siteLinks.commonsLink.toString(), cv.getAsString(COLUMN_COMMONS_LINK)) - assertEquals(examplePlaceBookmark.pic, cv.getAsString(COLUMN_PIC)) - assertEquals(examplePlaceBookmark.exists.toString(), cv.getAsString(COLUMN_EXISTS)) - } - } - - @Test - fun updateExistingLocationBookmark() { - whenever(client.delete(isA(), isNull(), isNull())).thenReturn(1) - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1)) - - assertFalse(testObject.updateBookmarkLocation(examplePlaceBookmark)) - verify(client).delete(eq(BookmarkLocationsContentProvider.uriForName(examplePlaceBookmark.name)), isNull(), isNull()) - } - - @Test - fun findExistingLocationBookmark() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1)) - assertTrue(testObject.findBookmarkLocation(examplePlaceBookmark)) - } - - @Test(expected = RuntimeException::class) - fun findLocationBookmarkTranslatesExceptions() { - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow(RemoteException("")) - testObject.findBookmarkLocation(examplePlaceBookmark) - } - - @Test - fun findNotExistingLocationBookmarkReturnsNull_emptyCursor() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(0)) - assertFalse(testObject.findBookmarkLocation(examplePlaceBookmark)) - } - - @Test - fun findNotExistingLocationBookmarkReturnsNull_nullCursor() { - whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(null) - assertFalse(testObject.findBookmarkLocation(examplePlaceBookmark)) - } - - @Test - fun cursorsAreClosedAfterFindLocationBookmarkQuery() { - val mockCursor: Cursor = mock() - whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor) - whenever(mockCursor.moveToFirst()).thenReturn(false) - - testObject.findBookmarkLocation(examplePlaceBookmark) - - verify(mockCursor).close() - } - - @Test - fun migrateTableVersionFrom_v1_to_v2() { - onUpdate(database, 1, 2) - // Table didn't exist before v5 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v2_to_v3() { - onUpdate(database, 2, 3) - // Table didn't exist before v5 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v3_to_v4() { - onUpdate(database, 3, 4) - // Table didnt exist before v5 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v4_to_v5() { - onUpdate(database, 4, 5) - // Table didnt change in version 5 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v5_to_v6() { - onUpdate(database, 5, 6) - // Table didnt change in version 6 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v6_to_v7() { - onUpdate(database, 6, 7) - // Table didnt change in version 7 - verifyNoInteractions(database) - } - - @Test - fun migrateTableVersionFrom_v7_to_v8() { - onUpdate(database, 7, 8) - verify(database).execSQL(CREATE_TABLE_STATEMENT) - } - - @Test - fun migrateTableVersionFrom_v12_to_v13() { - onUpdate(database, 12, 13) - verify(database).execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_destroyed STRING;") - } - - @Test - fun migrateTableVersionFrom_v13_to_v14() { - onUpdate(database, 13, 14) - verify(database).execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_language STRING;") - } - - @Test - fun migrateTableVersionFrom_v14_to_v15() { - onUpdate(database, 14, 15) - verify(database).execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_exists STRING;") - } - - private fun createCursor(rows: Int): Cursor = - MatrixCursor(columns, rows).apply { - repeat(rows) { - newRow().apply { - add("placeName") - add("en") - add("placeDescription") - add("placeCategory") - add(Label.FOREST.text) - add(Label.FOREST.icon) - add("placeImage") - add("wikipediaLink") - add("wikidataLink") - add("commonsLink") - add(40.0) - add(51.4) - add("picName") - add(false) - } - } - } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationControllerTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationControllerTest.kt index 3fd21c25f..a7afc6b14 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationControllerTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationControllerTest.kt @@ -2,6 +2,7 @@ package fr.free.nrw.commons.bookmarks.locations import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.nearby.Place +import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.Before import org.junit.Test @@ -19,9 +20,11 @@ class BookmarkLocationControllerTest { @Before fun setup() { - MockitoAnnotations.initMocks(this) - whenever(bookmarkDao!!.allBookmarksLocations) - .thenReturn(mockBookmarkList) + MockitoAnnotations.openMocks(this) + runBlocking { + whenever(bookmarkDao!!.getAllBookmarksLocationsPlace()) + .thenReturn(mockBookmarkList) + } } /** @@ -66,7 +69,7 @@ class BookmarkLocationControllerTest { * Test case where all bookmark locations are fetched and media is found against it */ @Test - fun loadBookmarkedLocations() { + fun loadBookmarkedLocations() = runBlocking { val bookmarkedLocations = bookmarkLocationsController.loadFavoritesLocations() Assert.assertEquals(2, bookmarkedLocations.size.toLong()) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationFragmentUnitTests.kt index 24693dc85..52121bf84 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationFragmentUnitTests.kt @@ -10,6 +10,7 @@ import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ApplicationProvider +import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.OkHttpConnectionFactory import fr.free.nrw.commons.R @@ -22,11 +23,14 @@ import fr.free.nrw.commons.nearby.Place import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions import fr.free.nrw.commons.nearby.fragments.PlaceAdapter import fr.free.nrw.commons.profile.ProfileActivity +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.spy import org.mockito.MockitoAnnotations import org.powermock.reflect.Whitebox import org.robolectric.Robolectric @@ -129,11 +133,13 @@ class BookmarkLocationFragmentUnitTests { */ @Test fun testInitNonEmpty() { - whenever(controller.loadFavoritesLocations()).thenReturn(mockBookmarkList) - val method: Method = - BookmarkLocationsFragment::class.java.getDeclaredMethod("initList") - method.isAccessible = true - method.invoke(fragment) + runBlocking { + whenever(controller.loadFavoritesLocations()).thenReturn(mockBookmarkList) + val method: Method = + BookmarkLocationsFragment::class.java.getDeclaredMethod("initList") + method.isAccessible = true + method.invoke(fragment) + } } /** @@ -168,7 +174,11 @@ class BookmarkLocationFragmentUnitTests { */ @Test @Throws(Exception::class) - fun testOnResume() { - fragment.onResume() + fun testOnResume() = runBlocking { + val fragmentSpy = spy(fragment) + whenever(controller.loadFavoritesLocations()).thenReturn(mockBookmarkList) + + fragmentSpy.onResume() + verify(fragmentSpy).initList() } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt index 80e9a53f9..96098e2a0 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentPresenterTest.kt @@ -8,6 +8,7 @@ import fr.free.nrw.commons.location.LatLng import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before @@ -463,7 +464,9 @@ class NearbyParentFragmentPresenterTest { nearbyPlacesInfo.searchLatLng = latestLocation nearbyPlacesInfo.placeList = emptyList() - whenever(bookmarkLocationsDao.allBookmarksLocations).thenReturn(Collections.emptyList()) + runBlocking { + whenever(bookmarkLocationsDao.getAllBookmarksLocations()).thenReturn(Collections.emptyList()) + } nearbyPresenter.updateMapMarkers(nearbyPlacesInfo.placeList, latestLocation, null) Mockito.verify(nearbyParentFragmentView).setProgressBarVisibility(false) }