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
This commit is contained in:
Victor-Bonin 2018-10-25 17:54:22 +02:00 committed by Josephine Lim
parent 89d2d0cfe0
commit 80a9c94653
42 changed files with 2361 additions and 12 deletions

View file

@ -153,6 +153,8 @@ android {
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\"" buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\""
buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.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", "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' dimension 'tier'
} }
@ -180,6 +182,8 @@ android {
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\"" buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\""
buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.beta.categories.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", "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' dimension 'tier'
} }

View file

@ -122,6 +122,10 @@
android:name=".achievements.AchievementsActivity" android:name=".achievements.AchievementsActivity"
android:label="@string/Achievements" /> android:label="@string/Achievements" />
<activity
android:name=".bookmarks.BookmarksActivity"
android:label="@string/title_activity_bookmarks" />
<activity android:name="com.github.pedrovgs.lynx.LynxActivity"/> <activity android:name="com.github.pedrovgs.lynx.LynxActivity"/>
<service android:name=".upload.UploadService" /> <service android:name=".upload.UploadService" />
@ -202,6 +206,20 @@
android:label="@string/provider_searches" android:label="@string/provider_searches"
android:syncable="false" /> android:syncable="false" />
<provider
android:name=".bookmarks.pictures.BookmarkPicturesContentProvider"
android:authorities="${applicationId}.bookmarks.contentprovider"
android:exported="false"
android:label="@string/provider_bookmarks"
android:syncable="false" />
<provider
android:name=".bookmarks.locations.BookmarkLocationsContentProvider"
android:authorities="${applicationId}.bookmarks.locations.contentprovider"
android:exported="false"
android:label="@string/provider_bookmarks_location"
android:syncable="false" />
<receiver android:name=".widget.PicOfDayAppWidget"> <receiver android:name=".widget.PicOfDayAppWidget">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />

View file

@ -29,6 +29,8 @@ import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import fr.free.nrw.commons.auth.SessionManager; 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.category.CategoryDao;
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler; import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler;
import fr.free.nrw.commons.concurrency.ThreadPoolService; import fr.free.nrw.commons.concurrency.ThreadPoolService;
@ -245,6 +247,8 @@ public class CommonsApplication extends Application {
ModifierSequenceDao.Table.onDelete(db); ModifierSequenceDao.Table.onDelete(db);
CategoryDao.Table.onDelete(db); CategoryDao.Table.onDelete(db);
ContributionDao.Table.onDelete(db); ContributionDao.Table.onDelete(db);
BookmarkPicturesDao.Table.onDelete(db);
BookmarkLocationsDao.Table.onDelete(db);
} }
/** /**

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Place> loadFavoritesLocations() {
return bookmarkLocationDao.getAllBookmarksLocations();
}
}

View file

@ -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<ContentProviderClient> clientProvider;
@Inject
public BookmarkLocationsDao(@Named("bookmarksLocation") Provider<ContentProviderClient> clientProvider) {
this.clientProvider = clientProvider;
}
/**
* Find all persisted locations bookmarks on database
*
* @return list of Place
*/
@NonNull
public List<Place> getAllBookmarksLocations() {
List<Place> 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;
}
}
}
}

View file

@ -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<Place>(),
() -> {
initList();
}
)
);
}
@Override
public void onResume() {
super.onResume();
initList();
}
/**
* Initialize the recycler view with bookmarked locations
*/
private void initList() {
List<Place> places = controller.loadFavoritesLocations();
adapterFactory.updateAdapterData(places, (RVRendererAdapter<Place>) 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);
}
}
}

View file

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

View file

@ -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<Bookmark> 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<Media> loadBookmarkedPictures() {
List<Bookmark> bookmarks = bookmarkDao.getAllBookmarks();
currentBookmarks = bookmarks;
ArrayList<Media> medias = new ArrayList<Media>();
for (Bookmark bookmark : bookmarks) {
List<Media> 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<Bookmark> bookmarks = bookmarkDao.getAllBookmarks();
if (bookmarks.size() == currentBookmarks.size()) {
return false;
}
return true;
}
/**
* Cancels the requests to the API and the DB
*/
void stop() {
//noop
}
}

