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

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