diff --git a/app/build.gradle b/app/build.gradle index 184e756ac..eab310df4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,11 +23,11 @@ dependencies { } - implementation "com.android.support:support-v4:${project.supportLibVersion}" - implementation "com.android.support:appcompat-v7:${project.supportLibVersion}" - implementation "com.android.support:design:${project.supportLibVersion}" + implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION" + implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION" + implementation "com.android.support:design:$SUPPORT_LIB_VERSION" - implementation "com.android.support:cardview-v7:${project.supportLibVersion}" + implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION" implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" @@ -44,7 +44,7 @@ dependencies { implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0' - implementation 'com.facebook.fresco:fresco:1.3.0' + implementation 'com.facebook.fresco:fresco:1.5.0' implementation 'com.facebook.stetho:stetho:1.5.0' implementation "com.google.dagger:dagger:$DAGGER_VERSION" @@ -62,17 +62,17 @@ dependencies { testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' - androidTestImplementation "com.android.support:support-annotations:${project.supportLibVersion}" + androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION" androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' - debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.1' - releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' - testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' + debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY" + releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" + testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" - implementation 'com.google.dagger:dagger:2.11' - implementation 'com.google.dagger:dagger-android-support:2.11' - annotationProcessor 'com.google.dagger:dagger-compiler:2.11' - annotationProcessor 'com.google.dagger:dagger-android-processor:2.11' + implementation "com.google.dagger:dagger:$DAGGER_VERSION" + implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION" + kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" + kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" } android { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e262e9088..253bdaea8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -87,6 +87,10 @@ android:label="@string/title_activity_nearby" android:parentActivityName=".contributions.ContributionsActivity" /> + + categoriesAdapter; private OnCategoriesSaveHandler onCategoriesSaveHandler; private HashMap> categoriesCache; private List selectedCategories = new ArrayList<>(); - private ContentProviderClient databaseClient; private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> { if (item.isSelected()) { selectedCategories.add(item); - updateCategoryCount(item, databaseClient); + updateCategoryCount(item); } else { selectedCategories.remove(item); } @@ -140,7 +137,6 @@ public class CategorizationFragment extends DaggerFragment { @Override public void onDestroy() { super.onDestroy(); - databaseClient.release(); } @Override @@ -178,7 +174,6 @@ public class CategorizationFragment extends DaggerFragment { setHasOptionsMenu(true); onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity(); getActivity().setTitle(R.string.categories_activity_title); - databaseClient = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY); } private void updateCategoryList(String filter) { @@ -261,7 +256,7 @@ public class CategorizationFragment extends DaggerFragment { } private Observable recentCategories() { - return Observable.fromIterable(Category.recentCategories(databaseClient, SEARCH_CATS_LIMIT)) + return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT)) .map(s -> new CategoryItem(s, false)); } @@ -311,24 +306,16 @@ public class CategorizationFragment extends DaggerFragment { || item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")); } - private void updateCategoryCount(CategoryItem item, ContentProviderClient client) { - Category cat = lookupCategory(item.getName()); - cat.incTimesUsed(); - cat.save(client); - } + private void updateCategoryCount(CategoryItem item) { + Category category = categoryDao.find(item.getName()); - private Category lookupCategory(String name) { - Category cat = Category.find(databaseClient, name); - - if (cat == null) { - // Newly used category... - cat = new Category(); - cat.setName(name); - cat.setLastUsed(new Date()); - cat.setTimesUsed(0); + // Newly used category... + if (category == null) { + category = new Category(null, item.getName(), new Date(), 0); } - return cat; + category.incTimesUsed(); + categoryDao.save(category); } public int getCurrentSelectedCount() { diff --git a/app/src/main/java/fr/free/nrw/commons/category/Category.java b/app/src/main/java/fr/free/nrw/commons/category/Category.java new file mode 100644 index 000000000..f2d83d2e5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/Category.java @@ -0,0 +1,96 @@ +package fr.free.nrw.commons.category; + +import android.net.Uri; + +import java.util.Date; + +/** + * Represents a category + */ +public class Category { + private Uri contentUri; + private String name; + private Date lastUsed; + private int timesUsed; + + public Category() { + } + + public Category(Uri contentUri, String name, Date lastUsed, int timesUsed) { + this.contentUri = contentUri; + this.name = name; + this.lastUsed = lastUsed; + this.timesUsed = timesUsed; + } + + /** + * Gets name + * + * @return name + */ + public String getName() { + return name; + } + + /** + * Modifies name + * + * @param name Category name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets last used date + * + * @return Last used date + */ + public Date getLastUsed() { + // warning: Date objects are mutable. + return (Date)lastUsed.clone(); + } + + /** + * Generates new last used date + */ + private void touch() { + lastUsed = new Date(); + } + + /** + * Gets no. of times the category is used + * + * @return no. of times used + */ + public int getTimesUsed() { + return timesUsed; + } + + /** + * Increments timesUsed by 1 and sets last used date as now. + */ + public void incTimesUsed() { + timesUsed++; + touch(); + } + + /** + * Gets the content URI for this category + * + * @return content URI + */ + public Uri getContentUri() { + return contentUri; + } + + /** + * Modifies the content URI - marking this category as already saved in the database + * + * @param contentUri the content URI + */ + public void setContentUri(Uri contentUri) { + this.contentUri = contentUri; + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java index ed698ec4c..dcc1bc6f2 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java @@ -17,9 +17,9 @@ import fr.free.nrw.commons.data.DBOpenHelper; import timber.log.Timber; import static android.content.UriMatcher.NO_MATCH; -import static fr.free.nrw.commons.data.Category.Table.ALL_FIELDS; -import static fr.free.nrw.commons.data.Category.Table.COLUMN_ID; -import static fr.free.nrw.commons.data.Category.Table.TABLE_NAME; +import static fr.free.nrw.commons.category.CategoryDao.Table.ALL_FIELDS; +import static fr.free.nrw.commons.category.CategoryDao.Table.COLUMN_ID; +import static fr.free.nrw.commons.category.CategoryDao.Table.TABLE_NAME; public class CategoryContentProvider extends ContentProvider { diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java new file mode 100644 index 000000000..e63b04c26 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java @@ -0,0 +1,184 @@ +package fr.free.nrw.commons.category; + +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 android.support.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; + +public class CategoryDao { + + private final Provider clientProvider; + + @Inject + public CategoryDao(@Named("category") Provider clientProvider) { + this.clientProvider = clientProvider; + } + + public void save(Category category) { + ContentProviderClient db = clientProvider.get(); + try { + if (category.getContentUri() == null) { + category.setContentUri(db.insert(CategoryContentProvider.BASE_URI, toContentValues(category))); + } else { + db.update(category.getContentUri(), toContentValues(category), null, null); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + db.release(); + } + } + + /** + * Find persisted category in database, based on its name. + * + * @param name Category's name + * @return category from database, or null if not found + */ + @Nullable + Category find(String name) { + Cursor cursor = null; + ContentProviderClient db = clientProvider.get(); + try { + cursor = db.query( + CategoryContentProvider.BASE_URI, + Table.ALL_FIELDS, + Table.COLUMN_NAME + "=?", + new String[]{name}, + null); + if (cursor != null && cursor.moveToFirst()) { + return fromCursor(cursor); + } + } 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 null; + } + + /** + * Retrieve recently-used categories, ordered by descending date. + * + * @return a list containing recent categories + */ + @NonNull + List recentCategories(int limit) { + List items = new ArrayList<>(); + Cursor cursor = null; + ContentProviderClient db = clientProvider.get(); + try { + cursor = db.query( + CategoryContentProvider.BASE_URI, + Table.ALL_FIELDS, + null, + new String[]{}, + Table.COLUMN_LAST_USED + " DESC"); + // fixme add a limit on the original query instead of falling out of the loop? + while (cursor != null && cursor.moveToNext() + && cursor.getPosition() < limit) { + items.add(fromCursor(cursor).getName()); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + if (cursor != null) { + cursor.close(); + } + db.release(); + } + return items; + } + + Category fromCursor(Cursor cursor) { + // Hardcoding column positions! + return new Category( + CategoryContentProvider.uriForId(cursor.getInt(0)), + cursor.getString(1), + new Date(cursor.getLong(2)), + cursor.getInt(3) + ); + } + + private ContentValues toContentValues(Category category) { + ContentValues cv = new ContentValues(); + cv.put(CategoryDao.Table.COLUMN_NAME, category.getName()); + cv.put(CategoryDao.Table.COLUMN_LAST_USED, category.getLastUsed().getTime()); + cv.put(CategoryDao.Table.COLUMN_TIMES_USED, category.getTimesUsed()); + return cv; + } + + public static class Table { + public static final String TABLE_NAME = "categories"; + + public static final String COLUMN_ID = "_id"; + static final String COLUMN_NAME = "name"; + static final String COLUMN_LAST_USED = "last_used"; + static final String COLUMN_TIMES_USED = "times_used"; + + // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. + public static final String[] ALL_FIELDS = { + COLUMN_ID, + COLUMN_NAME, + COLUMN_LAST_USED, + COLUMN_TIMES_USED + }; + + static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; + + static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" + + COLUMN_ID + " INTEGER PRIMARY KEY," + + COLUMN_NAME + " STRING," + + COLUMN_LAST_USED + " INTEGER," + + COLUMN_TIMES_USED + " INTEGER" + + ");"; + + 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 < 4) { + // doesn't exist yet + from++; + onUpdate(db, from, to); + return; + } + if (from == 4) { + // table added in version 5 + onCreate(db); + from++; + onUpdate(db, from, to); + return; + } + if (from == 5) { + from++; + onUpdate(db, from, to); + return; + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java index e673c7d9d..00baac847 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java @@ -1,14 +1,8 @@ package fr.free.nrw.commons.contributions; -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.Parcel; -import android.os.RemoteException; import android.support.annotation.NonNull; -import android.text.TextUtils; import java.text.SimpleDateFormat; import java.util.Date; @@ -43,7 +37,6 @@ public class Contribution extends Media { public static final String SOURCE_GALLERY = "gallery"; public static final String SOURCE_EXTERNAL = "external"; - private ContentProviderClient client; private Uri contentUri; private String source; private String editSummary; @@ -51,24 +44,42 @@ public class Contribution extends Media { private int state; private long transferred; private String decimalCoords; - private boolean isMultiple; - public boolean getMultiple() { - return isMultiple; + public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp, + int state, long dataLength, Date dateUploaded, long transferred, + String source, String description, String creator, boolean isMultiple, + int width, int height, String license) { + super(localUri, imageUrl, filename, description, dataLength, timestamp, dateUploaded, creator); + this.contentUri = contentUri; + this.state = state; + this.timestamp = timestamp; + this.transferred = transferred; + this.source = source; + this.isMultiple = isMultiple; + this.width = width; + this.height = height; + this.license = license; } - public void setMultiple(boolean multiple) { - isMultiple = multiple; - } - - public Contribution(Uri localUri, String remoteUri, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) { - super(localUri, remoteUri, filename, description, dataLength, dateCreated, dateUploaded, creator); + public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength, + Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) { + super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator); this.decimalCoords = decimalCoords; this.editSummary = editSummary; timestamp = new Date(System.currentTimeMillis()); } + public Contribution(Parcel in) { + super(in); + contentUri = in.readParcelable(Uri.class.getClassLoader()); + source = in.readString(); + timestamp = (Date) in.readSerializable(); + state = in.readInt(); + transferred = in.readLong(); + isMultiple = in.readInt() == 1; + } + @Override public void writeToParcel(Parcel parcel, int flags) { super.writeToParcel(parcel, flags); @@ -80,14 +91,12 @@ public class Contribution extends Media { parcel.writeInt(isMultiple ? 1 : 0); } - public Contribution(Parcel in) { - super(in); - contentUri = in.readParcelable(Uri.class.getClassLoader()); - source = in.readString(); - timestamp = (Date) in.readSerializable(); - state = in.readInt(); - transferred = in.readLong(); - isMultiple = in.readInt() == 1; + public boolean getMultiple() { + return isMultiple; + } + + public void setMultiple(boolean multiple) { + isMultiple = multiple; } public long getTransferred() { @@ -106,10 +115,18 @@ public class Contribution extends Media { return contentUri; } + public void setContentUri(Uri contentUri) { + this.contentUri = contentUri; + } + public Date getTimestamp() { return timestamp; } + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + public int getState() { return state; } @@ -155,62 +172,6 @@ public class Contribution extends Media { return buffer.toString(); } - public void setContentProviderClient(ContentProviderClient client) { - this.client = client; - } - - public void save() { - try { - if (contentUri == null) { - contentUri = client.insert(ContributionsContentProvider.BASE_URI, this.toContentValues()); - } else { - client.update(contentUri, toContentValues(), null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - - public void delete() { - try { - if (contentUri == null) { - // noooo - throw new RuntimeException("tried to delete item with no content URI"); - } else { - client.delete(contentUri, null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - - - public ContentValues toContentValues() { - ContentValues cv = new ContentValues(); - cv.put(Table.COLUMN_FILENAME, getFilename()); - if (getLocalUri() != null) { - cv.put(Table.COLUMN_LOCAL_URI, getLocalUri().toString()); - } - if (getImageUrl() != null) { - cv.put(Table.COLUMN_IMAGE_URL, getImageUrl()); - } - if (getDateUploaded() != null) { - cv.put(Table.COLUMN_UPLOADED, getDateUploaded().getTime()); - } - cv.put(Table.COLUMN_LENGTH, getDataLength()); - cv.put(Table.COLUMN_TIMESTAMP, getTimestamp().getTime()); - cv.put(Table.COLUMN_STATE, getState()); - cv.put(Table.COLUMN_TRANSFERRED, transferred); - cv.put(Table.COLUMN_SOURCE, source); - cv.put(Table.COLUMN_DESCRIPTION, description); - cv.put(Table.COLUMN_CREATOR, creator); - cv.put(Table.COLUMN_MULTIPLE, isMultiple ? 1 : 0); - cv.put(Table.COLUMN_WIDTH, width); - cv.put(Table.COLUMN_HEIGHT, height); - cv.put(Table.COLUMN_LICENSE, license); - return cv; - } - @Override public void setFilename(String filename) { this.filename = filename; @@ -224,33 +185,6 @@ public class Contribution extends Media { timestamp = new Date(System.currentTimeMillis()); } - public static Contribution fromCursor(Cursor cursor) { - // Hardcoding column positions! - Contribution c = new Contribution(); - - //Check that cursor has a value to avoid CursorIndexOutOfBoundsException - if (cursor.getCount() > 0) { - c.contentUri = ContributionsContentProvider.uriForId(cursor.getInt(0)); - c.filename = cursor.getString(1); - c.localUri = TextUtils.isEmpty(cursor.getString(2)) ? null : Uri.parse(cursor.getString(2)); - c.imageUrl = cursor.getString(3); - c.timestamp = cursor.getLong(4) == 0 ? null : new Date(cursor.getLong(4)); - c.state = cursor.getInt(5); - c.dataLength = cursor.getLong(6); - c.dateUploaded = cursor.getLong(7) == 0 ? null : new Date(cursor.getLong(7)); - c.transferred = cursor.getLong(8); - c.source = cursor.getString(9); - c.description = cursor.getString(10); - c.creator = cursor.getString(11); - c.isMultiple = cursor.getInt(12) == 1; - c.width = cursor.getInt(13); - c.height = cursor.getInt(14); - c.license = cursor.getString(15); - } - - return c; - } - public String getSource() { return source; } @@ -263,121 +197,6 @@ public class Contribution extends Media { this.localUri = localUri; } - public static class Table { - public static final String TABLE_NAME = "contributions"; - - public static final String COLUMN_ID = "_id"; - public static final String COLUMN_FILENAME = "filename"; - public static final String COLUMN_LOCAL_URI = "local_uri"; - public static final String COLUMN_IMAGE_URL = "image_url"; - public static final String COLUMN_TIMESTAMP = "timestamp"; - public static final String COLUMN_STATE = "state"; - public static final String COLUMN_LENGTH = "length"; - public static final String COLUMN_UPLOADED = "uploaded"; - public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes - public static final String COLUMN_SOURCE = "source"; - public static final String COLUMN_DESCRIPTION = "description"; - public static final String COLUMN_CREATOR = "creator"; // Initial uploader - public static final String COLUMN_MULTIPLE = "multiple"; - public static final String COLUMN_WIDTH = "width"; - public static final String COLUMN_HEIGHT = "height"; - public static final String COLUMN_LICENSE = "license"; - - // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. - public static final String[] ALL_FIELDS = { - COLUMN_ID, - COLUMN_FILENAME, - COLUMN_LOCAL_URI, - COLUMN_IMAGE_URL, - COLUMN_TIMESTAMP, - COLUMN_STATE, - COLUMN_LENGTH, - COLUMN_UPLOADED, - COLUMN_TRANSFERRED, - COLUMN_SOURCE, - COLUMN_DESCRIPTION, - COLUMN_CREATOR, - COLUMN_MULTIPLE, - COLUMN_WIDTH, - COLUMN_HEIGHT, - COLUMN_LICENSE - }; - - - private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + "_id INTEGER PRIMARY KEY," - + "filename STRING," - + "local_uri STRING," - + "image_url STRING," - + "uploaded INTEGER," - + "timestamp INTEGER," - + "state INTEGER," - + "length INTEGER," - + "transferred INTEGER," - + "source STRING," - + "description STRING," - + "creator STRING," - + "multiple INTEGER," - + "width INTEGER," - + "height INTEGER," - + "LICENSE STRING" - + ");"; - - - public static void onCreate(SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); - } - - public static void onDelete(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); - onCreate(db); - } - - public static void onUpdate(SQLiteDatabase db, int from, int to) { - if (from == to) { - return; - } - if (from == 1) { - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;"); - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;"); - from++; - onUpdate(db, from, to); - return; - } - if (from == 2) { - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;"); - db.execSQL("UPDATE " + TABLE_NAME + " SET multiple = 0"); - from++; - onUpdate(db, from, to); - return; - } - if (from == 3) { - // Do nothing - from++; - onUpdate(db, from, to); - return; - } - if (from == 4) { - // Do nothing -- added Category - from++; - onUpdate(db, from, to); - return; - } - if (from == 5) { - // Added width and height fields - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;"); - db.execSQL("UPDATE " + TABLE_NAME + " SET width = 0"); - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;"); - db.execSQL("UPDATE " + TABLE_NAME + " SET height = 0"); - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;"); - db.execSQL("UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA_3 + "';"); - from++; - onUpdate(db, from, to); - return; - } - } - } - @NonNull private String licenseTemplateFor(String license) { switch (license) { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java new file mode 100644 index 000000000..9d3038e03 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java @@ -0,0 +1,281 @@ +package fr.free.nrw.commons.contributions; + +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.Nullable; +import android.text.TextUtils; + +import java.util.Date; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; + +import fr.free.nrw.commons.settings.Prefs; + +import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS; +import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; +import static fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId; + +public class ContributionDao { + /* + This sorts in the following order: + Currently Uploading + Failed (Sorted in ascending order of time added - FIFO) + Queued to Upload (Sorted in ascending order of time added - FIFO) + Completed (Sorted in descending order of time added) + + This is why Contribution.STATE_COMPLETED is -1. + */ + static final String CONTRIBUTION_SORT = Table.COLUMN_STATE + " DESC, " + + Table.COLUMN_UPLOADED + " DESC , (" + + Table.COLUMN_TIMESTAMP + " * " + + Table.COLUMN_STATE + ")"; + + private final Provider clientProvider; + + @Inject + public ContributionDao(@Named("contribution") Provider clientProvider) { + this.clientProvider = clientProvider; + } + + Cursor loadAllContributions() { + ContentProviderClient db = clientProvider.get(); + try { + return db.query(BASE_URI, ALL_FIELDS, "", null, CONTRIBUTION_SORT); + } catch (RemoteException e) { + return null; + } finally { + db.release(); + } + } + + public void save(Contribution contribution) { + ContentProviderClient db = clientProvider.get(); + try { + if (contribution.getContentUri() == null) { + contribution.setContentUri(db.insert(BASE_URI, toContentValues(contribution))); + } else { + db.update(contribution.getContentUri(), toContentValues(contribution), null, null); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + db.release(); + } + } + + public void delete(Contribution contribution) { + ContentProviderClient db = clientProvider.get(); + try { + if (contribution.getContentUri() == null) { + // noooo + throw new RuntimeException("tried to delete item with no content URI"); + } else { + db.delete(contribution.getContentUri(), null, null); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + db.release(); + } + } + + ContentValues toContentValues(Contribution contribution) { + ContentValues cv = new ContentValues(); + cv.put(Table.COLUMN_FILENAME, contribution.getFilename()); + if (contribution.getLocalUri() != null) { + cv.put(Table.COLUMN_LOCAL_URI, contribution.getLocalUri().toString()); + } + if (contribution.getImageUrl() != null) { + cv.put(Table.COLUMN_IMAGE_URL, contribution.getImageUrl()); + } + if (contribution.getDateUploaded() != null) { + cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime()); + } + cv.put(Table.COLUMN_LENGTH, contribution.getDataLength()); + cv.put(Table.COLUMN_TIMESTAMP, contribution.getTimestamp().getTime()); + cv.put(Table.COLUMN_STATE, contribution.getState()); + cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred()); + cv.put(Table.COLUMN_SOURCE, contribution.getSource()); + cv.put(Table.COLUMN_DESCRIPTION, contribution.getDescription()); + cv.put(Table.COLUMN_CREATOR, contribution.getCreator()); + cv.put(Table.COLUMN_MULTIPLE, contribution.getMultiple() ? 1 : 0); + cv.put(Table.COLUMN_WIDTH, contribution.getWidth()); + cv.put(Table.COLUMN_HEIGHT, contribution.getHeight()); + cv.put(Table.COLUMN_LICENSE, contribution.getLicense()); + return cv; + } + + public Contribution fromCursor(Cursor cursor) { + // Hardcoding column positions! + //Check that cursor has a value to avoid CursorIndexOutOfBoundsException + if (cursor.getCount() > 0) { + return new Contribution( + uriForId(cursor.getInt(0)), + cursor.getString(1), + parseUri(cursor.getString(2)), + cursor.getString(3), + parseTimestamp(cursor.getLong(4)), + cursor.getInt(5), + cursor.getLong(6), + parseTimestamp(cursor.getLong(7)), + cursor.getLong(8), + cursor.getString(9), + cursor.getString(10), + cursor.getString(11), + cursor.getInt(12) == 1, + cursor.getInt(13), + cursor.getInt(14), + cursor.getString(15)); + } + + return null; + } + + @Nullable + private static Date parseTimestamp(long timestamp) { + return timestamp == 0 ? null : new Date(timestamp); + } + + @Nullable + private static Uri parseUri(String uriString) { + return TextUtils.isEmpty(uriString) ? null : Uri.parse(uriString); + } + + public static class Table { + public static final String TABLE_NAME = "contributions"; + + public static final String COLUMN_ID = "_id"; + public static final String COLUMN_FILENAME = "filename"; + public static final String COLUMN_LOCAL_URI = "local_uri"; + public static final String COLUMN_IMAGE_URL = "image_url"; + public static final String COLUMN_TIMESTAMP = "timestamp"; + public static final String COLUMN_STATE = "state"; + public static final String COLUMN_LENGTH = "length"; + public static final String COLUMN_UPLOADED = "uploaded"; + public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes + public static final String COLUMN_SOURCE = "source"; + public static final String COLUMN_DESCRIPTION = "description"; + public static final String COLUMN_CREATOR = "creator"; // Initial uploader + public static final String COLUMN_MULTIPLE = "multiple"; + public static final String COLUMN_WIDTH = "width"; + public static final String COLUMN_HEIGHT = "height"; + public static final String COLUMN_LICENSE = "license"; + + // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. + public static final String[] ALL_FIELDS = { + COLUMN_ID, + COLUMN_FILENAME, + COLUMN_LOCAL_URI, + COLUMN_IMAGE_URL, + COLUMN_TIMESTAMP, + COLUMN_STATE, + COLUMN_LENGTH, + COLUMN_UPLOADED, + COLUMN_TRANSFERRED, + COLUMN_SOURCE, + COLUMN_DESCRIPTION, + COLUMN_CREATOR, + COLUMN_MULTIPLE, + COLUMN_WIDTH, + COLUMN_HEIGHT, + COLUMN_LICENSE + }; + + public static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; + + public static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" + + "_id INTEGER PRIMARY KEY," + + "filename STRING," + + "local_uri STRING," + + "image_url STRING," + + "uploaded INTEGER," + + "timestamp INTEGER," + + "state INTEGER," + + "length INTEGER," + + "transferred INTEGER," + + "source STRING," + + "description STRING," + + "creator STRING," + + "multiple INTEGER," + + "width INTEGER," + + "height INTEGER," + + "LICENSE STRING" + + ");"; + + // Upgrade from version 1 -> + static final String ADD_CREATOR_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;"; + static final String ADD_DESCRIPTION_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;"; + + // Upgrade from version 2 -> + static final String ADD_MULTIPLE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;"; + static final String SET_DEFAULT_MULTIPLE = "UPDATE " + TABLE_NAME + " SET multiple = 0"; + + // Upgrade from version 5 -> + static final String ADD_WIDTH_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;"; + static final String SET_DEFAULT_WIDTH = "UPDATE " + TABLE_NAME + " SET width = 0"; + static final String ADD_HEIGHT_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;"; + static final String SET_DEFAULT_HEIGHT = "UPDATE " + TABLE_NAME + " SET height = 0"; + static final String ADD_LICENSE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;"; + static final String SET_DEFAULT_LICENSE = "UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA_3 + "';"; + + + 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 == 1) { + db.execSQL(ADD_DESCRIPTION_FIELD); + db.execSQL(ADD_CREATOR_FIELD); + from++; + onUpdate(db, from, to); + return; + } + if (from == 2) { + db.execSQL(ADD_MULTIPLE_FIELD); + db.execSQL(SET_DEFAULT_MULTIPLE); + from++; + onUpdate(db, from, to); + return; + } + if (from == 3) { + // Do nothing + from++; + onUpdate(db, from, to); + return; + } + if (from == 4) { + // Do nothing -- added Category + from++; + onUpdate(db, from, to); + return; + } + if (from == 5) { + // Added width and height fields + db.execSQL(ADD_WIDTH_FIELD); + db.execSQL(SET_DEFAULT_WIDTH); + db.execSQL(ADD_HEIGHT_FIELD); + db.execSQL(SET_DEFAULT_HEIGHT); + db.execSQL(ADD_LICENSE_FIELD); + db.execSQL(SET_DEFAULT_LICENSE); + from++; + onUpdate(db, from, to); + return; + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java index 6cda47d2e..5c1ecfaa0 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java @@ -42,8 +42,7 @@ import timber.log.Timber; import static android.content.ContentResolver.requestSync; import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; -import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS; -import static fr.free.nrw.commons.contributions.ContributionsContentProvider.AUTHORITY; +import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS; import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING; @@ -58,6 +57,7 @@ public class ContributionsActivity @Inject MediaWikiApi mediaWikiApi; @Inject SessionManager sessionManager; @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject ContributionDao contributionDao; private Cursor allContributions; private ContributionsListFragment contributionsList; @@ -65,21 +65,6 @@ public class ContributionsActivity private UploadService uploadService; private boolean isUploadServiceConnected; private ArrayList observersWaitingForLoad = new ArrayList<>(); - private String CONTRIBUTION_SELECTION = ""; - - /* - This sorts in the following order: - Currently Uploading - Failed (Sorted in ascending order of time added - FIFO) - Queued to Upload (Sorted in ascending order of time added - FIFO) - Completed (Sorted in descending order of time added) - - This is why Contribution.STATE_COMPLETED is -1. - */ - private String CONTRIBUTION_SORT = Contribution.Table.COLUMN_STATE + " DESC, " - + Contribution.Table.COLUMN_UPLOADED + " DESC , (" - + Contribution.Table.COLUMN_TIMESTAMP + " * " - + Contribution.Table.COLUMN_STATE + ")"; private CompositeDisposable compositeDisposable = new CompositeDisposable(); @@ -94,7 +79,7 @@ public class ContributionsActivity @Override public void onServiceDisconnected(ComponentName componentName) { // this should never happen - throw new RuntimeException("UploadService died but the rest of the process did not!"); + Timber.e(new RuntimeException("UploadService died but the rest of the process did not!")); } }; @@ -121,14 +106,13 @@ public class ContributionsActivity @Override protected void onAuthCookieAcquired(String authCookie) { // Do a sync everytime we get here! - requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.AUTHORITY, new Bundle()); + requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.CONTRIBUTION_AUTHORITY, new Bundle()); Intent uploadServiceIntent = new Intent(this, UploadService.class); uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); startService(uploadServiceIntent); bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE); - allContributions = getContentResolver().query(BASE_URI, ALL_FIELDS, - CONTRIBUTION_SELECTION, null, CONTRIBUTION_SORT); + allContributions = contributionDao.loadAllContributions(); getSupportLoaderManager().initLoader(0, null, this); } @@ -186,24 +170,23 @@ public class ContributionsActivity public void retryUpload(int i) { allContributions.moveToPosition(i); - Contribution c = Contribution.fromCursor(allContributions); + Contribution c = contributionDao.fromCursor(allContributions); if (c.getState() == STATE_FAILED) { uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c); - Timber.d("Restarting for %s", c.toContentValues()); + Timber.d("Restarting for %s", c.toString()); } else { - Timber.d("Skipping re-upload for non-failed %s", c.toContentValues()); + Timber.d("Skipping re-upload for non-failed %s", c.toString()); } } public void deleteUpload(int i) { allContributions.moveToPosition(i); - Contribution c = Contribution.fromCursor(allContributions); + Contribution c = contributionDao.fromCursor(allContributions); if (c.getState() == STATE_FAILED) { - Timber.d("Deleting failed contrib %s", c.toContentValues()); - c.setContentProviderClient(getContentResolver().acquireContentProviderClient(AUTHORITY)); - c.delete(); + Timber.d("Deleting failed contrib %s", c.toString()); + contributionDao.delete(c); } else { - Timber.d("Skipping deletion for non-failed contrib %s", c.toContentValues()); + Timber.d("Skipping deletion for non-failed contrib %s", c.toString()); } } @@ -239,8 +222,8 @@ public class ContributionsActivity public Loader onCreateLoader(int i, Bundle bundle) { int uploads = prefs.getInt(UPLOADS_SHOWING, 100); return new CursorLoader(this, BASE_URI, - ALL_FIELDS, CONTRIBUTION_SELECTION, null, - CONTRIBUTION_SORT + "LIMIT " + uploads); + ALL_FIELDS, "", null, + ContributionDao.CONTRIBUTION_SORT + "LIMIT " + uploads); } @Override @@ -249,7 +232,7 @@ public class ContributionsActivity if (contributionsList.getAdapter() == null) { contributionsList.setAdapter(new ContributionsListAdapter(getApplicationContext(), - cursor, 0)); + cursor, 0, contributionDao)); } else { ((CursorAdapter) contributionsList.getAdapter()).swapCursor(cursor); } @@ -270,7 +253,7 @@ public class ContributionsActivity // not yet ready to return data return null; } else { - return Contribution.fromCursor((Cursor) contributionsList.getAdapter().getItem(i)); + return contributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i)); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java index 5ec290026..4d82bdfb1 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java @@ -17,8 +17,8 @@ import fr.free.nrw.commons.data.DBOpenHelper; import timber.log.Timber; import static android.content.UriMatcher.NO_MATCH; -import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS; -import static fr.free.nrw.commons.contributions.Contribution.Table.TABLE_NAME; +import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS; +import static fr.free.nrw.commons.contributions.ContributionDao.Table.TABLE_NAME; public class ContributionsContentProvider extends ContentProvider { @@ -26,13 +26,13 @@ public class ContributionsContentProvider extends ContentProvider { private static final int CONTRIBUTIONS_ID = 2; private static final String BASE_PATH = "contributions"; private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH); - public static final String AUTHORITY = "fr.free.nrw.commons.contributions.contentprovider"; + public static final String CONTRIBUTION_AUTHORITY = "fr.free.nrw.commons.contributions.contentprovider"; - public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH); + public static final Uri BASE_URI = Uri.parse("content://" + CONTRIBUTION_AUTHORITY + "/" + BASE_PATH); static { - uriMatcher.addURI(AUTHORITY, BASE_PATH, CONTRIBUTIONS); - uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID); + uriMatcher.addURI(CONTRIBUTION_AUTHORITY, BASE_PATH, CONTRIBUTIONS); + uriMatcher.addURI(CONTRIBUTION_AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID); } public static Uri uriForId(int id) { @@ -176,7 +176,7 @@ public class ContributionsContentProvider extends ContentProvider { if (TextUtils.isEmpty(selection)) { rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, - Contribution.Table.COLUMN_ID + " = ?", + ContributionDao.Table.COLUMN_ID + " = ?", new String[]{String.valueOf(id)}); } else { throw new IllegalArgumentException( diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java index 7be6dd663..a31caf54f 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java @@ -11,8 +11,11 @@ import fr.free.nrw.commons.R; class ContributionsListAdapter extends CursorAdapter { - public ContributionsListAdapter(Context context, Cursor c, int flags) { + private final ContributionDao contributionDao; + + public ContributionsListAdapter(Context context, Cursor c, int flags, ContributionDao contributionDao) { super(context, c, flags); + this.contributionDao = contributionDao; } @Override @@ -26,7 +29,7 @@ class ContributionsListAdapter extends CursorAdapter { @Override public void bindView(View view, Context context, Cursor cursor) { final ContributionViewHolder views = (ContributionViewHolder)view.getTag(); - final Contribution contribution = Contribution.fromCursor(cursor); + final Contribution contribution = contributionDao.fromCursor(cursor); views.imageView.setMedia(contribution); views.titleView.setText(contribution.getDisplayTitle()); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java index e67b164a8..f8245032b 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java @@ -30,7 +30,7 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED; -import static fr.free.nrw.commons.contributions.Contribution.Table.COLUMN_FILENAME; +import static fr.free.nrw.commons.contributions.ContributionDao.Table.COLUMN_FILENAME; import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; @SuppressWarnings("WeakerAccess") @@ -89,6 +89,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { LogEventResult result; Boolean done = false; String queryContinue = null; + ContributionDao contributionDao = new ContributionDao(() -> contentProviderClient); while (!done) { try { @@ -121,7 +122,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { "", -1, dateUpdated, dateUpdated, user, "", ""); contrib.setState(STATE_COMPLETED); - imageValues.add(contrib.toContentValues()); + imageValues.add(contributionDao.toContentValues(contrib)); if (imageValues.size() % COMMIT_THRESHOLD == 0) { try { diff --git a/app/src/main/java/fr/free/nrw/commons/data/Category.java b/app/src/main/java/fr/free/nrw/commons/data/Category.java deleted file mode 100644 index be4a29846..000000000 --- a/app/src/main/java/fr/free/nrw/commons/data/Category.java +++ /dev/null @@ -1,276 +0,0 @@ -package fr.free.nrw.commons.data; - -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 android.support.annotation.Nullable; - -import java.util.ArrayList; -import java.util.Date; - -import fr.free.nrw.commons.category.CategoryContentProvider; - -/** - * Represents a category - */ -public class Category { - private Uri contentUri; - - private String name; - private Date lastUsed; - private int timesUsed; - - // Getters/setters - /** - * Gets name - * - * @return name - */ - public String getName() { - return name; - } - - /** - * Modifies name - * - * @param name Category name - */ - public void setName(String name) { - this.name = name; - } - - /** - * Gets last used date - * - * @return Last used date - */ - private Date getLastUsed() { - // warning: Date objects are mutable. - return (Date)lastUsed.clone(); - } - - /** - * Modifies last used date - * - * @param lastUsed Category date - */ - public void setLastUsed(Date lastUsed) { - // warning: Date objects are mutable. - this.lastUsed = (Date)lastUsed.clone(); - } - - /** - * Generates new last used date - */ - private void touch() { - lastUsed = new Date(); - } - - /** - * Gets no. of times the category is used - * - * @return no. of times used - */ - private int getTimesUsed() { - return timesUsed; - } - - /** - * Modifies no. of times used - * - * @param timesUsed Category used times - */ - public void setTimesUsed(int timesUsed) { - this.timesUsed = timesUsed; - } - - /** - * Increments timesUsed by 1 and sets last used date as now. - */ - public void incTimesUsed() { - timesUsed++; - touch(); - } - - //region Database/content-provider stuff - - /** - * Persist category. - * @param client ContentProviderClient to handle DB connection - */ - public void save(ContentProviderClient client) { - try { - if (contentUri == null) { - contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues()); - } else { - client.update(contentUri, toContentValues(), null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - - /** - * Gets content values - * - * @return Content values - */ - private ContentValues toContentValues() { - ContentValues cv = new ContentValues(); - cv.put(Table.COLUMN_NAME, getName()); - cv.put(Table.COLUMN_LAST_USED, getLastUsed().getTime()); - cv.put(Table.COLUMN_TIMES_USED, getTimesUsed()); - return cv; - } - - /** - * Gets category from cursor - * @param cursor Category cursor - * @return Category from cursor - */ - private static Category fromCursor(Cursor cursor) { - // Hardcoding column positions! - Category c = new Category(); - c.contentUri = CategoryContentProvider.uriForId(cursor.getInt(0)); - c.name = cursor.getString(1); - c.lastUsed = new Date(cursor.getLong(2)); - c.timesUsed = cursor.getInt(3); - return c; - } - - /** - * Find persisted category in database, based on its name. - * @param client ContentProviderClient to handle DB connection - * @param name Category's name - * @return category from database, or null if not found - */ - public static @Nullable Category find(ContentProviderClient client, String name) { - Cursor cursor = null; - try { - cursor = client.query( - CategoryContentProvider.BASE_URI, - Category.Table.ALL_FIELDS, - Category.Table.COLUMN_NAME + "=?", - new String[]{name}, - null); - if (cursor != null && cursor.moveToFirst()) { - return Category.fromCursor(cursor); - } - } catch (RemoteException e) { - // This feels lazy, but to hell with checked exceptions. :) - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - return null; - } - - /** - * Retrieve recently-used categories, ordered by descending date. - * @return a list containing recent categories - */ - public static @NonNull ArrayList recentCategories(ContentProviderClient client, int limit) { - ArrayList items = new ArrayList<>(); - Cursor cursor = null; - try { - cursor = client.query( - CategoryContentProvider.BASE_URI, - Category.Table.ALL_FIELDS, - null, - new String[]{}, - Category.Table.COLUMN_LAST_USED + " DESC"); - // fixme add a limit on the original query instead of falling out of the loop? - while (cursor != null && cursor.moveToNext() - && cursor.getPosition() < limit) { - Category cat = Category.fromCursor(cursor); - items.add(cat.getName()); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - return items; - } - - public static class Table { - public static final String TABLE_NAME = "categories"; - - public static final String COLUMN_ID = "_id"; - public static final String COLUMN_NAME = "name"; - public static final String COLUMN_LAST_USED = "last_used"; - public static final String COLUMN_TIMES_USED = "times_used"; - - // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. - public static final String[] ALL_FIELDS = { - COLUMN_ID, - COLUMN_NAME, - COLUMN_LAST_USED, - COLUMN_TIMES_USED - }; - - private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + COLUMN_ID + " INTEGER PRIMARY KEY," - + COLUMN_NAME + " STRING," - + COLUMN_LAST_USED + " INTEGER," - + COLUMN_TIMES_USED + " INTEGER" - + ");"; - - /** - * Creates new table with provided SQLite database - * - * @param db Category database - */ - public static void onCreate(SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); - } - - /** - * Deletes existing table - * @param db Category database - */ - public static void onDelete(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); - onCreate(db); - } - - /** - * Updates given database - * @param db Category database - * @param from Exiting category id - * @param to New category id - */ - public static void onUpdate(SQLiteDatabase db, int from, int to) { - if (from == to) { - return; - } - if (from < 4) { - // doesn't exist yet - from++; - onUpdate(db, from, to); - return; - } - if (from == 4) { - // table added in version 5 - onCreate(db); - from++; - onUpdate(db, from, to); - return; - } - if (from == 5) { - from++; - onUpdate(db, from, to); - return; - } - } - } - //endregion -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java index 5e28a8a32..35305c5ba 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java @@ -4,8 +4,9 @@ import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.modifications.ModifierSequence; +import fr.free.nrw.commons.category.CategoryDao; +import fr.free.nrw.commons.contributions.ContributionDao; +import fr.free.nrw.commons.modifications.ModifierSequenceDao; public class DBOpenHelper extends SQLiteOpenHelper { @@ -13,7 +14,8 @@ public class DBOpenHelper extends SQLiteOpenHelper { private static final int DATABASE_VERSION = 6; /** - * Do not use, please call CommonsApplication.getDBOpenHelper() + * Do not use directly - @Inject an instance where it's needed and let + * dependency injection take care of managing this as a singleton. */ public DBOpenHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -21,15 +23,15 @@ public class DBOpenHelper extends SQLiteOpenHelper { @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { - Contribution.Table.onCreate(sqLiteDatabase); - ModifierSequence.Table.onCreate(sqLiteDatabase); - Category.Table.onCreate(sqLiteDatabase); + ContributionDao.Table.onCreate(sqLiteDatabase); + ModifierSequenceDao.Table.onCreate(sqLiteDatabase); + CategoryDao.Table.onCreate(sqLiteDatabase); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) { - Contribution.Table.onUpdate(sqLiteDatabase, from, to); - ModifierSequence.Table.onUpdate(sqLiteDatabase, from, to); - Category.Table.onUpdate(sqLiteDatabase, from, to); + ContributionDao.Table.onUpdate(sqLiteDatabase, from, to); + ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to); + CategoryDao.Table.onUpdate(sqLiteDatabase, from, to); } } diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java index 27e16ce23..e4fb13427 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -8,6 +8,7 @@ import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.SignupActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.nearby.NearbyActivity; +import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.settings.SettingsActivity; import fr.free.nrw.commons.upload.MultipleShareActivity; import fr.free.nrw.commons.upload.ShareActivity; @@ -43,4 +44,6 @@ public abstract class ActivityBuilderModule { @ContributesAndroidInjector abstract NearbyActivity bindNearbyActivity(); + @ContributesAndroidInjector + abstract NotificationActivity bindNotificationActivity(); } diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index 99d3235e7..913eef57e 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.di; +import android.content.ContentProviderClient; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.v4.util.LruCache; @@ -22,10 +23,14 @@ import fr.free.nrw.commons.nearby.NearbyPlaces; import fr.free.nrw.commons.upload.UploadController; import static android.content.Context.MODE_PRIVATE; +import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY; +import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MODIFICATIONS_AUTHORITY; @Module @SuppressWarnings({"WeakerAccess", "unused"}) public class CommonsApplicationModule { + public static final String CATEGORY_AUTHORITY = "fr.free.nrw.commons.categories.contentprovider"; + private CommonsApplication application; public CommonsApplicationModule(CommonsApplication application) { @@ -37,6 +42,24 @@ public class CommonsApplicationModule { return new AccountUtil(application); } + @Provides + @Named("category") + public ContentProviderClient provideCategoryContentProviderClient() { + return application.getContentResolver().acquireContentProviderClient(CATEGORY_AUTHORITY); + } + + @Provides + @Named("contribution") + public ContentProviderClient provideContributionContentProviderClient() { + return application.getContentResolver().acquireContentProviderClient(CONTRIBUTION_AUTHORITY); + } + + @Provides + @Named("modification") + public ContentProviderClient provideModificationContentProviderClient() { + return application.getContentResolver().acquireContentProviderClient(MODIFICATIONS_AUTHORITY); + } + @Provides @Named("application_preferences") public SharedPreferences providesApplicationSharedPreferences() { diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java index 45a1a4181..d949189ed 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java @@ -31,20 +31,36 @@ public class LocationServiceManager implements LocationListener { private final List locationListeners = new CopyOnWriteArrayList<>(); private boolean isLocationManagerRegistered = false; + /** + * Constructs a new instance of LocationServiceManager. + * @param context the context + */ public LocationServiceManager(Context context) { this.context = context; this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); } + /** + * Returns the current status of the GPS provider. + * @return true if the GPS provider is enabled + */ public boolean isProviderEnabled() { return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); } + /** + * Returns whether the location permission is granted. + * @return true if the location permission is granted + */ public boolean isLocationPermissionGranted() { return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; } + /** + * Requests the location permission to be granted. + * @param activity the activity + */ public void requestPermissions(Activity activity) { if (activity.isFinishing()) { return; @@ -77,6 +93,11 @@ public class LocationServiceManager implements LocationListener { && requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); } + /** + * Requests location updates from the specified provider. + * @param locationProvider the location provider + * @return true if successful + */ private boolean requestLocationUpdatesFromProvider(String locationProvider) { try { locationManager.requestLocationUpdates(locationProvider, @@ -93,6 +114,12 @@ public class LocationServiceManager implements LocationListener { } } + /** + * Returns whether a given location is better than the current best location. + * @param location the location to be tested + * @param currentBestLocation the current best location + * @return true if the given location is better + */ protected boolean isBetterLocation(Location location, Location currentBestLocation) { if (currentBestLocation == null) { // A new location is always better than no location @@ -156,12 +183,20 @@ public class LocationServiceManager implements LocationListener { } } + /** + * Adds a new listener to the list of location listeners. + * @param listener the new listener + */ public void addLocationListener(LocationUpdateListener listener) { if (!locationListeners.contains(listener)) { locationListeners.add(listener); } } + /** + * Removes a listener from the list of location listeners. + * @param listener the listener to be removed + */ public void removeLocationListener(LocationUpdateListener listener) { locationListeners.remove(listener); } diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java index 6dc993c9e..3f2c01930 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java @@ -16,20 +16,22 @@ import dagger.android.AndroidInjection; import fr.free.nrw.commons.data.DBOpenHelper; import timber.log.Timber; +import static fr.free.nrw.commons.modifications.ModifierSequenceDao.Table.TABLE_NAME; + public class ModificationsContentProvider extends ContentProvider { private static final int MODIFICATIONS = 1; private static final int MODIFICATIONS_ID = 2; - public static final String AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider"; - private static final String BASE_PATH = "modifications"; + public static final String MODIFICATIONS_AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider"; + public static final String BASE_PATH = "modifications"; - public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH); + public static final Uri BASE_URI = Uri.parse("content://" + MODIFICATIONS_AUTHORITY + "/" + BASE_PATH); private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { - uriMatcher.addURI(AUTHORITY, BASE_PATH, MODIFICATIONS); - uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID); + uriMatcher.addURI(MODIFICATIONS_AUTHORITY, BASE_PATH, MODIFICATIONS); + uriMatcher.addURI(MODIFICATIONS_AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID); } public static Uri uriForId(int id) { @@ -47,7 +49,7 @@ public class ModificationsContentProvider extends ContentProvider { @Override public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); - queryBuilder.setTables(ModifierSequence.Table.TABLE_NAME); + queryBuilder.setTables(TABLE_NAME); int uriType = uriMatcher.match(uri); @@ -78,7 +80,7 @@ public class ModificationsContentProvider extends ContentProvider { long id = 0; switch (uriType) { case MODIFICATIONS: - id = sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, contentValues); + id = sqlDB.insert(TABLE_NAME, null, contentValues); break; default: throw new IllegalArgumentException("Unknown URI: " + uri); @@ -94,7 +96,7 @@ public class ModificationsContentProvider extends ContentProvider { switch (uriType) { case MODIFICATIONS_ID: String id = uri.getLastPathSegment(); - sqlDB.delete(ModifierSequence.Table.TABLE_NAME, + sqlDB.delete(TABLE_NAME, "_id = ?", new String[] { id } ); @@ -114,7 +116,7 @@ public class ModificationsContentProvider extends ContentProvider { case MODIFICATIONS: for (ContentValues value: values) { Timber.d("Inserting! %s", value); - sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, value); + sqlDB.insert(TABLE_NAME, null, value); } break; default: @@ -140,7 +142,7 @@ public class ModificationsContentProvider extends ContentProvider { int rowsUpdated = 0; switch (uriType) { case MODIFICATIONS: - rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME, + rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, selection, selectionArgs); @@ -149,9 +151,9 @@ public class ModificationsContentProvider extends ContentProvider { int id = Integer.valueOf(uri.getLastPathSegment()); if (TextUtils.isEmpty(selection)) { - rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME, + rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, - ModifierSequence.Table.COLUMN_ID + " = ?", + ModifierSequenceDao.Table.COLUMN_ID + " = ?", new String[] { String.valueOf(id) } ); } else { throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID"); diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java index 1e886f225..f81c273d2 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java @@ -18,6 +18,7 @@ import javax.inject.Inject; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; @@ -25,6 +26,8 @@ import timber.log.Timber; public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { @Inject MediaWikiApi mwApi; + @Inject ContributionDao contributionDao; + @Inject ModifierSequenceDao modifierSequenceDao; public ModificationsSyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); @@ -79,11 +82,10 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { ContentProviderClient contributionsClient = null; try { - contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY); + contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.CONTRIBUTION_AUTHORITY); while (!allModifications.isAfterLast()) { - ModifierSequence sequence = ModifierSequence.fromCursor(allModifications); - sequence.setContentProviderClient(contentProviderClient); + ModifierSequence sequence = modifierSequenceDao.fromCursor(allModifications); Contribution contrib; Cursor contributionCursor; @@ -93,7 +95,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { throw new RuntimeException(e); } contributionCursor.moveToFirst(); - contrib = Contribution.fromCursor(contributionCursor); + contrib = contributionDao.fromCursor(contributionCursor); if (contrib.getState() == Contribution.STATE_COMPLETED) { String pageContent; @@ -121,7 +123,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { // FIXME: Log this somewhere else Timber.d("Non success result! %s", editResult); } else { - sequence.delete(); + modifierSequenceDao.delete(sequence); } } allModifications.moveToNext(); diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java index 880b53313..93cb3bc3d 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java @@ -1,14 +1,8 @@ package fr.free.nrw.commons.modifications; -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 org.json.JSONArray; -import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; @@ -17,14 +11,13 @@ public class ModifierSequence { private Uri mediaUri; private ArrayList modifiers; private Uri contentUri; - private ContentProviderClient client; public ModifierSequence(Uri mediaUri) { this.mediaUri = mediaUri; modifiers = new ArrayList<>(); } - public ModifierSequence(Uri mediaUri, JSONObject data) { + ModifierSequence(Uri mediaUri, JSONObject data) { this(mediaUri); JSONArray modifiersJSON = data.optJSONArray("modifiers"); for (int i = 0; i < modifiersJSON.length(); i++) { @@ -32,7 +25,7 @@ public class ModifierSequence { } } - public Uri getMediaUri() { + Uri getMediaUri() { return mediaUri; } @@ -40,14 +33,14 @@ public class ModifierSequence { modifiers.add(modifier); } - public String executeModifications(String pageName, String pageContents) { + String executeModifications(String pageName, String pageContents) { for (PageModifier modifier: modifiers) { pageContents = modifier.doModification(pageName, pageContents); } return pageContents; } - public String getEditSummary() { + String getEditSummary() { StringBuilder editSummary = new StringBuilder(); for (PageModifier modifier: modifiers) { editSummary.append(modifier.getEditSumary()).append(" "); @@ -56,97 +49,16 @@ public class ModifierSequence { return editSummary.toString(); } - public JSONObject toJSON() { - JSONObject data = new JSONObject(); - try { - JSONArray modifiersJSON = new JSONArray(); - for (PageModifier modifier: modifiers) { - modifiersJSON.put(modifier.toJSON()); - } - data.put("modifiers", modifiersJSON); - return data; - } catch (JSONException e) { - throw new RuntimeException(e); - } + ArrayList getModifiers() { + return modifiers; } - public ContentValues toContentValues() { - ContentValues cv = new ContentValues(); - cv.put(Table.COLUMN_MEDIA_URI, mediaUri.toString()); - cv.put(Table.COLUMN_DATA, toJSON().toString()); - return cv; + Uri getContentUri() { + return contentUri; } - public static ModifierSequence fromCursor(Cursor cursor) { - // Hardcoding column positions! - ModifierSequence ms = null; - try { - ms = new ModifierSequence(Uri.parse(cursor.getString(1)), - new JSONObject(cursor.getString(2))); - } catch (JSONException e) { - throw new RuntimeException(e); - } - ms.contentUri = ModificationsContentProvider.uriForId(cursor.getInt(0)); - - return ms; + void setContentUri(Uri contentUri) { + this.contentUri = contentUri; } - public void save() { - try { - if (contentUri == null) { - contentUri = client.insert(ModificationsContentProvider.BASE_URI, this.toContentValues()); - } else { - client.update(contentUri, toContentValues(), null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - - public void delete() { - try { - client.delete(contentUri, null, null); - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - - public void setContentProviderClient(ContentProviderClient client) { - this.client = client; - } - - public static class Table { - public static final String TABLE_NAME = "modifications"; - - public static final String COLUMN_ID = "_id"; - public static final String COLUMN_MEDIA_URI = "mediauri"; - public static final String COLUMN_DATA = "data"; - - // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. - public static final String[] ALL_FIELDS = { - COLUMN_ID, - COLUMN_MEDIA_URI, - COLUMN_DATA - }; - - private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + "_id INTEGER PRIMARY KEY," - + "mediauri STRING," - + "data STRING" - + ");"; - - public static void onCreate(SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); - } - - public static void onUpdate(SQLiteDatabase db, int from, int to) { - db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); - onCreate(db); - } - - public static void onDelete(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); - onCreate(db); - } - } } diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java new file mode 100644 index 000000000..e6b741d7a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java @@ -0,0 +1,124 @@ +package fr.free.nrw.commons.modifications; + +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 org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; + +public class ModifierSequenceDao { + + private final Provider clientProvider; + + @Inject + public ModifierSequenceDao(@Named("modification") Provider clientProvider) { + this.clientProvider = clientProvider; + } + + public void save(ModifierSequence sequence) { + ContentProviderClient db = clientProvider.get(); + try { + if (sequence.getContentUri() == null) { + sequence.setContentUri(db.insert(ModificationsContentProvider.BASE_URI, toContentValues(sequence))); + } else { + db.update(sequence.getContentUri(), toContentValues(sequence), null, null); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + db.release(); + } + } + + public void delete(ModifierSequence sequence) { + ContentProviderClient db = clientProvider.get(); + try { + db.delete(sequence.getContentUri(), null, null); + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + db.release(); + } + } + + ModifierSequence fromCursor(Cursor cursor) { + // Hardcoding column positions! + ModifierSequence ms = null; + try { + ms = new ModifierSequence(Uri.parse(cursor.getString(1)), + new JSONObject(cursor.getString(2))); + } catch (JSONException e) { + throw new RuntimeException(e); + } + ms.setContentUri( ModificationsContentProvider.uriForId(cursor.getInt(0))); + + return ms; + } + + private JSONObject toJSON(ModifierSequence sequence) { + JSONObject data = new JSONObject(); + try { + JSONArray modifiersJSON = new JSONArray(); + for (PageModifier modifier: sequence.getModifiers()) { + modifiersJSON.put(modifier.toJSON()); + } + data.put("modifiers", modifiersJSON); + return data; + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + private ContentValues toContentValues(ModifierSequence sequence) { + ContentValues cv = new ContentValues(); + cv.put(Table.COLUMN_MEDIA_URI, sequence.getMediaUri().toString()); + cv.put(Table.COLUMN_DATA, toJSON(sequence).toString()); + return cv; + } + + public static class Table { + static final String TABLE_NAME = "modifications"; + + static final String COLUMN_ID = "_id"; + static final String COLUMN_MEDIA_URI = "mediauri"; + static final String COLUMN_DATA = "data"; + + // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. + public static final String[] ALL_FIELDS = { + COLUMN_ID, + COLUMN_MEDIA_URI, + COLUMN_DATA + }; + + static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; + + static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" + + "_id INTEGER PRIMARY KEY," + + "mediauri STRING," + + "data STRING" + + ");"; + + public static void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_TABLE_STATEMENT); + } + + public static void onUpdate(SQLiteDatabase db, int from, int to) { + db.execSQL(DROP_TABLE_STATEMENT); + onCreate(db); + } + + public static void onDelete(SQLiteDatabase db) { + db.execSQL(DROP_TABLE_STATEMENT); + onCreate(db); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/Notification.java b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java new file mode 100644 index 000000000..cb1aa62b6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java @@ -0,0 +1,26 @@ +package fr.free.nrw.commons.notification; + +/** + * Created by root on 18.12.2017. + */ + +public class Notification { + public NotificationType notificationType; + public String notificationText; + + + Notification (NotificationType notificationType, String notificationText) { + this.notificationType = notificationType; + this.notificationText = notificationText; + } + + + public enum NotificationType { + /* Added for test purposes, needs to be rescheduled after implementing + fetching notifications from server */ + edit, + mention, + message, + block; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java new file mode 100644 index 000000000..9da69e0eb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -0,0 +1,50 @@ +package fr.free.nrw.commons.notification; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.Optional; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.theme.NavigationBaseActivity; + +/** + * Created by root on 18.12.2017. + */ + +public class NotificationActivity extends NavigationBaseActivity { + NotificationAdapterFactory notificationAdapterFactory; + + @Nullable + @BindView(R.id.listView) RecyclerView recyclerView; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_notification); + ButterKnife.bind(this); + initListView(); + addNotifications(); + initDrawer(); + } + + private void initListView() { + recyclerView = findViewById(R.id.listView); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + notificationAdapterFactory = new NotificationAdapterFactory(new NotificationRenderer.NotificationClicked() { + @Override + public void notificationClicked(Notification notification) { + + } + }); + } + + private void addNotifications() { + + recyclerView.setAdapter(notificationAdapterFactory.create(NotificationController.loadNotifications())); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapterFactory.java new file mode 100644 index 000000000..83a60dcfb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapterFactory.java @@ -0,0 +1,30 @@ +package fr.free.nrw.commons.notification; + +import android.support.annotation.NonNull; + +import com.pedrogomez.renderers.ListAdapteeCollection; +import com.pedrogomez.renderers.RVRendererAdapter; +import com.pedrogomez.renderers.RendererBuilder; + +import java.util.Collections; +import java.util.List; + +/** + * Created by root on 19.12.2017. + */ + +class NotificationAdapterFactory { + private NotificationRenderer.NotificationClicked listener; + + NotificationAdapterFactory(@NonNull NotificationRenderer.NotificationClicked listener) { + this.listener = listener; + } + + public RVRendererAdapter create(List notifications) { + RendererBuilder builder = new RendererBuilder() + .bind(Notification.class, new NotificationRenderer(listener)); + ListAdapteeCollection collection = new ListAdapteeCollection<>( + notifications != null ? notifications : Collections.emptyList()); + return new RVRendererAdapter<>(builder, collection); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java new file mode 100644 index 000000000..84f5c15d8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java @@ -0,0 +1,23 @@ +package fr.free.nrw.commons.notification; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by root on 19.12.2017. + */ + +public class NotificationController { + + public static List loadNotifications() { + List notifications = new ArrayList<>(); + notifications.add(new Notification(Notification.NotificationType.message, "notification 1")); + notifications.add(new Notification(Notification.NotificationType.edit, "notification 2")); + notifications.add(new Notification(Notification.NotificationType.mention, "notification 3")); + notifications.add(new Notification(Notification.NotificationType.message, "notification 4")); + notifications.add(new Notification(Notification.NotificationType.edit, "notification 5")); + notifications.add(new Notification(Notification.NotificationType.mention, "notification 6")); + notifications.add(new Notification(Notification.NotificationType.message, "notification 7")); + return notifications; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java new file mode 100644 index 000000000..36272d4a2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java @@ -0,0 +1,70 @@ +package fr.free.nrw.commons.notification; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.pedrogomez.renderers.Renderer; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.R; + +/** + * Created by root on 19.12.2017. + */ + +public class NotificationRenderer extends Renderer { + @BindView(R.id.title) TextView title; + @BindView(R.id.description) TextView description; + @BindView(R.id.time) TextView time; + @BindView(R.id.icon) ImageView icon; + private NotificationClicked listener; + + + NotificationRenderer(NotificationClicked listener) { + this.listener = listener; + } + + @Override + protected void setUpView(View view) { } + + @Override + protected void hookListeners(View rootView) { + rootView.setOnClickListener(v -> listener.notificationClicked(getContent())); + } + + @Override + protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) { + View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false); + ButterKnife.bind(this, inflatedView); + return inflatedView; + } + + @Override + public void render() { + Notification notification = getContent(); + title.setText(notification.notificationText); + time.setText("3d"); + description.setText("Example notification description"); + switch (notification.notificationType) { + case edit: + icon.setImageResource(R.drawable.ic_edit_black_24dp); + break; + case message: + icon.setImageResource(R.drawable.ic_message_black_24dp); + break; + case mention: + icon.setImageResource(R.drawable.ic_chat_bubble_black_24px); + break; + default: + icon.setImageResource(R.drawable.round_icon_unknown); + } + } + + public interface NotificationClicked{ + void notificationClicked(Notification notification); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index 60ea325e4..4e7cf767f 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -27,6 +27,7 @@ import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.nearby.NearbyActivity; +import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.settings.SettingsActivity; import timber.log.Timber; @@ -143,6 +144,10 @@ public abstract class NavigationBaseActivity extends BaseActivity .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) .show(); return true; + case R.id.action_notifications: + drawerLayout.closeDrawer(navigationView); + startActivityWithFlags(this, NotificationActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + return true; default: Timber.e("Unknown option [%s] selected from the navigation menu", itemId); return false; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java index 8858a11f5..a41938cf7 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java @@ -2,7 +2,6 @@ package fr.free.nrw.commons.upload; import android.Manifest; import android.app.ProgressDialog; -import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -40,6 +39,7 @@ import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.modifications.CategoryModifier; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModifierSequence; +import fr.free.nrw.commons.modifications.ModifierSequenceDao; import fr.free.nrw.commons.modifications.TemplateRemoveModifier; import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; @@ -54,6 +54,7 @@ public class MultipleShareActivity extends AuthenticatedActivity @Inject MediaWikiApi mwApi; @Inject SessionManager sessionManager; @Inject UploadController uploadController; + @Inject ModifierSequenceDao modifierSequenceDao; @Inject @Named("default_preferences") SharedPreferences prefs; private ArrayList photosList = null; @@ -165,20 +166,18 @@ public class MultipleShareActivity extends AuthenticatedActivity @Override public void onCategoriesSave(List categories) { if (categories.size() > 0) { - ContentProviderClient client = getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY); for (Contribution contribution : photosList) { ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri()); categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{}))); categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized")); - categoriesSequence.setContentProviderClient(client); - categoriesSequence.save(); + modifierSequenceDao.save(categoriesSequence); } } // FIXME: Make sure that the content provider is up // This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin - ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default! + ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, true); // Enable sync by default! finish(); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java index 963ae3fd3..2f0a4977e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java @@ -39,7 +39,6 @@ import javax.inject.Inject; import javax.inject.Named; import butterknife.ButterKnife; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.AuthenticatedActivity; import fr.free.nrw.commons.auth.SessionManager; @@ -50,8 +49,8 @@ import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.modifications.CategoryModifier; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModifierSequence; +import fr.free.nrw.commons.modifications.ModifierSequenceDao; import fr.free.nrw.commons.modifications.TemplateRemoveModifier; -import fr.free.nrw.commons.mwapi.EventLog; import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; @@ -77,6 +76,7 @@ public class ShareActivity @Inject CacheController cacheController; @Inject SessionManager sessionManager; @Inject UploadController uploadController; + @Inject ModifierSequenceDao modifierSequenceDao; @Inject @Named("default_preferences") SharedPreferences prefs; private String source; @@ -167,13 +167,12 @@ public class ShareActivity categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{}))); categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized")); - categoriesSequence.setContentProviderClient(getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY)); - categoriesSequence.save(); + modifierSequenceDao.save(categoriesSequence); } // FIXME: Make sure that the content provider is up // This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin - ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default! + ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, true); // Enable sync by default! finish(); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index 1d080e78f..ade22ece6 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -36,6 +36,9 @@ public class UploadController { void onUploadStarted(Contribution contribution); } + /** + * Constructs a new UploadController. + */ public UploadController(SessionManager sessionManager, Context context, SharedPreferences sharedPreferences) { this.sessionManager = sessionManager; this.context = context; @@ -53,10 +56,13 @@ public class UploadController { @Override public void onServiceDisconnected(ComponentName componentName) { // this should never happen - throw new RuntimeException("UploadService died but the rest of the process did not!"); + Timber.e(new RuntimeException("UploadService died but the rest of the process did not!")); } }; + /** + * Prepares the upload service. + */ public void prepareService() { Intent uploadServiceIntent = new Intent(context, UploadService.class); uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); @@ -64,12 +70,25 @@ public class UploadController { context.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE); } + /** + * Disconnects the upload service. + */ public void cleanup() { if (isUploadServiceConnected) { context.unbindService(uploadServiceConnection); } } + /** + * Starts a new upload task. + * @param title the title of the contribution + * @param mediaUri the media URI of the contribution + * @param description the description of the contribution + * @param mimeType the MIME type of the contribution + * @param source the source of the contribution + * @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615") + * @param onComplete the progress tracker + */ public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, ContributionUploadProgress onComplete) { Contribution contribution; @@ -85,6 +104,11 @@ public class UploadController { startUpload(contribution, onComplete); } + /** + * Starts a new upload task. + * @param contribution the contribution object + * @param onComplete the progress tracker + */ public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) { //Set creator, desc, and license if (TextUtils.isEmpty(contribution.getCreator())) { @@ -173,6 +197,12 @@ public class UploadController { } + /** + * Counts the number of bytes in {@code stream}. + * @param stream the stream + * @return the number of bytes in {@code stream} + * @throws IOException if an I/O error occurs + */ private long countBytes(InputStream stream) throws IOException { long count = 0; BufferedInputStream bis = new BufferedInputStream(stream); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 8013c256c..1c8f9ac58 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -4,7 +4,6 @@ import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Intent; @@ -31,6 +30,7 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider; @@ -51,9 +51,9 @@ public class UploadService extends HandlerService { @Inject MediaWikiApi mwApi; @Inject SessionManager sessionManager; @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject ContributionDao contributionDao; private NotificationManager notificationManager; - private ContentProviderClient contributionsProviderClient; private NotificationCompat.Builder curProgressNotification; private int toUpload; @@ -105,7 +105,7 @@ public class UploadService extends HandlerService { startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build()); contribution.setTransferred(transferred); - contribution.save(); + contributionDao.save(contribution); } } @@ -113,7 +113,6 @@ public class UploadService extends HandlerService { @Override public void onDestroy() { super.onDestroy(); - contributionsProviderClient.release(); Timber.d("UploadService.onDestroy; %s are yet to be uploaded", unfinishedUploads); } @@ -122,7 +121,6 @@ public class UploadService extends HandlerService { super.onCreate(); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - contributionsProviderClient = this.getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY); } @Override @@ -144,9 +142,7 @@ public class UploadService extends HandlerService { contribution.setState(Contribution.STATE_QUEUED); contribution.setTransferred(0); - contribution.setContentProviderClient(contributionsProviderClient); - - contribution.save(); + contributionDao.save(contribution); toUpload++; if (curProgressNotification != null && toUpload != 1) { curProgressNotification.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload)); @@ -167,11 +163,11 @@ public class UploadService extends HandlerService { public int onStartCommand(Intent intent, int flags, int startId) { if (intent.getAction().equals(ACTION_START_SERVICE) && freshStart) { ContentValues failedValues = new ContentValues(); - failedValues.put(Contribution.Table.COLUMN_STATE, Contribution.STATE_FAILED); + failedValues.put(ContributionDao.Table.COLUMN_STATE, Contribution.STATE_FAILED); int updated = getContentResolver().update(ContributionsContentProvider.BASE_URI, failedValues, - Contribution.Table.COLUMN_STATE + " = ? OR " + Contribution.Table.COLUMN_STATE + " = ?", + ContributionDao.Table.COLUMN_STATE + " = ? OR " + ContributionDao.Table.COLUMN_STATE + " = ?", new String[]{ String.valueOf(Contribution.STATE_QUEUED), String.valueOf(Contribution.STATE_IN_PROGRESS) } ); Timber.d("Set %d uploads to failed", updated); @@ -261,7 +257,7 @@ public class UploadService extends HandlerService { contribution.setImageUrl(uploadResult.getImageUrl()); contribution.setState(Contribution.STATE_COMPLETED); contribution.setDateUploaded(uploadResult.getDateUploaded()); - contribution.save(); + contributionDao.save(contribution); } } catch (IOException e) { Timber.d("I have a network fuckup"); @@ -273,7 +269,7 @@ public class UploadService extends HandlerService { toUpload--; if (toUpload == 0) { // Sync modifications right after all uplaods are processed - ContentResolver.requestSync(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, new Bundle()); + ContentResolver.requestSync(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, new Bundle()); stopForeground(true); } } @@ -292,7 +288,7 @@ public class UploadService extends HandlerService { notificationManager.notify(NOTIFICATION_UPLOAD_FAILED, failureNotification); contribution.setState(Contribution.STATE_FAILED); - contribution.save(); + contributionDao.save(contribution); } private String findUniqueFilename(String fileName) throws IOException { diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java index 3992324b5..78c1ca155 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java @@ -11,6 +11,11 @@ import timber.log.Timber; public class DialogUtil { + /** + * Dismisses a dialog safely. + * @param activity the activity + * @param dialog the dialog to be dismissed + */ public static void dismissSafely(@Nullable Activity activity, @Nullable DialogFragment dialog) { boolean isActivityDestroyed = false; @@ -33,6 +38,11 @@ public class DialogUtil { } } + /** + * Shows a dialog safely. + * @param activity the activity + * @param dialog the dialog to be shown + */ public static void showSafely(Activity activity, Dialog dialog) { if (activity == null || dialog == null) { Timber.d("Show called with null activity / dialog. Ignoring."); @@ -54,6 +64,11 @@ public class DialogUtil { } } + /** + * Shows a dialog safely. + * @param activity the activity + * @param dialog the dialog to be shown + */ public static void showSafely(FragmentActivity activity, DialogFragment dialog) { boolean isActivityDestroyed = false; diff --git a/app/src/main/res/drawable/ic_chat_bubble_black_24px.xml b/app/src/main/res/drawable/ic_chat_bubble_black_24px.xml new file mode 100644 index 000000000..8d40c6d63 --- /dev/null +++ b/app/src/main/res/drawable/ic_chat_bubble_black_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit_black_24dp.xml b/app/src/main/res/drawable/ic_edit_black_24dp.xml new file mode 100644 index 000000000..2ab2fb753 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_message_black_24dp.xml b/app/src/main/res/drawable/ic_message_black_24dp.xml new file mode 100644 index 000000000..d2876bfad --- /dev/null +++ b/app/src/main/res/drawable/ic_message_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml new file mode 100644 index 000000000..7009a6763 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout-land/activity_login.xml b/app/src/main/res/layout-land/activity_login.xml index 7265ba671..9ecaf9855 100644 --- a/app/src/main/res/layout-land/activity_login.xml +++ b/app/src/main/res/layout-land/activity_login.xml @@ -8,17 +8,17 @@ android:layout_width="400sp" android:layout_height="wrap_content" android:layout_gravity="center" - android:layout_marginTop="8dp"> + android:layout_marginTop="@dimen/small_gap"> @@ -34,11 +34,11 @@ android:layout_height="wrap_content" android:background="@color/primaryColor" android:gravity="center" - android:paddingBottom="32dp" - android:paddingTop="32dp" + android:paddingBottom="@dimen/large_gap" + android:paddingTop="@dimen/large_gap" android:text="@string/login_to_your_account" android:textColor="@android:color/white" - android:textSize="24sp" /> + android:textSize="@dimen/heading_text_size" /> @@ -67,12 +67,12 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/error_message_container" - android:layout_marginBottom="16dp" - android:layout_marginEnd="16dp" - android:layout_marginLeft="16dp" - android:layout_marginRight="16dp" - android:layout_marginStart="16dp" - android:layout_marginTop="16dp"> + android:layout_marginBottom="@dimen/standard_gap" + android:layout_marginEnd="@dimen/standard_gap" + android:layout_marginLeft="@dimen/standard_gap" + android:layout_marginRight="@dimen/standard_gap" + android:layout_marginStart="@dimen/standard_gap" + android:layout_marginTop="@dimen/standard_gap"> @@ -141,11 +141,11 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/two_factor_container" - android:layout_marginBottom="16dp" - android:layout_marginEnd="16dp" - android:layout_marginLeft="16dp" - android:layout_marginRight="16dp" - android:layout_marginStart="16dp"> + android:layout_marginBottom="@dimen/standard_gap" + android:layout_marginEnd="@dimen/standard_gap" + android:layout_marginLeft="@dimen/standard_gap" + android:layout_marginRight="@dimen/standard_gap" + android:layout_marginStart="@dimen/standard_gap">