View file

@ -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<ContentProviderClient> clientProvider;
@Inject
public BookmarkPicturesDao(@Named("bookmarks") Provider<ContentProviderClient> clientProvider) {
this.clientProvider = clientProvider;
}
/**
* Find all persisted pictures bookmarks on database
*
* @return list of bookmarks
*/
@NonNull
public List<Bookmark> getAllBookmarks() {
List<Bookmark> items = new ArrayList<>();
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
BookmarkPicturesContentProvider.BASE_URI,
Table.ALL_FIELDS,
null,
new String[]{},
null);
while (cursor != null && cursor.moveToNext()) {
items.add(fromCursor(cursor));
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
db.release();
}
return items;
}
/**
* Look for a bookmark in database and in order to insert or delete it
*
* @param bookmark : Bookmark object
* @return boolean : is bookmark now fav ?
*/
public boolean updateBookmark(Bookmark bookmark) {
boolean bookmarkExists = findBookmark(bookmark);
if (bookmarkExists) {
deleteBookmark(bookmark);
} else {
addBookmark(bookmark);
}
return !bookmarkExists;
}
/**
* Add a Bookmark to database
*
* @param bookmark : Bookmark to add
*/
private void addBookmark(Bookmark bookmark) {
ContentProviderClient db = clientProvider.get();
try {
db.insert(BASE_URI, toContentValues(bookmark));
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Delete a bookmark from database
*
* @param bookmark : Bookmark to delete
*/
private void deleteBookmark(Bookmark bookmark) {
ContentProviderClient db = clientProvider.get();
try {
if (bookmark.getContentUri() == null) {
throw new RuntimeException("tried to delete item with no content URI");
} else {
db.delete(bookmark.getContentUri(), null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Find a bookmark from database based on its name
*
* @param bookmark : Bookmark to find
* @return boolean : is bookmark in database ?
*/
public boolean findBookmark(Bookmark bookmark) {
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;
}
}
}
}

View file

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

View file

@ -49,10 +49,19 @@ public class GridViewAdapter extends ArrayAdapter {
* @param images * @param images
*/ */
public boolean containsAll(List<Media> images){ public boolean containsAll(List<Media> images){
if (images == null || images.isEmpty()) {
return false;
}
if (data == null) { if (data == null) {
data = new ArrayList<>(); 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 @Override

View file

@ -4,6 +4,8 @@ import android.content.Context;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; 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.category.CategoryDao;
import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao; import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
@ -12,7 +14,7 @@ import fr.free.nrw.commons.modifications.ModifierSequenceDao;
public class DBOpenHelper extends SQLiteOpenHelper { public class DBOpenHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "commons.db"; 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 * 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); ContributionDao.Table.onCreate(sqLiteDatabase);
ModifierSequenceDao.Table.onCreate(sqLiteDatabase); ModifierSequenceDao.Table.onCreate(sqLiteDatabase);
CategoryDao.Table.onCreate(sqLiteDatabase); CategoryDao.Table.onCreate(sqLiteDatabase);
BookmarkPicturesDao.Table.onCreate(sqLiteDatabase);
BookmarkLocationsDao.Table.onCreate(sqLiteDatabase);
RecentSearchesDao.Table.onCreate(sqLiteDatabase); RecentSearchesDao.Table.onCreate(sqLiteDatabase);
} }
@ -35,6 +39,8 @@ public class DBOpenHelper extends SQLiteOpenHelper {
ContributionDao.Table.onUpdate(sqLiteDatabase, from, to); ContributionDao.Table.onUpdate(sqLiteDatabase, from, to);
ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to); ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to);
CategoryDao.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); RecentSearchesDao.Table.onUpdate(sqLiteDatabase, from, to);
} }
} }

