From 80a9c9465396cb453dc89264bcd71278626a0814 Mon Sep 17 00:00:00 2001 From: Victor-Bonin Date: Thu, 25 Oct 2018 17:54:22 +0200 Subject: [PATCH] Feature #1756 : Bookmark System (#1935) * Add bookmark star images * Add bookmark item in navigation menu * Add Activity for bookmarks * Implement bookmarks viewpager * Bookmark object and bookmarkDao * Implement Bookmark Picture Fragment and Controller * Implement image detail bookmark menu action UI * contentProvider + config + dao rework * Fix Dao and Content Provider crashes * Link bookmark controllers and dao * Implement bookmark location fragment, controller * Add bookmark icon to location items * Add empty bookmark list behavior and refactoring * bookmarkLocation dao and contentProvider * Fix bookmarks location crashes * Rename and refactor classes * Implement location list refresh * Fix picture list update When user come back from detail picture fragment, it solve the refresh bug. * full test coverage * Refactor bookmarks classes * Fix bookmarks pictures loading * Fix bookmark locations list display * Java Documetation * Fix Code review quality * Fix DB version update * Remove forgotten todo * Update bookmark activity base class Change from AuthenticatedActivity to BaseNavigationActivity --- app/build.gradle | 4 + app/src/main/AndroidManifest.xml | 18 ++ .../free/nrw/commons/CommonsApplication.java | 4 + .../free/nrw/commons/bookmarks/Bookmark.java | 50 ++++ .../nrw/commons/bookmarks/BookmarkPages.java | 32 +++ .../commons/bookmarks/BookmarksActivity.java | 154 ++++++++++ .../bookmarks/BookmarksPagerAdapter.java | 66 +++++ .../BookmarkLocationsContentProvider.java | 93 +++++++ .../BookmarkLocationsController.java | 26 ++ .../locations/BookmarkLocationsDao.java | 261 +++++++++++++++++ .../locations/BookmarkLocationsFragment.java | 152 ++++++++++ .../BookmarkPicturesContentProvider.java | 94 +++++++ .../pictures/BookmarkPicturesController.java | 66 +++++ .../pictures/BookmarkPicturesDao.java | 216 +++++++++++++++ .../pictures/BookmarkPicturesFragment.java | 230 +++++++++++++++ .../nrw/commons/category/GridViewAdapter.java | 11 +- .../free/nrw/commons/data/DBOpenHelper.java | 8 +- .../nrw/commons/di/ActivityBuilderModule.java | 4 + .../commons/di/CommonsApplicationModule.java | 12 + .../di/ContentProviderBuilderModule.java | 8 + .../nrw/commons/di/FragmentBuilderModule.java | 8 + .../media/MediaDetailPagerFragment.java | 26 ++ .../commons/nearby/NearbyAdapterFactory.java | 13 +- .../nrw/commons/nearby/NearbyMapFragment.java | 22 ++ .../fr/free/nrw/commons/nearby/Place.java | 2 + .../nrw/commons/nearby/PlaceRenderer.java | 48 +++- .../commons/theme/NavigationBaseActivity.java | 8 + .../drawable/ic_round_star_border_24px.xml | 9 + .../drawable/ic_round_star_filled_24px.xml | 9 + .../main/res/layout/activity_bookmarks.xml | 58 ++++ .../main/res/layout/bottom_sheet_details.xml | 28 ++ .../layout/fragment_bookmarks_locations.xml | 32 +++ .../layout/fragment_bookmarks_pictures.xml | 37 +++ app/src/main/res/layout/nearby_row_button.xml | 28 ++ app/src/main/res/menu/drawer.xml | 5 + .../main/res/menu/fragment_image_detail.xml | 7 +- app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values/strings.xml | 15 +- .../locations/BookMarkLocationDaoTest.kt | 262 ++++++++++++++++++ .../pictures/BookmarkPictureDaoTest.kt | 217 +++++++++++++++ .../nrw/commons/category/CategoryDaoTest.kt | 14 + .../contributions/ContributionDaoTest.kt | 14 + 42 files changed, 2361 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/Bookmark.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsContentProvider.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesContentProvider.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java create mode 100644 app/src/main/res/drawable/ic_round_star_border_24px.xml create mode 100644 app/src/main/res/drawable/ic_round_star_filled_24px.xml create mode 100644 app/src/main/res/layout/activity_bookmarks.xml create mode 100644 app/src/main/res/layout/fragment_bookmarks_locations.xml create mode 100644 app/src/main/res/layout/fragment_bookmarks_pictures.xml create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookMarkLocationDaoTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPictureDaoTest.kt diff --git a/app/build.gradle b/app/build.gradle index 2f133b602..6d1c349d9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -153,6 +153,8 @@ android { buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\"" buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\"" buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\"" + buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\"" + buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\"" dimension 'tier' } @@ -180,6 +182,8 @@ android { buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\"" buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.beta.categories.contentprovider\"" buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.beta.explore.recentsearches.contentprovider\"" + buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\"" + buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\"" dimension 'tier' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9d0662299..2d9c9a438 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -122,6 +122,10 @@ android:name=".achievements.AchievementsActivity" android:label="@string/Achievements" /> + + @@ -202,6 +206,20 @@ android:label="@string/provider_searches" android:syncable="false" /> + + + + diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index 45260dc78..d874bae40 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -29,6 +29,8 @@ import javax.inject.Inject; import javax.inject.Named; import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao; import fr.free.nrw.commons.category.CategoryDao; import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler; import fr.free.nrw.commons.concurrency.ThreadPoolService; @@ -245,6 +247,8 @@ public class CommonsApplication extends Application { ModifierSequenceDao.Table.onDelete(db); CategoryDao.Table.onDelete(db); ContributionDao.Table.onDelete(db); + BookmarkPicturesDao.Table.onDelete(db); + BookmarkLocationsDao.Table.onDelete(db); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/Bookmark.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/Bookmark.java new file mode 100644 index 000000000..7eaec5471 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/Bookmark.java @@ -0,0 +1,50 @@ +package fr.free.nrw.commons.bookmarks; + +import android.net.Uri; + +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider; + +public class Bookmark { + private Uri contentUri; + private String mediaName; + private String mediaCreator; + + public Bookmark(String mediaName, String mediaCreator) { + this.contentUri = BookmarkPicturesContentProvider.uriForName(mediaName); + this.mediaName = mediaName == null ? "" : mediaName; + this.mediaCreator = mediaCreator == null ? "" : mediaCreator; + } + + /** + * Gets the media name + * @return the media name + */ + public String getMediaName() { + return mediaName; + } + + + /** + * Gets media creator + * @return creator name + */ + public String getMediaCreator() { return mediaCreator; } + + + + /** + * Gets the content URI for this bookmark + * @return content URI + */ + public Uri getContentUri() { + return contentUri; + } + + /** + * Modifies the content URI - marking this bookmark as already saved in the database + * @param contentUri the content URI + */ + public void setContentUri(Uri contentUri) { + this.contentUri = contentUri; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java new file mode 100644 index 000000000..51deb5891 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java @@ -0,0 +1,32 @@ +package fr.free.nrw.commons.bookmarks; + +import android.support.v4.app.Fragment; + +/** + * Data class for handling a bookmark fragment and it title + */ +public class BookmarkPages { + private Fragment page; + private String title; + + BookmarkPages(Fragment fragment, String title) { + this.title = title; + this.page = fragment; + } + + /** + * Return the fragment + * @return fragment object + */ + public Fragment getPage() { + return page; + } + + /** + * Return the fragment title + * @return title + */ + public String getTitle() { + return title; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksActivity.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksActivity.java new file mode 100644 index 000000000..35c465288 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksActivity.java @@ -0,0 +1,154 @@ +package fr.free.nrw.commons.bookmarks; + +import android.content.Context; +import android.content.Intent; +import android.database.DataSetObserver; +import android.os.Bundle; +import android.support.design.widget.TabLayout; +import android.support.v4.app.FragmentManager; +import android.support.v4.view.ViewPager; +import android.view.View; +import android.widget.AdapterView; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.auth.AuthenticatedActivity; +import fr.free.nrw.commons.media.MediaDetailPagerFragment; +import fr.free.nrw.commons.theme.NavigationBaseActivity; + +public class BookmarksActivity extends NavigationBaseActivity + implements FragmentManager.OnBackStackChangedListener, + MediaDetailPagerFragment.MediaDetailProvider, + AdapterView.OnItemClickListener { + + private FragmentManager supportFragmentManager; + private BookmarksPagerAdapter adapter; + private MediaDetailPagerFragment mediaDetails; + @BindView(R.id.viewPagerBookmarks) + ViewPager viewPager; + @BindView(R.id.tabLayoutBookmarks) + TabLayout tabLayout; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_bookmarks); + ButterKnife.bind(this); + + // Activity can call methods in the fragment by acquiring a + // reference to the Fragment from FragmentManager, using findFragmentById() + supportFragmentManager = getSupportFragmentManager(); + supportFragmentManager.addOnBackStackChangedListener(this); + initDrawer(); + + adapter = new BookmarksPagerAdapter(supportFragmentManager, this); + viewPager.setAdapter(adapter); + tabLayout.setupWithViewPager(viewPager); + } + + /** + * Consumers should be simply using this method to use this activity. + * @param context A Context of the application package implementing this class. + */ + public static void startYourself(Context context) { + Intent intent = new Intent(context, BookmarksActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + context.startActivity(intent); + } + + @Override + public void onBackStackChanged() { + if (supportFragmentManager.getBackStackEntryCount() == 0) { + // The activity has the focus + adapter.requestPictureListUpdate(); + initDrawer(); + } + } + + /** + * This method is called onClick of media inside category details (CategoryImageListFragment). + */ + @Override + public void onItemClick(AdapterView adapterView, View view, int i, long l) { + if (mediaDetails == null || !mediaDetails.isVisible()) { + mediaDetails = new MediaDetailPagerFragment(false, true); + supportFragmentManager + .beginTransaction() + .hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount())) + .add(R.id.fragmentContainer, mediaDetails) + .addToBackStack(null) + .commit(); + supportFragmentManager.executePendingTransactions(); + } + mediaDetails.showImage(i); + forceInitBackButton(); + } + + /** + * This method is called on success of API call for featured Images. + * The viewpager will notified that number of items have changed. + */ + public void viewPagerNotifyDataSetChanged() { + if (mediaDetails!=null){ + mediaDetails.notifyDataSetChanged(); + } + } + + /** + * 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 (adapter.getMediaAdapter() == null) { + // not yet ready to return data + return null; + } else { + return (Media) adapter.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 (adapter.getMediaAdapter() == null) { + return 0; + } + return adapter.getMediaAdapter().getCount(); + } + + /** + * This method is never called but it was in MediaDetailProvider Interface + * so it needs to be overrided. + */ + @Override + public void notifyDatasetChanged() { + + } + + /** + * This method is never called but it was in MediaDetailProvider Interface + * so it needs to be overrided. + */ + @Override + public void registerDataSetObserver(DataSetObserver observer) { + + } + + /** + * This method is never called but it was in MediaDetailProvider Interface + * so it needs to be overrided. + */ + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java new file mode 100644 index 000000000..cad65a87a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java @@ -0,0 +1,66 @@ +package fr.free.nrw.commons.bookmarks; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.widget.ListAdapter; + +import java.util.ArrayList; + +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; + +public class BookmarksPagerAdapter extends FragmentPagerAdapter { + + private ArrayList pages; + + BookmarksPagerAdapter(FragmentManager fm, Context context) { + super(fm); + pages = new ArrayList<>(); + pages.add(new BookmarkPages( + BookmarkPicturesFragment.newInstance(), + context.getString(R.string.title_page_bookmarks_pictures) + )); + pages.add(new BookmarkPages( + BookmarkLocationsFragment.newInstance(), + context.getString(R.string.title_page_bookmarks_locations) + )); + 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)(pages.get(0).getPage()); + return fragment.getAdapter(); + } + + /** + * Update the pictures list for the bookmark fragment + */ + public void requestPictureListUpdate() { + BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(pages.get(0).getPage()); + fragment.onResume(); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsContentProvider.java new file mode 100644 index 000000000..f48745cd2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsContentProvider.java @@ -0,0 +1,93 @@ +package fr.free.nrw.commons.bookmarks.locations; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +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.locations.BookmarkLocationsDao.Table.COLUMN_NAME; +import static fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao.Table.TABLE_NAME; + +public class BookmarkLocationsContentProvider extends CommonsDaggerContentProvider { + + private static final String BASE_PATH = "bookmarksLocations"; + public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY + "/" + BASE_PATH); + + 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; + } + + @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; + } + + @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_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; + } + + @SuppressWarnings("ConstantConditions") + @Override + public Uri insert(@NonNull Uri uri, ContentValues contentValues) { + SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); + long id = sqlDB.insert(BookmarkLocationsDao.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, + "location_name = ?", + new String[]{uri.getLastPathSegment()} + ); + getContext().getContentResolver().notifyChange(uri, null); + return rows; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsController.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsController.java new file mode 100644 index 000000000..6e4c17c2e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsController.java @@ -0,0 +1,26 @@ +package fr.free.nrw.commons.bookmarks.locations; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import fr.free.nrw.commons.nearby.Place; + +@Singleton +public class BookmarkLocationsController { + + @Inject + BookmarkLocationsDao bookmarkLocationDao; + + @Inject + public BookmarkLocationsController() {} + + /** + * Load from DB the bookmarked locations + * @return a list of Place objects. + */ + public List loadFavoritesLocations() { + return bookmarkLocationDao.getAllBookmarksLocations(); + } +} 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 new file mode 100644 index 000000000..06f1b26f5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java @@ -0,0 +1,261 @@ +package fr.free.nrw.commons.bookmarks.locations; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.RemoteException; +import android.support.annotation.NonNull; + +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.Place; +import fr.free.nrw.commons.nearby.Sitelinks; + +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); + } else { + addBookmarkLocation(bookmarkLocation); + } + 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; + } + + @NonNull + Place fromCursor(Cursor cursor) { + LatLng location = new LatLng(cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LAT)), + cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LONG)), 1F); + + 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))); + + Uri uri = Uri.parse(cursor.getString(cursor.getColumnIndex(Table.COLUMN_IMAGE_URL))); + + return new Place( + cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)), + Place.Label.fromText((cursor.getString(cursor.getColumnIndex(Table.COLUMN_LABEL_TEXT)))), + cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)), + uri, + location, + cursor.getString(cursor.getColumnIndex(Table.COLUMN_CATEGORY)), + builder.build() + ); + } + + private ContentValues toContentValues(Place bookmarkLocation) { + ContentValues cv = new ContentValues(); + cv.put(BookmarkLocationsDao.Table.COLUMN_NAME, bookmarkLocation.getName()); + 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().getText()); + cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_ICON, bookmarkLocation.getLabel().getIcon()); + cv.put(BookmarkLocationsDao.Table.COLUMN_IMAGE_URL, bookmarkLocation.getSecondaryImageUrl().toString()); + 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()); + return cv; + } + + public static class Table { + public static final String TABLE_NAME = "bookmarksLocations"; + + static final String COLUMN_NAME = "location_name"; + 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"; + + // 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_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 + }; + + 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_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" + + ");"; + + 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; + } + } + } +} 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 new file mode 100644 index 000000000..5d67c3093 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java @@ -0,0 +1,152 @@ +package fr.free.nrw.commons.bookmarks.locations; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +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 com.pedrogomez.renderers.RVRendererAdapter; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; + +import butterknife.BindView; +import butterknife.ButterKnife; +import dagger.android.support.DaggerFragment; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.contributions.ContributionController; +import fr.free.nrw.commons.nearby.NearbyAdapterFactory; +import fr.free.nrw.commons.nearby.Place; +import timber.log.Timber; + +import static android.app.Activity.RESULT_OK; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + +public class BookmarkLocationsFragment extends DaggerFragment { + + @BindView(R.id.statusMessage) TextView statusTextView; + @BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar; + @BindView(R.id.listView) RecyclerView recyclerView; + @BindView(R.id.parentLayout) RelativeLayout parentLayout; + + @Inject + BookmarkLocationsController controller; + @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; + private NearbyAdapterFactory adapterFactory; + private ContributionController contributionController; + + /** + * 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 + ) { + View v = inflater.inflate(R.layout.fragment_bookmarks_locations, container, false); + ButterKnife.bind(this, v); + contributionController = new ContributionController(this); + adapterFactory = new NearbyAdapterFactory(this, contributionController); + return v; + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + progressBar.setVisibility(View.VISIBLE); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter( + adapterFactory.create( + new ArrayList(), + () -> { + initList(); + } + ) + ); + } + + @Override + public void onResume() { + super.onResume(); + initList(); + } + + /** + * Initialize the recycler view with bookmarked locations + */ + private void initList() { + List places = controller.loadFavoritesLocations(); + adapterFactory.updateAdapterData(places, (RVRendererAdapter) recyclerView.getAdapter()); + progressBar.setVisibility(View.GONE); + if (places.size() <= 0) { + statusTextView.setText(R.string.bookmark_empty); + statusTextView.setVisibility(View.VISIBLE); + } else { + statusTextView.setVisibility(View.GONE); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + Timber.d("onRequestPermissionsResult: req code = " + " perm = " + permissions + " grant =" + grantResults); + + switch (requestCode) { + // 4 = "Read external storage" allowed when gallery selected + case 4: { + if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) { + Timber.d("Call controller.startGalleryPick()"); + contributionController.startGalleryPick(); + } + } + break; + + // 5 = "Write external storage" allowed when camera selected + case 5: { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Timber.d("Call controller.startCameraCapture()"); + contributionController.startCameraCapture(); + } + } + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (resultCode == RESULT_OK) { + Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", + requestCode, resultCode, data); + String wikidataEntityId = directPrefs.getString("WikiDataEntityId", null); + if (requestCode == ContributionController.SELECT_FROM_CAMERA) { + // If coming from camera, pass null as uri. Because camera photos get saved to a + // fixed directory + contributionController.handleImagePicked(requestCode, null, true, wikidataEntityId); + } else { + contributionController.handleImagePicked(requestCode, data.getData(), true, wikidataEntityId); + } + } else { + Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", + requestCode, resultCode, data); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesContentProvider.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesContentProvider.java new file mode 100644 index 000000000..00622f065 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesContentProvider.java @@ -0,0 +1,94 @@ +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; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +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; + +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); + + 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; + } + + @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; + } + + @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; + } + + @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; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java new file mode 100644 index 000000000..cca5867bb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesController.java @@ -0,0 +1,66 @@ +package fr.free.nrw.commons.bookmarks.pictures; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.bookmarks.Bookmark; +import fr.free.nrw.commons.mwapi.MediaWikiApi; + +@Singleton +public class BookmarkPicturesController { + + private MediaWikiApi mediaWikiApi; + + @Inject + BookmarkPicturesDao bookmarkDao; + + private List currentBookmarks; + + @Inject public BookmarkPicturesController(MediaWikiApi mediaWikiApi) { + this.mediaWikiApi = mediaWikiApi; + currentBookmarks = new ArrayList<>(); + } + + /** + * Loads the Media objects from the raw data stored in DB and the API. + * @return a list of bookmarked Media object + */ + List loadBookmarkedPictures() { + List bookmarks = bookmarkDao.getAllBookmarks(); + currentBookmarks = bookmarks; + ArrayList medias = new ArrayList(); + for (Bookmark bookmark : bookmarks) { + List tmpMedias = mediaWikiApi.searchImages(bookmark.getMediaName(), 0); + for (Media m : tmpMedias) { + if (m.getCreator().equals(bookmark.getMediaCreator())) { + medias.add(m); + break; + } + } + } + return medias; + } + + /** + * Loads the Media objects from the raw data stored in DB and the API. + * @return a list of bookmarked Media object + */ + boolean needRefreshBookmarkedPictures() { + List bookmarks = bookmarkDao.getAllBookmarks(); + if (bookmarks.size() == currentBookmarks.size()) { + return false; + } + return true; + } + + /** + * Cancels the requests to the API and the DB + */ + void stop() { + //noop + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java new file mode 100644 index 000000000..c09b6d8c1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java @@ -0,0 +1,216 @@ +package fr.free.nrw.commons.bookmarks.pictures; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.os.RemoteException; +import android.support.annotation.NonNull; + +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.bookmarks.Bookmark; + +import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.BASE_URI; + +public class BookmarkPicturesDao { + + private final Provider clientProvider; + + @Inject + public BookmarkPicturesDao(@Named("bookmarks") Provider clientProvider) { + this.clientProvider = clientProvider; + } + + + /** + * Find all persisted pictures bookmarks on database + * + * @return list of bookmarks + */ + @NonNull + public List getAllBookmarks() { + List 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) { + 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; + } + + @NonNull + Bookmark fromCursor(Cursor cursor) { + return new Bookmark( + cursor.getString(cursor.getColumnIndex(Table.COLUMN_MEDIA_NAME)), + cursor.getString(cursor.getColumnIndex(Table.COLUMN_CREATOR)) + ); + } + + 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; + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java new file mode 100644 index 000000000..4232c1750 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java @@ -0,0 +1,230 @@ +package fr.free.nrw.commons.bookmarks.pictures; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.GridView; +import android.widget.ListAdapter; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; +import dagger.android.support.DaggerFragment; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.bookmarks.BookmarksActivity; +import fr.free.nrw.commons.category.GridViewAdapter; +import fr.free.nrw.commons.utils.NetworkUtils; +import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +public class BookmarkPicturesFragment extends DaggerFragment { + + private static final int TIMEOUT_SECONDS = 15; + + private GridViewAdapter gridAdapter; + + @BindView(R.id.statusMessage) TextView statusTextView; + @BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar; + @BindView(R.id.bookmarkedPicturesList) GridView gridView; + @BindView(R.id.parentLayout) RelativeLayout parentLayout; + + @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 + ) { + View v = inflater.inflate(R.layout.fragment_bookmarks_pictures, container, false); + ButterKnife.bind(this, v); + return v; + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity()); + initList(); + } + + @Override + public void onStop() { + super.onStop(); + controller.stop(); + } + + @Override + public void onResume() { + super.onResume(); + if (controller.needRefreshBookmarkedPictures()) { + gridView.setVisibility(GONE); + if (gridAdapter != null) { + gridAdapter.clear(); + try { + ((BookmarksActivity) getContext()).viewPagerNotifyDataSetChanged(); + }catch (Exception e){ + e.printStackTrace(); + } + } + 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; + } + + progressBar.setVisibility(VISIBLE); + statusTextView.setVisibility(GONE); + + Observable.fromCallable(() -> controller.loadBookmarkedPictures()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .subscribe(this::handleSuccess, this::handleError); + } + + /** + * Handles the UI updates for no internet scenario + */ + private void handleNoInternet() { + progressBar.setVisibility(GONE); + if (gridAdapter == null || gridAdapter.isEmpty()) { + statusTextView.setVisibility(VISIBLE); + statusTextView.setText(getString(R.string.no_internet)); + } else { + ViewUtil.showSnackbar(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.showSnackbar(parentLayout, R.string.error_loading_images); + initErrorView(); + }catch (Exception e){ + e.printStackTrace(); + } + } + + /** + * Handles the UI updates for a error scenario + */ + private void initErrorView() { + progressBar.setVisibility(GONE); + if (gridAdapter == null || gridAdapter.isEmpty()) { + statusTextView.setVisibility(VISIBLE); + statusTextView.setText(getString(R.string.no_images_found)); + } else { + statusTextView.setVisibility(GONE); + } + } + + /** + * Handles the UI updates when there is no bookmarks + */ + private void initEmptyBookmarkListView() { + progressBar.setVisibility(GONE); + if (gridAdapter == null || gridAdapter.isEmpty()) { + statusTextView.setVisibility(VISIBLE); + statusTextView.setText(getString(R.string.bookmark_empty)); + } else { + statusTextView.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 collection) { + if(collection == null) { + initErrorView(); + return; + } + if (collection.isEmpty()) { + initEmptyBookmarkListView(); + return; + } + + if(gridAdapter == null) { + setAdapter(collection); + } else { + if (gridAdapter.containsAll(collection)) { + return; + } + gridAdapter.addItems(collection); + try { + ((BookmarksActivity) getContext()).viewPagerNotifyDataSetChanged(); + }catch (Exception e){ + e.printStackTrace(); + } + } + progressBar.setVisibility(GONE); + statusTextView.setVisibility(GONE); + gridView.setVisibility(VISIBLE); + } + + /** + * Initializes the adapter with a list of Media objects + * @param mediaList List of new Media to be displayed + */ + private void setAdapter(List mediaList) { + gridAdapter = new GridViewAdapter( + this.getContext(), + R.layout.layout_category_images, + mediaList + ); + gridView.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 gridView.getAdapter(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java index 5b32805c7..ca4059e9d 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java @@ -49,10 +49,19 @@ public class GridViewAdapter extends ArrayAdapter { * @param images */ public boolean containsAll(List images){ + if (images == null || images.isEmpty()) { + return false; + } if (data == null) { data = new ArrayList<>(); + return false; } - return images.get(0).getFilename().equals(data.get(0).getFilename()); + if (data.size() <= 0) { + return false; + } + String fileName = data.get(0).getFilename(); + String imageName = images.get(0).getFilename(); + return imageName.equals(fileName); } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java index b019a6303..076eacf75 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java @@ -4,6 +4,8 @@ import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao; import fr.free.nrw.commons.category.CategoryDao; import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao; @@ -12,7 +14,7 @@ import fr.free.nrw.commons.modifications.ModifierSequenceDao; public class DBOpenHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "commons.db"; - private static final int DATABASE_VERSION = 7; + private static final int DATABASE_VERSION = 8; /** * Do not use directly - @Inject an instance where it's needed and let @@ -27,6 +29,8 @@ public class DBOpenHelper extends SQLiteOpenHelper { ContributionDao.Table.onCreate(sqLiteDatabase); ModifierSequenceDao.Table.onCreate(sqLiteDatabase); CategoryDao.Table.onCreate(sqLiteDatabase); + BookmarkPicturesDao.Table.onCreate(sqLiteDatabase); + BookmarkLocationsDao.Table.onCreate(sqLiteDatabase); RecentSearchesDao.Table.onCreate(sqLiteDatabase); } @@ -35,6 +39,8 @@ public class DBOpenHelper extends SQLiteOpenHelper { ContributionDao.Table.onUpdate(sqLiteDatabase, from, to); ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to); CategoryDao.Table.onUpdate(sqLiteDatabase, from, to); + BookmarkPicturesDao.Table.onUpdate(sqLiteDatabase, from, to); + BookmarkLocationsDao.Table.onUpdate(sqLiteDatabase, from, to); RecentSearchesDao.Table.onUpdate(sqLiteDatabase, from, to); } } diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java index 308d60cc3..d26bae178 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -7,6 +7,7 @@ import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.achievements.AchievementsActivity; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.SignupActivity; +import fr.free.nrw.commons.bookmarks.BookmarksActivity; import fr.free.nrw.commons.category.CategoryDetailsActivity; import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; @@ -63,4 +64,7 @@ public abstract class ActivityBuilderModule { @ContributesAndroidInjector abstract AchievementsActivity bindAchievementsActivity(); + @ContributesAndroidInjector + abstract BookmarksActivity bindBookmarksActivity(); + } diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index 7eb15d633..e6a61f307 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -73,6 +73,18 @@ public class CommonsApplicationModule { return context.getContentResolver().acquireContentProviderClient(BuildConfig.MODIFICATION_AUTHORITY); } + @Provides + @Named("bookmarks") + public ContentProviderClient provideBookmarkContentProviderClient(Context context) { + return context.getContentResolver().acquireContentProviderClient(BuildConfig.BOOKMARK_AUTHORITY); + } + + @Provides + @Named("bookmarksLocation") + public ContentProviderClient provideBookmarkLocationContentProviderClient(Context context) { + return context.getContentResolver().acquireContentProviderClient(BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY); + } + @Provides @Named("application_preferences") public SharedPreferences providesApplicationSharedPreferences(Context context) { diff --git a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java index 0db0ff7fb..c52d2c194 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java @@ -2,6 +2,8 @@ package fr.free.nrw.commons.di; import dagger.Module; import dagger.android.ContributesAndroidInjector; +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.contributions.ContributionsContentProvider; import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider; @@ -23,4 +25,10 @@ public abstract class ContentProviderBuilderModule { @ContributesAndroidInjector abstract RecentSearchesContentProvider bindRecentSearchesContentProvider(); + @ContributesAndroidInjector + abstract BookmarkPicturesContentProvider bindBookmarkContentProvider(); + + @ContributesAndroidInjector + abstract BookmarkLocationsContentProvider bindBookmarkLocationContentProvider(); + } diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java index 387e1a837..9868f3576 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -2,6 +2,8 @@ package fr.free.nrw.commons.di; import dagger.Module; import dagger.android.ContributesAndroidInjector; +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.category.CategoryImagesListFragment; import fr.free.nrw.commons.category.SubCategoryListFragment; @@ -67,4 +69,10 @@ public abstract class FragmentBuilderModule { @ContributesAndroidInjector abstract RecentSearchesFragment bindRecentSearchesFragment(); + @ContributesAndroidInjector + abstract BookmarkPicturesFragment bindBookmarkPictureListFragment(); + + @ContributesAndroidInjector + abstract BookmarkLocationsFragment bindBookmarkLocationListFragment(); + } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index 40afaaae4..23d3f2466 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -33,6 +33,8 @@ import butterknife.ButterKnife; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.bookmarks.Bookmark; +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao; import fr.free.nrw.commons.category.CategoryDetailsActivity; import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.contributions.Contribution; @@ -59,11 +61,15 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple @Named("default_preferences") SharedPreferences prefs; + @Inject + BookmarkPicturesDao bookmarkDao; + @BindView(R.id.mediaDetailsPager) ViewPager pager; private Boolean editable; private boolean isFeaturedImage; MediaDetailAdapter adapter; + private Bookmark bookmark; public MediaDetailPagerFragment() { this(false, false); @@ -134,6 +140,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple MediaDetailProvider provider = (MediaDetailProvider) getActivity(); Media m = provider.getMediaAtPosition(pager.getCurrentItem()); switch (item.getItemId()) { + case R.id.menu_bookmark_current_image: + bookmarkDao.updateBookmark(bookmark); + updateBookmarkState(item); + return true; case R.id.menu_share_current_image: Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); @@ -257,6 +267,14 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true); menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true); menu.findItem(R.id.menu_download_current_image).setEnabled(true).setVisible(true); + menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true).setVisible(true); + + // Initialize bookmark object + bookmark = new Bookmark( + m.getFilename(), + m.getCreator() + ); + updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image)); if (m instanceof Contribution ) { Contribution c = (Contribution) m; @@ -267,6 +285,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple menu.findItem(R.id.menu_browser_current_image).setEnabled(false).setVisible(false); menu.findItem(R.id.menu_share_current_image).setEnabled(false).setVisible(false); menu.findItem(R.id.menu_download_current_image).setEnabled(false).setVisible(false); + menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false).setVisible(false); break; case Contribution.STATE_IN_PROGRESS: case Contribution.STATE_QUEUED: @@ -275,6 +294,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple menu.findItem(R.id.menu_browser_current_image).setEnabled(false).setVisible(false); menu.findItem(R.id.menu_share_current_image).setEnabled(false).setVisible(false); menu.findItem(R.id.menu_download_current_image).setEnabled(false).setVisible(false); + menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false).setVisible(false); break; case Contribution.STATE_COMPLETED: // Default set of menu items works fine. Treat same as regular media object @@ -286,6 +306,12 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple } } + private void updateBookmarkState(MenuItem item) { + boolean isBookmarked = bookmarkDao.findBookmark(bookmark); + int icon = isBookmarked ? R.drawable.ic_round_star_filled_24px : R.drawable.ic_round_star_border_24px; + item.setIcon(icon); + } + public void showImage(int i) { Handler handler = new Handler(); handler.postDelayed(() -> pager.setCurrentItem(i), 5); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapterFactory.java index f596dc05a..3adc8e529 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapterFactory.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapterFactory.java @@ -12,7 +12,7 @@ import java.util.List; import fr.free.nrw.commons.contributions.ContributionController; -class NearbyAdapterFactory { +public class NearbyAdapterFactory { private Fragment fragment; private ContributionController controller; @@ -21,14 +21,21 @@ class NearbyAdapterFactory { } - NearbyAdapterFactory(Fragment fragment, ContributionController controller) { + public NearbyAdapterFactory(Fragment fragment, ContributionController controller) { this.fragment = fragment; this.controller = controller; } public RVRendererAdapter create(List placeList) { + return create(placeList, null); + } + + public RVRendererAdapter create( + List placeList, + PlaceRenderer.OnBookmarkClick onBookmarkClick + ) { RendererBuilder builder = new RendererBuilder() - .bind(Place.class, new PlaceRenderer(fragment, controller)); + .bind(Place.class, new PlaceRenderer(fragment, controller, onBookmarkClick)); ListAdapteeCollection collection = new ListAdapteeCollection<>( placeList != null ? placeList : Collections.emptyList()); return new RVRendererAdapter<>(builder, collection); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java index 7ce3aa737..129fe55f0 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java @@ -58,6 +58,7 @@ import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.LoginActivity; +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.utils.UriDeserializer; import fr.free.nrw.commons.utils.ViewUtil; @@ -86,6 +87,7 @@ public class NearbyMapFragment extends DaggerFragment { private LinearLayout wikidataButton; private LinearLayout directionsButton; private LinearLayout commonsButton; + private LinearLayout bookmarkButton; private FloatingActionButton fabPlus; private FloatingActionButton fabCamera; private FloatingActionButton fabGallery; @@ -95,6 +97,7 @@ public class NearbyMapFragment extends DaggerFragment { private TextView title; private TextView distance; private ImageView icon; + private ImageView bookmarkButtonImage; private TextView wikipediaButtonText; private TextView wikidataButtonText; @@ -131,6 +134,8 @@ public class NearbyMapFragment extends DaggerFragment { @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; + @Inject + BookmarkLocationsDao bookmarkLocationDao; public NearbyMapFragment() { } @@ -374,6 +379,9 @@ public class NearbyMapFragment extends DaggerFragment { directionsButtonText = getActivity().findViewById(R.id.directionsButtonText); commonsButtonText = getActivity().findViewById(R.id.commonsButtonText); + bookmarkButton = getActivity().findViewById(R.id.bookmarkButton); + bookmarkButtonImage = getActivity().findViewById(R.id.bookmarkButtonImage); + } private void setListeners() { @@ -721,6 +729,20 @@ public class NearbyMapFragment extends DaggerFragment { private void passInfoToSheet(Place place) { this.place = place; + + int bookmarkIcon; + if (bookmarkLocationDao.findBookmarkLocation(place)) { + bookmarkIcon = R.drawable.ic_round_star_filled_24px; + } else { + bookmarkIcon = R.drawable.ic_round_star_border_24px; + } + bookmarkButtonImage.setImageResource(bookmarkIcon); + bookmarkButton.setOnClickListener(view -> { + boolean isBookmarked = bookmarkLocationDao.updateBookmarkLocation(place); + int updatedIcon = isBookmarked ? R.drawable.ic_round_star_filled_24px : R.drawable.ic_round_star_border_24px; + bookmarkButtonImage.setImageResource(updatedIcon); + }); + wikipediaButton.setEnabled(place.hasWikipediaLink()); wikipediaButton.setOnClickListener(view -> openWebView(place.siteLinks.getWikipediaLink())); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java index 285fcf83e..b13f63bc2 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java @@ -52,6 +52,8 @@ public class Place { this.distance = distance; } + public Uri getSecondaryImageUrl() { return this.secondaryImageUrl; } + /** * Extracts the entity id from the wikidata link * @return returns the entity id if wikidata link exists diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java index 56451e84a..9ea492e81 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java @@ -27,6 +27,7 @@ import butterknife.ButterKnife; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.LoginActivity; +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.di.ApplicationlessInjection; import timber.log.Timber; @@ -50,6 +51,9 @@ public class PlaceRenderer extends Renderer { @BindView(R.id.iconOverflow) LinearLayout iconOverflow; @BindView(R.id.cameraButtonText) TextView cameraButtonText; @BindView(R.id.galleryButtonText) TextView galleryButtonText; + @BindView(R.id.bookmarkButton) LinearLayout bookmarkButton; + @BindView(R.id.bookmarkButtonText) TextView bookmarkButtonText; + @BindView(R.id.bookmarkButtonImage) ImageView bookmarkButtonImage; @BindView(R.id.directionsButtonText) TextView directionsButtonText; @BindView(R.id.iconOverflowText) TextView iconOverflowText; @@ -60,8 +64,10 @@ public class PlaceRenderer extends Renderer { private Fragment fragment; private ContributionController controller; + private OnBookmarkClick onBookmarkClick; - + @Inject + BookmarkLocationsDao bookmarkLocationDao; @Inject @Named("prefs") SharedPreferences prefs; @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; @@ -69,10 +75,15 @@ public class PlaceRenderer extends Renderer { openedItems = new ArrayList<>(); } - public PlaceRenderer(Fragment fragment, ContributionController controller) { + public PlaceRenderer( + Fragment fragment, + ContributionController controller, + OnBookmarkClick onBookmarkClick + ) { this.fragment = fragment; this.controller = controller; openedItems = new ArrayList<>(); + this.onBookmarkClick = onBookmarkClick; } @Override @@ -84,6 +95,7 @@ public class PlaceRenderer extends Renderer { @Override protected void setUpView(View view) { ButterKnife.bind(this, view); + closeLayout(buttonLayout); } @Override @@ -151,6 +163,27 @@ public class PlaceRenderer extends Renderer { } }); + bookmarkButton.setOnClickListener(view4 -> { + if (applicationPrefs.getBoolean("login_skipped", false)) { + // prompt the user to login + new AlertDialog.Builder(getContext()) + .setMessage(R.string.login_alert_message) + .setPositiveButton(R.string.login, (dialog, which) -> { + startActivityWithFlags( getContext(), LoginActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, + Intent.FLAG_ACTIVITY_SINGLE_TOP); + prefs.edit().putBoolean("login_skipped", false).apply(); + fragment.getActivity().finish(); + }) + .show(); + } else { + boolean isBookmarked = bookmarkLocationDao.updateBookmarkLocation(place); + int icon = isBookmarked ? R.drawable.ic_round_star_filled_24px : R.drawable.ic_round_star_border_24px; + bookmarkButtonImage.setImageResource(icon); + if (onBookmarkClick != null) { + onBookmarkClick.onClick(); + } + } + }); } private void storeSharedPrefs() { @@ -197,6 +230,13 @@ public class PlaceRenderer extends Renderer { iconOverflow.setVisibility(showMenu() ? View.VISIBLE : View.GONE); iconOverflow.setOnClickListener(v -> popupMenuListener()); + int icon; + if (bookmarkLocationDao.findBookmarkLocation(place)) { + icon = R.drawable.ic_round_star_filled_24px; + } else { + icon = R.drawable.ic_round_star_border_24px; + } + bookmarkButtonImage.setImageResource(icon); } private void popupMenuListener() { @@ -241,4 +281,8 @@ public class PlaceRenderer extends Renderer { return place.hasCommonsLink() || place.hasWikidataLink(); } + public interface OnBookmarkClick { + void onClick(); + } + } diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index 56732abeb..7bf8289bb 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -32,6 +32,8 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.achievements.AchievementsActivity; import fr.free.nrw.commons.auth.LoginActivity; +import fr.free.nrw.commons.bookmarks.BookmarksActivity; +import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.nearby.NearbyActivity; @@ -77,6 +79,7 @@ public abstract class NavigationBaseActivity extends BaseActivity nav_Menu.findItem(R.id.action_notifications).setVisible(false); nav_Menu.findItem(R.id.action_settings).setVisible(false); nav_Menu.findItem(R.id.action_logout).setVisible(false); + nav_Menu.findItem(R.id.action_bookmarks).setVisible(true); }else { userIcon.setVisibility(View.VISIBLE); nav_Menu.findItem(R.id.action_login).setVisible(false); @@ -84,6 +87,7 @@ public abstract class NavigationBaseActivity extends BaseActivity nav_Menu.findItem(R.id.action_notifications).setVisible(true); nav_Menu.findItem(R.id.action_settings).setVisible(true); nav_Menu.findItem(R.id.action_logout).setVisible(true); + nav_Menu.findItem(R.id.action_bookmarks).setVisible(true); } } @@ -210,6 +214,10 @@ public abstract class NavigationBaseActivity extends BaseActivity drawerLayout.closeDrawer(navigationView); CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_explore), FEATURED_IMAGES_CATEGORY); return true; + case R.id.action_bookmarks: + drawerLayout.closeDrawer(navigationView); + BookmarksActivity.startYourself(this); + return true; default: Timber.e("Unknown option [%s] selected from the navigation menu", itemId); return false; diff --git a/app/src/main/res/drawable/ic_round_star_border_24px.xml b/app/src/main/res/drawable/ic_round_star_border_24px.xml new file mode 100644 index 000000000..d01dfa7fa --- /dev/null +++ b/app/src/main/res/drawable/ic_round_star_border_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_round_star_filled_24px.xml b/app/src/main/res/drawable/ic_round_star_filled_24px.xml new file mode 100644 index 000000000..f9a701c2d --- /dev/null +++ b/app/src/main/res/drawable/ic_round_star_filled_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_bookmarks.xml b/app/src/main/res/layout/activity_bookmarks.xml new file mode 100644 index 000000000..2ceca03cd --- /dev/null +++ b/app/src/main/res/layout/activity_bookmarks.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_details.xml b/app/src/main/res/layout/bottom_sheet_details.xml index c964fda87..63d653bbc 100644 --- a/app/src/main/res/layout/bottom_sheet_details.xml +++ b/app/src/main/res/layout/bottom_sheet_details.xml @@ -61,6 +61,34 @@ android:orientation="horizontal" > + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_bookmarks_pictures.xml b/app/src/main/res/layout/fragment_bookmarks_pictures.xml new file mode 100644 index 000000000..45c8ea460 --- /dev/null +++ b/app/src/main/res/layout/fragment_bookmarks_pictures.xml @@ -0,0 +1,37 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/nearby_row_button.xml b/app/src/main/res/layout/nearby_row_button.xml index bee0fabe8..62c39c4f3 100644 --- a/app/src/main/res/layout/nearby_row_button.xml +++ b/app/src/main/res/layout/nearby_row_button.xml @@ -10,6 +10,34 @@ xmlns:android="http://schemas.android.com/apk/res/android" > + + + + + + + + + app:showAsAction="always" /> Notification de Commons Droit de stockage Nous avons besoin de votre autorisation pour accéder au stockage externe de votre appareil, afin de téléverser des images. + Favoris + Favoris Enregistrement dans le journal, commencé. Veuillez REDEMARRER l\'application, exécutez l\'action que vous souhaitez tracer, et cliquez de nouveau sur \'Envoyer les journaux\' Bienvenue sur Commons !\n\nTéléversez votre premier média en cliquant sur l’icône de l’appareil photo ou sur la galerie ci-dessus. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f8227ffee..a14377ccc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -77,8 +77,8 @@ Starting %1$d uploads - %1$d upload - %1$d uploads + %1$d upload + %1$d uploads No categories matching %1$s found Add categories to make your images more discoverable on Wikimedia Commons.\nStart typing to add categories. @@ -192,7 +192,7 @@ Unable to display more than 500 Set Recent Upload Limit Two factor authentication is currently not supported. - Do you really want to logout? + Do you really want to logout? Commons Logo Commons Website Commons Facebook Page @@ -356,7 +356,14 @@ Commons Notification Storage Permission We need your permission to access the external storage of your device in order to upload images. - + Bookmarks + Bookmarks + Pictures + Locations + Add/Remove to bookmarks + Bookmarks + You haven\'t added any bookmarks + Bookmarks Log collection started. Please RESTART the app, perform action that you wish to log, and then tap \'Send log file\' again Welcome to Commons!\n Upload your first media by touching the camera or gallery icon above. 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 new file mode 100644 index 000000000..80cbc09d7 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/locations/BookMarkLocationDaoTest.kt @@ -0,0 +1,262 @@ +package fr.free.nrw.commons.bookmarks.locations + +import android.content.ContentProviderClient +import android.content.ContentValues +import android.database.Cursor +import android.database.MatrixCursor +import android.database.sqlite.SQLiteDatabase +import android.net.Uri +import android.os.RemoteException +import com.nhaarman.mockito_kotlin.* +import fr.free.nrw.commons.BuildConfig +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.* +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.nearby.Sitelinks +import junit.framework.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = [21], application = TestCommonsApplication::class) +class BookMarkLocationDaoTest { + private val columns = arrayOf(COLUMN_NAME, + 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) + private val client: ContentProviderClient = mock() + private val database: SQLiteDatabase = mock() + private val captor = argumentCaptor() + + private lateinit var testObject: BookmarkLocationsDao + private lateinit var examplePlaceBookmark: Place + private lateinit var exampleLabel: Place.Label + private lateinit var exampleUri: Uri + private lateinit var exampleLocation: LatLng + private lateinit var builder: Sitelinks.Builder + + @Before + fun setUp() { + exampleLabel = Place.Label.FOREST + exampleUri = Uri.parse("wikimedia/uri") + exampleLocation = LatLng(40.0,51.4, 1f) + + builder = Sitelinks.Builder() + builder.setWikipediaLink("wikipediaLink") + builder.setWikidataLink("wikidataLink") + builder.setCommonsLink("commonsLink") + + + examplePlaceBookmark = Place("placeName", exampleLabel, "placeDescription", + exampleUri, exampleLocation, "placeCategory", builder.build()) + testObject = BookmarkLocationsDao { client } + } + + @Test + fun createTable() { + onCreate(database) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + + @Test + fun deleteTable() { + onDelete(database) + inOrder(database) { + verify(database).execSQL(DROP_TABLE_STATEMENT) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + } + + @Test + fun createFromCursor() { + createCursor(1).let { cursor -> + cursor.moveToFirst() + testObject.fromCursor(cursor).let { + assertEquals("placeName", it.name) + assertEquals(Place.Label.FOREST, it.label) + assertEquals("placeDescription", it.longDescription) + assertEquals(exampleUri, it.secondaryImageUrl) + assertEquals(40.0, it.location.latitude) + assertEquals(51.4, it.location.longitude) + 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) + } + } + } + + @Test + fun getAllLocationBookmarks() { + whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(14)) + + var result = testObject.allBookmarksLocations + + assertEquals(14,(result.size)) + + } + + @Test(expected = RuntimeException::class) + fun getAllLocationBookmarksTranslatesExceptions() { + whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow(RemoteException("")) + testObject.allBookmarksLocations + } + + @Test + fun getAllLocationBookmarksReturnsEmptyList_emptyCursor() { + whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(0)) + assertTrue(testObject.allBookmarksLocations.isEmpty()) + } + + @Test + fun getAllLocationBookmarksReturnsEmptyList_nullCursor() { + whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null) + assertTrue(testObject.allBookmarksLocations.isEmpty()) + } + + @Test + fun cursorsAreClosedAfterGetAllLocationBookmarksQuery() { + val mockCursor: Cursor = mock() + whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor) + whenever(mockCursor.moveToFirst()).thenReturn(false) + + testObject.allBookmarksLocations + + verify(mockCursor).close() + } + + + @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(11, cv.size()) + assertEquals(examplePlaceBookmark.name, cv.getAsString(COLUMN_NAME)) + 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)) + assertEquals(examplePlaceBookmark.location.longitude, cv.getAsDouble(COLUMN_LONG)) + 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)) + } + } + + @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(), any(), 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 didnt exist before v5 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v2_to_v3() { + onUpdate(database, 2, 3) + // Table didnt exist before v5 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v3_to_v4() { + onUpdate(database, 3, 4) + // Table didnt exist before v5 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v4_to_v5() { + onUpdate(database, 4, 5) + // Table didnt change in version 5 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v5_to_v6() { + onUpdate(database, 5, 6) + // Table didnt change in version 6 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v6_to_v7() { + onUpdate(database, 6, 7) + // Table didnt change in version 7 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v7_to_v8() { + onUpdate(database, 7, 8) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + + private fun createCursor(rowCount: Int) = MatrixCursor(columns, rowCount).apply { + + for (i in 0 until rowCount) { + addRow(listOf("placeName", "placeDescription","placeCategory", exampleLabel.text, exampleLabel.icon, + exampleUri, builder.build().wikipediaLink, builder.build().wikidataLink, builder.build().commonsLink, + exampleLocation.latitude, exampleLocation.longitude)) + } + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPictureDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPictureDaoTest.kt new file mode 100644 index 000000000..48fae26b7 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/pictures/BookmarkPictureDaoTest.kt @@ -0,0 +1,217 @@ +package fr.free.nrw.commons.bookmarks.pictures + +import android.content.ContentProviderClient +import android.content.ContentValues +import android.database.Cursor +import android.database.MatrixCursor +import android.database.sqlite.SQLiteDatabase +import android.os.RemoteException +import com.nhaarman.mockito_kotlin.* +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.bookmarks.Bookmark +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.BASE_URI +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.* +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = [21], application = TestCommonsApplication::class) +class BookmarkPictureDaoTest { + + private val columns = arrayOf(COLUMN_MEDIA_NAME, COLUMN_CREATOR) + private val client: ContentProviderClient = mock() + private val database: SQLiteDatabase = mock() + private val captor = argumentCaptor() + + private lateinit var testObject: BookmarkPicturesDao + private lateinit var exampleBookmark: Bookmark + + @Before + fun setUp() { + exampleBookmark = Bookmark("mediaName", "creatorName") + testObject = BookmarkPicturesDao { client } + } + + @Test + fun createTable() { + onCreate(database) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + + @Test + fun deleteTable() { + onDelete(database) + inOrder(database) { + verify(database).execSQL(DROP_TABLE_STATEMENT) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + } + + @Test + fun createFromCursor() { + createCursor(1).let { cursor -> + cursor.moveToFirst() + testObject.fromCursor(cursor).let { + assertEquals("mediaName", it.mediaName) + assertEquals("creatorName", it.mediaCreator) + } + } + } + + @Test + fun getAllBookmarks() { + whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(14)) + + var result = testObject.allBookmarks + + assertEquals(14,(result.size)) + + } + + @Test(expected = RuntimeException::class) + fun getAllBookmarksTranslatesExceptions() { + whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow(RemoteException("")) + testObject.allBookmarks + } + + @Test + fun getAllBookmarksReturnsEmptyList_emptyCursor() { + whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(createCursor(0)) + assertTrue(testObject.allBookmarks.isEmpty()) + } + + @Test + fun getAllBookmarksReturnsEmptyList_nullCursor() { + whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null) + assertTrue(testObject.allBookmarks.isEmpty()) + } + + @Test + fun cursorsAreClosedAfterGetAllBookmarksQuery() { + val mockCursor: Cursor = mock() + whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor) + whenever(mockCursor.moveToFirst()).thenReturn(false) + + testObject.allBookmarks + + verify(mockCursor).close() + } + + + @Test + fun updateNewBookmark() { + whenever(client.insert(any(), any())).thenReturn(exampleBookmark.contentUri) + whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(null) + + assertTrue(testObject.updateBookmark(exampleBookmark)) + verify(client).insert(eq(BASE_URI), captor.capture()) + captor.firstValue.let { cv -> + assertEquals(2, cv.size()) + assertEquals(exampleBookmark.mediaName, cv.getAsString(COLUMN_MEDIA_NAME)) + assertEquals(exampleBookmark.mediaCreator, cv.getAsString(COLUMN_CREATOR)) + } + } + + @Test + fun updateExistingBookmark() { + whenever(client.delete(isA(), isNull(), isNull())).thenReturn(1) + whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1)) + + assertFalse(testObject.updateBookmark(exampleBookmark)) + verify(client).delete(eq(exampleBookmark.contentUri), isNull(), isNull()) + } + + @Test + fun findExistingBookmark() { + whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1)) + assertTrue(testObject.findBookmark(exampleBookmark)) + } + + @Test(expected = RuntimeException::class) + fun findBookmarkTranslatesExceptions() { + whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow(RemoteException("")) + testObject.findBookmark(exampleBookmark) + } + + @Test + fun findNotExistingBookmarkReturnsNull_emptyCursor() { + whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(0)) + assertFalse(testObject.findBookmark(exampleBookmark)) + } + + @Test + fun findNotExistingBookmarkReturnsNull_nullCursor() { + whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(null) + assertFalse(testObject.findBookmark(exampleBookmark)) + } + + @Test + fun cursorsAreClosedAfterFindBookmarkQuery() { + val mockCursor: Cursor = mock() + whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(mockCursor) + whenever(mockCursor.moveToFirst()).thenReturn(false) + + testObject.findBookmark(exampleBookmark) + + verify(mockCursor).close() + } + + @Test + fun migrateTableVersionFrom_v1_to_v2() { + onUpdate(database, 1, 2) + // Table didnt exist before v5 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v2_to_v3() { + onUpdate(database, 2, 3) + // Table didnt exist before v5 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v3_to_v4() { + onUpdate(database, 3, 4) + // Table didnt exist before v5 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v4_to_v5() { + onUpdate(database, 4, 5) + // Table didnt change in version 5 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v5_to_v6() { + onUpdate(database, 5, 6) + // Table didnt change in version 6 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v6_to_v7() { + onUpdate(database, 6, 7) + // Table didnt change in version 7 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v7_to_v8() { + onUpdate(database, 7, 8) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + + private fun createCursor(rowCount: Int) = MatrixCursor(columns, rowCount).apply { + for (i in 0 until rowCount) { + addRow(listOf("mediaName", "creatorName")) + } + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt index b64d3b8aa..b3870c60f 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt @@ -86,6 +86,20 @@ class CategoryDaoTest { verifyZeroInteractions(database) } + @Test + fun migrateTableVersionFrom_v6_to_v7() { + onUpdate(database, 6, 7) + // Table didnt change in version 7 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v7_to_v8() { + onUpdate(database, 7, 8) + // Table didnt change in version 8 + verifyZeroInteractions(database) + } + @Test fun createFromCursor() { createCursor(1).let { cursor -> diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionDaoTest.kt index 762e0bb85..0d93a43ed 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionDaoTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionDaoTest.kt @@ -105,6 +105,20 @@ class ContributionDaoTest { } } + @Test + fun migrateTableVersionFrom_v6_to_v7() { + Table.onUpdate(database, 6, 7) + // Table didnt change in version 7 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v7_to_v8() { + Table.onUpdate(database, 7, 8) + // Table didnt change in version 8 + verifyZeroInteractions(database) + } + @Test fun saveNewContribution_nonNullFields() { whenever(client.insert(isA(), isA())).thenReturn(contentUri)