View file

@ -7,6 +7,7 @@ import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.achievements.AchievementsActivity; import fr.free.nrw.commons.achievements.AchievementsActivity;
import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity; 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.CategoryDetailsActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
@ -63,4 +64,7 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract AchievementsActivity bindAchievementsActivity(); abstract AchievementsActivity bindAchievementsActivity();
@ContributesAndroidInjector
abstract BookmarksActivity bindBookmarksActivity();
} }

View file

@ -73,6 +73,18 @@ public class CommonsApplicationModule {
return context.getContentResolver().acquireContentProviderClient(BuildConfig.MODIFICATION_AUTHORITY); 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 @Provides
@Named("application_preferences") @Named("application_preferences")
public SharedPreferences providesApplicationSharedPreferences(Context context) { public SharedPreferences providesApplicationSharedPreferences(Context context) {

View file

@ -2,6 +2,8 @@ package fr.free.nrw.commons.di;
import dagger.Module; import dagger.Module;
import dagger.android.ContributesAndroidInjector; 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.category.CategoryContentProvider;
import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider; import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
@ -23,4 +25,10 @@ public abstract class ContentProviderBuilderModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract RecentSearchesContentProvider bindRecentSearchesContentProvider(); abstract RecentSearchesContentProvider bindRecentSearchesContentProvider();
@ContributesAndroidInjector
abstract BookmarkPicturesContentProvider bindBookmarkContentProvider();
@ContributesAndroidInjector
abstract BookmarkLocationsContentProvider bindBookmarkLocationContentProvider();
} }

View file

@ -2,6 +2,8 @@ package fr.free.nrw.commons.di;
import dagger.Module; import dagger.Module;
import dagger.android.ContributesAndroidInjector; 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.CategorizationFragment;
import fr.free.nrw.commons.category.CategoryImagesListFragment; import fr.free.nrw.commons.category.CategoryImagesListFragment;
import fr.free.nrw.commons.category.SubCategoryListFragment; import fr.free.nrw.commons.category.SubCategoryListFragment;
@ -67,4 +69,10 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract RecentSearchesFragment bindRecentSearchesFragment(); abstract RecentSearchesFragment bindRecentSearchesFragment();
@ContributesAndroidInjector
abstract BookmarkPicturesFragment bindBookmarkPictureListFragment();
@ContributesAndroidInjector
abstract BookmarkLocationsFragment bindBookmarkLocationListFragment();
} }

View file

@ -33,6 +33,8 @@ import butterknife.ButterKnife;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager; 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.CategoryDetailsActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
@ -59,11 +61,15 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
@Named("default_preferences") @Named("default_preferences")
SharedPreferences prefs; SharedPreferences prefs;
@Inject
BookmarkPicturesDao bookmarkDao;
@BindView(R.id.mediaDetailsPager) @BindView(R.id.mediaDetailsPager)
ViewPager pager; ViewPager pager;
private Boolean editable; private Boolean editable;
private boolean isFeaturedImage; private boolean isFeaturedImage;
MediaDetailAdapter adapter; MediaDetailAdapter adapter;
private Bookmark bookmark;
public MediaDetailPagerFragment() { public MediaDetailPagerFragment() {
this(false, false); this(false, false);
@ -134,6 +140,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
MediaDetailProvider provider = (MediaDetailProvider) getActivity(); MediaDetailProvider provider = (MediaDetailProvider) getActivity();
Media m = provider.getMediaAtPosition(pager.getCurrentItem()); Media m = provider.getMediaAtPosition(pager.getCurrentItem());
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.menu_bookmark_current_image:
bookmarkDao.updateBookmark(bookmark);
updateBookmarkState(item);
return true;
case R.id.menu_share_current_image: case R.id.menu_share_current_image:
Intent shareIntent = new Intent(Intent.ACTION_SEND); Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain"); 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_browser_current_image).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_share_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_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 ) { if (m instanceof Contribution ) {
Contribution c = (Contribution) m; 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_browser_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_share_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_download_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false).setVisible(false);
break; break;
case Contribution.STATE_IN_PROGRESS: case Contribution.STATE_IN_PROGRESS:
case Contribution.STATE_QUEUED: 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_browser_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_share_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_download_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false).setVisible(false);
break; break;
case Contribution.STATE_COMPLETED: case Contribution.STATE_COMPLETED:
// Default set of menu items works fine. Treat same as regular media object // 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) { public void showImage(int i) {
Handler handler = new Handler(); Handler handler = new Handler();
handler.postDelayed(() -> pager.setCurrentItem(i), 5); handler.postDelayed(() -> pager.setCurrentItem(i), 5);

View file

@ -12,7 +12,7 @@ import java.util.List;
import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.contributions.ContributionController;
class NearbyAdapterFactory { public class NearbyAdapterFactory {
private Fragment fragment; private Fragment fragment;
private ContributionController controller; private ContributionController controller;
@ -21,14 +21,21 @@ class NearbyAdapterFactory {
} }
NearbyAdapterFactory(Fragment fragment, ContributionController controller) { public NearbyAdapterFactory(Fragment fragment, ContributionController controller) {
this.fragment = fragment; this.fragment = fragment;
this.controller = controller; this.controller = controller;
} }
public RVRendererAdapter<Place> create(List<Place> placeList) { public RVRendererAdapter<Place> create(List<Place> placeList) {
return create(placeList, null);
}
public RVRendererAdapter<Place> create(
List<Place> placeList,
PlaceRenderer.OnBookmarkClick onBookmarkClick
) {
RendererBuilder<Place> builder = new RendererBuilder<Place>() RendererBuilder<Place> builder = new RendererBuilder<Place>()
.bind(Place.class, new PlaceRenderer(fragment, controller)); .bind(Place.class, new PlaceRenderer(fragment, controller, onBookmarkClick));
ListAdapteeCollection<Place> collection = new ListAdapteeCollection<>( ListAdapteeCollection<Place> collection = new ListAdapteeCollection<>(
placeList != null ? placeList : Collections.emptyList()); placeList != null ? placeList : Collections.emptyList());
return new RVRendererAdapter<>(builder, collection); return new RVRendererAdapter<>(builder, collection);

View file

@ -58,6 +58,7 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.LoginActivity; 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.contributions.ContributionController;
import fr.free.nrw.commons.utils.UriDeserializer; import fr.free.nrw.commons.utils.UriDeserializer;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
@ -86,6 +87,7 @@ public class NearbyMapFragment extends DaggerFragment {
private LinearLayout wikidataButton; private LinearLayout wikidataButton;
private LinearLayout directionsButton; private LinearLayout directionsButton;
private LinearLayout commonsButton; private LinearLayout commonsButton;
private LinearLayout bookmarkButton;
private FloatingActionButton fabPlus; private FloatingActionButton fabPlus;
private FloatingActionButton fabCamera; private FloatingActionButton fabCamera;
private FloatingActionButton fabGallery; private FloatingActionButton fabGallery;
@ -95,6 +97,7 @@ public class NearbyMapFragment extends DaggerFragment {
private TextView title; private TextView title;
private TextView distance; private TextView distance;
private ImageView icon; private ImageView icon;
private ImageView bookmarkButtonImage;
private TextView wikipediaButtonText; private TextView wikipediaButtonText;
private TextView wikidataButtonText; private TextView wikidataButtonText;
@ -131,6 +134,8 @@ public class NearbyMapFragment extends DaggerFragment {
@Inject @Inject
@Named("direct_nearby_upload_prefs") @Named("direct_nearby_upload_prefs")
SharedPreferences directPrefs; SharedPreferences directPrefs;
@Inject
BookmarkLocationsDao bookmarkLocationDao;
public NearbyMapFragment() { public NearbyMapFragment() {
} }
@ -374,6 +379,9 @@ public class NearbyMapFragment extends DaggerFragment {
directionsButtonText = getActivity().findViewById(R.id.directionsButtonText); directionsButtonText = getActivity().findViewById(R.id.directionsButtonText);
commonsButtonText = getActivity().findViewById(R.id.commonsButtonText); commonsButtonText = getActivity().findViewById(R.id.commonsButtonText);
bookmarkButton = getActivity().findViewById(R.id.bookmarkButton);
bookmarkButtonImage = getActivity().findViewById(R.id.bookmarkButtonImage);
} }
private void setListeners() { private void setListeners() {
@ -721,6 +729,20 @@ public class NearbyMapFragment extends DaggerFragment {
private void passInfoToSheet(Place place) { private void passInfoToSheet(Place place) {
this.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.setEnabled(place.hasWikipediaLink());
wikipediaButton.setOnClickListener(view -> openWebView(place.siteLinks.getWikipediaLink())); wikipediaButton.setOnClickListener(view -> openWebView(place.siteLinks.getWikipediaLink()));

View file

@ -52,6 +52,8 @@ public class Place {
this.distance = distance; this.distance = distance;
} }
public Uri getSecondaryImageUrl() { return this.secondaryImageUrl; }
/** /**
* Extracts the entity id from the wikidata link * Extracts the entity id from the wikidata link
* @return returns the entity id if wikidata link exists * @return returns the entity id if wikidata link exists

View file

@ -27,6 +27,7 @@ import butterknife.ButterKnife;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.LoginActivity; 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.contributions.ContributionController;
import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.di.ApplicationlessInjection;
import timber.log.Timber; import timber.log.Timber;
@ -50,6 +51,9 @@ public class PlaceRenderer extends Renderer<Place> {
@BindView(R.id.iconOverflow) LinearLayout iconOverflow; @BindView(R.id.iconOverflow) LinearLayout iconOverflow;
@BindView(R.id.cameraButtonText) TextView cameraButtonText; @BindView(R.id.cameraButtonText) TextView cameraButtonText;
@BindView(R.id.galleryButtonText) TextView galleryButtonText; @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.directionsButtonText) TextView directionsButtonText;
@BindView(R.id.iconOverflowText) TextView iconOverflowText; @BindView(R.id.iconOverflowText) TextView iconOverflowText;
@ -60,8 +64,10 @@ public class PlaceRenderer extends Renderer<Place> {
private Fragment fragment; private Fragment fragment;
private ContributionController controller; private ContributionController controller;
private OnBookmarkClick onBookmarkClick;
@Inject
BookmarkLocationsDao bookmarkLocationDao;
@Inject @Named("prefs") SharedPreferences prefs; @Inject @Named("prefs") SharedPreferences prefs;
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
@ -69,10 +75,15 @@ public class PlaceRenderer extends Renderer<Place> {
openedItems = new ArrayList<>(); openedItems = new ArrayList<>();
} }
public PlaceRenderer(Fragment fragment, ContributionController controller) { public PlaceRenderer(
Fragment fragment,
ContributionController controller,
OnBookmarkClick onBookmarkClick
) {
this.fragment = fragment; this.fragment = fragment;
this.controller = controller; this.controller = controller;
openedItems = new ArrayList<>(); openedItems = new ArrayList<>();
this.onBookmarkClick = onBookmarkClick;
} }
@Override @Override
@ -84,6 +95,7 @@ public class PlaceRenderer extends Renderer<Place> {
@Override @Override
protected void setUpView(View view) { protected void setUpView(View view) {
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
closeLayout(buttonLayout);
} }
@Override @Override
@ -151,6 +163,27 @@ public class PlaceRenderer extends Renderer<Place> {
} }
}); });
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() { private void storeSharedPrefs() {
@ -197,6 +230,13 @@ public class PlaceRenderer extends Renderer<Place> {
iconOverflow.setVisibility(showMenu() ? View.VISIBLE : View.GONE); iconOverflow.setVisibility(showMenu() ? View.VISIBLE : View.GONE);
iconOverflow.setOnClickListener(v -> popupMenuListener()); 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() { private void popupMenuListener() {
@ -241,4 +281,8 @@ public class PlaceRenderer extends Renderer<Place> {
return place.hasCommonsLink() || place.hasWikidataLink(); return place.hasCommonsLink() || place.hasWikidataLink();
} }
public interface OnBookmarkClick {
void onClick();
}
} }

View file

@ -32,6 +32,8 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.achievements.AchievementsActivity; import fr.free.nrw.commons.achievements.AchievementsActivity;
import fr.free.nrw.commons.auth.LoginActivity; 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.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.nearby.NearbyActivity; 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_notifications).setVisible(false);
nav_Menu.findItem(R.id.action_settings).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_logout).setVisible(false);
nav_Menu.findItem(R.id.action_bookmarks).setVisible(true);
}else { }else {
userIcon.setVisibility(View.VISIBLE); userIcon.setVisibility(View.VISIBLE);
nav_Menu.findItem(R.id.action_login).setVisible(false); 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_notifications).setVisible(true);
nav_Menu.findItem(R.id.action_settings).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_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); drawerLayout.closeDrawer(navigationView);
CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_explore), FEATURED_IMAGES_CATEGORY); CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_explore), FEATURED_IMAGES_CATEGORY);
return true; return true;
case R.id.action_bookmarks:
drawerLayout.closeDrawer(navigationView);
BookmarksActivity.startYourself(this);
return true;
default: default:
Timber.e("Unknown option [%s] selected from the navigation menu", itemId); Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
return false; return false;

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/white"
android:pathData="M19.65,9.04l-4.84,-0.42 -1.89,-4.45c-0.34,-0.81 -1.5,-0.81 -1.84,0L9.19,8.63l-4.83,0.41c-0.88,0.07 -1.24,1.17 -0.57,1.75l3.67,3.18 -1.1,4.72c-0.2,0.86 0.73,1.54 1.49,1.08l4.15,-2.5 4.15,2.51c0.76,0.46 1.69,-0.22 1.49,-1.08l-1.1,-4.73 3.67,-3.18c0.67,-0.58 0.32,-1.68 -0.56,-1.75zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/white"
android:pathData="M19.65,9.04l-4.84,-0.42 -1.89,-4.45c-0.34,-0.81 -1.5,-0.81 -1.84,0L9.19,8.63l-4.83,0.41c-0.88,0.07 -1.24,1.17 -0.57,1.75l3.67,3.18 -1.1,4.72c-0.2,0.86 0.73,1.54 1.49,1.08l4.15,-2.5 4.15,2.51c0.76,0.46 1.69,-0.22 1.49,-1.08l-1.1,-4.73 3.67,-3.18c0.67,-0.58 0.32,-1.68 -0.56,-1.75z"/>
</vector>

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/toolbar"
layout="@layout/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll" />
<android.support.design.widget.TabLayout
android:id="@+id/tabLayoutBookmarks"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<android.support.v4.view.ViewPager
android:id="@+id/viewPagerBookmarks"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tabLayoutBookmarks" />
<FrameLayout
android:id="@+id/fragmentContainer"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar"/>
</android.support.constraint.ConstraintLayout>
<android.support.design.widget.NavigationView
android:id="@+id/navigation_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/drawer_header"
app:menu="@menu/drawer" />
</android.support.v4.widget.DrawerLayout>

View file

@ -61,6 +61,34 @@
android:orientation="horizontal" android:orientation="horizontal"
> >
<LinearLayout
android:id="@+id/bookmarkButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="16dp"
android:clickable="true"
android:orientation="vertical"
android:background="@drawable/button_background_selector"
>
<ImageView
android:id="@+id/bookmarkButtonImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
app:srcCompat="@drawable/ic_round_star_border_24px"
android:tint="@color/button_blue"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/bookmarkButtonText"
android:paddingTop="8dp"
android:layout_gravity="center_horizontal"
android:text="CAMERA"
android:visibility="gone"
/>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/directionsButton" android:id="@+id/directionsButton"
android:layout_width="0dp" android:layout_width="0dp"

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/parentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/statusMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_gravity="center"
android:text="@string/waiting_first_sync"
android:visibility="gone"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/loadingImagesProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone" />
<android.support.v7.widget.RecyclerView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/parentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/statusMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_gravity="center"
android:text="@string/waiting_first_sync"
android:visibility="gone"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/loadingImagesProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone" />
<GridView
android:id="@+id/bookmarkedPicturesList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnWidth="240dp"
android:fadingEdge="none"
android:fastScrollEnabled="true"
android:listSelector="@null"
android:numColumns="auto_fit"
android:stretchMode="columnWidth" />
</RelativeLayout>

View file

@ -10,6 +10,34 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
> >
<LinearLayout
android:id="@+id/bookmarkButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="16dp"
android:clickable="true"
android:orientation="vertical"
android:background="@drawable/button_background_selector"
>
<ImageView
android:id="@+id/bookmarkButtonImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
app:srcCompat="@drawable/ic_round_star_border_24px"
android:tint="@color/button_blue"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/bookmarkButtonText"
android:paddingTop="8dp"
android:layout_gravity="center_horizontal"
android:text="CAMERA"
android:visibility="gone"
/>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/cameraButton" android:id="@+id/cameraButton"
android:layout_width="0dp" android:layout_width="0dp"

View file

@ -22,6 +22,11 @@
android:icon="@drawable/ic_explore_24dp" android:icon="@drawable/ic_explore_24dp"
android:title="@string/navigation_item_explore"/> android:title="@string/navigation_item_explore"/>
<item
android:id="@+id/action_bookmarks"
android:icon="@drawable/ic_round_star_filled_24px"
android:title="@string/navigation_item_bookmarks"/>
</group> </group>
<group android:id="@+id/drawer_account"> <group android:id="@+id/drawer_account">
<item <item

View file

@ -2,11 +2,16 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_bookmark_current_image"
android:icon="@drawable/ic_round_star_border_24px"
android:title="@string/menu_bookmark"
app:showAsAction="always" />
<item <item
android:id="@+id/menu_share_current_image" android:id="@+id/menu_share_current_image"
android:icon="@drawable/ic_share_black_24dp" android:icon="@drawable/ic_share_black_24dp"
android:title="@string/menu_share" android:title="@string/menu_share"
app:showAsAction="ifRoom" /> app:showAsAction="always" />
<item <item
android:id="@+id/menu_browser_current_image" android:id="@+id/menu_browser_current_image"
android:title="@string/menu_open_in_browser" android:title="@string/menu_open_in_browser"

View file

@ -361,6 +361,8 @@
<string name="notifications_channel_name_all">Notification de Commons</string> <string name="notifications_channel_name_all">Notification de Commons</string>
<string name="storage_permission">Droit de stockage</string> <string name="storage_permission">Droit de stockage</string>
<string name="write_storage_permission_rationale_for_image_share">Nous avons besoin de votre autorisation pour accéder au stockage externe de votre appareil, afin de téléverser des images.</string> <string name="write_storage_permission_rationale_for_image_share">Nous avons besoin de votre autorisation pour accéder au stockage externe de votre appareil, afin de téléverser des images.</string>
<string name="navigation_item_bookmarks">Favoris</string>
<string name="title_activity_bookmarks">Favoris</string>
<string name="log_collection_started">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\'</string> <string name="log_collection_started">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\'</string>
<string name="no_uploads">Bienvenue sur Commons !\n\nTéléversez votre premier média en cliquant sur licône de lappareil photo ou sur la galerie ci-dessus.</string> <string name="no_uploads">Bienvenue sur Commons !\n\nTéléversez votre premier média en cliquant sur licône de lappareil photo ou sur la galerie ci-dessus.</string>
</resources> </resources>

View file

@ -77,8 +77,8 @@
<item quantity="other">Starting %1$d uploads</item> <item quantity="other">Starting %1$d uploads</item>
</plurals> </plurals>
<plurals name="multiple_uploads_title"> <plurals name="multiple_uploads_title">
<item quantity="one">%1$d upload</item> <item quantity="one">%1$d upload</item>
<item quantity="other">%1$d uploads</item> <item quantity="other">%1$d uploads</item>
</plurals> </plurals>
<string name="categories_not_found">No categories matching %1$s found</string> <string name="categories_not_found">No categories matching %1$s found</string>
<string name="categories_skip_explanation">Add categories to make your images more discoverable on Wikimedia Commons.\nStart typing to add categories.</string> <string name="categories_skip_explanation">Add categories to make your images more discoverable on Wikimedia Commons.\nStart typing to add categories.</string>
@ -192,7 +192,7 @@
<string name="maximum_limit_alert">Unable to display more than 500</string> <string name="maximum_limit_alert">Unable to display more than 500</string>
<string name="set_limit">Set Recent Upload Limit</string> <string name="set_limit">Set Recent Upload Limit</string>
<string name="login_failed_2fa_not_supported">Two factor authentication is currently not supported.</string> <string name="login_failed_2fa_not_supported">Two factor authentication is currently not supported.</string>
<string name="logout_verification">Do you really want to logout?</string> <string name="logout_verification">Do you really want to logout?</string>
<string name="commons_logo">Commons Logo</string> <string name="commons_logo">Commons Logo</string>
<string name="commons_website">Commons Website</string> <string name="commons_website">Commons Website</string>
<string name="commons_facebook">Commons Facebook Page</string> <string name="commons_facebook">Commons Facebook Page</string>
@ -356,7 +356,14 @@
<string name="notifications_channel_name_all">Commons Notification</string> <string name="notifications_channel_name_all">Commons Notification</string>
<string name="storage_permission">Storage Permission</string> <string name="storage_permission">Storage Permission</string>
<string name="write_storage_permission_rationale_for_image_share">We need your permission to access the external storage of your device in order to upload images.</string> <string name="write_storage_permission_rationale_for_image_share">We need your permission to access the external storage of your device in order to upload images.</string>
<string name="navigation_item_bookmarks">Bookmarks</string>
<string name="title_activity_bookmarks">Bookmarks</string>
<string name="title_page_bookmarks_pictures">Pictures</string>
<string name="title_page_bookmarks_locations">Locations</string>
<string name="menu_bookmark">Add/Remove to bookmarks</string>
<string name="provider_bookmarks">Bookmarks</string>
<string name="bookmark_empty">You haven\'t added any bookmarks</string>
<string name="provider_bookmarks_location">Bookmarks</string>
<string name="log_collection_started">Log collection started. Please RESTART the app, perform action that you wish to log, and then tap \'Send log file\' again</string> <string name="log_collection_started">Log collection started. Please RESTART the app, perform action that you wish to log, and then tap \'Send log file\' again</string>
<string name="no_uploads">Welcome to Commons!\n <string name="no_uploads">Welcome to Commons!\n
Upload your first media by touching the camera or gallery icon above.</string> Upload your first media by touching the camera or gallery icon above.</string>

View file

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

View file

@ -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<ContentValues>()
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"))
}
}
}

View file

@ -86,6 +86,20 @@ class CategoryDaoTest {
verifyZeroInteractions(database) 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 @Test
fun createFromCursor() { fun createFromCursor() {
createCursor(1).let { cursor -> createCursor(1).let { cursor ->

View file

@ -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 @Test
fun saveNewContribution_nonNullFields() { fun saveNewContribution_nonNullFields() {
whenever(client.insert(isA(), isA())).thenReturn(contentUri) whenever(client.insert(isA(), isA())).thenReturn(contentUri)