#3222 Merge master into Structured Data branch, fix conflicts (#3447)

* [WIP] Fixes #2942. Set 'depicts' automatically for images uploaded via 'Nearby'

* Feature/refractor uploads [WIP] (#2887)

* Fix duplicate param information (#2515)

* Bug fix issue #2476 (#2526)

* Added wikidataEntityID in all db versions, handled db.execSql via method runQuery

* Versioning and changelog for v2.10.2 (#2531)

* Update changelog.md

* Versioning for v2.10.2

* Update changelog.md

* Bugfix/issue 2580 (#2584)

* Corrected string placedholders in certain string files

* Corrected string placedholders in certain string files[Bug fix #2580]

* Bug Fix #2585 (#2647)

* Bug Fix #2585
* Added null checks on view in SearchImageFragment when updating views from external sources
* Disposed the disposables in SearchActivity and SearchImageFragment when no longer in active lifecycle

* use FragmentUtils to verify fragment active state

* Bug Fix issue #2648 (#2678)

* Bug Fix issue #2648
* Handled external storage permission before file download

* * Removed redudant check for permission in MediaDetailPagerFragment (Dexter already does that)
* Removed duplicate code in PermissionUtil$checkPermissionsAndPerformAction, used the existing function with conditional extra parameters

* string name typo correction

* BugFix issue #2652 (#2706)

* Addded null check on bookmark before operating on it

* BugFix issue #2711 (#2712)

* Added null checks in OkHttpJsonApiClient$searchImages MwQueryResponse

* BugFix #2718 (#2719)

* Handled null auth cookies

* Fix #2791: NPE when nominating for deletion and leaving screen (#2792)

* Bug Fix issue #2789 (#2790)

* Handled Illegal State Exception for non existent appropriate view parents in ViewUtils$showShortSnackbar

* BugFix #2720 (#2831)

BugFix deprecated licenes #2720

* ui fixes, wip, upload

* *Issue #2886, BugFix #2832[wip]
* updated UploadActivity code
* modified ui
* Updated UploadPresenterTest

* * updated interfaces names to follow names suffixed with Contract
* added test cases

* card view elevation

* view pager disabled swipe

* bug fix, duplicate image

* used existing non-swipable view pager

* Avoid image view resize with keyboard, added adjustPan and stateVisible as softinputMode for UploadActivity

* retain UploadBaseFragment instances on orientation changes

* * Added test cases for UploadMediaPresenter
* Injected io and main thread schedulers

* categories presenter test cased wip

* Added CategoriesPresenter test

* * Added the logic to show open map (with to be uploaded image's coordinates while uploading image)

* codacy suggested changes * added java docs

* Added travis_wait fot android-wait-for-emulator

* ranamed interface onResponseCallback to Callback

* * Added api to delete picture in UploadModel
* cleanUp in UploadModel. once upload has been initiated
* Removed unused methods from UploadModel and the corresponding test class

* * Added tests for UploadPresenter
* Travis suggested changes
* Addded copy previous title and description

* * Made the upload add descriptions visible when keyboard visible
* add description request focus only when user manually requests it

* Added JavaDocs, review suggested changes

* Fix dagger injection

* use DialogUtil to show info in descriptions

* use activity context for DialogUtil

* Minor changes

* refactored title

* ui for depicts

* bug fix

* basic architecture for depicts

* adde architecture components for depicts

* [WIP] ApacheHttpClientMediaWikiApi.wikidataEditEntity: JSON param creation uses object instead of string

* resolved dagger errors

* multilingual captions and next button error resolved

* fixed next button issues in depicts fragment

* captions and depicts

* resolved previous button click issues

* fixed bindview error and added multi-captions

* replaced description and caption with uploadmediadetail

* refactored few classes

* modified ui of depicts

* minor fixes

* Bug fix, reduced the add description edit text clickable bound (#2973)

* moved depicts before categories

* replaced previous filename with captions

* removed time from filename

* added depicts suggestions

* [WIP] Wikidata Sandbox (Q4115189) test

* changes layout of layout_upload_depicts

* changed layout of upload_depicts

* code stuck at IO_SCHEDULER

* labels and description for depicts activity

* Bugfix/uploads (#3000)

* merged with master

* BugFix IllegalStateException
* setRetainState(true), not required with FragmentStatePagerAdapter
* Increase the ViewPager's Offscreen Limit, we want all the fragments to be active

* BugFix, clear selected categoris for previous upload session
* Clear Selected Categories
* Addded JavaDocs for CategoriesModel

* Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java

* Added class level JavaDoc UploadRemoteDataSource

* Added class level JavaDoc for UploadRepository

* Added JavaDocs for ThumbnailsAdapter

* Added JavaDocs for MediaLicensePresenter, CategoriesPresenter

* Removed null check on category query
* Show default catgeories based on image title and gps location when category text empty
* Allow search for empty category search

* Attached image scale listener to upload media image

* Bug fix, reduced the add description edit text clickable bound

* Fix memory leak (#3001)

* Bugfix/uploads (#3002)

* merged with master

* BugFix IllegalStateException
* setRetainState(true), not required with FragmentStatePagerAdapter
* Increase the ViewPager's Offscreen Limit, we want all the fragments to be active

* BugFix, clear selected categoris for previous upload session
* Clear Selected Categories
* Addded JavaDocs for CategoriesModel

* Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java

* Added class level JavaDoc UploadRemoteDataSource

* Added class level JavaDoc for UploadRepository

* Added JavaDocs for ThumbnailsAdapter

* Added JavaDocs for MediaLicensePresenter, CategoriesPresenter

* Removed null check on category query
* Show default catgeories based on image title and gps location when category text empty
* Allow search for empty category search

* Attached image scale listener to upload media image

* Bug fix, reduced the add description edit text clickable bound

* Added tooltip in Title in UploadMediaFragment

* BugFix recent categories

* Updated test methods

* Bugfix/uploads (#3011)

* merged with master

* BugFix IllegalStateException
* setRetainState(true), not required with FragmentStatePagerAdapter
* Increase the ViewPager's Offscreen Limit, we want all the fragments to be active

* BugFix, clear selected categoris for previous upload session
* Clear Selected Categories
* Addded JavaDocs for CategoriesModel

* Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java

* Added class level JavaDoc UploadRemoteDataSource

* Added class level JavaDoc for UploadRepository

* Added JavaDocs for ThumbnailsAdapter

* Added JavaDocs for MediaLicensePresenter, CategoriesPresenter

* Removed null check on category query
* Show default catgeories based on image title and gps location when category text empty
* Allow search for empty category search

* Attached image scale listener to upload media image

* Bug fix, reduced the add description edit text clickable bound

* Added tooltip in Title in UploadMediaFragment

* BugFix recent categories

* Updated test methods

* Avoid memory leak, free the adpater in MediaLicenseFragment.onDestroyView

* bugfix/uploads (#3012)

* merged with master

* BugFix IllegalStateException
* setRetainState(true), not required with FragmentStatePagerAdapter
* Increase the ViewPager's Offscreen Limit, we want all the fragments to be active

* BugFix, clear selected categoris for previous upload session
* Clear Selected Categories
* Addded JavaDocs for CategoriesModel

* Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java

* Added class level JavaDoc UploadRemoteDataSource

* Added class level JavaDoc for UploadRepository

* Added JavaDocs for ThumbnailsAdapter

* Added JavaDocs for MediaLicensePresenter, CategoriesPresenter

* Removed null check on category query
* Show default catgeories based on image title and gps location when category text empty
* Allow search for empty category search

* Attached image scale listener to upload media image

* Bug fix, reduced the add description edit text clickable bound

* Added tooltip in Title in UploadMediaFragment

* BugFix recent categories

* Updated test methods

* Avoid memory leak, free the adpater in MediaLicenseFragment.onDestroyView

* BugFix Illegal State Exception in ViewpPagerAdapter

* Remove irrelevant comment

* merge conflict with strings (#3016)

* [WIP] Fixed duplicated subscriprion for 'addPropertyP180'

* added documentation

* fixed issue #3006

* resolved issue #3004

* fixed issue with categoryPresenterTest.kt

* send captions as labels

* fixed issue with the captions

* optimised imports

* added upload for captions

* minor changes

* resolved issue with uploading captions

* resolved issue with api call

* uploading captions to wikibase

* added some tests and documentation

* undo formatting changes

* uploaded captions as labels to wikibase

* minor changes

* resolved error with spinner adpater

* adding captions to local database

* Fixed issue #3035

* fixed issue #3033

* fixed issue #3005

* fixed issue #3005

* added search for depicts

* fixed issue with compile time

* fixe issue with project build

* fixed issue #3044

* merged uploading depicts into branch

* uploading depicts

* rebased branch

* fixed crash due to depicts

* modified depicts interface

* Resolve merge conflicts

* Fix issues with API calls

* Use wikidata token

* searching depictions from depicts activity

* added some documentation and other changes

* fixed crash on selecting depictions

* sending wikidataentity id to upload depictions

* added changes after review

* Fixed issue with next button diabling in media detail activity

* added tests for depictions

* added all the unit tests and fixed few more issues

* showing captions in media details

* show captions in media details

* added documentations and worked upon review comments

* parsing response for depictions

* displaying captions and depiction QID in media detail

* added documentation

* fetching labels from QIDs

* captions working perfectly

* added documentations and code cleaning

* minor changes

* minor changes

* Showing items in explore

* added search via depicts in explore

* Added setOffscreenPageLimit in ViewPager

* show captions in explore

* show captions in home

* showing depict images under items

* added documentation and code refactoring

* enabled pagination in depiction search

* added some tests and media deatils in depiction detail activity

* fixed bug with back button in media

* fixed issue #3100

* fixed issue #3098

* fixed issue #3099

* fixed issue #3104 and #3098

* showing captions in place of title in home and explore:media

* show captions in explore:depiction image list activity

* showing depictions in media details

* showing depictions in media details in production flavor

* fixed issue #3108 and #3107

* fix isse #3108

* fixed issue #3110 and #3112

* fixed issue #3113

* added documentations

* fixed issue #3076 and #3109

* added depiction search test

* fixed issue #3113

* fixed issue #3111

* fixed issue #3106

* Showing items in explore

* minor change

* fixed issue #3118 and some other changes

* added MVP in searchdepictionsfragment

* added mvp architecture

* added MVP architecture to DepictedImagesDetailsActivity

* added documentation and some minor changes

* added image to depicted item in search depictions

* * Use callbacks from renderer to fetch thumbnails

* adding fresco to load image in depictions

* adding thumbnail image for depictions in upload and explore

* pagination issues

* fixed issue --(showing previous depiction thumbnail in explore)

* Fixed the logic for pagination

* hide progress on success of last page

* adding sub-items and parent items to search in explore

* minor changes for review comments

* fixed issue #3119

* fixed issue #3130

* changes after review comments

* showing child classes for depictions

* Showing child items

* showing parent classes for depicted items

* adding localised search for parent and child items

* clicking on any child class or parent class should call the corresponding class items

* fixed issue of showing wrong thumbnail for P18 item

* fixed issue #3132

* added test for DepictedImagesPresenter.java

* added unit tests for depicted items parent and child classes

* removed unused imports and code formatting

* fixed issue in search test

* deleting unnecessary .attach_pid9313 file

* deleting unnecessary .attach_pid9655 file

* added SearchDepictionsPresenterTest

* changes after review comments

* updates for review comments

* added more documentations

* removed unused code and classes and addressed spacing changes

* changes after review

* fixed build issues in the app

* worked on some review comments

* fixed issue:wrong thumbnail appears on wikidata item

* minor change

* worked on some review changes

* worked on review comments

* minor change

* addressed remaining review comments

* replaced hardcoded jpgs with pageIds to fetch captions

* added documentation

* removed hardcoded extensions and worked on review comments

* review comments

* [WIP] Added Depicts values for flavors

* [WIP] Minor fix

* [WIP] Minor fixes

* [WIP] Fixed URL

* [WIP] Fixed URLs and tokens

* Fixed MediaClient: added check for null in continuation store

* Fixed Media::from, changed return from null to new Media()

* [WIP] Merged with master

* Fix #3254 Displays a proper message in explore section when no result for caption

* Updated Mockito to org.mockito:mockito-inline:2.13.0

* [WIP] Fixed tests after merging

* [WIP] Fixed some JUnit tests

* Fixed 'accessing from wrong thread' error

* #3222 Delete manifest declaration of activity as fragment - stop casting MainActivity to CatgoryImagesCallback - fix tests

* Remove unit test not associated with any class - make CategoryPresenterTest more idiomatic

* fix compilation errors

Co-authored-by: Vitaly V. Pinchuk <vetal.978@gmail.com>
Co-authored-by: Ashish Kumar <ashishkumar468@gmail.com>
Co-authored-by: vanshikaarora <vanshikaa937@gmail.com>
Co-authored-by: Vivek Maskara <maskaravivek@gmail.com>
Co-authored-by: Vanshika Arora <34261945+vanshikaarora@users.noreply.github.com>
Co-authored-by: Somanshu and Himanshu <somanshS14@gmail.com>
This commit is contained in:
Seán Mac Gillicuddy 2020-03-10 12:30:30 +00:00 committed by GitHub
parent 99c6f5f105
commit 942cef5d5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
144 changed files with 7190 additions and 278 deletions

View file

@ -42,6 +42,7 @@ dependencies {
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
implementation 'com.karumi:dexter:5.0.0'
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
// Logging
@ -63,7 +64,7 @@ dependencies {
//Mocking
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
testImplementation 'org.mockito:mockito-inline:2.8.47'
testImplementation 'org.mockito:mockito-inline:2.13.0'
testImplementation 'org.mockito:mockito-core:2.23.0'
testImplementation "org.powermock:powermock-module-junit4:2.0.0-beta.5"
testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5"
@ -200,6 +201,7 @@ android {
buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json\""
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
@ -215,12 +217,14 @@ android {
buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\""
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\""
buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\""
buildConfigField "String", "DEPICTION_AUTHORITY", "\"fr.free.nrw.commons.depicts.contentprovider\""
buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\""
buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.contentprovider\""
buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.locations.contentprovider\""
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
buildConfigField "String", "TEST_USERNAME", "\"" + System.getenv("test_user_name") + "\""
buildConfigField "String", "TEST_PASSWORD", "\"" + System.getenv("test_user_password") + "\""
buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\""
dimension 'tier'
}
@ -232,6 +236,7 @@ android {
buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\""
buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns_beta_active.json\""
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
@ -247,12 +252,14 @@ android {
buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\""
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\""
buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.beta.categories.contentprovider\""
buildConfigField "String", "DEPICTION_AUTHORITY", "\"fr.free.nrw.commons.beta.depicts.contentprovider\""
buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.beta.explore.recentsearches.contentprovider\""
buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\""
buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\""
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
buildConfigField "String", "TEST_USERNAME", "\"" + System.getenv("test_user_name") + "\""
buildConfigField "String", "TEST_PASSWORD", "\"" + System.getenv("test_user_password") + "\""
buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\""
dimension 'tier'
}

View file

@ -0,0 +1,46 @@
package fr.free.nrw.commons
import androidx.test.runner.AndroidJUnit4
import org.junit.Rule
import org.junit.runner.RunWith
import android.net.Uri
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.rule.ActivityTestRule
import fr.free.nrw.commons.upload.UploadActivity
import org.hamcrest.Matchers
import org.hamcrest.core.AllOf
import org.junit.Test
@RunWith(AndroidJUnit4::class)
class DepictionSearchTest {
@get:Rule
var activityRule = ActivityTestRule(UploadActivity::class.java)
@Test
fun TestForCaptionsAndDepictions() {
val imageUri = Uri.parse("file://mnt/sdcard/image.jpg")
// Build a result to return from the Camera app
// Stub out the File picker. When an intent is sent to the File picker, this tells
// Espresso to respond with the ActivityResult we just created
Espresso.onView(ViewMatchers.withId(R.id.caption_item_edit_text))
.perform(ViewActions.typeText("caption in english"))
Espresso.onView(ViewMatchers.withId(R.id.description_item_edit_text))
.perform(ViewActions.typeText("description in english"))
Espresso.onView(ViewMatchers.withId(R.id.spinner_description_languages))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.spinner_description_languages)).perform(ViewActions.click());
Espresso.onData(AllOf.allOf(Matchers.anything("spinner text"))).atPosition(1).perform(ViewActions.click());
Espresso.onView(ViewMatchers.withId(R.id.caption_item_edit_text))
.perform(ViewActions.typeText("caption in some other language"))
Espresso.onView(ViewMatchers.withId(R.id.description_item_edit_text))
.perform(ViewActions.typeText("description in some other language"))
Espresso.onView(ViewMatchers.withId(R.id.btn_next))
.perform(ViewActions.click())
}
}

View file

@ -1,8 +1,17 @@
package fr.free.nrw.commons
import android.net.Uri
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import fr.free.nrw.commons.upload.UploadActivity
import fr.free.nrw.commons.upload.depicts.DepictsFragment
import org.hamcrest.Matchers
import org.hamcrest.core.AllOf
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -16,4 +25,25 @@ class UploadActivityTest {
fun orientationChange() {
UITestHelper.changeOrientation(activityRule)
}
}
@Test
fun TestForCaptionsAndDepictions() {
val imageUri = Uri.parse("file://mnt/sdcard/image.jpg")
Espresso.onView(ViewMatchers.withId(R.id.caption_item_edit_text))
.perform(ViewActions.typeText("caption in english"))
Espresso.onView(ViewMatchers.withId(R.id.description_item_edit_text))
.perform(ViewActions.typeText("description in english"))
Espresso.onView(ViewMatchers.withId(R.id.spinner_description_languages))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.spinner_description_languages)).perform(ViewActions.click());
Espresso.onData(AllOf.allOf(Matchers.anything("spinner text"))).atPosition(1).perform(ViewActions.click());
Espresso.onView(ViewMatchers.withId(R.id.caption_item_edit_text))
.perform(ViewActions.typeText("caption in some other language"))
Espresso.onView(ViewMatchers.withId(R.id.description_item_edit_text))
.perform(ViewActions.typeText("description in some other language"))
Espresso.onView(ViewMatchers.withId(R.id.btn_next))
.perform(ViewActions.click())
Intents.intended(IntentMatchers.hasComponent(DepictsFragment::class.java.name))
}
}

View file

@ -119,6 +119,11 @@
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".depictions.WikidataItemDetailsActivity"
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".explore.categories.ExploreActivity"
android:label="@string/title_activity_explore"
@ -180,6 +185,13 @@
android:label="@string/provider_categories"
android:syncable="false" />
<provider
android:authorities="${applicationId}.depicts.contentprovider"
android:name=".upload.structure.depictions.DepictsContentProvider"
android:exported="false"
android:label="@string/provider_depictions"
android:syncable="false"/>
<provider
android:name=".explore.recentsearches.RecentSearchesContentProvider"
android:authorities="${applicationId}.explore.recentsearches.contentprovider"

View file

@ -53,6 +53,7 @@ import fr.free.nrw.commons.logging.FileLoggingTree;
import fr.free.nrw.commons.logging.LogUtils;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.upload.structure.depictions.DepictionDao;
import fr.free.nrw.commons.utils.ConfigUtils;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.internal.functions.Functions;
@ -311,6 +312,7 @@ public class CommonsApplication extends Application {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
CategoryDao.Table.onDelete(db);
DepictionDao.Table.onDelete(db);
dbOpenHelper.deleteTable(db,CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions
appDatabase.getContributionDao().deleteAll();
BookmarkPicturesDao.Table.onDelete(db);

View file

@ -49,6 +49,13 @@ public class Media implements Parcelable {
public String thumbUrl;
public String imageUrl;
public String filename;
public String thumbnailTitle;
/**
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
* This is a replacement of the previously used titles for images (titles were not multilingual)
* Also now captions replace the previous convention of using title for filename
*/
private String caption;
public String description; // monolingual description on input...
public String discussion;
long dataLength;
@ -59,9 +66,30 @@ public class Media implements Parcelable {
public String license;
public String licenseUrl;
public String creator;
/**
* Wikibase Identifier associated with media files
*/
public String pageId;
public ArrayList<String> categories; // as loaded at runtime?
/**
* Depicts is a feature part of Structured data. Multiple Depictions can be added for an image just like categories.
* However unlike categories depictions is multi-lingual
*/
public ArrayList<Map<String, String>> depictionList;
/**
* The above hashmap is fetched from API and to diplay in Explore
* However this list of depictions is for storing and retrieving depictions from local storage or cache
*/
public ArrayList<String> depictions;
public boolean requestedDeletion;
public HashMap<String, String> descriptions; // multilingual descriptions as loaded
public Map<String, String> descriptions; // multilingual descriptions as loaded
/**
* This hasmap stores the list of multilingual captions, where
* key of the HashMap is the language and value is the caption in the corresponding language
* Ex: key = "en", value: "<caption in short in English>"
* key = "de" , value: "<caption in german>"
*/
public HashMap<String, String> captions;
public HashMap<String, String> tags = new HashMap<>();
@Nullable public LatLng coordinates;
@ -70,7 +98,9 @@ public class Media implements Parcelable {
*/
protected Media() {
this.categories = new ArrayList<>();
this.depictions = new ArrayList<>();
this.descriptions = new HashMap<>();
this.captions = new HashMap<>();
}
/**
@ -88,25 +118,28 @@ public class Media implements Parcelable {
* @param localUri Media URI
* @param imageUrl Media image URL
* @param filename Media filename
* @param captions Media captions
* @param description Media description
* @param dataLength Media date length
* @param dateCreated Media creation date
* @param dateUploaded Media date uploaded
* @param creator Media creator
*/
public Media(Uri localUri, String imageUrl, String filename, String description,
public Media(Uri localUri, String imageUrl, String filename, HashMap<String, String> captions, String description,
long dataLength, Date dateCreated, Date dateUploaded, String creator) {
this();
this.localUri = localUri;
this.thumbUrl = imageUrl;
this.imageUrl = imageUrl;
this.filename = filename;
this.captions = captions;
this.description = description;
this.dataLength = dataLength;
this.dateCreated = dateCreated;
this.dateUploaded = dateUploaded;
this.creator = creator;
this.categories = new ArrayList<>();
this.depictions = new ArrayList<>();
this.descriptions = new HashMap<>();
}
@ -116,6 +149,7 @@ public class Media implements Parcelable {
thumbUrl = in.readString();
imageUrl = in.readString();
filename = in.readString();
caption = in.readString();
description = in.readString();
dataLength = in.readLong();
dateCreated = (Date) in.readSerializable();
@ -128,7 +162,11 @@ public class Media implements Parcelable {
if (categories != null) {
in.readStringList(categories);
}
if (depictions != null) {
in.readStringList(depictions);
}
descriptions = in.readHashMap(ClassLoader.getSystemClassLoader());
captions = in.readHashMap(ClassLoader.getSystemClassLoader());
}
/**
@ -143,12 +181,12 @@ public class Media implements Parcelable {
public static Media from(MwQueryPage page) {
ImageInfo imageInfo = page.imageInfo();
if (imageInfo == null) {
return null;
return new Media(); // null is not allowed
}
ExtMetadata metadata = imageInfo.getMetadata();
if (metadata == null) {
Media media = new Media(null, imageInfo.getOriginalUrl(),
page.title(), "", 0, null, null, null);
page.title(), new HashMap<>() , "", 0, null, null, null);
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
media.setThumbUrl(imageInfo.getThumbUrl());
}
@ -158,6 +196,7 @@ public class Media implements Parcelable {
Media media = new Media(null,
imageInfo.getOriginalUrl(),
page.title(),
new HashMap<>(),
"",
0,
safeParseDate(metadata.dateTime()),
@ -169,6 +208,8 @@ public class Media implements Parcelable {
media.setThumbUrl(imageInfo.getThumbUrl());
}
media.setPageId(String.valueOf(page.pageId()));
String language = Locale.getDefault().getLanguage();
if (StringUtils.isBlank(language)) {
language = "default";
@ -203,6 +244,18 @@ public class Media implements Parcelable {
}
}
/**
* @return pageId for the current media object*/
public String getPageId() {
return pageId;
}
/**
*sets pageId for the current media object
*/
private void setPageId(String pageId) {
this.pageId = pageId;
}
public String getThumbUrl() {
return thumbUrl;
}
@ -233,6 +286,21 @@ public class Media implements Parcelable {
return filename != null ? getPageTitle().getDisplayTextWithoutNamespace().replaceFirst("[.][^.]+$", "") : "";
}
/**
* Set Caption(if available) as the thumbnail title of the image
*/
public void setThumbnailTitle(String title) {
this.thumbnailTitle = title;
}
/**
* @return title to be shown on image thumbnail
* If caption is available for the image then it returns caption else filename
*/
public String getThumbnailTitle() {
return thumbnailTitle != null? thumbnailTitle : getDisplayTitle();
}
/**
* Gets file page title
* @return New media page title
@ -299,6 +367,37 @@ public class Media implements Parcelable {
return description;
}
/**
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
* This is a replacement of the previously used titles for images (titles were not multilingual)
* Also now captions replace the previous convention of using title for filename
*
* @return caption
*/
public String getCaption() {
return caption;
}
/**
* @return depictions associated with the current media
*/
public ArrayList<Map<String, String>> getDepiction() {
return depictionList;
}
/**
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
* This is a replacement of the previously used titles for images (titles were not multilingual)
* Also now captions replace the previous convention of using title for filename
*
* key of the HashMap is the language and value is the caption in the corresponding language
*
* returns list of captions stored in hashmap
*/
public HashMap<String, String> getCaptions() {
return captions;
}
/**
* Sets the file description.
* @param description the new description of the file
@ -452,6 +551,13 @@ public class Media implements Parcelable {
return (ArrayList<String>) categories.clone(); // feels dirty
}
/**
* @return array list of depictions associated with the current media
*/
public ArrayList<String> getDepictions() {
return (ArrayList<String>) depictions.clone();
}
/**
* Sets the categories the file falls under.
* </p>
@ -464,6 +570,11 @@ public class Media implements Parcelable {
this.categories.addAll(categories);
}
public void setDepictions(List<String> depictions) {
this.depictions.clear();
this.depictions.addAll(depictions);
}
/**
* Modifies (or sets) media descriptions
* @param descriptions Media descriptions
@ -523,6 +634,7 @@ public class Media implements Parcelable {
parcel.writeString(thumbUrl);
parcel.writeString(imageUrl);
parcel.writeString(filename);
parcel.writeString(caption);
parcel.writeString(description);
parcel.writeLong(dataLength);
parcel.writeSerializable(dateCreated);
@ -533,7 +645,9 @@ public class Media implements Parcelable {
parcel.writeInt(height);
parcel.writeString(license);
parcel.writeStringList(categories);
parcel.writeStringList(depictions);
parcel.writeMap(descriptions);
parcel.writeMap(captions);
}
/**
@ -559,4 +673,27 @@ public class Media implements Parcelable {
public void setLicense(String license) {
this.license = license;
}
/**
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
* This is a replacement of the previously used titles for images (titles were not multilingual)
* Also now captions replace the previous convention of using title for filename
*
* This function sets captions
* @param caption
*/
public void setCaption(String caption) {
this.caption = caption;
}
public void setCaptions(HashMap<String, String> captions) {
this.captions = captions;
}
/**
* Sets depictions for the current media obtained fro Wikibase API
*/
public void setDepiction(ArrayList<Map<String, String>> depictions) {
this.depictionList = depictions;
}
}

View file

@ -2,6 +2,14 @@ package fr.free.nrw.commons;
import androidx.core.text.HtmlCompat;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -26,16 +34,20 @@ public class MediaDataExtractor {
/**
* Simplified method to extract all details required to show media details.
* It fetches media object, deletion status and talk page for the filename
* It fetches media object, deletion status, talk page and captions for the filename
* @param filename for which the details are to be fetched
* @return full Media object with all details including deletion status and talk page
*/
public Single<Media> fetchMediaDetails(String filename) {
public Single<Media> fetchMediaDetails(String filename, String pageId) {
Single<Media> mediaSingle = getMediaFromFileName(filename);
Single<Boolean> pageExistsSingle = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + filename);
Single<String> discussionSingle = getDiscussion(filename);
return Single.zip(mediaSingle, pageExistsSingle, discussionSingle, (media, deletionStatus, discussion) -> {
Single<String> captionSingle = getCaption("M"+pageId);
Single<JsonObject> depictionSingle = getDepictions(filename);
return Single.zip(mediaSingle, pageExistsSingle, discussionSingle, captionSingle, depictionSingle, (media, deletionStatus, discussion, caption, depiction) -> {
media.setDiscussion(discussion);
media.setCaption(caption);
media.setDepiction(formatDepictions(depiction));
if (deletionStatus) {
media.setRequestedDeletion();
}
@ -43,6 +55,62 @@ public class MediaDataExtractor {
});
}
/**
* Obtains captions using filename
* @param wikibaseIdentifier
*
* @return caption for the image in user's locale
* Ex: "a nice painting" (english locale) and "No Caption" in case the caption is not available for the image
*/
private Single<String> getCaption(String wikibaseIdentifier) {
return mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier);
}
/**
* From the Json Object extract depictions into an array list
* @param mediaResponse
* @return List containing map for depictions, the map has two keys,
* first key is for the label and second is for the url of the item
*/
private ArrayList<Map<String, String>> formatDepictions(JsonObject mediaResponse) {
try {
JsonArray depictionArray = (JsonArray) mediaResponse.get("Depiction");
ArrayList<Map<String, String>> depictedItemList = new ArrayList<>();
try {
for (int i = 0; i <depictionArray.size() ; i++) {
JsonObject depictedItem = (JsonObject) depictionArray.get(i);
Map <String, String> depictedObject = new HashMap<>();
String label = depictedItem.get("label").toString();
String id = depictedItem.get("id").toString();
String transformedLabel = label.substring(3, label.length()-3);
String transformedId = id.substring(1,id.length() - 1);
depictedObject.put("label", transformedLabel); //remove the additional characters obtained in label and ID object to extract the relevant string (since the string also contains extra quites that are not required)
depictedObject.put("id", transformedId);
depictedItemList.add(depictedObject);
}
return depictedItemList;
} catch (NullPointerException e) {
return new ArrayList<>();
}
} catch (ClassCastException c) {
return new ArrayList<>();
}
}
/**
* Fetch caption and depictions from the MediaWiki API
* @param filename the filename we will return the caption for
* @return a map containing caption and depictions (empty string in the map if no caption/depictions)
*/
private Single<JsonObject> getDepictions(String filename) {
return mediaClient.getCaptionAndDepictions(filename)
.map(mediaResponse -> {
return mediaResponse;
}).doOnError(throwable -> {
Timber.e(throwable+ "error while fetching depictions");
});
}
/**
* Method can be used to fetch media for a given filename
* @param filename Eg. File:Test.jpg

View file

@ -44,6 +44,10 @@ import static android.view.View.VISIBLE;
public class CategoryImagesListFragment extends DaggerFragment {
private static int TIMEOUT_SECONDS = 15;
/**
* counts the total number of items loaded from the API
*/
private int mediaSize = 0;
private GridViewAdapter gridAdapter;
@ -256,6 +260,35 @@ public class CategoryImagesListFragment extends DaggerFragment {
progressBar.setVisibility(GONE);
isLoading = false;
statusTextView.setVisibility(GONE);
for (Media m : collection) {
replaceTitlesWithCaptions("M"+m.getPageId(), mediaSize++);
}
}
/**
* fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available)
* else show filename
*/
public void replaceTitlesWithCaptions(String wikibaseIdentifier, int i) {
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(subscriber -> {
handleLabelforImage(subscriber, i);
}));
}
/**
* If caption is available for the image, then modify grid adapter
* to show captions
*/
private void handleLabelforImage(String s, int position) {
if (!s.trim().equals(getString(R.string.detail_caption_empty))) {
gridAdapter.getItem(position).setThumbnailTitle(s);
gridAdapter.notifyDataSetChanged();
}
}
/**

View file

@ -8,6 +8,8 @@ import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.facebook.drawee.view.SimpleDraweeView;
import java.util.ArrayList;
@ -55,7 +57,7 @@ public class GridViewAdapter extends ArrayAdapter {
data = new ArrayList<>();
return false;
}
if (data.size() <= 0) {
if (data.isEmpty()) {
return false;
}
String fileName = data.get(0).getFilename();
@ -86,12 +88,22 @@ public class GridViewAdapter extends ArrayAdapter {
SimpleDraweeView imageView = convertView.findViewById(R.id.categoryImageView);
TextView fileName = convertView.findViewById(R.id.categoryImageTitle);
TextView author = convertView.findViewById(R.id.categoryImageAuthor);
fileName.setText(item.getDisplayTitle());
fileName.setText(item.getThumbnailTitle());
setAuthorView(item, author);
imageView.setImageURI(item.getThumbUrl());
return convertView;
}
/**
* @return the Media item at the given position
*/
@Nullable
@Override
public Media getItem(int position) {
return data.get(position);
}
/**
* Shows author information if its present
* @param item

View file

@ -12,7 +12,9 @@ import androidx.room.PrimaryKey;
import org.apache.commons.lang3.StringUtils;
import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import fr.free.nrw.commons.CommonsApplication;
@ -71,11 +73,17 @@ public class Contribution extends Media {
public Uri contentProviderUri;
public String dateCreatedSource;
/**
* Each depiction loaded in depictions activity is associated with a wikidata entity id,
* this Id is in turn used to upload depictions to wikibase
*/
public ArrayList<String> depictionsEntityIds;
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date dateCreated,
int state, long dataLength, Date dateUploaded, long transferred,
String source, String description, String creator, boolean isMultiple,
String source, HashMap<String, String> captions, String description, String creator, boolean isMultiple,
int width, int height, String license) {
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
super(localUri, imageUrl, filename, captions, description, dataLength, dateCreated, dateUploaded, creator);
this.contentUri = contentUri;
this.state = state;
this.transferred = transferred;
@ -87,17 +95,18 @@ public class Contribution extends Media {
this.dateCreatedSource = "";
}
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);
public Contribution(Uri localUri, String imageUrl, String filename, HashMap<String, String> captions, String description, long dataLength,
Date dateCreated, Date dateUploaded, String creator, String editSummary, ArrayList<String> depictionsEntityIds, String decimalCoords) {
super(localUri, imageUrl, filename, captions, description, dataLength, dateCreated, dateUploaded, creator);
this.decimalCoords = decimalCoords;
this.editSummary = editSummary;
this.dateCreatedSource = "";
this.depictionsEntityIds = depictionsEntityIds;
}
public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength,
public Contribution(Uri localUri, String imageUrl, String filename, HashMap<String, String> captions, String description, long dataLength,
Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords, int state) {
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
super(localUri, imageUrl, filename, captions, description, dataLength, dateCreated, dateUploaded, creator);
this.decimalCoords = decimalCoords;
this.editSummary = editSummary;
this.dateCreatedSource = "";
@ -167,6 +176,13 @@ public class Contribution extends Media {
this.dateUploaded = date;
}
/**
* sets depiction entity ids for the given contribution
*/
public void setDepictions(ArrayList<String> depictionsEntityIds) {
this.depictionsEntityIds = depictionsEntityIds;
}
public String getPageContents(Context applicationContext) {
StringBuilder buffer = new StringBuilder();
buffer
@ -275,4 +291,10 @@ public class Contribution extends Media {
this.contentProviderUri = contentProviderUri;
}
/**
* @return array list of entityids for the depictions
*/
public ArrayList<String> getDepictionsEntityIds() {
return depictionsEntityIds;
}
}

View file

@ -12,6 +12,8 @@ import com.facebook.drawee.view.SimpleDraweeView;
import org.apache.commons.lang3.StringUtils;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
@ -23,6 +25,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.contributions.model.DisplayableContribution;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.upload.FileUtils;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
@ -43,6 +46,9 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
@Inject
MediaDataExtractor mediaDataExtractor;
@Inject
MediaClient mediaClient;
@Inject
@Named("thumbnail-cache")
@ -51,6 +57,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
private DisplayableContribution contribution;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private int position;
private static int TIMEOUT_SECONDS = 15;
private static final String NO_CAPTION = "No caption";
ContributionViewHolder(View parent, Callback callback) {
super(parent);
@ -64,6 +72,7 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
this.position=position;
this.contribution = contribution;
fetchAndDisplayThumbnail(contribution);
fetchAndDisplayCaption(contribution);
titleView.setText(contribution.getDisplayTitle());
seqNumView.setText(String.valueOf(contribution.getPosition() + 1));
@ -103,6 +112,30 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
}
}
/**
* In contributions first we show the title for the image stored in cache,
* then we fetch captions associated with the image and replace title on the thumbnail with caption
*
* @param contribution
*/
private void fetchAndDisplayCaption(DisplayableContribution contribution) {
if ((contribution.getState() != Contribution.STATE_COMPLETED)) {
titleView.setText(contribution.getDisplayTitle());
} else {
Timber.d("Fetching caption for %s", contribution.getFilename());
String wikibaseMediaId = "M"+contribution.getPageId(); // Create Wikibase media id from the page id. Example media id: M80618155 for https://commons.wikimedia.org/wiki/File:Tantanmen.jpeg with has the pageid 80618155
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseMediaId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(subscriber -> {
if (!subscriber.trim().equals(NO_CAPTION)) {
titleView.setText(subscriber);
} else titleView.setText(contribution.getDisplayTitle());
}));
}
}
/**
* This method fetches the thumbnail url from file name
* If the thumbnail url is present in cache, then it is used otherwise API call is made to fetch the thumbnail

View file

@ -41,6 +41,7 @@ import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
@ -58,6 +59,8 @@ import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import timber.log.Timber;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;

View file

@ -14,6 +14,7 @@ import androidx.lifecycle.Observer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import javax.inject.Inject;
@ -102,8 +103,8 @@ public class ContributionsPresenter implements UserActionListener {
.filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted()).doOnNext(mwQueryLogEvent -> Timber.d("Image %s passed filters", mwQueryLogEvent.title()))
.map(image -> {
Contribution contribution = new Contribution(null, null, image.title(),
"", -1, image.date(), image.date(), user,
"", "", STATE_COMPLETED);
new HashMap<>(), "", -1, image.date(), image.date(), user,
"", "", STATE_COMPLETED);
return contribution;
})
.toList()

View file

@ -16,6 +16,7 @@ public class DisplayableContribution extends Contribution {
contribution.getDateUploaded(),
contribution.getTransferred(),
contribution.getSource(),
contribution.getCaptions(),
contribution.getDescription(),
contribution.getCreator(),
contribution.getMultiple(),

View file

@ -9,6 +9,7 @@ import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
import fr.free.nrw.commons.category.CategoryDao;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.upload.structure.depictions.DepictionDao;
public class DBOpenHelper extends SQLiteOpenHelper {
@ -28,6 +29,7 @@ public class DBOpenHelper extends SQLiteOpenHelper {
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
CategoryDao.Table.onCreate(sqLiteDatabase);
DepictionDao.Table.onCreate(sqLiteDatabase);
BookmarkPicturesDao.Table.onCreate(sqLiteDatabase);
BookmarkLocationsDao.Table.onCreate(sqLiteDatabase);
RecentSearchesDao.Table.onCreate(sqLiteDatabase);
@ -36,6 +38,7 @@ public class DBOpenHelper extends SQLiteOpenHelper {
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
DepictionDao.Table.onUpdate(sqLiteDatabase, from, to);
BookmarkPicturesDao.Table.onUpdate(sqLiteDatabase, from, to);
BookmarkLocationsDao.Table.onUpdate(sqLiteDatabase, from, to);
RecentSearchesDao.Table.onUpdate(sqLiteDatabase, from, to);

View file

@ -1,21 +1,16 @@
package fr.free.nrw.commons.db;
import android.net.Uri;
import androidx.room.TypeConverter;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.wikipedia.json.GsonUtil;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.location.LatLng;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class Converters {
@ -59,7 +54,7 @@ public class Converters {
}
@TypeConverter
public static HashMap<String,String> stringToMap(String objectList) {
public static HashMap<String,String> stringToHashMap(String objectList) {
return objectList == null ? null : getGson().fromJson(objectList,new TypeToken<HashMap<String,String>>(){}.getType());
}
@ -73,4 +68,24 @@ public class Converters {
return objectList == null ? null : getGson().fromJson(objectList,LatLng.class);
}
@TypeConverter
public static String listOfMapToString(ArrayList<Map<String,String>> listOfMaps) {
return listOfMaps == null ? null : getGson().toJson(listOfMaps);
}
@TypeConverter
public static ArrayList<Map<String,String>> stringToListOfMap(String listOfMaps) {
return listOfMaps == null ? null :getGson().fromJson(listOfMaps,new TypeToken<ArrayList<Map<String,String>>>(){}.getType());
}
@TypeConverter
public static String mapToString(Map<String,String> map) {
return map == null ? null : getGson().toJson(map);
}
@TypeConverter
public static Map<String,String> stringToMap(String map) {
return map == null ? null :getGson().fromJson(map,new TypeToken<Map<String,String>>(){}.getType());
}
}

View file

@ -0,0 +1,27 @@
package fr.free.nrw.commons.depictions;
import dagger.Binds;
import dagger.Module;
import fr.free.nrw.commons.depictions.Media.DepictedImagesContract;
import fr.free.nrw.commons.depictions.Media.DepictedImagesPresenter;
import fr.free.nrw.commons.depictions.SubClass.SubDepictionListContract;
import fr.free.nrw.commons.depictions.SubClass.SubDepictionListPresenter;
/**
* The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
*/
@Module
public abstract class DepictionModule {
@Binds
public abstract DepictedImagesContract.UserActionListener bindsDepictedImagesPresenter(
DepictedImagesPresenter
presenter
);
@Binds
public abstract SubDepictionListContract.UserActionListener bindsSubDepictionListPresenter(
SubDepictionListPresenter
presenter
);
}

View file

@ -0,0 +1,119 @@
package fr.free.nrw.commons.depictions;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.facebook.drawee.view.SimpleDraweeView;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
/**
* Adapter for Items in DepictionDetailsActivity
*/
public class GridViewAdapter extends ArrayAdapter {
private List<Media> data;
public GridViewAdapter(Context context, int layoutResourceId, List<Media> data) {
super(context, layoutResourceId, data);
this.data = data;
}
/**
* Adds more item to the list
* Its triggered on scrolling down in the list
* @param images
*/
public void addItems(List<Media> images) {
if (data == null) {
data = new ArrayList<>();
}
data.addAll(images);
notifyDataSetChanged();
}
/**
* Check the first item in the new list with old list and returns true if they are same
* Its triggered on successful response of the fetch images API.
* @param images
*/
public boolean containsAll(List<Media> images){
if (images == null || images.isEmpty()) {
return false;
}
if (data == null) {
data = new ArrayList<>();
return false;
}
if (data.size() <= 0) {
return false;
}
String fileName = data.get(0).getFilename();
String imageName = images.get(0).getFilename();
return imageName.equals(fileName);
}
@Override
public boolean isEmpty() {
return data == null || data.isEmpty();
}
/**
* Sets up the UI for the depicted image item
* @param position
* @param convertView
* @param parent
* @return
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.layout_depict_image, null);
}
Media item = data.get(position);
SimpleDraweeView imageView = convertView.findViewById(R.id.depict_image_view);
TextView fileName = convertView.findViewById(R.id.depict_image_title);
TextView author = convertView.findViewById(R.id.depict_image_author);
fileName.setText(item.getThumbnailTitle());
setAuthorView(item, author);
imageView.setImageURI(item.getThumbUrl());
return convertView;
}
@Nullable
@Override
public Media getItem(int position) {
return data.get(position);
}
/**
* Shows author information if its present
* @param item
* @param author
*/
private void setAuthorView(Media item, TextView author) {
if (!TextUtils.isEmpty(item.getCreator())) {
String uploadedByTemplate = getContext().getString(R.string.image_uploaded_by);
String uploadedBy = String.format(Locale.getDefault(), uploadedByTemplate, item.getCreator());
author.setText(uploadedBy);
} else {
author.setVisibility(View.GONE);
}
}
}

View file

@ -0,0 +1,107 @@
package fr.free.nrw.commons.depictions.Media;
import android.widget.ListAdapter;
import java.util.List;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.Media;
/**
* Contract with which DepictedImagesFragment and its presenter will talk to each other
*/
public interface DepictedImagesContract {
interface View {
/**
* Handles the UI updates for no internet scenario
*/
void handleNoInternet();
/**
* Handles the UI updates for a error scenario
*/
void initErrorView();
/**
* Initializes the adapter with a list of Media objects
*
* @param mediaList List of new Media to be displayed
*/
void setAdapter(List<Media> mediaList);
/**
* Seat caption to the image at the given position
*/
void handleLabelforImage(String s, int position);
/**
* Display snackbar
*/
void showSnackBar();
/**
* Inform the view that there are no more items to be loaded for this search query
* or reset the isLastPage for the current query
* @param isLastPage
*/
void setIsLastPage(boolean isLastPage);
/**
* Set visibility of progressbar depending on the boolean value
*/
void progressBarVisible(Boolean value);
/**
* It return an instance of gridView adapter which helps in extracting media details
* used by the gridView
*
* @return GridView Adapter
*/
ListAdapter getAdapter();
/**
* adds list to adapter
*/
void addItemsToAdapter(List<Media> media);
/**
* Sets loading status depending on the boolean value
*/
void setLoadingStatus(Boolean value);
/**
* Handles the success scenario
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
*
* @param collection List of new Media to be displayed
*/
void handleSuccess(List<Media> collection);
}
interface UserActionListener extends BasePresenter<View> {
/**
* Checks for internet connection and then initializes the grid view with first 10 images of that depiction
*/
void initList(String entityId);
/**
* Fetches more images for the item and adds it to the grid view adapter
*/
void fetchMoreImages();
/**
* fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available)
* else show filename
*/
void replaceTitlesWithCaptions(String title, int position);
/**
* add items to query list
*/
void addItemsToQueryList(List<Media> collection);
}
}

View file

@ -0,0 +1,264 @@
package fr.free.nrw.commons.depictions.Media;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ListAdapter;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.depictions.GridViewAdapter;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
/**
* Fragment for showing image list after selected an item from SearchActivity In Explore
*/
public class DepictedImagesFragment extends DaggerFragment implements DepictedImagesContract.View {
@BindView(R.id.statusMessage)
TextView statusTextView;
@BindView(R.id.loadingImagesProgressBar)
ProgressBar progressBar;
@BindView(R.id.depicts_image_list)
GridView gridView;
@BindView(R.id.parentLayout)
RelativeLayout parentLayout;
@Inject
DepictedImagesPresenter presenter;
private GridViewAdapter gridAdapter;
private String entityId = null;
private boolean isLastPage;
private boolean isLoading = true;
private int mediaSize = 0;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_depict_image, container, false);
ButterKnife.bind(this, v);
presenter.onAttachView(this);
return v;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
initViews();
}
/**
* Initializes the UI elements for the fragment
* Setup the grid view to and scroll listener for it
*/
private void initViews() {
String depictsName = getArguments().getString("wikidataItemName");
entityId = getArguments().getString("entityId");
if (getArguments() != null && depictsName != null) {
initList();
setScrollListener();
}
}
private void initList() {
presenter.initList(entityId);
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
} else presenter.initList(entityId);
}
/**
* Handles the UI updates for no internet scenario
*/
@Override
public void handleNoInternet() {
progressBar.setVisibility(GONE);
if (gridAdapter == null || gridAdapter.isEmpty()) {
statusTextView.setVisibility(VISIBLE);
statusTextView.setText(getString(R.string.no_internet));
} else {
ViewUtil.showShortSnackbar(parentLayout, R.string.no_internet);
}
}
/**
* Handles the UI updates for a error scenario
*/
@Override
public void initErrorView() {
progressBar.setVisibility(GONE);
if (gridAdapter == null || gridAdapter.isEmpty()) {
statusTextView.setVisibility(VISIBLE);
statusTextView.setText(getString(R.string.no_images_found));
} else {
statusTextView.setVisibility(GONE);
}
}
/**
* Sets the scroll listener for the grid view so that more images are fetched when the user scrolls down
* Checks if the item has more images before loading
* Also checks whether images are currently being fetched before triggering another request
*/
private void setScrollListener() {
gridView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (!isLastPage && !isLoading && (firstVisibleItem + visibleItemCount >= totalItemCount)) {
isLoading = true;
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
} else {
presenter.fetchMoreImages();
}
}
if (isLastPage) {
progressBar.setVisibility(GONE);
}
}
});
}
/**
* Seat caption to the image at the given position
*/
@Override
public void handleLabelforImage(String s, int position) {
if (!s.trim().equals(getString(R.string.detail_caption_empty))) {
gridAdapter.getItem(position).setThumbnailTitle(s);
gridAdapter.notifyDataSetChanged();
}
}
/**
* Display snackbar
*/
@Override
public void showSnackBar() {
ViewUtil.showShortSnackbar(parentLayout, R.string.error_loading_images);
}
/**
* Set visibility of progressbar depending on the boolean value
*/
@Override
public void progressBarVisible(Boolean value) {
if (value) {
progressBar.setVisibility(VISIBLE);
} else {
progressBar.setVisibility(GONE);
}
}
/**
* It return an instance of gridView adapter which helps in extracting media details
* used by the gridView
*
* @return GridView Adapter
*/
@Override
public ListAdapter getAdapter() {
return gridAdapter;
}
/**
* Initializes the adapter with a list of Media objects
*
* @param mediaList List of new Media to be displayed
*/
@Override
public void setAdapter(List<Media> mediaList) {
gridAdapter = new fr.free.nrw.commons.depictions.GridViewAdapter(getContext(), R.layout.layout_depict_image, mediaList);
gridView.setAdapter(gridAdapter);
}
/**
* adds list to adapter
*/
@Override
public void addItemsToAdapter(List<Media> media) {
gridAdapter.addAll(media);
gridAdapter.notifyDataSetChanged();
}
/**
* Sets loading status depending on the boolean value
*/
@Override
public void setLoadingStatus(Boolean value) {
if (!value) {
statusTextView.setVisibility(GONE);
}
isLoading = value;
}
/**
* Inform the view that there are no more items to be loaded for this search query
* or reset the isLastPage for the current query
* @param isLastPage
*/
@Override
public void setIsLastPage(boolean isLastPage) {
this.isLastPage=isLastPage;
progressBar.setVisibility(GONE);
}
/**
* Handles the success scenario
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
*
* @param collection List of new Media to be displayed
*/
@Override
public void handleSuccess(List<Media> collection) {
presenter.addItemsToQueryList(collection);
if (gridAdapter == null) {
setAdapter(collection);
} else {
if (gridAdapter.containsAll(collection)) {
return;
}
gridAdapter.addItems(collection);
try {
((WikidataItemDetailsActivity) getContext()).viewPagerNotifyDataSetChanged();
} catch (Exception e) {
e.printStackTrace();
}
}
progressBar.setVisibility(GONE);
isLoading = false;
statusTextView.setVisibility(GONE);
for (Media m : collection) {
presenter.replaceTitlesWithCaptions("M"+m.getPageId(), mediaSize++);
}
}
}

View file

@ -0,0 +1,167 @@
package fr.free.nrw.commons.depictions.Media;
import android.annotation.SuppressLint;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import timber.log.Timber;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
/**
* Presenter for DepictedImagesFragment
*/
public class DepictedImagesPresenter implements DepictedImagesContract.UserActionListener {
private static final DepictedImagesContract.View DUMMY = (DepictedImagesContract.View) Proxy
.newProxyInstance(
DepictedImagesContract.View.class.getClassLoader(),
new Class[]{DepictedImagesContract.View.class},
(proxy, method, methodArgs) -> null);
private static int TIMEOUT_SECONDS = 15;
DepictsClient depictsClient;
MediaClient mediaClient;
@Named("default_preferences")
JsonKvStore depictionKvStore;
private final Scheduler ioScheduler;
private final Scheduler mainThreadScheduler;
private DepictedImagesContract.View view = DUMMY;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
/**
* Wikibase enitityId for the depicted Item
* Ex: Q9394
*/
private String entityId = null;
private List<Media> queryList = new ArrayList<>();
@Inject
public DepictedImagesPresenter(@Named("default_preferences") JsonKvStore depictionKvStore, DepictsClient depictsClient, MediaClient mediaClient, @Named(IO_THREAD) Scheduler ioScheduler,
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
this.depictionKvStore = depictionKvStore;
this.depictsClient = depictsClient;
this.ioScheduler = ioScheduler;
this.mainThreadScheduler = mainThreadScheduler;
this.mediaClient = mediaClient;
}
@Override
public void onAttachView(DepictedImagesContract.View view) {
this.view = view;
}
@Override
public void onDetachView() {
this.view = DUMMY;
}
/**
* Checks for internet connection and then initializes the grid view with first 10 images of that depiction
*/
@SuppressLint("CheckResult")
@Override
public void initList(String entityId) {
view.setLoadingStatus(true);
view.progressBarVisible(true);
view.setIsLastPage(false);
compositeDisposable.add(depictsClient.fetchImagesForDepictedItem(entityId, 25, 0)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(this::handleSuccess, this::handleError));
}
/**
* Fetches more images for the item and adds it to the grid view adapter
*/
@SuppressLint("CheckResult")
@Override
public void fetchMoreImages() {
view.progressBarVisible(true);
compositeDisposable.add(depictsClient.fetchImagesForDepictedItem(entityId, 25, queryList.size())
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(this::handlePaginationSuccess, this::handleError));
}
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
*/
private void handlePaginationSuccess(List<Media> media) {
queryList.addAll(media);
view.progressBarVisible(false);
view.addItemsToAdapter(media);
}
/**
* Logs and handles API error scenario
*
* @param throwable
*/
public void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading images inside items");
try {
view.initErrorView();
view.showSnackBar();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Handles the success scenario
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
* @param collection List of new Media to be displayed
*/
public void handleSuccess(List<Media> collection) {
if (collection == null || collection.isEmpty()) {
if (queryList.isEmpty()) {
view.initErrorView();
} else {
view.setIsLastPage(true);
}
} else {
this.queryList.addAll(collection);
view.handleSuccess(collection);
}
}
/**
* fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available)
* else show filename
*/
@Override
public void replaceTitlesWithCaptions(String wikibaseIdentifier, int position) {
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(subscriber -> {
view.handleLabelforImage(subscriber, position);
}));
}
/**
* add items to query list
*/
@Override
public void addItemsToQueryList(List<Media> collection) {
queryList.addAll(collection);
}
}

View file

@ -0,0 +1,39 @@
package fr.free.nrw.commons.depictions.SubClass;
import java.io.IOException;
import java.util.List;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
/**
* The contract with which SubDepictionListFragment and its presenter would talk to each other
*/
public interface SubDepictionListContract {
interface View {
void onImageUrlFetched(String response, int position);
void onSuccess(List<DepictedItem> mediaList);
void initErrorView();
void showSnackbar();
void setIsLastPage(boolean b);
boolean isParentClass();
}
interface UserActionListener extends BasePresenter<View> {
void saveQuery();
void fetchThumbnailForEntityId(String entityId, int position);
void initSubDepictionList(String qid, Boolean isParentClass) throws IOException;
String getQuery();
}
}

View file

@ -0,0 +1,191 @@
package fr.free.nrw.commons.depictions.SubClass;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.explore.depictions.SearchDepictionsAdapterFactory;
import fr.free.nrw.commons.explore.depictions.SearchDepictionsRenderer;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
/**
* Fragment for parent classes and child classes of Depicted items in Explore
*/
public class SubDepictionListFragment extends DaggerFragment implements SubDepictionListContract.View {
@BindView(R.id.imagesListBox)
RecyclerView depictionsRecyclerView;
@BindView(R.id.imageSearchInProgress)
ProgressBar progressBar;
@BindView(R.id.imagesNotFound)
TextView depictionNotFound;
@BindView(R.id.bottomProgressBar)
ProgressBar bottomProgressBar;
/**
* Keeps a record of whether current instance of the fragment if of SubClass or ParentClass
*/
private boolean isParentClass = false;
private RVRendererAdapter<DepictedItem> depictionsAdapter;
/**
* Used by scroll state listener, when hasMoreImages is false scrolling does not fetches any more images
*/
private boolean hasMoreImages = true;
RecyclerView.LayoutManager layoutManager;
/**
* Stores entityId for the depiction
*/
private String entityId;
/**
* Stores name of the depiction searched
*/
private String depictsName;
@Inject SubDepictionListPresenter presenter;
private final SearchDepictionsAdapterFactory adapterFactory = new SearchDepictionsAdapterFactory(new SearchDepictionsRenderer.DepictCallback() {
@Override
public void depictsClicked(DepictedItem item) {
// Open SubDepiction Details page
getActivity().finish();
WikidataItemDetailsActivity.startYourself(getContext(), item);
}
@Override
public void fetchThumbnailUrlForEntity(String entityId, int position) {
presenter.fetchThumbnailForEntityId(entityId, position);
}
});
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
private void initViews() {
if (getArguments() != null) {
depictsName = getArguments().getString("wikidataItemName");
entityId = getArguments().getString("entityId");
isParentClass = getArguments().getBoolean("isParentClass");
if (entityId != null) {
initList(entityId, isParentClass);
}
}
}
private void initList(String qid, Boolean isParentClass) {
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
} else {
progressBar.setVisibility(View.VISIBLE);
try {
presenter.initSubDepictionList(qid, isParentClass);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_browse_image, container, false);
ButterKnife.bind(this, v);
presenter.onAttachView(this);
isParentClass = false;
depictionNotFound.setVisibility(GONE);
if (getActivity().getResources().getConfiguration().orientation
== Configuration.ORIENTATION_PORTRAIT) {
layoutManager = new LinearLayoutManager(getContext());
} else {
layoutManager = new GridLayoutManager(getContext(), 2);
}
initViews();
depictionsRecyclerView.setLayoutManager(layoutManager);
depictionsAdapter = adapterFactory.create();
depictionsRecyclerView.setAdapter(depictionsAdapter);
return v;
}
private void handleNoInternet() {
progressBar.setVisibility(GONE);
ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.no_internet);
}
@Override
public void onImageUrlFetched(String response, int position) {
depictionsAdapter.getItem(position).setImageUrl(response);
depictionsAdapter.notifyItemChanged(position);
}
@Override
public void onSuccess(List<DepictedItem> mediaList) {
hasMoreImages = false;
progressBar.setVisibility(View.GONE);
depictionNotFound.setVisibility(GONE);
bottomProgressBar.setVisibility(GONE);
int itemCount=layoutManager.getItemCount();
depictionsAdapter.addAll(mediaList);
depictionsRecyclerView.getRecycledViewPool().clear();
if(itemCount!=0) {
depictionsAdapter.notifyItemRangeInserted(itemCount, mediaList.size()-1);
}else{
depictionsAdapter.notifyDataSetChanged();
}
}
@Override
public void initErrorView() {
hasMoreImages = false;
progressBar.setVisibility(GONE);
bottomProgressBar.setVisibility(GONE);
depictionNotFound.setVisibility(VISIBLE);
String no_depiction = getString(isParentClass? R.string.no_parent_classes: R.string.no_child_classes);
depictionNotFound.setText(String.format(Locale.getDefault(), no_depiction, depictsName));
}
@Override
public void showSnackbar() {
ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.error_loading_depictions);
}
@Override
public void setIsLastPage(boolean b) {
hasMoreImages = !b;
}
@Override
public boolean isParentClass() {
return isParentClass;
}
}

View file

@ -0,0 +1,167 @@
package fr.free.nrw.commons.depictions.SubClass;
import java.io.IOException;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import io.reactivex.Scheduler;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
/**
* Presenter for parent classes and child classes of Depicted items in Explore
*/
public class SubDepictionListPresenter implements SubDepictionListContract.UserActionListener {
/**
* This creates a dynamic proxy instance of the class,
* proxy is to control access to the target object
* here our target object is the view.
* Thus we when onDettach method of fragment is called we replace the binding of view to our object with the proxy instance
*/
private static final SubDepictionListContract.View DUMMY = (SubDepictionListContract.View) Proxy
.newProxyInstance(
SubDepictionListContract.View.class.getClassLoader(),
new Class[]{SubDepictionListContract.View.class},
(proxy, method, methodArgs) -> null);
private final Scheduler ioScheduler;
private final Scheduler mainThreadScheduler;
private SubDepictionListContract.View view = DUMMY;
RecentSearchesDao recentSearchesDao;
/**
* Value of the search query
*/
public String query;
protected CompositeDisposable compositeDisposable = new CompositeDisposable();
DepictsClient depictsClient;
private static int TIMEOUT_SECONDS = 15;
private List<DepictedItem> queryList = new ArrayList<>();
OkHttpJsonApiClient okHttpJsonApiClient;
/**
* variable used to record the number of API calls already made for fetching Thumbnails
*/
private int size = 0;
@Inject
public SubDepictionListPresenter(RecentSearchesDao recentSearchesDao, DepictsClient depictsClient, OkHttpJsonApiClient okHttpJsonApiClient, @Named(IO_THREAD) Scheduler ioScheduler,
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
this.recentSearchesDao = recentSearchesDao;
this.ioScheduler = ioScheduler;
this.mainThreadScheduler = mainThreadScheduler;
this.depictsClient = depictsClient;
this.okHttpJsonApiClient = okHttpJsonApiClient;
}
@Override
public void onAttachView(SubDepictionListContract.View view) {
this.view = view;
}
@Override
public void onDetachView() {
this.view = DUMMY;
}
/**
* Store the current query in Recent searches
*/
@Override
public void saveQuery() {
RecentSearch recentSearch = recentSearchesDao.find(query);
// Newly searched query...
if (recentSearch == null) {
recentSearch = new RecentSearch(null, query, new Date());
} else {
recentSearch.setLastSearched(new Date());
}
recentSearchesDao.save(recentSearch);
}
/**
* Calls Wikibase APIs to fetch Thumbnail image for a given wikidata item
*/
@Override
public void fetchThumbnailForEntityId(String entityId, int position) {
compositeDisposable.add(depictsClient.getP18ForItem(entityId)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(response -> {
view.onImageUrlFetched(response,position);
}));
}
@Override
public void initSubDepictionList(String qid, Boolean isParentClass) throws IOException {
size = 0;
if (isParentClass) {
compositeDisposable.add(okHttpJsonApiClient.getParentQIDs(qid)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.subscribe(this::handleSuccess, this::handleError));
} else {
compositeDisposable.add(okHttpJsonApiClient.getChildQIDs(qid)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.subscribe(this::handleSuccess, this::handleError));
}
}
@Override
public String getQuery() {
return query;
}
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
*/
public void handleSuccess(List<DepictedItem> mediaList) {
if (mediaList == null || mediaList.isEmpty()) {
if(queryList.isEmpty()){
view.initErrorView();
}else{
view.setIsLastPage(true);
}
} else {
this.queryList.addAll(mediaList);
view.onSuccess(mediaList);
for (DepictedItem m : mediaList) {
fetchThumbnailForEntityId(m.getEntityId(), size++);
}
}
}
/**
* Logs and handles API error scenario
*/
private void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading queried depictions");
try {
view.initErrorView();
view.showSnackbar();
} catch (Exception e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,51 @@
package fr.free.nrw.commons.depictions.SubClass.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Model class for parsing SparqlQueryResponse
*/
public class Binding {
@SerializedName("subclass")
@Expose
private Subclass subclass;
@SerializedName("subclassLabel")
@Expose
private SubclassLabel subclassLabel;
/**
* No args constructor for use in serialization
*
*/
public Binding() {
}
/**
*
* @param subclassLabel
* @param subclass
*/
public Binding(Subclass subclass, SubclassLabel subclassLabel) {
super();
this.subclass = subclass;
this.subclassLabel = subclassLabel;
}
public Subclass getSubclass() {
return subclass;
}
public void setSubclass(Subclass subclass) {
this.subclass = subclass;
}
public SubclassLabel getSubclassLabel() {
return subclassLabel;
}
public void setSubclassLabel(SubclassLabel subclassLabel) {
this.subclassLabel = subclassLabel;
}
}

View file

@ -0,0 +1,39 @@
package fr.free.nrw.commons.depictions.SubClass.models;
import java.util.List;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Model class for parsing SparqlQueryResponse
*/
public class Head {
@SerializedName("vars")
@Expose
private List<String> vars = null;
/**
* No args constructor for use in serialization
*
*/
public Head() {
}
/**
*
* @param vars
*/
public Head(List<String> vars) {
super();
this.vars = vars;
}
public List<String> getVars() {
return vars;
}
public void setVars(List<String> vars) {
this.vars = vars;
}
}

View file

@ -0,0 +1,37 @@
package fr.free.nrw.commons.depictions.SubClass.models;
import java.util.List;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Model class for parsing SparqlQueryResponse
*/
public class Results {
@SerializedName("bindings")
@Expose
private List<Binding> bindings = null;
/**
* No args constructor for use in serialization
*/
public Results() {
}
/**
* @param bindings
*/
public Results(List<Binding> bindings) {
super();
this.bindings = bindings;
}
public List<Binding> getBindings() {
return bindings;
}
public void setBindings(List<Binding> bindings) {
this.bindings = bindings;
}
}

View file

@ -0,0 +1,52 @@
package fr.free.nrw.commons.depictions.SubClass.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* SparqlQueryResponse obtained while fetching parent classes and sub classes for depicted items in explore
*/
public class SparqlQueryResponse {
@SerializedName("head")
@Expose
private Head head;
@SerializedName("results")
@Expose
private Results results;
/**
* No args constructor for use in serialization
*
*/
public SparqlQueryResponse() {
}
/**
*
* @param results
* @param head
*/
public SparqlQueryResponse(Head head, Results results) {
super();
this.head = head;
this.results = results;
}
public Head getHead() {
return head;
}
public void setHead(Head head) {
this.head = head;
}
public Results getResults() {
return results;
}
public void setResults(Results results) {
this.results = results;
}
}

View file

@ -0,0 +1,51 @@
package fr.free.nrw.commons.depictions.SubClass.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Model class for parsing SparqlQueryResponse
*/
public class Subclass {
@SerializedName("type")
@Expose
private String type;
@SerializedName("value")
@Expose
private String value;
/**
* No args constructor for use in serialization
*
*/
public Subclass() {
}
/**
*
* @param value
* @param type
*/
public Subclass(String type, String value) {
super();
this.type = type;
this.value = value;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

View file

@ -0,0 +1,68 @@
package fr.free.nrw.commons.depictions.SubClass.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Model class for parsing SparqlQueryResponse
*/
public class SubclassLabel {
@SerializedName("type")
@Expose
private String type;
@SerializedName("value")
@Expose
private String value;
@SerializedName("xml:lang")
@Expose
private String xmlLang;
/**
* No args constructor for use in serialization
*
*/
public SubclassLabel() {
}
/**
*
* @param value
* @param xmlLang
* @param type
*/
public SubclassLabel(String type, String value, String xmlLang) {
super();
this.type = type;
this.value = value;
this.xmlLang = xmlLang;
}
public String getType() {
return type;
}
/**
* returns type
*/
public void setType(String type) {
this.type = type;
}
/**
* gets value of the depiction
*/
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
/**
* get language in which the depiction was requested
*/
public String getXmlLang() {
return xmlLang;
}
}

View file

@ -0,0 +1,204 @@
package fr.free.nrw.commons.depictions;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.FrameLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment;
import fr.free.nrw.commons.depictions.SubClass.SubDepictionListFragment;
import fr.free.nrw.commons.explore.ViewPagerAdapter;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
/**
* Activity to show depiction media, parent classes and child classes of depicted items in Explore
*/
public class WikidataItemDetailsActivity extends NavigationBaseActivity implements MediaDetailPagerFragment.MediaDetailProvider, AdapterView.OnItemClickListener {
private FragmentManager supportFragmentManager;
private DepictedImagesFragment depictionImagesListFragment;
private MediaDetailPagerFragment mediaDetails;
/**
* Name of the depicted item
* Ex: Rabbit
*/
private String wikidataItemName;
@BindView(R.id.mediaContainer)
FrameLayout mediaContainer;
@BindView(R.id.tab_layout)
TabLayout tabLayout;
@BindView(R.id.viewPager)
ViewPager viewPager;
ViewPagerAdapter viewPagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_wikidata_item_details);
ButterKnife.bind(this);
supportFragmentManager = getSupportFragmentManager();
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(viewPagerAdapter);
viewPager.setOffscreenPageLimit(2);
tabLayout.setupWithViewPager(viewPager);
setTabs();
setPageTitle();
initDrawer();
forceInitBackButton();
}
/**
* Gets the passed wikidataItemName from the intents and displays it as the page title
*/
private void setPageTitle() {
if (getIntent() != null && getIntent().getStringExtra("wikidataItemName") != null) {
setTitle(getIntent().getStringExtra("wikidataItemName"));
}
}
/**
* This method is called on success of API call for featured Images.
* The viewpager will notified that number of items have changed.
*/
public void viewPagerNotifyDataSetChanged() {
if (mediaDetails!=null){
mediaDetails.notifyDataSetChanged();
}
}
/**
* This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab,
* Set the fragments according to the tab selected in the viewPager.
*/
private void setTabs() {
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
depictionImagesListFragment = new DepictedImagesFragment();
SubDepictionListFragment subDepictionListFragment = new SubDepictionListFragment();
SubDepictionListFragment parentDepictionListFragment = new SubDepictionListFragment();
wikidataItemName = getIntent().getStringExtra("wikidataItemName");
String entityId = getIntent().getStringExtra("entityId");
if (getIntent() != null && wikidataItemName != null) {
Bundle arguments = new Bundle();
arguments.putString("wikidataItemName", wikidataItemName);
arguments.putString("entityId", entityId);
arguments.putBoolean("isParentClass", false);
depictionImagesListFragment.setArguments(arguments);
subDepictionListFragment.setArguments(arguments);
Bundle parentClassArguments = new Bundle();
parentClassArguments.putString("wikidataItemName", wikidataItemName);
parentClassArguments.putString("entityId", entityId);
parentClassArguments.putBoolean("isParentClass", true);
parentDepictionListFragment.setArguments(parentClassArguments);
}
fragmentList.add(depictionImagesListFragment);
titleList.add(getResources().getString(R.string.title_for_media));
fragmentList.add(subDepictionListFragment);
titleList.add(getResources().getString(R.string.title_for_child_classes));
fragmentList.add(parentDepictionListFragment);
titleList.add(getResources().getString(R.string.title_for_parent_classes));
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPager.setOffscreenPageLimit(2);
viewPagerAdapter.notifyDataSetChanged();
}
/**
* Shows media detail fragment when user clicks on any image in the list
*/
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
tabLayout.setVisibility(View.GONE);
viewPager.setVisibility(View.GONE);
mediaContainer.setVisibility(View.VISIBLE);
if (mediaDetails == null || !mediaDetails.isVisible()) {
// set isFeaturedImage true for featured images, to include author field on media detail
mediaDetails = new MediaDetailPagerFragment(false, true);
FragmentManager supportFragmentManager = getSupportFragmentManager();
supportFragmentManager
.beginTransaction()
.replace(R.id.mediaContainer, mediaDetails)
.addToBackStack(null)
.commit();
supportFragmentManager.executePendingTransactions();
}
mediaDetails.showImage(position);
forceInitBackButton();
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
* @param i It is the index of which media object is to be returned which is same as
* current index of viewPager.
* @return Media Object
*/
@Override
public Media getMediaAtPosition(int i) {
if (depictionImagesListFragment.getAdapter() == null) {
// not yet ready to return data
return null;
} else {
return (Media) depictionImagesListFragment.getAdapter().getItem(i);
}
}
/**
* This method is called on backPressed of anyFragment in the activity.
* If condition is called when mediaDetailFragment is opened.
*/
@Override
public void onBackPressed() {
if (supportFragmentManager.getBackStackEntryCount() == 1){
// back to search so show search toolbar and hide navigation toolbar
tabLayout.setVisibility(View.VISIBLE);
viewPager.setVisibility(View.VISIBLE);
mediaContainer.setVisibility(View.GONE);
}
super.onBackPressed();
}
/**
* This method is called on from getCount of MediaDetailPagerFragment
* The viewpager will contain same number of media items as that of media elements in adapter.
* @return Total Media count in the adapter
*/
@Override
public int getTotalMediaCount() {
if (depictionImagesListFragment.getAdapter() == null) {
return 0;
}
return depictionImagesListFragment.getAdapter().getCount();
}
/**
* Consumers should be simply using this method to use this activity.
*
* @param context A Context of the application package implementing this class.
* @param depictedItem Name of the depicts for displaying its details
*/
public static void startYourself(Context context, DepictedItem depictedItem) {
Intent intent = new Intent(context, WikidataItemDetailsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra("wikidataItemName", depictedItem.getDepictsLabel());
intent.putExtra("entityId", depictedItem.getEntityId());
context.startActivity(intent);
}
}

View file

@ -0,0 +1,57 @@
package fr.free.nrw.commons.depictions.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Model class for object obtained while parsing depiction response
*/
public class Continue {
@SerializedName("sroffset")
@Expose
private Integer sroffset;
@SerializedName("continue")
@Expose
private String _continue;
/**
* No args constructor for use in serialization
*
*/
public Continue() {
}
/**
*
* @param sroffset
* @param _continue
*/
public Continue(Integer sroffset, String _continue) {
super();
this.sroffset = sroffset;
this._continue = _continue;
}
/**
* gets sroffset from Continue object
*/
public Integer getSroffset() {
return sroffset;
}
public void setSroffset(Integer sroffset) {
this.sroffset = sroffset;
}
/**
* gets continue string from Continue object
*/
public String getContinue() {
return _continue;
}
public void setContinue(String _continue) {
this._continue = _continue;
}
}

View file

@ -0,0 +1,73 @@
package fr.free.nrw.commons.depictions.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Model class for list of depicted images obtained by fetching using depiction entity
*/
public class DepictionResponse {
@SerializedName("batchcomplete")
@Expose
private String batchcomplete;
@SerializedName("continue")
@Expose
private Continue _continue;
@SerializedName("query")
@Expose
private Query query;
/**
* No args constructor for use in serialization
*
*/
public DepictionResponse() {
}
/**
*
* @param query
* @param batchcomplete
* @param _continue
*/
public DepictionResponse(String batchcomplete, Continue _continue, Query query) {
super();
this.batchcomplete = batchcomplete;
this._continue = _continue;
this.query = query;
}
/**
* returns batchcomplete string from DepictionResponse object
*/
public String getBatchcomplete() {
return batchcomplete;
}
public void setBatchcomplete(String batchcomplete) {
this.batchcomplete = batchcomplete;
}
/**
* returns continue object from DepictionResponse object
*/
public Continue getContinue() {
return _continue;
}
public void setContinue(Continue _continue) {
this._continue = _continue;
}
/**
* returns query object from DepictionResponse object
*/
public Query getQuery() {
return query;
}
public void setQuery(Query query) {
this.query = query;
}
}

View file

@ -0,0 +1,60 @@
package fr.free.nrw.commons.depictions.models;
import java.util.List;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Model class for object obtained while parsing depiction response
*
* the getSearch() function is used to parse media
*/
public class Query {
@SerializedName("searchinfo")
@Expose
private Searchinfo searchinfo;
@SerializedName("search")
@Expose
private List<Search> search = null;
/**
* No args constructor for use in serialization
*
*/
public Query() {
}
/**
*
* @param search
* @param searchinfo
*/
public Query(Searchinfo searchinfo, List<Search> search) {
super();
this.searchinfo = searchinfo;
this.search = search;
}
/**
* return searchInfo
*/
public Searchinfo getSearchinfo() {
return searchinfo;
}
public void setSearchinfo(Searchinfo searchinfo) {
this.searchinfo = searchinfo;
}
/**
* the getSearch() function is used to parse media
*/
public List<Search> getSearch() {
return search;
}
public void setSearch(List<Search> search) {
this.search = search;
}
}

View file

@ -0,0 +1,140 @@
package fr.free.nrw.commons.depictions.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Model class for object obtained while parsing depiction response
* this class contains all the details of for the media object
*/
public class Search {
@SerializedName("ns")
@Expose
private Integer ns;
@SerializedName("title")
@Expose
private String title;
@SerializedName("pageid")
@Expose
private Integer pageid;
@SerializedName("size")
@Expose
private Integer size;
@SerializedName("wordcount")
@Expose
private Integer wordcount;
@SerializedName("snippet")
@Expose
private String snippet;
@SerializedName("timestamp")
@Expose
private String timestamp;
/**
* No args constructor for use in serialization
*
*/
public Search() {
}
/**
*
* @param timestamp
* @param title
* @param ns
* @param snippet
* @param wordcount
* @param size
* @param pageid
*/
public Search(Integer ns, String title, Integer pageid, Integer size, Integer wordcount, String snippet, String timestamp) {
super();
this.ns = ns;
this.title = title;
this.pageid = pageid;
this.size = size;
this.wordcount = wordcount;
this.snippet = snippet;
this.timestamp = timestamp;
}
/**
* returns ns int from Search object
*/
public Integer getNs() {
return ns;
}
public void setNs(Integer ns) {
this.ns = ns;
}
/**
* returns title string from Search object
*/
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
/**
* returns pageid int from Search object
*/
public Integer getPageid() {
return pageid;
}
public void setPageid(Integer pageid) {
this.pageid = pageid;
}
/**
* returns size int from Search object
*/
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
/**
* returns wordcount int from Search object
*/
public Integer getWordcount() {
return wordcount;
}
public void setWordcount(Integer wordcount) {
this.wordcount = wordcount;
}
/**
* returns snippet String from Search object
*/
public String getSnippet() {
return snippet;
}
public void setSnippet(String snippet) {
this.snippet = snippet;
}
/**
* returns ns int from Search object
*/
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
}

View file

@ -0,0 +1,42 @@
package fr.free.nrw.commons.depictions.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* Model class for object obtained while parsing query object
*/
public class Searchinfo {
@SerializedName("totalhits")
@Expose
private Integer totalhits;
/**
* No args constructor for use in serialization
*
*/
public Searchinfo() {
}
/**
*
* @param totalhits
*/
public Searchinfo(Integer totalhits) {
super();
this.totalhits = totalhits;
}
/**
* returns "totalhint" integer in SearchInfo object
*/
public Integer getTotalhits() {
return totalhits;
}
public void setTotalhits(Integer totalhits) {
this.totalhits = totalhits;
}
}

View file

@ -11,6 +11,7 @@ import fr.free.nrw.commons.bookmarks.BookmarksActivity;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.explore.categories.ExploreActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
@ -21,7 +22,7 @@ import fr.free.nrw.commons.upload.UploadActivity;
/**
* This Class handles the dependency injection (using dagger)
* so, if a developer needs to add a new activity to the commons app
* then that must be mentioned here to inject the dependencies
* then that must be mentioned here to inject the dependencies
*/
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
@ -60,6 +61,9 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract CategoryDetailsActivity bindCategoryDetailsActivity();
@ContributesAndroidInjector
abstract WikidataItemDetailsActivity bindDepictionDetailsActivity();
@ContributesAndroidInjector
abstract ExploreActivity bindExploreActivity();

View file

@ -12,6 +12,8 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionViewHolder;
import fr.free.nrw.commons.contributions.ContributionsModule;
import fr.free.nrw.commons.depictions.DepictionModule;
import fr.free.nrw.commons.explore.SearchModule;
import fr.free.nrw.commons.nearby.PlaceRenderer;
import fr.free.nrw.commons.review.ReviewController;
import fr.free.nrw.commons.settings.SettingsFragment;
@ -33,7 +35,7 @@ import fr.free.nrw.commons.widget.PicOfDayAppWidget;
ActivityBuilderModule.class,
FragmentBuilderModule.class,
ServiceBuilderModule.class,
ContentProviderBuilderModule.class, UploadModule.class, ContributionsModule.class
ContentProviderBuilderModule.class, UploadModule.class, ContributionsModule.class, SearchModule.class, DepictionModule.class
})
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
void inject(CommonsApplication application);

View file

@ -110,6 +110,17 @@ public class CommonsApplicationModule {
return context.getContentResolver().acquireContentProviderClient(BuildConfig.CATEGORY_AUTHORITY);
}
/**
* This method is used to provide instance of DepictsContentProviderClient
* @param context context
* @return DepictsContentProviderClient*/
@Provides
@Named("depictions")
public ContentProviderClient provideDepictsContentProviderClient(Context context) {
return context.getContentResolver().acquireContentProviderClient(BuildConfig.DEPICTION_AUTHORITY);
}
/**
* This method is used to provide instance of RecentSearchContentProviderClient
* which provides content of Recent Searches from database

View file

@ -6,6 +6,7 @@ import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
import fr.free.nrw.commons.category.CategoryContentProvider;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
import fr.free.nrw.commons.upload.structure.depictions.DepictsContentProvider;
/**
* This Class Represents the Module for dependency injection (using dagger)
@ -19,6 +20,9 @@ public abstract class ContentProviderBuilderModule {
@ContributesAndroidInjector
abstract CategoryContentProvider bindCategoryContentProvider();
@ContributesAndroidInjector
abstract DepictsContentProvider bindDepictsContentProvider();
@ContributesAndroidInjector
abstract RecentSearchesContentProvider bindRecentSearchesContentProvider();

View file

@ -8,7 +8,10 @@ import fr.free.nrw.commons.category.CategoryImagesListFragment;
import fr.free.nrw.commons.category.SubCategoryListFragment;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.contributions.ContributionsListFragment;
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment;
import fr.free.nrw.commons.depictions.SubClass.SubDepictionListFragment;
import fr.free.nrw.commons.explore.categories.SearchCategoryFragment;
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragment;
import fr.free.nrw.commons.explore.images.SearchImageFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailFragment;
@ -17,6 +20,7 @@ import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment;
import fr.free.nrw.commons.review.ReviewImageFragment;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
import fr.free.nrw.commons.upload.depicts.DepictsFragment;
import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
@ -44,6 +48,12 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract CategoryImagesListFragment bindFeaturedImagesListFragment();
@ContributesAndroidInjector
abstract DepictedImagesFragment bindDepictedImagesFragment();
@ContributesAndroidInjector
abstract SubDepictionListFragment bindSubDepictionListFragment();
@ContributesAndroidInjector
abstract SubCategoryListFragment bindSubCategoryListFragment();
@ -53,6 +63,9 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract SearchCategoryFragment bindSearchCategoryListFragment();
@ContributesAndroidInjector
abstract SearchDepictionsFragment bindSearchDepictionListFragment();
@ContributesAndroidInjector
abstract RecentSearchesFragment bindRecentSearchesFragment();
@ -77,6 +90,9 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract UploadCategoriesFragment bindUploadCategoriesFragment();
@ContributesAndroidInjector
abstract DepictsFragment bindDepictsFragment();
@ContributesAndroidInjector
abstract MediaLicenseFragment bindMediaLicenseFragment();
}

View file

@ -28,6 +28,7 @@ import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.actions.PageEditInterface;
import fr.free.nrw.commons.category.CategoryInterface;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaDetailInterface;
import fr.free.nrw.commons.media.MediaInterface;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.mwapi.UserInterface;
@ -35,6 +36,9 @@ import fr.free.nrw.commons.review.ReviewInterface;
import fr.free.nrw.commons.upload.UploadInterface;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.wikidata.WikidataInterface;
import fr.free.nrw.commons.upload.WikiBaseInterface;
import fr.free.nrw.commons.upload.depicts.DepictsInterface;
import fr.free.nrw.commons.upload.mediaDetails.CaptionInterface;
import okhttp3.Cache;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
@ -53,6 +57,7 @@ public class NetworkingModule {
public static final String NAMED_COMMONS_WIKI_SITE = "commons-wikisite";
private static final String NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite";
private static final String NAMED_COMMONS_WIKI = "commonswiki";
public static final String NAMED_COMMONS_CSRF = "commons-csrf";
@ -142,6 +147,13 @@ public class NetworkingModule {
return new WikiSite(BuildConfig.WIKIDATA_URL);
}
@Provides
@Singleton
@Named(NAMED_COMMONS_WIKI)
public WikiSite provideCommonsWiki() {
return new WikiSite(BuildConfig.COMMONS_URL);
}
/**
* Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere.
* @return returns a singleton Gson instance
@ -172,6 +184,24 @@ public class NetworkingModule {
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class);
}
@Provides
@Singleton
public CaptionInterface provideCaptionInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) {
return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, CaptionInterface.class);
}
@Provides
@Singleton
public DepictsInterface provideDepictsInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) {
return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, DepictsInterface.class);
}
@Provides
@Singleton
public WikiBaseInterface provideWikiBaseInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, WikiBaseInterface.class);
}
@Provides
@Singleton
public UploadInterface provideUploadInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
@ -207,6 +237,12 @@ public class NetworkingModule {
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, MediaInterface.class);
}
@Provides
@Singleton
public MediaDetailInterface providesMediaDetailInterface(@Named(NAMED_COMMONS_WIKI) WikiSite commonsWikisite) {
return ServiceFactory.get(commonsWikisite, BuildConfig.COMMONS_URL, MediaDetailInterface.class);
}
@Provides
@Singleton
public CategoryInterface provideCategoryInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {

View file

@ -26,6 +26,7 @@ import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.explore.categories.SearchCategoryFragment;
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragment;
import fr.free.nrw.commons.explore.images.SearchImageFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
@ -50,6 +51,7 @@ public class SearchActivity extends NavigationBaseActivity
private SearchImageFragment searchImageFragment;
private SearchCategoryFragment searchCategoryFragment;
private SearchDepictionsFragment searchDepictionsFragment;
private RecentSearchesFragment recentSearchesFragment;
private FragmentManager supportFragmentManager;
private MediaDetailPagerFragment mediaDetails;
@ -68,6 +70,7 @@ public class SearchActivity extends NavigationBaseActivity
setSearchHistoryFragment();
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(viewPagerAdapter);
viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive
tabLayout.setupWithViewPager(viewPager);
setTabs();
searchView.setQueryHint(getString(R.string.search_commons));
@ -93,11 +96,14 @@ public class SearchActivity extends NavigationBaseActivity
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
searchImageFragment = new SearchImageFragment();
searchDepictionsFragment = new SearchDepictionsFragment();
searchCategoryFragment= new SearchCategoryFragment();
fragmentList.add(searchImageFragment);
titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase());
fragmentList.add(searchCategoryFragment);
titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase());
fragmentList.add(searchDepictionsFragment);
titleList.add(getResources().getString(R.string.search_tab_title_depictions).toUpperCase());
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPagerAdapter.notifyDataSetChanged();
@ -112,6 +118,11 @@ public class SearchActivity extends NavigationBaseActivity
viewPager.setVisibility(View.VISIBLE);
tabLayout.setVisibility(View.VISIBLE);
searchHistoryContainer.setVisibility(View.GONE);
if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) {
searchDepictionsFragment.updateDepictionList(query.toString());
}
if (FragmentUtils.isFragmentUIActive(searchImageFragment)) {
searchImageFragment.updateImageList(query.toString());
}
@ -119,6 +130,7 @@ public class SearchActivity extends NavigationBaseActivity
if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) {
searchCategoryFragment.updateCategoryList(query.toString());
}
}else {
//Open RecentSearchesFragment
recentSearchesFragment.updateRecentSearches();

View file

@ -0,0 +1,19 @@
package fr.free.nrw.commons.explore;
import dagger.Binds;
import dagger.Module;
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentContract;
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentPresenter;
/**
* The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
*/
@Module
public abstract class SearchModule {
@Binds
public abstract SearchDepictionsFragmentContract.UserActionListener bindsSearchDepictionsFragmentPresenter(
SearchDepictionsFragmentPresenter
presenter
);
}

View file

@ -217,7 +217,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
private void initErrorView() {
progressBar.setVisibility(GONE);
categoriesNotFoundView.setVisibility(VISIBLE);
categoriesNotFoundView.setText(getString(R.string.categories_not_found));
categoriesNotFoundView.setText(getString(R.string.categories_not_found,query));
}
/**

View file

@ -0,0 +1,186 @@
package fr.free.nrw.commons.explore.depictions;
import androidx.annotation.Nullable;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.depictions.models.Search;
import fr.free.nrw.commons.media.MediaInterface;
import fr.free.nrw.commons.upload.depicts.DepictsInterface;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import io.reactivex.Observable;
import io.reactivex.Single;
/**
* Depicts Client to handle custom calls to Commons Wikibase APIs
*/
@Singleton
public class DepictsClient {
private final DepictsInterface depictsInterface;
private final MediaInterface mediaInterface;
private static final String NO_DEPICTED_IMAGE = "No Image for Depiction";
@Inject
public DepictsClient(DepictsInterface depictsInterface, MediaInterface mediaInterface) {
this.depictsInterface = depictsInterface;
this.mediaInterface = mediaInterface;
}
/**
* Search for depictions using the search item
* @return list of depicted items
*/
public Observable<DepictedItem> searchForDepictions(String query, int limit, int offset) {
return depictsInterface.searchForDepicts(
query,
String.valueOf(limit),
Locale.getDefault().getLanguage(),
Locale.getDefault().getLanguage(),
String.valueOf(offset)
)
.flatMap(depictSearchResponse ->Observable.fromIterable(depictSearchResponse.getSearch()))
.map(depictSearchItem -> new DepictedItem(depictSearchItem.getLabel(), depictSearchItem.getDescription(), "", false, depictSearchItem.getId()));
}
/**
* Get URL for image using image name
* Ex: title = Guion Bluford
* Url = https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/Guion_Bluford.jpg/70px-Guion_Bluford.jpg
*/
private String getThumbnailUrl(String title) {
String baseUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/";
title = title.replace(" ", "_");
String MD5Hash = getMd5(title);
/**
* We use 70 pixels as the size of our Thumbnail (as it is the perfect fits our UI)
*/
return baseUrl + MD5Hash.charAt(0) + '/' + MD5Hash.charAt(0) + MD5Hash.charAt(1) + '/' + title + "/70px-" + title;
}
/**
* Ex: entityId = Q357458
* value returned = Elgin Baylor Night program.jpeg
*/
public Single<String> getP18ForItem(String entityId) {
return depictsInterface.getImageForEntity(entityId)
.map(commonsFilename -> {
String name;
try {
JsonObject claims = commonsFilename.getAsJsonObject("claims").getAsJsonObject();
JsonObject p18 = claims.get("P18").getAsJsonArray().get(0).getAsJsonObject();
JsonObject mainsnak = p18.get("mainsnak").getAsJsonObject();
JsonObject datavalue = mainsnak.get("datavalue").getAsJsonObject();
JsonPrimitive value = datavalue.get("value").getAsJsonPrimitive();
name = value.toString();
name = name.substring(1, name.length() - 1);
} catch (Exception e) {
name="";
}
if (!name.isEmpty()){
return getThumbnailUrl(name);
} else return NO_DEPICTED_IMAGE;
})
.singleOrError();
}
/**
* @return list of images for a particular depict entity
*/
public Observable<List<Media>> fetchImagesForDepictedItem(String query, int limit, int sroffset) {
return mediaInterface.fetchImagesForDepictedItem("haswbstatement:" + BuildConfig.DEPICTS_PROPERTY + "=" + query, String.valueOf(sroffset))
.map(mwQueryResponse -> {
List<Media> mediaList = new ArrayList<>();
for (Search s: mwQueryResponse.getQuery().getSearch()) {
Media media = new Media(null,
getUrl(s.getTitle()),
s.getTitle(),
new HashMap<>(),
"",
0,
safeParseDate(s.getTimestamp()),
safeParseDate(s.getTimestamp()),
""
);
mediaList.add(media);
}
return mediaList;
});
}
/**
* Get url for the image from media of depictions
* Ex: Tiger_Woods
* Value: https://upload.wikimedia.org/wikipedia/commons/thumb/6/67/Tiger_Woods.jpg/70px-Tiger_Woods.jpg
*/
private String getUrl(String title) {
String baseUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/";
title = title.substring(title.indexOf(':')+1);
title = title.replace(" ", "_");
String MD5Hash = getMd5(title);
return baseUrl + MD5Hash.charAt(0) + '/' + MD5Hash.charAt(0) + MD5Hash.charAt(1) + '/' + title + "/640px-" + title;
}
/**
* Generates MD5 hash for the filename
*/
public String getMd5(String input)
{
try {
// Static getInstance method is called with hashing MD5
MessageDigest md = MessageDigest.getInstance("MD5");
// digest() method is called to calculate message digest
// of an input digest() return array of byte
byte[] messageDigest = md.digest(input.getBytes());
// Convert byte array into signum representation
BigInteger no = new BigInteger(1, messageDigest);
// Convert message digest into hex value
String hashtext = no.toString(16);
while (hashtext.length() < 32) {
hashtext = "0" + hashtext;
}
return hashtext;
}
// For specifying wrong message digest algorithms
catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* Parse the date string into the required format
* @param dateStr
* @return date in the required format
*/
@Nullable
private static Date safeParseDate(String dateStr) {
try {
return CommonsDateUtil.getIso8601DateFormatShort().parse(dateStr);
} catch (ParseException e) {
return null;
}
}
}

View file

@ -0,0 +1,31 @@
package fr.free.nrw.commons.explore.depictions;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
/**
* Adapter factory for Items in Explore
*/
public class SearchDepictionsAdapterFactory {
private final SearchDepictionsRenderer.DepictCallback listener;
public SearchDepictionsAdapterFactory(SearchDepictionsRenderer.DepictCallback listener) {
this.listener = listener;
}
public RVRendererAdapter<DepictedItem> create() {
List<DepictedItem> searchImageItemList = new ArrayList<>();
RendererBuilder<DepictedItem> builder = new RendererBuilder<DepictedItem>().bind(DepictedItem.class, new SearchDepictionsRenderer(listener));
ListAdapteeCollection<DepictedItem> collection = new ListAdapteeCollection<>(
searchImageItemList != null ? searchImageItemList : Collections.<DepictedItem>emptyList());
return new RVRendererAdapter<>(builder, collection);
}
}

View file

@ -0,0 +1,235 @@
package fr.free.nrw.commons.explore.depictions;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.pedrogomez.renderers.RVRendererAdapter;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
/**
* Display depictions in search fragment
*/
public class SearchDepictionsFragment extends CommonsDaggerSupportFragment implements SearchDepictionsFragmentContract.View {
@BindView(R.id.imagesListBox)
RecyclerView depictionsRecyclerView;
@BindView(R.id.imageSearchInProgress)
ProgressBar progressBar;
@BindView(R.id.imagesNotFound)
TextView depictionNotFound;
@BindView(R.id.bottomProgressBar)
ProgressBar bottomProgressBar;
RecyclerView.LayoutManager layoutManager;
private boolean isLoading = true;
private int PAGE_SIZE = 25;
@Inject
SearchDepictionsFragmentPresenter presenter;
private final SearchDepictionsAdapterFactory adapterFactory = new SearchDepictionsAdapterFactory(new SearchDepictionsRenderer.DepictCallback() {
@Override
public void depictsClicked(DepictedItem item) {
WikidataItemDetailsActivity.startYourself(getContext(), item);
presenter.saveQuery();
}
/**
*fetch thumbnail image for all the depicted items (if available)
*/
@Override
public void fetchThumbnailUrlForEntity(String entityId, int position) {
presenter.fetchThumbnailForEntityId(entityId,position);
}
});
private RVRendererAdapter<DepictedItem> depictionsAdapter;
private boolean isLastPage;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
ButterKnife.bind(this, rootView);
if (getActivity().getResources().getConfiguration().orientation
== Configuration.ORIENTATION_PORTRAIT) {
layoutManager = new LinearLayoutManager(getContext());
} else {
layoutManager = new GridLayoutManager(getContext(), 2);
}
depictionsRecyclerView.setLayoutManager(layoutManager);
depictionsAdapter = adapterFactory.create();
depictionsRecyclerView.setAdapter(depictionsAdapter);
depictionsRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
int firstVisibleItemPosition=0;
if(layoutManager instanceof GridLayoutManager){
firstVisibleItemPosition=((GridLayoutManager) layoutManager).findFirstVisibleItemPosition();
} else {
firstVisibleItemPosition=((LinearLayoutManager)layoutManager).findFirstVisibleItemPosition();
}
/**
* If the user isn't currently loading items and the last page hasnt been reached,
* then it checks against the current position in view to decide whether or not to load more items.
*/
if (!isLoading && !isLastPage) {
if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount
&& firstVisibleItemPosition >= 0
&& totalItemCount >= PAGE_SIZE) {
loadMoreItems(false);
}
}
}
});
return rootView;
}
/**
* Fetch PAGE_SIZE number of items
*/
private void loadMoreItems(boolean reInitialise) {
presenter.updateDepictionList(presenter.getQuery(),PAGE_SIZE, reInitialise);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
presenter.onAttachView(this);
}
/**
* Called when user selects "Items" from Search Activity
* to load the list of depictions from API
*
* @param query string searched in the Explore Activity
*/
public void updateDepictionList(String query) {
presenter.initializeQuery(query);
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
loadMoreItems(true);
}
/**
* Handles the UI updates for a error scenario
*/
@Override
public void initErrorView() {
progressBar.setVisibility(GONE);
bottomProgressBar.setVisibility(GONE);
depictionNotFound.setVisibility(VISIBLE);
String no_depiction = getString(R.string.depictions_not_found);
depictionNotFound.setText(String.format(Locale.getDefault(), no_depiction, presenter.getQuery()));
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public void onResume() {
super.onResume();
depictionsAdapter.clear();
depictionsRecyclerView.cancelPendingInputEvents();
}
/**
* Handles the UI updates for no internet scenario
*/
@Override
public void handleNoInternet() {
progressBar.setVisibility(GONE);
ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.no_internet);
}
/**
* If a non empty list is successfully returned from the api then modify the view
* like hiding empty labels, hiding progressbar and notifying the apdapter that list of items has been fetched from the API
*/
@Override
public void onSuccess(List<DepictedItem> mediaList) {
isLoading = false;
progressBar.setVisibility(View.GONE);
depictionNotFound.setVisibility(GONE);
bottomProgressBar.setVisibility(GONE);
int itemCount = layoutManager.getItemCount();
depictionsAdapter.addAll(mediaList);
if(itemCount!=0) {
depictionsAdapter.notifyItemRangeInserted(itemCount, mediaList.size()-1);
}else{
depictionsAdapter.notifyDataSetChanged();
}
}
@Override
public void loadingDepictions() {
depictionNotFound.setVisibility(GONE);
bottomProgressBar.setVisibility(View.VISIBLE);
progressBar.setVisibility(GONE);
}
@Override
public void clearAdapter() {
depictionsAdapter.clear();
}
@Override
public void showSnackbar() {
ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.error_loading_depictions);
}
@Override
public RVRendererAdapter<DepictedItem> getAdapter() {
return depictionsAdapter;
}
@Override
public void onImageUrlFetched(String response, int position) {
depictionsAdapter.getItem(position).setImageUrl(response);
depictionsAdapter.notifyItemChanged(position);
}
/**
* Inform the view that there are no more items to be loaded for this search query
* or reset the isLastPage for the current query
* @param isLastPage
*/
@Override
public void setIsLastPage(boolean isLastPage) {
this.isLastPage=isLastPage;
progressBar.setVisibility(GONE);
}
}

View file

@ -0,0 +1,94 @@
package fr.free.nrw.commons.explore.depictions;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.List;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
/**
* The contract with with SearchDepictionsFragment and its presenter would talk to each other
*/
public interface SearchDepictionsFragmentContract {
interface View {
/**
* Handles the UI updates for a error scenario
*/
void initErrorView();
/**
* Handles the UI updates for no internet scenario
*/
void handleNoInternet();
/**
* If a non empty list is successfully returned from the api then modify the view
* like hiding empty labels, hiding progressbar and notifying the apdapter that list of items has been fetched from the API
*/
void onSuccess(List<DepictedItem> mediaList);
/**
* load depictions
*/
void loadingDepictions();
/**
* clear adapter
*/
void clearAdapter();
/**
* show snackbar
*/
void showSnackbar();
/**
* @return adapter
*/
RVRendererAdapter<DepictedItem> getAdapter();
void onImageUrlFetched(String response, int position);
/**
* Inform the view that there are no more items to be loaded for this search query
* or reset the isLastPage for the current query
* @param isLastPage
*/
void setIsLastPage(boolean isLastPage);
}
interface UserActionListener extends BasePresenter<View> {
/**
* Called when user selects "Items" from Search Activity
* to load the list of depictions from API
*
* @param query string searched in the Explore Activity
* @param reInitialise
*/
void updateDepictionList(String query, int pageSize, boolean reInitialise);
/**
* This method saves Search Query in the Recent Searches Database.
*/
void saveQuery();
/**
* Whenever a new query is initiated from the search activity clear the previous adapter
* and add new value of the query
*/
void initializeQuery(String query);
/**
* @return query
*/
String getQuery();
/**
* After all the depicted items are loaded fetch thumbnail image for all the depicted items (if available)
*/
void fetchThumbnailForEntityId(String entityId,int position);
}
}

View file

@ -0,0 +1,188 @@
package fr.free.nrw.commons.explore.depictions;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import timber.log.Timber;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
/**
* The presenter class for SearchDepictionsFragment
*/
public class SearchDepictionsFragmentPresenter extends CommonsDaggerSupportFragment implements SearchDepictionsFragmentContract.UserActionListener {
/**
* This creates a dynamic proxy instance of the class,
* proxy is to control access to the target object
* here our target object is the view.
* Thus we when onDettach method of fragment is called we replace the binding of view to our object with the proxy instance
*/
private static final SearchDepictionsFragmentContract.View DUMMY = (SearchDepictionsFragmentContract.View) Proxy
.newProxyInstance(
SearchDepictionsFragmentContract.View.class.getClassLoader(),
new Class[]{SearchDepictionsFragmentContract.View.class},
(proxy, method, methodArgs) -> null);
private static int TIMEOUT_SECONDS = 15;
protected CompositeDisposable compositeDisposable = new CompositeDisposable();
private final Scheduler ioScheduler;
private final Scheduler mainThreadScheduler;
boolean isLoadingDepictions;
String query;
RecentSearchesDao recentSearchesDao;
DepictsClient depictsClient;
JsonKvStore basicKvStore;
private SearchDepictionsFragmentContract.View view = DUMMY;
private List<DepictedItem> queryList = new ArrayList<>();
int offset=0;
int size = 0;
@Inject
public SearchDepictionsFragmentPresenter(@Named("default_preferences") JsonKvStore basicKvStore,
RecentSearchesDao recentSearchesDao,
DepictsClient depictsClient,
@Named(IO_THREAD) Scheduler ioScheduler,
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
this.basicKvStore = basicKvStore;
this.recentSearchesDao = recentSearchesDao;
this.depictsClient = depictsClient;
this.ioScheduler = ioScheduler;
this.mainThreadScheduler = mainThreadScheduler;
}
@Override
public void onAttachView(SearchDepictionsFragmentContract.View view) {
this.view = view;
}
@Override
public void onDetachView() {
this.view = DUMMY;
}
/**
* Called when user selects "Items" from Search Activity
* to load the list of depictions from API
*
* @param query string searched in the Explore Activity
* @param reInitialise
*/
@Override
public void updateDepictionList(String query, int pageSize, boolean reInitialise) {
this.query = query;
view.loadingDepictions();
if (reInitialise) {
size = 0;
}
saveQuery();
compositeDisposable.add(depictsClient.searchForDepictions(query, 25, offset)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.doOnSubscribe(disposable -> saveQuery())
.collect(ArrayList<DepictedItem>::new, ArrayList::add)
.subscribe(this::handleSuccess, this::handleError));
}
/**
* Logs and handles API error scenario
*/
private void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading queried depictions");
try {
view.initErrorView();
view.showSnackbar();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* This method saves Search Query in the Recent Searches Database.
*/
@Override
public void saveQuery() {
RecentSearch recentSearch = recentSearchesDao.find(query);
// Newly searched query...
if (recentSearch == null) {
recentSearch = new RecentSearch(null, query, new Date());
} else {
recentSearch.setLastSearched(new Date());
}
recentSearchesDao.save(recentSearch);
}
/**
* Whenever a new query is initiated from the search activity clear the previous adapter
* and add new value of the query
*/
@Override
public void initializeQuery(String query) {
this.query = query;
this.queryList.clear();
offset = 0;//Reset the offset on query change
compositeDisposable.clear();
view.setIsLastPage(false);
view.clearAdapter();
}
@Override
public String getQuery() {
return query;
}
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
*/
public void handleSuccess(List<DepictedItem> mediaList) {
if (mediaList == null || mediaList.isEmpty()) {
if(queryList.isEmpty()){
view.initErrorView();
}else{
view.setIsLastPage(true);
}
} else {
this.queryList.addAll(mediaList);
view.onSuccess(mediaList);
offset=queryList.size();
for (DepictedItem m : mediaList) {
fetchThumbnailForEntityId(m.getEntityId(), size++);
}
}
}
/**
* After all the depicted items are loaded fetch thumbnail image for all the depicted items (if available)
*/
@Override
public void fetchThumbnailForEntityId(String entityId,int position) {
compositeDisposable.add(depictsClient.getP18ForItem(entityId)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(response -> {
view.onImageUrlFetched(response,position);
}));
}
}

View file

@ -0,0 +1,127 @@
package fr.free.nrw.commons.explore.depictions;
import android.graphics.Bitmap;
import android.net.Uri;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import com.pedrogomez.renderers.Renderer;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import timber.log.Timber;
/**
* Renderer for DepictedItem
*/
public class SearchDepictionsRenderer extends Renderer<DepictedItem> {
@BindView(R.id.depicts_label)
TextView tvDepictionLabel;
@BindView(R.id.description)
TextView tvDepictionDesc;
@BindView(R.id.depicts_image)
ImageView imageView;
private DepictCallback listener;
int size = 0;
private final static String NO_IMAGE_FOR_DEPICTION = "No Image for Depiction";
public SearchDepictionsRenderer(DepictCallback listener) {
this.listener = listener;
}
@Override
protected void setUpView(View rootView) {
ButterKnife.bind(this, rootView);
}
@Override
protected void hookListeners(View rootView) {
rootView.setOnClickListener(v -> {
DepictedItem item = getContent();
if (listener != null) {
listener.depictsClicked(item);
}
});
}
@Override
protected View inflate(LayoutInflater inflater, ViewGroup parent) {
return inflater.inflate(R.layout.item_depictions, parent, false);
}
/**
* Render value to all the items in the search depictions list
*/
@Override
public void render() {
DepictedItem item = getContent();
tvDepictionLabel.setText(item.getDepictsLabel());
tvDepictionDesc.setText(item.getDescription());
imageView.setImageDrawable(getContext().getResources().getDrawable(R.drawable.ic_wikidata_logo_24dp));
Timber.e("line86"+item.getImageUrl());
if (!TextUtils.isEmpty(item.getImageUrl())) {
if (!item.getImageUrl().equals(NO_IMAGE_FOR_DEPICTION) && !item.getImageUrl().equals(""))
{
ImageRequest imageRequest = ImageRequestBuilder
.newBuilderWithSource(Uri.parse(item.getImageUrl()))
.setAutoRotateEnabled(true)
.build();
ImagePipeline imagePipeline = Fresco.getImagePipeline();
final DataSource<CloseableReference<CloseableImage>>
dataSource = imagePipeline.fetchDecodedImage(imageRequest, getContext());
dataSource.subscribe(new BaseBitmapDataSubscriber() {
@Override
public void onNewResultImpl(@Nullable Bitmap bitmap) {
if (dataSource.isFinished() && bitmap != null) {
Timber.d("Bitmap loaded from url %s", item.getImageUrl());
//imageView.setImageBitmap(Bitmap.createBitmap(bitmap));
imageView.post(() -> imageView.setImageBitmap(Bitmap.createBitmap(bitmap)));
dataSource.close();
}
}
@Override
public void onFailureImpl(DataSource dataSource) {
Timber.d("Error getting bitmap from image url %s", item.getImageUrl());
if (dataSource != null) {
dataSource.close();
}
}
}, CallerThreadExecutor.getInstance());
}
}
}
public interface DepictCallback {
void depictsClicked(DepictedItem item);
void fetchThumbnailUrlForEntity(String entityId,int position);
}
}

View file

@ -67,6 +67,11 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
@Named("default_preferences")
JsonKvStore defaultKvStore;
/**
* A variable to store number of list items for whom API has been called to fetch captions
*/
private int mediaSize = 0;
private RVRendererAdapter<Media> imagesAdapter;
private List<Media> queryList = new ArrayList<>();
@ -101,7 +106,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
ButterKnife.bind(this, rootView);
if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
if (getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
imagesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
}
else{
@ -198,10 +203,36 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
progressBar.setVisibility(GONE);
imagesAdapter.addAll(mediaList);
imagesAdapter.notifyDataSetChanged();
((SearchActivity) getContext()).viewPagerNotifyDataSetChanged();
((SearchActivity)getContext()).viewPagerNotifyDataSetChanged();
for (Media m : mediaList) {
replaceTitlesWithCaptions("M"+m.getPageId(), mediaSize++);
}
}
}
/**
* In explore we first show title and simultaneously call the API to retrieve captions
* When captions are retrieved they replace title
*/
public void replaceTitlesWithCaptions(String wikibaseIdentifier, int i) {
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(subscriber -> {
handleLabelforImage(subscriber, i);
}));
}
private void handleLabelforImage(String s, int position) {
if (!s.trim().equals(getString(R.string.detail_caption_empty))) {
imagesAdapter.getItem(position).setThumbnailTitle(s);
imagesAdapter.notifyDataSetChanged();
}
}
/**
* Logs and handles API error scenario
* @param throwable
@ -221,7 +252,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
private void initErrorView() {
progressBar.setVisibility(GONE);
imagesNotFoundView.setVisibility(VISIBLE);
imagesNotFoundView.setText(getString(R.string.images_not_found));
imagesNotFoundView.setText(getString(R.string.images_not_found,query));
}
/**

View file

@ -50,7 +50,7 @@ class SearchImagesRenderer extends Renderer<Media> {
@Override
public void render() {
Media item = getContent();
tvImageName.setText(item.getDisplayTitle());
tvImageName.setText(item.getThumbnailTitle());
browseImage.setImageURI(item.getThumbUrl());
setAuthorView(item, categoryImageAuthor);
}

View file

@ -0,0 +1,43 @@
package fr.free.nrw.commons.media;
import com.google.gson.annotations.SerializedName;
/**
* Model class for parsing Captions when fetching captions using filename in MediaClient
*/
public class Caption {
/**
* users language in which caption is written
*/
@SerializedName("language")
private String language;
@SerializedName("value")
private String value;
/**
* No args constructor for use in serialization
*/
public Caption() {
}
/**
* @param value
* @param language
*/
public Caption(String language, String value) {
super();
this.language = language;
this.value = value;
}
@SerializedName("language")
public String getLanguage() {
return language;
}
@SerializedName("value")
public String getValue() {
return value;
}
}

View file

@ -0,0 +1,76 @@
package fr.free.nrw.commons.media;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
/**
* Represents the Wikibase item associated with a Wikimedia Commons file.
* For instance the Wikibase item M63996 represents the Commons file "Paul Cézanne - The Pigeon Tower at Bellevue - 1936.19 - Cleveland Museum of Art.jpg"
*/
public class CommonsWikibaseItem {
@SerializedName("type")
private String type;
@SerializedName("id")
private String id;
@SerializedName("labels")
private Map<String, Caption> labels;
@SerializedName("statements")
private Object statements = null;
/**
* No args constructor for use in serialization
*/
public CommonsWikibaseItem() {
}
/**
* @param id
* @param statements
* @param labels
* @param type
*/
public CommonsWikibaseItem(String type, String id, Map<String, Caption> labels, Object statements) {
super();
this.type = type;
this.id = id;
this.labels = labels;
this.statements = statements;
}
/**
* Ex: "mediainfo
*/
@SerializedName("type")
public String getType() {
return type;
}
/**
* @return Wikibase Id
*/
@SerializedName("id")
public String getId() {
return id;
}
/**
* @return value of captions
*/
@SerializedName("labels")
public Map<String, Caption> getLabels() {
return labels;
}
/**
* Contains the Depicts item
*/
@SerializedName("statements")
public Object getStatements() {
return statements;
}
}

View file

@ -1,24 +1,37 @@
package fr.free.nrw.commons.media;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.internal.LinkedTreeMap;
import java.util.Date;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
/**
@ -28,13 +41,17 @@ import timber.log.Timber;
public class MediaClient {
private final MediaInterface mediaInterface;
private final MediaDetailInterface mediaDetailInterface;
//OkHttpJsonApiClient used JsonKvStore for this. I don't know why.
private Map<String, Map<String, String>> continuationStore;
private static final String NO_CAPTION = "No caption";
private static final String NO_DEPICTION = "No depiction";
@Inject
public MediaClient(MediaInterface mediaInterface) {
public MediaClient(MediaInterface mediaInterface, MediaDetailInterface mediaDetailInterface) {
this.mediaInterface = mediaInterface;
this.mediaDetailInterface = mediaDetailInterface;
this.continuationStore = new HashMap<>();
}
@ -88,7 +105,7 @@ public class MediaClient {
*/
public Single<List<Media>> getMediaListFromSearch(String keyword) {
return responseToMediaList(
continuationStore.containsKey("search_" + keyword) ?
continuationStore.containsKey("search_" + keyword) && (continuationStore.get("search_" + keyword) != null) ?
mediaInterface.getMediaListFromSearch(keyword, 10, continuationStore.get("search_" + keyword)) : //if true
mediaInterface.getMediaListFromSearch(keyword, 10, Collections.emptyMap()), //if false
"search_" + keyword);
@ -108,7 +125,7 @@ public class MediaClient {
.map(Media::from)
.collect(ArrayList<Media>::new, List::add);
}
/**
* Fetches Media object from the imageInfo API
*
@ -152,6 +169,7 @@ public class MediaClient {
.single(Media.EMPTY);
}
@NonNull
public Single<String> getPageHtml(String title){
return mediaInterface.getPageHtml(title)
@ -160,4 +178,143 @@ public class MediaClient {
.map(MwParseResult::text)
.first("");
}
}
/**
* @return caption for image using wikibaseIdentifier
*/
public Single<String> getCaptionByWikibaseIdentifier(String wikibaseIdentifier) {
return mediaDetailInterface.getCaptionForImage(Locale.getDefault().getLanguage(), wikibaseIdentifier)
.map(mediaDetailResponse -> {
if (mediaDetailResponse != null && mediaDetailResponse.getSuccess() != null && mediaDetailResponse.getSuccess() == 1 && mediaDetailResponse.getEntities() != null) {
Map<String, CommonsWikibaseItem> entities = mediaDetailResponse.getEntities();
try {
Map.Entry<String, CommonsWikibaseItem> entry = entities.entrySet().iterator().next();
CommonsWikibaseItem commonsWikibaseItem = entry.getValue();
Map<String, Caption> labels = commonsWikibaseItem.getLabels();
Map.Entry<String, Caption> captionEntry = labels.entrySet().iterator().next();
Caption caption = captionEntry.getValue();
return caption.getValue();
} catch (Exception e) {
return NO_CAPTION;
}
}
return NO_CAPTION;
})
.singleOrError();
}
/**
* Fetches Structured data from API
*
* @param filename
* @return a map containing caption and depictions (empty string in the map if no caption/depictions)
*/
public Single<JsonObject> getCaptionAndDepictions(String filename) {
return mediaDetailInterface.fetchStructuredDataByFilename(Locale.getDefault().getLanguage(), filename)
.map(mediaDetailResponse -> {
return fetchCaptionandDepictionsFromMediaDetailResponse(mediaDetailResponse);
})
.singleOrError();
}
/**
* Parses the mediaDetailResponse from API to extract captions and depictions
* @param mediaDetailResponse Response obtained from API for Media Details
* @return a map containing caption and depictions (empty string in the map if no caption/depictions)
*/
@SuppressLint("CheckResult")
private JsonObject fetchCaptionandDepictionsFromMediaDetailResponse(MediaDetailResponse mediaDetailResponse) {
JsonObject mediaDetails = new JsonObject();
if (mediaDetailResponse != null && mediaDetailResponse.getSuccess() != null && mediaDetailResponse.getSuccess() == 1 && mediaDetailResponse.getEntities() != null) {
Map<String, CommonsWikibaseItem> entities = mediaDetailResponse.getEntities();
try {
Map.Entry<String, CommonsWikibaseItem> entry = entities.entrySet().iterator().next();
CommonsWikibaseItem commonsWikibaseItem = entry.getValue();
try {
Map<String, Caption> labels = commonsWikibaseItem.getLabels();
Map.Entry<String, Caption> captionEntry = labels.entrySet().iterator().next();
Caption caption = captionEntry.getValue();
JsonElement jsonElement = new JsonPrimitive(caption.getValue());
mediaDetails.add("Caption", jsonElement);
} catch (Exception e) {
JsonElement jsonElement = new JsonPrimitive(NO_CAPTION);
mediaDetails.add("Caption", jsonElement);
}
try {
LinkedTreeMap statements = (LinkedTreeMap) commonsWikibaseItem.getStatements();
ArrayList<LinkedTreeMap> depictsItemList = (ArrayList<LinkedTreeMap>) statements.get(BuildConfig.DEPICTS_PROPERTY);
String depictions = null;
JsonArray jsonArray = new JsonArray();
for (int i = 0; i < depictsItemList.size(); i++) {
LinkedTreeMap depictedItem = depictsItemList.get(i);
LinkedTreeMap mainsnak = (LinkedTreeMap) depictedItem.get("mainsnak");
Map<String, LinkedTreeMap> datavalue = (Map<String, LinkedTreeMap>) mainsnak.get("datavalue");
LinkedTreeMap value = datavalue.get("value");
String id = value.get("id").toString();
JsonObject jsonObject = getLabelForDepiction(id, Locale.getDefault().getLanguage())
.subscribeOn(Schedulers.newThread())
.blockingGet();
jsonArray.add(jsonObject);
}
mediaDetails.add("Depiction", jsonArray);
} catch (Exception e) {
JsonElement jsonElement = new JsonPrimitive(NO_DEPICTION);
mediaDetails.add("Depiction", jsonElement);
}
} catch (Exception e) {
JsonElement jsonElement = new JsonPrimitive(NO_CAPTION);
mediaDetails.add("Caption", jsonElement);
jsonElement = null;
jsonElement = new JsonPrimitive(NO_DEPICTION);
mediaDetails.add("Depiction", jsonElement);
}
} else {
JsonElement jsonElement = new JsonPrimitive(NO_CAPTION);
mediaDetails.add("Caption", jsonElement);
jsonElement = null;
jsonElement = new JsonPrimitive(NO_DEPICTION);
mediaDetails.add("Depiction", jsonElement);
}
return mediaDetails;
}
/**
* Gets labels for Depictions using Entity Id from MediaWikiAPI
*
* @param entityId EntityId (Ex: Q81566) of the depict entity
* @return Json Object having label and Wikidata URL for the Depiction Entity
*/
public Single<JsonObject> getLabelForDepiction(String entityId, String language) {
return mediaDetailInterface.getDepictions(entityId, language)
.map(jsonResponse -> {
try {
if (jsonResponse.get("success").toString().equals("1")) {
JsonObject entities = (JsonObject) jsonResponse.getAsJsonObject().get("entities");
JsonObject responseObject = (JsonObject) entities.getAsJsonObject().get(entityId);
JsonObject labels = responseObject.getAsJsonObject("labels");
JsonObject languageObject = labels.getAsJsonObject(language);
String label = String.valueOf(languageObject.get("value"));
JsonElement labelJson = new JsonPrimitive(label);
JsonElement idJson = new JsonPrimitive(entityId);
JsonObject jsonObject = new JsonObject();
jsonObject.add("label", labelJson);
jsonObject.add("id", idJson);
return jsonObject;
}
} catch (Exception e) {
Timber.e("Label not found");
return new JsonObject();
}return new JsonObject();
})
.singleOrError();
}
}

View file

@ -54,14 +54,18 @@ import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.delete.DeleteHelper;
import fr.free.nrw.commons.delete.ReasonBuilder;
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.ui.widget.CompatTextView;
import fr.free.nrw.commons.ui.widget.HtmlTextView;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.Map;
import timber.log.Timber;
import static android.view.View.GONE;
@ -107,6 +111,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
SimpleDraweeView image;
@BindView(R.id.mediaDetailTitle)
TextView title;
@BindView(R.id.caption_layout)
LinearLayout captionLayout;
@BindView(R.id.depicts_layout)
LinearLayout depictsLayout;
@BindView(R.id.media_detail_caption)
TextView mediaCaption;
@BindView(R.id.mediaDetailDesc)
HtmlTextView desc;
@BindView(R.id.mediaDetailAuthor)
@ -125,6 +135,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
LinearLayout nominatedForDeletion;
@BindView(R.id.mediaDetailCategoryContainer)
LinearLayout categoryContainer;
@BindView(R.id.media_detail_depiction_container)
LinearLayout depictionContainer;
@BindView(R.id.authorLinearLayout)
LinearLayout authorLayout;
@BindView(R.id.nominateDeletion)
@ -133,8 +145,15 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
ScrollView scrollView;
private ArrayList<String> categoryNames;
/**
* Depicts is a feature part of Structured data. Multiple Depictions can be added for an image just like categories.
* However unlike categories depictions is multi-lingual
* Ex: key: en value: monument
*/
private ArrayList<Map<String, String>> depictions;
private boolean categoriesLoaded = false;
private boolean categoriesPresent = false;
private boolean depictionLoaded = false;
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
private ViewTreeObserver.OnScrollChangedListener scrollListener;
@ -187,6 +206,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
categoryNames = new ArrayList<>();
categoryNames.add(getString(R.string.detail_panel_cats_loading));
depictions = new ArrayList<>();
final View view = inflater.inflate(R.layout.fragment_media_detail, container, false);
ButterKnife.bind(this,view);
@ -234,7 +255,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
desc.setHtmlText(media.getDescription());
license.setText(media.getLicense());
Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename())
Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename(), media.getPageId())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::setTextFields);
@ -299,18 +320,33 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
coordinates.setText(prettyCoordinates(media));
uploadedDate.setText(prettyUploadedDate(media));
mediaDiscussion.setText(prettyDiscussion(media));
if (prettyCaption(media).equals(getContext().getString(R.string.detail_caption_empty))) {
captionLayout.setVisibility(GONE);
} else mediaCaption.setText(prettyCaption(media));
categoryNames.clear();
categoryNames.addAll(media.getCategories());
depictions.clear();
depictions.addAll(media.getDepiction());
depictionLoaded = true;
categoriesLoaded = true;
categoriesPresent = (categoryNames.size() > 0);
if (!categoriesPresent) {
// Stick in a filler element.
categoryNames.add(getString(R.string.detail_panel_cats_none));
}
rebuildCatList();
if(depictions != null && depictions.size() != 0) {
rebuildDepictionList();
}
else depictsLayout.setVisibility(GONE);
if (media.getCreator() == null || media.getCreator().equals("")) {
authorLayout.setVisibility(GONE);
} else {
@ -320,6 +356,19 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
checkDeletion(media);
}
/**
* Populates media details fragment with depiction list
*/
private void rebuildDepictionList() {
depictionContainer.removeAllViews();
for (int i = 0; i<depictions.size(); i++) {
String depictionName = depictions.get(i).get("label");
String entityId = depictions.get(i).get("id");
View depictLabel = buildDepictLabel(depictionName, entityId, depictionContainer);
depictionContainer.addView(depictLabel);
}
}
@OnClick(R.id.mediaDetailLicense)
public void onMediaDetailLicenceClicked(){
String url = media.getLicenseUrl();
@ -480,6 +529,26 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
}
/**
* Add view to depictions obtained also tapping on depictions should open the url
*/
private View buildDepictLabel(String depictionName, String entityId, LinearLayout depictionContainer) {
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_depicts_item, depictionContainer, false);
final CompatTextView textView = item.findViewById(R.id.media_detail_depicted_item_text);
textView.setText(depictionName);
if (depictionLoaded) {
item.setOnClickListener(view -> {
DepictedItem depictedItem = new DepictedItem(depictionName, "", "", false, entityId);
Intent intent = new Intent(getContext(), WikidataItemDetailsActivity.class);
intent.putExtra("wikidataItemName", depictedItem.getDepictsLabel());
intent.putExtra("entityId", depictedItem.getEntityId());
getContext().startActivity(intent);
});
}
return item;
}
private View buildCatLabel(final String catName, ViewGroup categoryContainer) {
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false);
final CompatTextView textView = item.findViewById(R.id.mediaDetailCategoryItemText);
@ -509,6 +578,21 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
image.setAlpha(1.0f - scrollPercentage);
}
/**
* Returns captions for media details
*
* @param media object of class media
* @return caption as string
*/
private String prettyCaption(Media media) {
String caption = media.getCaption().trim();
if (caption.equals("")) {
return getString(R.string.detail_caption_empty);
} else {
return caption;
}
}
private String prettyDescription(Media media) {
// @todo use UI language when multilingual descs are available
String desc = media.getDescription(locale.getLanguage()).trim();

View file

@ -0,0 +1,41 @@
package fr.free.nrw.commons.media;
import com.google.gson.JsonObject;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import io.reactivex.Observable;
import retrofit2.http.GET;
import retrofit2.http.Query;
/**
* Interface for interacting with Commons Structured Data related APIs
*/
public interface MediaDetailInterface {
/**
* Fetches caption using file name
*
* @param filename name of the file to be used for fetching captions
* Please note that languages=en does not have an impact on the languages returned. All captions are returned for all languages.
*/
@GET("w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1&sites=commonswiki")
Observable<MediaDetailResponse> fetchStructuredDataByFilename(@Query("languages") String language, @Query("titles") String filename);
/**
* Gets labels for Depictions using Entity Id from MediaWikiAPI
*
* @param entityId EntityId (Ex: Q81566) of the depict entity
* @param language user's locale
*/
@GET("/w/api.php?format=json&action=wbgetentities&props=labels&languagefallback=1")
Observable<JsonObject> getDepictions(@Query("ids") String entityId, @Query("languages") String language);
/**
* Fetches caption using wikibaseIdentifier
*
* @param wikibaseIdentifier pageId for the media
*/
@GET("/w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1&sites=commonswiki")
Observable<MediaDetailResponse> getCaptionForImage(@Query("languages") String language, @Query("ids") String wikibaseIdentifier);
}

View file

@ -14,26 +14,20 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.snackbar.Snackbar;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.google.android.material.snackbar.Snackbar;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.bookmarks.Bookmark;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
@ -41,6 +35,8 @@ import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
@ -198,7 +194,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
*
* @param m Media file to download
*/
private void downloadMedia(Media m) {
public void downloadMedia(Media m) {
String imageUrl = m.getImageUrl(), fileName = m.getFilename();
if (imageUrl == null
@ -329,7 +325,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
Timber.d("Returning as activity is destroyed!");
return;
}
if (i+1 >= adapter.getCount())
if (i+1 >= adapter.getCount() && getContext() instanceof CategoryImagesCallback)
((CategoryImagesCallback) getContext()).requestMoreImages();
getActivity().invalidateOptionsMenu();

View file

@ -0,0 +1,46 @@
package fr.free.nrw.commons.media;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
/**
* Model class for object while fetching structured data
*/
public class MediaDetailResponse {
@SerializedName("entities")
private Map<String, CommonsWikibaseItem> entities;
@SerializedName("success")
private Integer success;
/**
* No args constructor for use in serialization
*/
public MediaDetailResponse() {
}
/**
* @param success
* @param entities
*/
public MediaDetailResponse(Map<String, CommonsWikibaseItem> entities, Integer success) {
super();
this.entities = entities;
this.success = success;
}
public Map<String, CommonsWikibaseItem> getEntities() {
return entities;
}
public Integer getSuccess() {
return success;
}
public void setSuccess(Integer success) {
this.success = success;
}
}

View file

@ -4,6 +4,7 @@ import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import java.util.Map;
import fr.free.nrw.commons.depictions.models.DepictionResponse;
import io.reactivex.Observable;
import retrofit2.http.GET;
import retrofit2.http.Query;
@ -83,4 +84,23 @@ public interface MediaInterface {
@GET("w/api.php?format=json&action=parse&prop=text")
Observable<MwParseResponse> getPageHtml(@Query("page") String title);
/**
* Fetches caption using file name
*
* @param filename name of the file to be used for fetching captions
* */
@GET("w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1")
Observable<MwQueryResponse> fetchCaptionByFilename(@Query("language") String language, @Query("titles") String filename);
/**
* Fetches list of images from a depiction entity
*
* @param query depictionEntityId
* @param sroffset number od depictions already fetched, this is useful in implementing pagination
*/
@GET("w/api.php?action=query&list=search&format=json&srnamespace=6")
Observable<DepictionResponse> fetchImagesForDepictedItem(@Query("srsearch") String query, @Query("sroffset") String sroffset);
}

View file

@ -7,6 +7,8 @@ import androidx.annotation.NonNull;
import com.google.gson.Gson;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
@ -19,11 +21,14 @@ import javax.inject.Singleton;
import fr.free.nrw.commons.achievements.FeaturedImages;
import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import fr.free.nrw.commons.depictions.SubClass.models.Binding;
import fr.free.nrw.commons.depictions.SubClass.models.SparqlQueryResponse;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.model.NearbyResponse;
import fr.free.nrw.commons.nearby.model.NearbyResultItem;
import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.wikidata.model.GetWikidataEditCountResponse;
import io.reactivex.Observable;
@ -208,6 +213,94 @@ public class OkHttpJsonApiClient {
});
}
/**
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item.
* Example: bridge -> suspended bridge, aqueduct, etc
*/
public Observable<ArrayList<DepictedItem>> getChildQIDs(String qid) throws IOException {
String queryString = FileUtils.readFromResource("/queries/subclasses_query.rq");
String query = queryString.
replace("${QID}", qid)
.replace("${LANG}", "\""+Locale.getDefault().getLanguage()+"\"");
Timber.e(query);
HttpUrl.Builder urlBuilder = HttpUrl
.parse(sparqlQueryUrl)
.newBuilder()
.addQueryParameter("query", query)
.addQueryParameter("format", "json");
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return Observable.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
String json = response.body().string();
SparqlQueryResponse example = gson.fromJson(json, SparqlQueryResponse.class);
List<Binding> bindings = example.getResults().getBindings();
ArrayList<DepictedItem> subItems = new ArrayList<>();
for (Binding binding : bindings) {
if (binding.getSubclassLabel().getXmlLang() != null) {
String label = binding.getSubclassLabel().getValue();
String entityId = binding.getSubclass().getValue();
entityId = entityId.substring(entityId.lastIndexOf("/") - 1);
subItems.add(new DepictedItem(label, "", "", false,entityId ));
Timber.e(label);
}
}
return subItems;
}).doOnError(throwable -> {
Timber.e(throwable.toString());
});
}
/**
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item.
* Example: bridge -> suspended bridge, aqueduct, etc
*/
public Observable<ArrayList<DepictedItem>> getParentQIDs(String qid) throws IOException {
String queryString = FileUtils.readFromResource("/queries/parentclasses_query.rq");
String query = queryString.
replace("${QID}", qid)
.replace("${LANG}", "\""+Locale.getDefault().getLanguage()+"\"");
Timber.e(query);
HttpUrl.Builder urlBuilder = HttpUrl
.parse(sparqlQueryUrl)
.newBuilder()
.addQueryParameter("query", query)
.addQueryParameter("format", "json");
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return Observable.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
try {
String json = response.body().string();
JSONObject jsonObject = new JSONObject(json);
ArrayList<DepictedItem> subItems = new ArrayList<>();
JSONObject results = (JSONObject) jsonObject.get("results");
JSONArray bindings = (JSONArray) results.get("bindings");
for (int i = 0; i < bindings.length(); i++) {
Timber.e(bindings.get(i).getClass().toString());
JSONObject object = (JSONObject) bindings.get(i);
JSONObject parentClassLabel = (JSONObject) object.get("parentClassLabel");
if (parentClassLabel.get("value") != null) {
String labelString = parentClassLabel.getString("value");
JSONObject parentClass = (JSONObject) object.get("parentClass");
if (parentClass.get("value") != null) {
String entityId = parentClass.getString("value");
entityId = entityId.substring(entityId.lastIndexOf("/") + 1);
subItems.add(new DepictedItem(labelString, "", "", false, entityId));
}
}
}
return subItems;
} catch (Exception e) {
return new ArrayList<DepictedItem>();
}
}).doOnError(throwable -> {
Timber.e("line578"+throwable.toString());
});
}
public Single<CampaignResponseDTO> getCampaigns() {
return Single.fromCallable(() -> {
Request request = new Request.Builder().url(campaignsUrl)
@ -223,25 +316,4 @@ public class OkHttpJsonApiClient {
return null;
});
}
/**
* Whenever imageInfo is fetched, these common properties can be specified for the API call
* https://www.mediawiki.org/wiki/API:Imageinfo
*
* @param builder
* @return
*/
private HttpUrl.Builder appendMediaProperties(HttpUrl.Builder builder) {
builder.addQueryParameter("prop", "imageinfo")
.addQueryParameter("iiprop", "url|extmetadata")
.addQueryParameter("iiurlwidth", THUMB_SIZE)
.addQueryParameter("iiextmetadatafilter", "DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl");
String language = Locale.getDefault().getLanguage();
if (!StringUtils.isBlank(language)) {
builder.addQueryParameter("iiextmetadatalanguage", language);
}
return builder;
}
}

View file

@ -19,6 +19,9 @@ import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadController;
import fr.free.nrw.commons.upload.UploadModel;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import fr.free.nrw.commons.upload.structure.depictions.DepictModel;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import io.reactivex.Observable;
import io.reactivex.Single;
@ -33,16 +36,17 @@ public class UploadRemoteDataSource {
private UploadModel uploadModel;
private UploadController uploadController;
private CategoriesModel categoriesModel;
private DepictModel depictModel;
private NearbyPlaces nearbyPlaces;
@Inject
public UploadRemoteDataSource(UploadModel uploadModel, UploadController uploadController,
CategoriesModel categoriesModel,
NearbyPlaces nearbyPlaces) {
CategoriesModel categoriesModel, NearbyPlaces nearbyPlaces, DepictModel depictModel) {
this.uploadModel = uploadModel;
this.uploadController = uploadController;
this.categoriesModel = categoriesModel;
this.nearbyPlaces = nearbyPlaces;
this.depictModel = depictModel;
}
/**
@ -203,4 +207,39 @@ public class UploadRemoteDataSource {
return null;
}
}
/**
* handles category selection/unselection
* @param depictedItem
*/
public void onDepictedItemClicked(DepictedItem depictedItem) {
depictModel.onDepictItemClicked(depictedItem);
}
/**
* returns the list of selected depictions
* @return
*/
public List<DepictedItem> getSelectedDepictions() {
return depictModel.getSelectedDepictions();
}
/**
* get all depictions
*/
public Observable<DepictedItem> searchAllEntities(String query, List<String> imageTitleList) {
return depictModel.searchAllEntities(query, imageTitleList);
}
public void setSelectedDepictions(List<String> selectedDepictions) {
uploadModel.setSelectedDepictions(selectedDepictions);
}
public List<String> depictionsEntityIdList() {
return depictModel.depictionsEntityIdList();
}
}

View file

@ -12,6 +12,8 @@ import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import io.reactivex.Observable;
import io.reactivex.Single;
@ -112,6 +114,10 @@ public class UploadRepository {
remoteDataSource.setSelectedCategories(categoryStringList);
}
public void setSelectedDepictions(List<String> selectedDepictions) {
remoteDataSource.setSelectedDepictions(selectedDepictions);
}
/**
* handles the category selection/deselection
*
@ -263,6 +269,36 @@ public class UploadRepository {
localDataSource.setSelectedLicense(licenseName);
}
public void onDepictItemClicked(DepictedItem depictedItem) {
remoteDataSource.onDepictedItemClicked(depictedItem);
}
/**
* Fetches and returns the selected depictions for the current upload
*
* @return
*/
public List<DepictedItem> getSelectedDepictions() {
return remoteDataSource.getSelectedDepictions();
}
/**
* Search all depictions from
*
* @param query
* @param imageTitleList
* @return
*/
public Observable<DepictedItem> searchAllEntities(String query, List<String> imageTitleList) {
return remoteDataSource.searchAllEntities(query, imageTitleList);
}
public List<String> getDepictionsEntityIdList() {
return remoteDataSource.depictionsEntityIdList();
}
/**
* Returns nearest place matching the passed latitude and longitude
* @param decLatitude
@ -272,4 +308,5 @@ public class UploadRepository {
public Place checkNearbyPlaces(double decLatitude, double decLongitude) {
return remoteDataSource.getNearbyPlaces(decLatitude, decLongitude);
}
}

View file

@ -4,6 +4,8 @@ import android.content.Context;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -14,7 +16,7 @@ import fr.free.nrw.commons.utils.ImageUtilsWrapper;
import io.reactivex.Single;
import timber.log.Timber;
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE;
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION;
import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
@ -46,7 +48,7 @@ public class ImageProcessingService {
* - checks duplicate image
* - checks dark image
* - checks geolocation for image
* - check for valid title
* - check for valid caption
*/
Single<Integer> validateImage(UploadModel.UploadItem uploadItem, boolean checkTitle) {
int currentImageQuality = uploadItem.getImageQuality();
@ -95,18 +97,18 @@ public class ImageProcessingService {
/**
* Checks item title
* - empty title
* - existing title
* Checks item caption
* - empty caption
* - existing caption
*
* @param uploadItem
* @return
*/
private Single<Integer> validateItemTitle(UploadModel.UploadItem uploadItem) {
Timber.d("Checking for image title %s", uploadItem.getTitle());
Title title = uploadItem.getTitle();
if (title.isEmpty()) {
return Single.just(EMPTY_TITLE);
Timber.d("Checking for image title %s", uploadItem.getUploadMediaDetails());
List<UploadMediaDetail> captions = uploadItem.getUploadMediaDetails();
if (captions.isEmpty()) {
return Single.just(EMPTY_CAPTION);
}
return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.getFileName())

View file

@ -8,6 +8,7 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -122,9 +123,11 @@ public class SpinnerLanguagesAdapter extends ArrayAdapter {
public class ViewHolder {
@Nullable
@BindView(R.id.tv_language)
TextView tvLanguage;
@Nullable
@BindView(R.id.view)
View view;
@ -136,8 +139,17 @@ public class SpinnerLanguagesAdapter extends ArrayAdapter {
String languageCode = LangCodeUtils.fixLanguageCode(languageCodesList.get(position));
final String languageName = StringUtils.capitalize(languageNamesList.get(position));
if(TextUtils.isEmpty(savedLanguageValue)){
savedLanguageValue = Locale.getDefault().getLanguage();
}
if (!isDropDownView) {
view.setVisibility(View.GONE);
if( !dropDownClicked && savedLanguageValue !=null){
languageCode = LangCodeUtils.fixLanguageCode(savedLanguageValue);
}
if (view != null) {
view.setVisibility(View.GONE);
}
if (languageCode.length() > 2)
tvLanguage.setText(languageCode.substring(0, 2));
else

View file

@ -44,9 +44,11 @@ import fr.free.nrw.commons.mwapi.UserClient;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
import fr.free.nrw.commons.upload.depicts.DepictsFragment;
import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback;
import fr.free.nrw.commons.upload.structure.depictions.DepictModel;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -102,6 +104,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
private UploadImageAdapter uploadImagesAdapter;
private List<Fragment> fragments;
private UploadCategoriesFragment uploadCategoriesFragment;
private DepictsFragment depictsFragment;
private MediaLicenseFragment mediaLicenseFragment;
private ThumbnailsAdapter thumbnailsAdapter;
@ -288,6 +291,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@ -357,12 +361,17 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
}
uploadCategoriesFragment = new UploadCategoriesFragment();
uploadCategoriesFragment.setMediaDetailList(presenter.getImageDetailList());
uploadCategoriesFragment.setCallback(this);
depictsFragment = new DepictsFragment();
depictsFragment.setMediaDetailList(presenter.getImageDetailList());
depictsFragment.setCallback(this);
mediaLicenseFragment = new MediaLicenseFragment();
mediaLicenseFragment.setCallback(this);
fragments.add(depictsFragment);
fragments.add(uploadCategoriesFragment);
fragments.add(mediaLicenseFragment);

View file

@ -35,6 +35,8 @@ public interface UploadContract {
void handleSubmit();
List<UploadMediaDetail> getImageDetailList();
void deletePictureAtIndex(int index);
}
}

View file

@ -131,6 +131,10 @@ public class UploadController {
contribution.setDescription("");
}
if (contribution.getCaption() == null) {
contribution.setCaption("");
}
String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
contribution.setLicense(license);

View file

@ -0,0 +1,31 @@
package fr.free.nrw.commons.upload;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder;
import java.util.Collections;
import java.util.List;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.upload.structure.depictions.UploadDepictsCallback;
/**
* Adapter Factory for DepictsClicked Listener
*/
public class UploadDepictsAdapterFactory {
private final UploadDepictsCallback listener;
public UploadDepictsAdapterFactory(UploadDepictsCallback listener) {
this.listener = listener;
}
public RVRendererAdapter<DepictedItem> create(List<DepictedItem> itemList) {
RendererBuilder<DepictedItem> builder = new RendererBuilder<DepictedItem>()
.bind(DepictedItem.class, new UploadDepictsRenderer(listener));
ListAdapteeCollection<DepictedItem> collection = new ListAdapteeCollection<>(
itemList != null ? itemList : Collections.emptyList());
return new RVRendererAdapter<>(builder, collection);
}
}

View file

@ -0,0 +1,135 @@
package fr.free.nrw.commons.upload;
import android.graphics.Bitmap;
import android.net.Uri;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import com.pedrogomez.renderers.Renderer;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.upload.structure.depictions.UploadDepictsCallback;
import timber.log.Timber;
/**
* Depicts Renderer for setting up inflating layout,
* and setting views for the layout of each depicted Item
*/
public class UploadDepictsRenderer extends Renderer<DepictedItem> {
private final UploadDepictsCallback listener;
@BindView(R.id.depict_checkbox)
CheckBox checkedView;
@BindView(R.id.depicts_label)
TextView depictsLabel;
@BindView(R.id.description) TextView description;
@BindView(R.id.depicted_image)
ImageView imageView;
private final static String NO_IMAGE_FOR_DEPICTION="No Image for Depiction";
public UploadDepictsRenderer(UploadDepictsCallback listener) {
this.listener = listener;
}
@Override
protected void setUpView(View rootView) {
ButterKnife.bind(this, rootView);
}
/**
* Setup OnClicklisteners on the views
*/
@Override
protected void hookListeners(View rootView) {
rootView.setOnClickListener(v -> {
DepictedItem item = getContent();
item.setSelected(!item.isSelected());
checkedView.setChecked(item.isSelected());
if (listener != null) {
listener.depictsClicked(item);
}
});
checkedView.setOnClickListener(v -> {
DepictedItem item = getContent();
item.setSelected(!item.isSelected());
checkedView.setChecked(item.isSelected());
if (listener != null) {
listener.depictsClicked(item);
}
});
}
@Override
protected View inflate(LayoutInflater inflater, ViewGroup parent) {
return inflater.inflate(R.layout.layout_upload_depicts_item, parent, false);
}
/**
* initialise views for every item in the adapter
*/
@Override
public void render() {
DepictedItem item = getContent();
checkedView.setChecked(item.isSelected());
depictsLabel.setText(item.getDepictsLabel());
description.setText(item.getDescription());
if (!TextUtils.isEmpty(item.getImageUrl())) {
if (!item.getImageUrl().equals(NO_IMAGE_FOR_DEPICTION))
setImageView(Uri.parse(item.getImageUrl()), imageView);
}else{
listener.fetchThumbnailUrlForEntity(item.getEntityId(),item.getPosition());
}
}
/**
* Set thumbnail for the depicted item
*/
private void setImageView(Uri imageUrl, ImageView imageView) {
ImageRequest imageRequest = ImageRequestBuilder
.newBuilderWithSource(imageUrl)
.setAutoRotateEnabled(true)
.build();
ImagePipeline imagePipeline = Fresco.getImagePipeline();
final DataSource<CloseableReference<CloseableImage>>
dataSource = imagePipeline.fetchDecodedImage(imageRequest, getContext());
dataSource.subscribe(new BaseBitmapDataSubscriber() {
@Override
public void onNewResultImpl(@Nullable Bitmap bitmap) {
if (dataSource.isFinished() && bitmap != null) {
Timber.d("Bitmap loaded from url %s", imageUrl.toString());
imageView.post(() -> imageView.setImageBitmap(Bitmap.createBitmap(bitmap)));
dataSource.close();
}
}
@Override
public void onFailureImpl(DataSource dataSource) {
Timber.d("Error getting bitmap from image url %s", imageUrl.toString());
if (dataSource != null) {
dataSource.close();
}
}
}, CallerThreadExecutor.getInstance());
}
}

View file

@ -0,0 +1,117 @@
package fr.free.nrw.commons.upload;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import timber.log.Timber;
/**
* Holds a description of an item being uploaded by {@link UploadActivity}
*/
public class UploadMediaDetail {
private String languageCode;
private String descriptionText;
public String captionText;
private int selectedLanguageIndex = -1;
private boolean isManuallyAdded=false;
/**
* Formatting captions to the Wikibase format for sending labels
* @param uploadMediaDetails list of media Details
*/
public static HashMap<String, String> formatCaptions(List<UploadMediaDetail> uploadMediaDetails) {
HashMap<String, String> caption = new HashMap<>();
for (UploadMediaDetail uploadMediaDetail : uploadMediaDetails) {
caption.put(uploadMediaDetail.getLanguageCode(),uploadMediaDetail.getCaptionText());
}
return caption;
}
public String getCaptionText() {
return captionText;
}
public void setCaptionText(String captionText) {
this.captionText = captionText;
}
/**
* @return The language code ie. "en" or "fr"
*/
String getLanguageCode() {
return languageCode;
}
/**
* @param languageCode The language code ie. "en" or "fr"
*/
public void setLanguageCode(String languageCode) {
this.languageCode = languageCode;
}
String getDescriptionText() {
return descriptionText;
}
public void setDescriptionText(String descriptionText) {
this.descriptionText = descriptionText;
}
/**
* @return the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter}
*/
int getSelectedLanguageIndex() {
return selectedLanguageIndex;
}
/**
* @param selectedLanguageIndex the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter}
*/
void setSelectedLanguageIndex(int selectedLanguageIndex) {
this.selectedLanguageIndex = selectedLanguageIndex;
}
/**
* returns if the description was added manually (by the user, or we have added it programaticallly)
* @return
*/
public boolean isManuallyAdded() {
return isManuallyAdded;
}
/**
* sets to true if the description was manually added by the user
* @param manuallyAdded
*/
public void setManuallyAdded(boolean manuallyAdded) {
isManuallyAdded = manuallyAdded;
}
/**
* Formats the list of descriptions into the format Commons requires for uploads.
*
* @param descriptions the list of descriptions, description is ignored if text is null.
* @return a string with the pattern of {{en|1=descriptionText}}
*/
static String formatList(List<UploadMediaDetail> descriptions) {
StringBuilder descListString = new StringBuilder();
for (UploadMediaDetail description : descriptions) {
if (!description.isEmpty()) {
String individualDescription = String.format("{{%s|1=%s}}", description.getLanguageCode(),
description.getDescriptionText());
descListString.append(individualDescription);
}
}
return descListString.toString();
}
public boolean isEmpty() {
return descriptionText == null || descriptionText.isEmpty();
}
}

View file

@ -2,7 +2,9 @@ package fr.free.nrw.commons.upload;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@ -27,16 +29,17 @@ import fr.free.nrw.commons.utils.AbstractTextWatcher;
import fr.free.nrw.commons.utils.BiMap;
import timber.log.Timber;
public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewHolder> {
public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDetailAdapter.ViewHolder> {
private List<Description> descriptions;
private List<UploadMediaDetail> uploadMediaDetails;
private Callback callback;
private EventListener eventListener;
private BiMap<AdapterView, String> selectedLanguages;
private String savedLanguageValue;
public DescriptionsAdapter(String savedLanguageValue) {
descriptions = new ArrayList<>();
public UploadMediaDetailAdapter() {
uploadMediaDetails = new ArrayList<>();
selectedLanguages = new BiMap<>();
this.savedLanguageValue = savedLanguageValue;
}
@ -45,8 +48,12 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
this.callback = callback;
}
public void setItems(List<Description> descriptions) {
this.descriptions = descriptions;
public void setEventListener(EventListener eventListener) {
this.eventListener = eventListener;
}
public void setItems(List<UploadMediaDetail> uploadMediaDetails) {
this.uploadMediaDetails = uploadMediaDetails;
selectedLanguages = new BiMap<>();
notifyDataSetChanged();
}
@ -65,7 +72,7 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
@Override
public int getItemCount() {
return descriptions.size();
return uploadMediaDetails.size();
}
/**
@ -73,13 +80,13 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
*
* @return List of descriptions
*/
public List<Description> getDescriptions() {
return descriptions;
public List<UploadMediaDetail> getUploadMediaDetails() {
return uploadMediaDetails;
}
public void addDescription(Description description) {
this.descriptions.add(description);
notifyItemInserted(descriptions.size());
public void addDescription(UploadMediaDetail uploadMediaDetail) {
this.uploadMediaDetails.add(uploadMediaDetail);
//notifyItemInserted(uploadMediaDetails.size());
}
public class ViewHolder extends RecyclerView.ViewHolder {
@ -91,6 +98,9 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
@BindView(R.id.description_item_edit_text)
AppCompatEditText descItemEditText;
@BindView(R.id.caption_item_edit_text)
AppCompatEditText captionItemEditText;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
@ -98,14 +108,54 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
}
public void init(int position) {
Description description = descriptions.get(position);
Timber.d("Description is " + description);
if (!TextUtils.isEmpty(description.getDescriptionText())) {
descItemEditText.setText(description.getDescriptionText());
UploadMediaDetail uploadMediaDetail = uploadMediaDetails.get(position);
Timber.d("UploadMediaDetail is " + uploadMediaDetail);
if (!TextUtils.isEmpty(uploadMediaDetail.getCaptionText())) {
captionItemEditText.setText(uploadMediaDetail.getCaptionText());
} else {
captionItemEditText.setText("");
}
if (!TextUtils.isEmpty(uploadMediaDetail.getDescriptionText())) {
descItemEditText.setText(uploadMediaDetail.getDescriptionText());
} else {
descItemEditText.setText("");
}
captionItemEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (s.length() != 0) {
eventListener.onEvent(true);
} else eventListener.onEvent(false);
}
});
if (position == 0) {
captionItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(),
null);
captionItemEditText.setOnTouchListener((v, event) -> {
//2 is for drawable right
if (event.getAction() == MotionEvent.ACTION_UP && (event.getRawX() >= (captionItemEditText.getRight() - captionItemEditText.getCompoundDrawables()[2].getBounds().width()))) {
if (getAdapterPosition() == 0) {
callback.showAlert(R.string.media_detail_caption,
R.string.caption_info);
}
return true;
}
return false;
});
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(),
null);
descItemEditText.setOnTouchListener((v, event) -> {
@ -122,18 +172,25 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
});
} else {
captionItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
}
captionItemEditText.addTextChangedListener(new AbstractTextWatcher(
captionText -> uploadMediaDetails.get(position)
.setCaptionText(captionText)));
initLanguageSpinner(position, uploadMediaDetail);
descItemEditText.addTextChangedListener(new AbstractTextWatcher(
descriptionText -> descriptions.get(position).setDescriptionText(descriptionText)));
initLanguageSpinner(position, description);
descriptionText -> uploadMediaDetails.get(position)
.setDescriptionText(descriptionText)));
initLanguageSpinner(position, uploadMediaDetail);
//If the description was manually added by the user, it deserves focus, if not, let the user decide
if (description.isManuallyAdded()) {
descItemEditText.requestFocus();
if (uploadMediaDetail.isManuallyAdded()) {
captionItemEditText.requestFocus();
} else {
descItemEditText.clearFocus();
captionItemEditText.clearFocus();
}
}
@ -142,7 +199,7 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
* @param position
* @param description
*/
private void initLanguageSpinner(int position, Description description) {
private void initLanguageSpinner(int position, UploadMediaDetail description) {
SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(
spinnerDescriptionLanguages.getContext(),
R.layout.row_item_languages_spinner, selectedLanguages,
@ -205,6 +262,10 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
void showAlert(int mediaDetailDescription, int descriptionInfo);
}
public interface EventListener {
void onEvent(Boolean data);
}
/**
* converts dp to pixel
* @param dp

View file

@ -17,6 +17,15 @@ import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager;
@ -57,6 +66,7 @@ public class UploadModel {
private FileProcessor fileProcessor;
private final ImageProcessingService imageProcessingService;
private List<String> selectedCategories;
private ArrayList<String> selectedDepictions;
@Inject
UploadModel(@Named("licenses") List<String> licenses,
@ -86,6 +96,9 @@ public class UploadModel {
if (this.selectedCategories != null) {
this.selectedCategories.clear();
}
if (this.selectedDepictions != null) {
this.selectedDepictions.clear();
}
}
public void setSelectedCategories(List<String> selectedCategories) {
@ -108,7 +121,6 @@ public class UploadModel {
similarImageInterface));
}
/**
* pre process a one item at a time
*/
@ -146,11 +158,15 @@ public class UploadModel {
createdTimestampSource);
if (place != null) {
uploadItem.title.setTitleText(place.name);
if(uploadItem.descriptions.isEmpty()) {
uploadItem.descriptions.add(new Description());
if(uploadItem.uploadMediaDetails.isEmpty()) {
uploadItem.uploadMediaDetails.add(new UploadMediaDetail());
}
uploadItem.descriptions.get(0).setDescriptionText(place.getLongDescription());
uploadItem.descriptions.get(0).setLanguageCode("en");
uploadItem.uploadMediaDetails.get(0).setDescriptionText(place.getLongDescription());
uploadItem.uploadMediaDetails.get(0).setLanguageCode("en");
String languageCode = Locale.getDefault().getLanguage();
uploadItem.uploadMediaDetails.get(0).setDescriptionText(place.getLongDescription());
uploadItem.uploadMediaDetails.get(0).setLanguageCode(languageCode);
uploadItem.uploadMediaDetails.get(0).setCaptionText(place.name);
}
if (!items.contains(uploadItem)) {
items.add(uploadItem);
@ -191,16 +207,19 @@ public class UploadModel {
return Observable.fromIterable(items).map(item ->
{
Contribution contribution = new Contribution(item.mediaUri, null,
item.getFileName(),
Description.formatList(item.descriptions), -1,
item.getFileName(), item.uploadMediaDetails.size()!=0? UploadMediaDetail.formatCaptions(item.uploadMediaDetails):new HashMap<>(),
UploadMediaDetail.formatList(item.uploadMediaDetails), -1,
null, null, sessionManager.getAuthorName(),
CommonsApplication.DEFAULT_EDIT_SUMMARY, item.gpsCoords.getCoords());
CommonsApplication.DEFAULT_EDIT_SUMMARY, selectedDepictions, item.gpsCoords.getCoords());
if (item.place != null) {
contribution.setWikiDataEntityId(item.place.getWikiDataEntityId());
}
if (null == selectedCategories) {//Just a fail safe, this should never be null
selectedCategories = new ArrayList<>();
}
if (selectedDepictions == null) {
selectedDepictions = new ArrayList<>();
}
contribution.setCategories(selectedCategories);
contribution.setTag("mimeType", item.mimeType);
contribution.setSource(item.source);
@ -238,10 +257,17 @@ public class UploadModel {
public void updateUploadItem(int index, UploadItem uploadItem) {
UploadItem uploadItem1 = items.get(index);
uploadItem1.setDescriptions(uploadItem.descriptions);
uploadItem1.setMediaDetails(uploadItem.uploadMediaDetails);
uploadItem1.setTitle(uploadItem.title);
}
public void setSelectedDepictions(List<String> selectedDepictions) {
if (null == selectedDepictions) {
selectedDepictions = new ArrayList<>();
}
this.selectedDepictions = (ArrayList<String>) selectedDepictions;
}
@SuppressWarnings("WeakerAccess")
public static class UploadItem {
@ -254,7 +280,7 @@ public class UploadModel {
private boolean selected = false;
private boolean first = false;
private Title title;
private List<Description> descriptions;
private List<UploadMediaDetail> uploadMediaDetails;
private Place place;
private boolean visited;
private boolean error;
@ -271,7 +297,8 @@ public class UploadModel {
this.originalContentUri = originalContentUri;
this.createdTimestampSource = createdTimestampSource;
title = new Title();
descriptions = new ArrayList<>();
uploadMediaDetails = new ArrayList<>();
uploadMediaDetails.add(new UploadMediaDetail());
this.place = place;
this.mediaUri = mediaUri;
this.mimeType = mimeType;
@ -305,8 +332,8 @@ public class UploadModel {
return first;
}
public List<Description> getDescriptions() {
return descriptions;
public List<UploadMediaDetail> getUploadMediaDetails() {
return uploadMediaDetails;
}
public boolean isVisited() {
@ -341,11 +368,6 @@ public class UploadModel {
return MimeTypeMapWrapper.getExtensionFromMimeType(mimeType);
}
public String getFileName() {
return title
!= null ? Utils.fixExtension(title.toString(), getFileExt()) : null;
}
public Place getPlace() {
return place;
}
@ -354,8 +376,9 @@ public class UploadModel {
this.title = title;
}
public void setDescriptions(List<Description> descriptions) {
this.descriptions = descriptions;
public void setMediaDetails(List<UploadMediaDetail> uploadMediaDetails) {
this.uploadMediaDetails = uploadMediaDetails;
}
public Uri getContentUri() {
@ -376,6 +399,14 @@ public class UploadModel {
public int hashCode() {
return super.hashCode();
}
/**
* Choose a filename for the media.
* Currently, the caption is used as a filename. If several languages have been entered, the first language is used.
*/
public String getFileName() {
return uploadMediaDetails.get(0).getCaptionText();
}
}
}

View file

@ -4,6 +4,8 @@ import dagger.Binds;
import dagger.Module;
import fr.free.nrw.commons.upload.categories.CategoriesContract;
import fr.free.nrw.commons.upload.categories.CategoriesPresenter;
import fr.free.nrw.commons.upload.depicts.DepictsContract;
import fr.free.nrw.commons.upload.depicts.DepictsPresenter;
import fr.free.nrw.commons.upload.license.MediaLicenseContract;
import fr.free.nrw.commons.upload.license.MediaLicensePresenter;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract;
@ -33,4 +35,9 @@ public abstract class UploadModule {
UploadMediaPresenter
presenter);
@Binds
public abstract DepictsContract.UserActionListener bindsDepictsPresenter(
DepictsPresenter
presenter
);
}

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
@ -83,13 +84,29 @@ public class UploadPresenter implements UploadContract.UserActionListener {
}
}
/**
* Returns list of UploadMedia Details
* to be passed on to categories and depicts fragment
*/
public List<UploadMediaDetail> getImageDetailList() {
int titleListCount = 0;
List<UploadMediaDetail> titleList = new ArrayList<>();
for (UploadModel.UploadItem item : repository.getUploads()) {
if (!item.getUploadMediaDetails().isEmpty()) {
titleList.add(item.getUploadMediaDetails().get(titleListCount));
titleListCount++;
}
}
return titleList;
}
@Override
public void deletePictureAtIndex(int index) {
List<UploadableFile> uploadableFiles = view.getUploadableFiles();
if (index == uploadableFiles.size() - 1) {//If the next fragment to be shown is not one of the MediaDetailsFragment, lets hide the top card
view.showHideTopCard(false);
}
//Ask the repository to delete the picture
repository.deletePicture(uploadableFiles.get(index).getFilePath());
if (uploadableFiles.size() == 1) {
view.showMessage(R.string.upload_cancelled);

View file

@ -39,6 +39,14 @@ import io.reactivex.SingleObserver;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import timber.log.Timber;
public class UploadService extends HandlerService<Contribution> {
@ -280,7 +288,15 @@ public class UploadService extends HandlerService<Contribution> {
String canonicalFilename = "File:" + uploadResult.getFilename();
Timber.d("Contribution upload success. Initiating Wikidata edit for entity id %s",
contribution.getWikiDataEntityId());
// to perform upload of depictions we pass on depiction entityId of the selected depictions to the wikidataEditService
if (contribution.getDepictionsEntityIds() != null) {
for (String s : contribution.getDepictionsEntityIds()) {
wikidataEditService.createClaimWithLogging(s, canonicalFilename);
}
}
wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), canonicalFilename);
wikidataEditService.createLabelforWikidataEntity(contribution.getWikiDataEntityId(), canonicalFilename,
(Map) contribution.getCaptions());
contribution.setFilename(canonicalFilename);
contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl());
contribution.setState(Contribution.STATE_COMPLETED);

View file

@ -0,0 +1,34 @@
package fr.free.nrw.commons.upload;
import androidx.annotation.NonNull;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import io.reactivex.Observable;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.Query;
import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
/**
* Retrofit calls for managing responses network calls of entity ids required for uploading depictions
*/
public interface WikiBaseInterface {
@Headers("Cache-Control: no-cache")
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=wbeditentity")
Observable<MwPostResponse> postEditEntity(@NonNull @Field("id") String fileEntityId,
@NonNull @Field("token") String editToken,
@NonNull @Field("data") String data);
@GET(MW_API_PREFIX + "action=query&prop=info")
Observable<MwQueryResponse> getFileEntityId(@Query("titles") String fileName);
}

View file

@ -32,6 +32,7 @@ import fr.free.nrw.commons.category.CategoryClickedListener;
import fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.upload.UploadBaseFragment;
import fr.free.nrw.commons.upload.UploadCategoriesAdapterFactory;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.utils.DialogUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@ -54,7 +55,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
@Inject
CategoriesContract.UserActionListener presenter;
private RVRendererAdapter<CategoryItem> adapter;
private List<String> mediaTitleList=new ArrayList<>();
private List<UploadMediaDetail> mediaTitleList;
private Disposable subscribe;
private List<CategoryItem> categories;
private boolean isVisible;
@ -64,7 +65,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
super.onCreate(savedInstanceState);
}
public void setMediaTitleList(List<String> mediaTitleList) {
public void setMediaDetailList(List<UploadMediaDetail> mediaTitleList) {
this.mediaTitleList = mediaTitleList;
}

View file

@ -0,0 +1,81 @@
package fr.free.nrw.commons.upload.depicts;
import java.util.List;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
/**
* The contract with which DepictsFragment and its presenter would talk to each other
*/
public interface DepictsContract {
interface View {
/**
* Go to category screen
*/
void goToNextScreen();
/**
* Go to media detail screen
*/
void goToPreviousScreen();
/**
* show error in case of no depiction selected
*/
void noDepictionSelected();
/**
* Show progress/Hide progress depending on the boolean value
*/
void showProgress(boolean shouldShow);
/**
* decides whether to show error values or not depending on the boolean value
*/
void showError(Boolean value);
/**
* add depictions to list
*/
void setDepictsList(List<DepictedItem> depictedItemList);
/**
* Set thumbnail image for depicted item
*/
void onImageUrlFetched(String response, int position);
}
interface UserActionListener extends BasePresenter<View> {
/**
* Takes to previous screen
*/
void onPreviousButtonClicked();
/**
* Listener for the depicted items selected from the list
*/
void onDepictItemClicked(DepictedItem depictedItem);
/**
* asks the repository to fetch depictions for the query
* @param query
*/
void searchForDepictions(String query);
/**
* Check if depictions were selected
* from the depiction list
*/
void verifyDepictions();
/**
* Fetch thumbnail for the Wikidata Item
* @param entityId entityId of the item
* @param position position of the item
*/
void fetchThumbnailForEntityId(String entityId, int position);
}
}

View file

@ -0,0 +1,211 @@
package fr.free.nrw.commons.upload.depicts;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxTextView;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.upload.UploadBaseFragment;
import fr.free.nrw.commons.upload.UploadDepictsAdapterFactory;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.upload.structure.depictions.UploadDepictsCallback;
import fr.free.nrw.commons.utils.DialogUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import timber.log.Timber;
/**
* Fragment for showing depicted items list in Upload activity after media details
*/
public class DepictsFragment extends UploadBaseFragment implements DepictsContract.View, UploadDepictsCallback {
@BindView(R.id.depicts_title)
TextView depictsTitle;
@BindView(R.id.depicts_subtitle)
TextView depictsSubtitile;
@BindView(R.id.depicts_search_container)
TextInputLayout depictsSearchContainer;
@BindView(R.id.depicts_search)
TextInputEditText depictsSearch;
@BindView(R.id.depictsSearchInProgress)
ProgressBar depictsSearchInProgress;
@BindView(R.id.depicts_recycler_view)
RecyclerView depictsRecyclerView;
@Inject
DepictsContract.UserActionListener presenter;
private RVRendererAdapter<DepictedItem> adapter;
private List<UploadMediaDetail> mediaTitleList;
private Disposable subscribe;
@Nullable
@Override
public android.view.View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.upload_depicts_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull android.view.View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ButterKnife.bind(this, view);
init();
}
/**
* Initialize presenter and views
*/
private void init() {
depictsTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1,
callback.getTotalNumberOfSteps()));
presenter.onAttachView(this);
initRecyclerView();
addTextChangeListenerToSearchBox();
}
/**
* Initialise recyclerView and set adapter
*/
private void initRecyclerView() {
adapter = new UploadDepictsAdapterFactory(this)
.create(new ArrayList<>());
depictsRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
depictsRecyclerView.setAdapter(adapter);
}
@Override
public void goToNextScreen() {
callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
}
@Override
public void goToPreviousScreen() {
callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
}
@Override
public void noDepictionSelected() {
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.no_depictions_selected),
getString(R.string.no_depictions_selected_warning_desc),
getString(R.string.no_go_back),
getString(R.string.yes_submit),
null,
() -> goToNextScreen());
}
@Override
public void onDestroyView() {
super.onDestroyView();
presenter.onDetachView();
subscribe.dispose();
}
@Override
public void showProgress(boolean shouldShow) {
depictsSearchInProgress.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
}
@Override
public void showError(Boolean value) {
if (value)
depictsSearchContainer.setError(getString(R.string.no_depiction_found));
else depictsSearchContainer.setErrorEnabled(false);
}
@Override
public void setDepictsList(List<DepictedItem> depictedItemList) {
adapter.clear();
if (depictedItemList != null) {
adapter.addAll(depictedItemList);
adapter.notifyDataSetChanged();
}
}
/**
* Set thumbnail image for depicted item
*/
@Override
public void onImageUrlFetched(String response, int position) {
adapter.getItem(position).setImageUrl(response);
adapter.notifyItemChanged(position);
}
@OnClick(R.id.depicts_next)
public void onNextButtonClicked() {
presenter.verifyDepictions();
}
@OnClick(R.id.depicts_previous)
public void onPreviousButtonClicked() {
callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
}
@Override
public void depictsClicked(DepictedItem item) {
presenter.onDepictItemClicked(item);
}
/**
* Fetch thumbnail for the given entityId at the given position
*/
@Override
public void fetchThumbnailUrlForEntity(String entityId, int position) {
presenter.fetchThumbnailForEntityId(entityId,position);
}
/**
* Text change listener for the edit text view of depicts
*/
private void addTextChangeListenerToSearchBox() {
subscribe = RxTextView.textChanges(depictsSearch)
.doOnEach(v -> depictsSearchContainer.setError(null))
.takeUntil(RxView.detaches(depictsSearch))
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(filter -> searchForDepictions(filter.toString()), Timber::e);
}
/**
* Search for depictions for the following query
* @param query query string
*/
private void searchForDepictions(String query) {
if (!TextUtils.isEmpty(query)) {
presenter.searchForDepictions(query);
}
}
/**
* sets mediaList of UploadMediaDetail object
*/
public void setMediaDetailList(List<UploadMediaDetail> imageDetailList) {
this.mediaTitleList = imageDetailList;
}
}

View file

@ -0,0 +1,30 @@
package fr.free.nrw.commons.upload.depicts;
import com.google.gson.JsonObject;
import fr.free.nrw.commons.depictions.models.DepictionResponse;
import fr.free.nrw.commons.wikidata.model.DepictSearchResponse;
import io.reactivex.Observable;
import retrofit2.http.GET;
import retrofit2.http.Query;
/**
* Manges retrofit calls for Searching of depicts from DepictsFragment
*/
public interface DepictsInterface {
/**
* Search for depictions using the wbsearchentities API
* @param query search for depictions based on user query
* @param limit number of depictions to be retrieved
* @param language current locale of the phone
* @param uselang current locale of the phone
* @param offset number of depictions already fetched useful in implementing pagination
*/
@GET("/w/api.php?action=wbsearchentities&format=json&type=item&uselang=en")
Observable<DepictSearchResponse> searchForDepicts(@Query("search") String query, @Query("limit") String limit, @Query("language") String language, @Query("uselang") String uselang, @Query("continue") String offset);
@GET("/w/api.php?action=wbgetclaims&format=json&property=P18")
Observable<JsonObject> getImageForEntity(@Query("entity") String entityId);
}

View file

@ -0,0 +1,173 @@
package fr.free.nrw.commons.upload.depicts;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
import android.util.Log;
import fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.repository.UploadRepository;
import fr.free.nrw.commons.upload.UploadModel;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
/**
* presenter for DepictsFragment
*/
@Singleton
public class DepictsPresenter implements DepictsContract.UserActionListener {
private static final DepictsContract.View DUMMY = (DepictsContract.View) Proxy
.newProxyInstance(
DepictsContract.View.class.getClassLoader(),
new Class[]{DepictsContract.View.class},
(proxy, method, methodArgs) -> null);
private final Scheduler ioScheduler;
private final Scheduler mainThreadScheduler;
private DepictsContract.View view = DUMMY;
private UploadRepository repository;
private DepictsClient depictsClient;
private static int TIMEOUT_SECONDS = 15;
private CompositeDisposable compositeDisposable;
@Inject
public DepictsPresenter(UploadRepository uploadRepository, @Named(IO_THREAD) Scheduler ioScheduler,
@Named(MAIN_THREAD) Scheduler mainThreadScheduler, DepictsClient depictsClient) {
this.repository = uploadRepository;
this.ioScheduler = ioScheduler;
this.mainThreadScheduler = mainThreadScheduler;
this.depictsClient = depictsClient;
compositeDisposable = new CompositeDisposable();
}
@Override
public void onAttachView(DepictsContract.View view) {
this.view = view;
}
@Override
public void onDetachView() {
this.view = DUMMY;
}
@Override
public void onPreviousButtonClicked() {
view.goToPreviousScreen();
}
@Override
public void onDepictItemClicked(DepictedItem depictedItem) {
repository.onDepictItemClicked(depictedItem);
}
/**
* asks the repository to fetch depictions for the query
* @param query
*/
@Override
public void searchForDepictions(String query) {
List<DepictedItem> depictedItemList = new ArrayList<>();
List<String> imageTitleList = getImageTitleList();
Observable<DepictedItem> distinctDepictsObservable = Observable
.fromIterable(repository.getSelectedDepictions())
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.doOnSubscribe(disposable -> {
view.showError(true);
view.showProgress(true);
view.setDepictsList(null);
})
.observeOn(ioScheduler)
.concatWith(
repository.searchAllEntities(query, imageTitleList)
)
.distinct();
Disposable searchDepictsDisposable = distinctDepictsObservable
.observeOn(mainThreadScheduler)
.subscribe(
s -> depictedItemList.add(s),
Timber::e,
() -> {
view.showProgress(false);
if (null == depictedItemList || depictedItemList.isEmpty()) {
view.showError(true);
} else {
view.showError(false);
//Understand this is shitty, but yes, doing it the other way is even worse and adapter positions can not be trusted
for (int position = 0; position < depictedItemList.size();
position++) {
//depictedItemList.get(position).setPosition(position);
}
view.setDepictsList(depictedItemList);
}
}
);
compositeDisposable.add(searchDepictsDisposable);
view.setDepictsList(depictedItemList);
}
/**
* Check if depictions were selected
* from the depiction list
*/
@Override
public void verifyDepictions() {
List<DepictedItem> selectedDepictions = repository.getSelectedDepictions();
if (selectedDepictions != null && !selectedDepictions.isEmpty()) {
repository.setSelectedDepictions(repository.getDepictionsEntityIdList());
view.goToNextScreen();
} else {
view.noDepictionSelected();
}
}
/**
* Fetch thumbnail for the Wikidata Item
* @param entityId entityId of the item
* @param position position of the item
*/
@Override
public void fetchThumbnailForEntityId(String entityId, int position) {
compositeDisposable.add(depictsClient.getP18ForItem(entityId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(response -> {
view.onImageUrlFetched(response,position);
}));
}
/**
* Returns image title list from UploadItem
* @return
*/
private List<String> getImageTitleList() {
List<String> titleList = new ArrayList<>();
for (UploadModel.UploadItem item : repository.getUploads()) {
if (item.getTitle().isSet()) {
titleList.add(item.getTitle().toString());
}
}
return titleList;
}
}

View file

@ -0,0 +1,31 @@
package fr.free.nrw.commons.upload.mediaDetails;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
import java.util.Map;
import io.reactivex.Observable;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.POST;
import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
public interface CaptionInterface {
/**
* Upload Captions for the image when upload is successful
*
* @param FileEntityId enityId for the uploaded file
* @param editToken editToken for the file
* @param captionValue value of the caption to be uploaded
* @param caption additional data associated with caption
*/
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=wbsetlabel&language=en")
Observable<MwPostResponse> addLabelstoWikidata(@Field("id") String FileEntityId,
@Field("token") String editToken,
@Field("value") String captionValue,
@Field("data") Map<String, String> caption);
}

View file

@ -44,7 +44,9 @@ import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.Description;
import fr.free.nrw.commons.upload.DescriptionsAdapter;
//import fr.free.nrw.commons.upload.DescriptionsAdapter;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter;
import fr.free.nrw.commons.upload.SimilarImageDialogFragment;
import fr.free.nrw.commons.upload.Title;
import fr.free.nrw.commons.upload.UploadBaseFragment;
@ -59,7 +61,7 @@ import timber.log.Timber;
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
public class UploadMediaDetailFragment extends UploadBaseFragment implements
UploadMediaDetailsContract.View {
UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener {
@BindView(R.id.tv_title)
TextView tvTitle;
@ -79,12 +81,12 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
AppCompatButton btnNext;
@BindView(R.id.btn_previous)
AppCompatButton btnPrevious;
private DescriptionsAdapter descriptionsAdapter;
private UploadMediaDetailAdapter uploadMediaDetailAdapter;
@BindView(R.id.btn_copy_prev_title_desc)
AppCompatButton btnCopyPreviousTitleDesc;
private UploadModel.UploadItem uploadItem;
private List<Description> descriptions;
private List<UploadMediaDetail> descriptions;
@Inject
UploadMediaDetailsContract.UserActionListener presenter;
@ -222,13 +224,14 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
}
/**
* init the recycler veiw
* init the description recycler veiw and caption recyclerview
*/
private void initRecyclerView() {
descriptionsAdapter = new DescriptionsAdapter(defaultKvStore.getString(Prefs.KEY_LANGUAGE_VALUE, ""));
descriptionsAdapter.setCallback(this::showInfoAlert);
uploadMediaDetailAdapter = new UploadMediaDetailAdapter();
uploadMediaDetailAdapter.setCallback(this::showInfoAlert);
uploadMediaDetailAdapter.setEventListener(this::onEvent);
rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
rvDescriptions.setAdapter(descriptionsAdapter);
rvDescriptions.setAdapter(uploadMediaDetailAdapter);
}
/**
@ -250,7 +253,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
@OnClick(R.id.btn_next)
public void onNextButtonClicked() {
uploadItem.setDescriptions(descriptionsAdapter.getDescriptions());
uploadItem.setMediaDetails(uploadMediaDetailAdapter.getUploadMediaDetails());
presenter.verifyImageQuality(uploadItem, true);
}
@ -261,9 +264,9 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
@OnClick(R.id.btn_add_description)
public void onButtonAddDescriptionClicked() {
Description description = new Description();
description.setManuallyAdded(true);//This was manually added by the user
descriptionsAdapter.addDescription(description);
UploadMediaDetail uploadMediaDetail = new UploadMediaDetail();
uploadMediaDetail.setManuallyAdded(true);//This was manually added by the user
uploadMediaDetailAdapter.addDescription(uploadMediaDetail);
}
@Override
@ -290,11 +293,11 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
@Override
public void onImageProcessed(UploadItem uploadItem, Place place) {
this.uploadItem = uploadItem;
if (uploadItem.getTitle() != null) {
etTitle.setText(uploadItem.getTitle().toString());
if (uploadItem.getFileName() != null) {
setDescriptionsInAdapter(uploadItem.getUploadMediaDetails());
}
descriptions = uploadItem.getDescriptions();
descriptions = uploadItem.getUploadMediaDetails();
photoViewBackgroundImage.setImageURI(uploadItem.getMediaUri());
setDescriptionsInAdapter(descriptions);
}
@ -317,7 +320,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
},
() -> {
etTitle.setText(place.getName());
Description description = new Description();
UploadMediaDetail description = new UploadMediaDetail();
description.setLanguageCode("en");
description.setDescriptionText(place.getLongDescription());
descriptions = Arrays.asList(description);
@ -384,9 +387,9 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
}
@Override
public void setTitleAndDescription(String title, List<Description> descriptions) {
public void setTitleAndDescription(String title, List<UploadMediaDetail> uploadMediaDetails) {
etTitle.setText(title);
setDescriptionsInAdapter(descriptions);
setDescriptionsInAdapter(uploadMediaDetails);
}
private void deleteThisPicture() {
@ -420,6 +423,13 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
uploadItem.getGpsCoords().getDecLongitude(), 0.0f));
}
@Override
public void onEvent(Boolean data) {
btnNext.setEnabled(data);
btnNext.setClickable(data);
btnNext.setAlpha(data ? 1.0f: 0.5f);
}
public interface UploadMediaDetailFragmentCallback extends Callback {
@ -432,15 +442,14 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
presenter.fetchPreviousTitleAndDescription(callback.getIndexInViewFlipper(this));
}
private void setDescriptionsInAdapter(List<Description> descriptions) {
if (descriptions == null) {
descriptions = new ArrayList<>();
private void setDescriptionsInAdapter(List<UploadMediaDetail> uploadMediaDetails){
if(uploadMediaDetails==null){
uploadMediaDetails=new ArrayList<>();
}
if (descriptions.size() == 0) {
descriptionsAdapter.addDescription(new Description());
} else {
descriptionsAdapter.setItems(descriptions);
}
}
if(uploadMediaDetails.size()==0){
uploadMediaDetails.add(new UploadMediaDetail());
}
uploadMediaDetailAdapter.setItems(uploadMediaDetails);
}
}

View file

@ -6,7 +6,7 @@ import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.upload.Description;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
@ -35,7 +35,7 @@ public interface UploadMediaDetailsContract {
void showMapWithImageCoordinates(boolean shouldShow);
void setTitleAndDescription(String title, List<Description> descriptions);
void setTitleAndDescription(String title, List<UploadMediaDetail> uploadMediaDetails);
}
interface UserActionListener extends BasePresenter<View> {

View file

@ -22,7 +22,7 @@ import timber.log.Timber;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE;
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION;
import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
@ -146,7 +146,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
}
/**
* Fetches and sets the title and desctiption of the previous item
* Fetches and sets the caption and desctiption of the previous item
*
* @param indexInViewFlipper
*/
@ -154,7 +154,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
public void fetchPreviousTitleAndDescription(int indexInViewFlipper) {
UploadItem previousUploadItem = repository.getPreviousUploadItem(indexInViewFlipper);
if (null != previousUploadItem) {
view.setTitleAndDescription(previousUploadItem.getTitle().getTitleText(), previousUploadItem.getDescriptions());
view.setTitleAndDescription(previousUploadItem.getTitle().getTitleText(), previousUploadItem.getUploadMediaDetails());
} else {
view.showMessage(R.string.previous_image_title_description_not_found, R.color.color_error);
}
@ -174,7 +174,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
}
/**
* Handle images, say empty title, duplicate file name, bad picture(in all other cases)
* Handle images, say empty caption, duplicate file name, bad picture(in all other cases)
*
* @param errorCode
*/
@ -186,9 +186,9 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
}
switch (errorCode) {
case EMPTY_TITLE:
Timber.d("Title is empty. Showing toast");
view.showMessage(R.string.add_title_toast, R.color.color_error);
case EMPTY_CAPTION:
Timber.d("Captions are empty. Showing toast");
view.showMessage(R.string.add_caption_toast, R.color.color_error);
break;
case FILE_NAME_EXISTS:
Timber.d("Trying to show duplicate picture popup");

View file

@ -0,0 +1,141 @@
package fr.free.nrw.commons.upload.structure.depictions;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.upload.depicts.DepictsInterface;
import fr.free.nrw.commons.utils.StringSortingUtils;
import fr.free.nrw.commons.wikidata.model.DepictSearchItem;
import io.reactivex.Observable;
import io.reactivex.disposables.CompositeDisposable;
import timber.log.Timber;
/**
* The model class for depictions in upload
*/
public class DepictModel {
private static final int SEARCH_DEPICTS_LIMIT = 25;
private final DepictionDao depictDao;
private final DepictsInterface depictsInterface;
private final JsonKvStore directKvStore;
@Inject
DepictsClient depictsClient;
private List<DepictedItem> selectedDepictedItems;
private HashMap<String, ArrayList<String>> depictsCache;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
@Inject
public DepictModel(DepictionDao depictDao, @Named("default_preferences") JsonKvStore directKvStore, DepictsInterface depictsInterface) {
this.depictDao = depictDao;
this.directKvStore = directKvStore;
this.depictsInterface = depictsInterface;
this.depictsCache = new HashMap<>();
this.selectedDepictedItems = new ArrayList<>();
}
public Comparator<DepictedItem> sortBySimilarity(final String filter) {
Comparator<String> stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
return (firstItem, secondItem) -> stringSimilarityComparator
.compare(firstItem.getDepictsLabel(), secondItem.getDescription());
}
public void cacheAll(HashMap<String, ArrayList<String>> depictsCache) {
depictsCache.putAll(depictsCache);
}
public HashMap<String, ArrayList<String>> getDepictsCache() {
return depictsCache;
}
boolean cacheContainsKey(String term) {
return depictsCache.containsKey(term);
}
public void onDepictItemClicked(DepictedItem depictedItem) {
if (depictedItem.isSelected()) {
selectDepictItem(depictedItem);
// updateDepictCount(depictedItem);
} else {
unselectDepiction(depictedItem);
}
}
private void unselectDepiction(DepictedItem depictedItem) {
selectedDepictedItems.remove(depictedItem);
}
private void updateDepictCount(DepictedItem depictedItem) {
Depiction depiction = depictDao.find(depictedItem.getDepictsLabel());
if (depictedItem == null) {
depiction = new Depiction(null, depictedItem.getDepictsLabel(), new Date(), 0);
}
depiction.incTimesUsed();
depictDao.save(depiction);
}
private void selectDepictItem(DepictedItem depictedItem) {
selectedDepictedItems.add(depictedItem);
}
private Observable<DepictedItem> titleDepicts(List<String> titleList) {
return Observable.fromIterable(titleList)
.concatMap(this::getTitleDepicts);
}
private Observable<DepictedItem> getTitleDepicts(String title) {
return depictsInterface.searchForDepicts(title, String.valueOf(SEARCH_DEPICTS_LIMIT), Locale.getDefault().getLanguage(), Locale.getDefault().getLanguage(),"0")
.map(depictSearchResponse -> {
DepictSearchItem depictedItem = depictSearchResponse.getSearch().get(0);
return new DepictedItem(depictedItem.getLabel(), depictedItem.getDescription(), "", false, depictedItem.getId());
});
}
private Observable<DepictedItem> recentDepicts() {
return Observable.fromIterable(depictDao.recentDepicts(SEARCH_DEPICTS_LIMIT))
.map(s -> new DepictedItem(s, "", "", false, ""));
}
/**
* Get selected Depictions
* @return selected depictions
*/
public List<DepictedItem> getSelectedDepictions() {
return selectedDepictedItems;
}
/**
* Search for depictions
* @param query
* @param imageTitleList
* @return
*/
public Observable<DepictedItem> searchAllEntities(String query, List<String> imageTitleList) {
return depictsInterface.searchForDepicts(query, String.valueOf(SEARCH_DEPICTS_LIMIT), Locale.getDefault().getLanguage(), Locale.getDefault().getLanguage(), "0")
.flatMap(depictSearchResponse -> Observable.fromIterable(depictSearchResponse.getSearch()))
.map(depictSearchItem -> new DepictedItem(depictSearchItem.getLabel(), depictSearchItem.getDescription(), "", false, depictSearchItem.getId()));
}
public List<String> depictionsEntityIdList() {
List<String> output = new ArrayList<>();
for (DepictedItem d : selectedDepictedItems) {
output.add(d.getEntityId());
}
return output;
}
}

View file

@ -0,0 +1,81 @@
package fr.free.nrw.commons.upload.structure.depictions;
/**
* Model class for Depicted Item in Upload and Explore
*/
public class DepictedItem {
private final String depictsLabel;
private final String description;
private String imageUrl;
private boolean selected;
private String entityId;
private int position;
public DepictedItem(String depictsLabel, String description, String imageUrl, boolean selected, String entityId) {
this.depictsLabel = depictsLabel;
this.selected = selected;
this.description = description;
this.imageUrl = imageUrl;
this.entityId = entityId;
}
public String getEntityId() {
return entityId;
}
public String getDescription() {
return description;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getDepictsLabel() {
return depictsLabel;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public String toString() {
return super.toString();
}
public void setPosition(int position) {
this.position = position;
}
public int getPosition() {
return position;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DepictedItem that = (DepictedItem) o;
return depictsLabel.equals(that.depictsLabel);
}
}

View file

@ -0,0 +1,106 @@
package fr.free.nrw.commons.upload.structure.depictions;
import android.net.Uri;
import java.util.Date;
/**
* Represents the fact that a given Commons picture depicts a given Wikidata item.
* Example: https://commons.wikimedia.org/wiki/File:Sorting_quicksort_anim.gif depicts https://www.wikidata.org/wiki/Q486598
*/
public class Depiction {
private Uri contentUri;
private String name;
private Date lastUsed;
private int timesUsed;
public Depiction() {
}
public Depiction(Uri contentUri, String name, Date lastUsed, int timesUsed) {
this.contentUri = contentUri;
this.name = name;
this.lastUsed = lastUsed;
this.timesUsed = timesUsed;
}
/**
* Gets the content URI for this category
*
* @return content URI
*/
public Uri getContentUri() {
return contentUri;
}
/**
* Modifies the content URI - marking this depiction as already saved in the database
*
* @param contentUri the content URI
*/
public void setContentUri(Uri contentUri) {
this.contentUri = contentUri;
}
/**
* Gets name
*
* @return name
*/
public String getName() {
return name;
}
/**
* Modifies name
*
* @param name Depicts name
*/
public void setName(String name) {
this.name = name;
}
/**
* Gets last used date
*
* @return Last used date
*/
public Date getLastUsed() {
return lastUsed;
}
/**
* Set last used date
*
* @param lastUsed last used date of depiction
*/
public void setLastUsed(Date lastUsed) {
this.lastUsed = lastUsed;
}
/**
* Gets no. of times the depiction 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();
}
/**
* Generates new last used date
*/
private void touch() {
lastUsed = new Date();
}
}

View file

@ -0,0 +1,176 @@
package fr.free.nrw.commons.upload.structure.depictions;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.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 DepictionDao {
private final Provider<ContentProviderClient> clientProvider;
@Inject
public DepictionDao(@Named("depictions") Provider<ContentProviderClient> clientProvider) {
this.clientProvider = clientProvider;
}
public void save(Depiction depiction) {
ContentProviderClient db = clientProvider.get();
try {
if (depiction.getContentUri() == null) {
depiction.setContentUri(db.insert(DepictsContentProvider.BASE_URI, toContentValues(depiction)));
} else {
db.update(depiction.getContentUri(), toContentValues(depiction), null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Find persisted depicts in database, based on its name.
*
* @param name Depiction name
* @return depiction from database, or null if not found
*/
@Nullable
Depiction find(String name) {
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
DepictsContentProvider.BASE_URI,
Table.ALL_FIELDS,
Table.COLUMN_NAME + "=?",
new String[]{name},
null);
if (cursor != null && cursor.moveToFirst()) {
return fromCursor(cursor);
}
db.release();
} 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 depictions, ordered by descending date.
*
* @return a list containing recent depicts
*/
@NonNull
List<String> recentDepicts(int limit) {
List<String> items = new ArrayList<>();
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
DepictsContentProvider.BASE_URI,
Table.ALL_FIELDS,
null,
new String[]{},
Table.COLUMN_LAST_USED + "DESC");
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;
}
@NonNull
Depiction fromCursor(Cursor cursor) {
// Hardcoding column positions!
return new Depiction(
DepictsContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(DepictionDao.Table.COLUMN_ID))),
cursor.getString(cursor.getColumnIndex(DepictionDao.Table.COLUMN_NAME)),
new Date(cursor.getLong(cursor.getColumnIndex(DepictionDao.Table.COLUMN_LAST_USED))),
cursor.getInt(cursor.getColumnIndex(DepictionDao.Table.COLUMN_TIMES_USED))
);
}
private ContentValues toContentValues(Depiction depiction) {
ContentValues cv = new ContentValues();
cv.put(DepictionDao.Table.COLUMN_NAME, depiction.getName());
cv.put(DepictionDao.Table.COLUMN_LAST_USED, depiction.getLastUsed().getTime());
cv.put(DepictionDao.Table.COLUMN_TIMES_USED, depiction.getTimesUsed());
return cv;
}
/**
* Example Table: TABLE_NAME: depictions
* COLUMN_ID: unique id for the column
* COLUMN_NAME: depiction name
* COLUMN_LAST_USED: Time stamp for the previous usage of the depiction
* COLUMN_TIMES_USED: Number of times the depiction was used previously
*/
public static class Table {
public static final String TABLE_NAME = "depictions";
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;
}
}
}
}

View file

@ -0,0 +1,56 @@
package fr.free.nrw.commons.upload.structure.depictions;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckedTextView;
import android.widget.TextView;
import com.pedrogomez.renderers.Renderer;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
public class DepictionRenderer extends Renderer<DepictedItem> {
@BindView(R.id.depict_checkbox)
CheckedTextView checkedView;
private final UploadDepictsCallback listener;
@BindView(R.id.depicts_label)
TextView depictsLabel;
@BindView(R.id.description) TextView description;
public DepictionRenderer(UploadDepictsCallback listener) {
this.listener = listener;
}
@Override
protected void setUpView(View rootView) {
ButterKnife.bind(this, rootView);
}
@Override
protected void hookListeners(View rootView) {
rootView.setOnClickListener( v -> {
DepictedItem item = getContent();
item.setSelected(true);
checkedView.setChecked(item.isSelected());
if (listener != null) {
listener.depictsClicked(item);
}
});
}
@Override
protected View inflate(LayoutInflater inflater, ViewGroup parent) {
return inflater.inflate(R.layout.layout_upload_depicts_item, parent, false);
}
@Override
public void render() {
DepictedItem item = getContent();
checkedView.setChecked(item.isSelected());
depictsLabel.setText(item.getDepictsLabel());
description.setText(item.getDescription());
}
}

View file

@ -0,0 +1,153 @@
package fr.free.nrw.commons.upload.structure.depictions;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import org.jetbrains.annotations.NotNull;
import javax.inject.Inject;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
import timber.log.Timber;
import static fr.free.nrw.commons.BuildConfig.DEPICTION_AUTHORITY;
import static fr.free.nrw.commons.upload.structure.depictions.DepictionDao.Table.ALL_FIELDS;
import static fr.free.nrw.commons.upload.structure.depictions.DepictionDao.Table.COLUMN_ID;
import static fr.free.nrw.commons.upload.structure.depictions.DepictionDao.Table.TABLE_NAME;
@SuppressLint("Registered")
public class DepictsContentProvider extends CommonsDaggerContentProvider {
private static final int DEPICTS = 1;
private static final int DEPICTS_ID = 2;
private static final String BASE_PATH = "depictions";
public static final Uri BASE_URI = Uri.parse("content://" + DEPICTION_AUTHORITY + "/" + BASE_PATH);
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI(DEPICTION_AUTHORITY, BASE_PATH, DEPICTS);
uriMatcher.addURI(DEPICTION_AUTHORITY, BASE_PATH + "/#", DEPICTS_ID);
}
@Inject
DBOpenHelper dbOpenHelper;
public static Uri uriForId(int id) {
return Uri.parse(BASE_URI.toString() + "/" + id);
}
@Override
public Cursor query(@NotNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(TABLE_NAME);
int uriType = uriMatcher.match(uri);
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor;
switch (uriType) {
case DEPICTS:
cursor = queryBuilder.query(db, projection, selection, selectionArgs,
null, null, sortOrder);
break;
case DEPICTS_ID:
cursor = queryBuilder.query(db,
ALL_FIELDS,
"_id = ?",
new String[]{uri.getLastPathSegment()},
null,
null,
sortOrder
);
break;
default:
throw new IllegalArgumentException("Unknown URI" + uri);
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public String getType(@NotNull Uri uri) {
return null;
}
@Override
public Uri insert(@NotNull Uri uri, ContentValues values) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id;
switch (uriType) {
case DEPICTS:
id = sqlDB.insert(TABLE_NAME, null, values);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(BASE_URI + "/" + id);
}
@Override
public int delete(@NotNull Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int bulkInsert(@NotNull Uri uri, @NotNull ContentValues[] values) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
sqlDB.beginTransaction();
switch (uriType) {
case DEPICTS:
for (ContentValues value : values) {
Timber.d("Inserting! %s", value);
sqlDB.insert(TABLE_NAME, null, value);
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
sqlDB.setTransactionSuccessful();
sqlDB.endTransaction();
getContext().getContentResolver().notifyChange(uri, null);
return values.length;
}
@Override
public int update(@NotNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated;
switch (uriType) {
case DEPICTS:
if (TextUtils.isEmpty(selection)) {
int id = Integer.valueOf(uri.getLastPathSegment());
rowsUpdated = sqlDB.update(TABLE_NAME,
values,
COLUMN_ID + " = ?",
new String[]{String.valueOf(id)});
} else {
throw new IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID");
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType);
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsUpdated;
}
}

View file

@ -0,0 +1,10 @@
package fr.free.nrw.commons.upload.structure.depictions;
/**
* Listener to trigger callback whenever a depicts item is clicked
*/
public interface UploadDepictsCallback {
void depictsClicked(DepictedItem item);
void fetchThumbnailUrlForEntity(String entityId,int position);
}

View file

@ -65,7 +65,7 @@ public class ImageUtils {
public static final int IMAGE_OK = 0;
public static final int IMAGE_KEEP = -1;
public static final int IMAGE_WAIT = -2;
public static final int EMPTY_TITLE = -3;
public static final int EMPTY_CAPTION = -3;
public static final int FILE_NAME_EXISTS = -4;
static final int NO_CATEGORY_SELECTED = -5;
@ -80,7 +80,7 @@ public class ImageUtils {
IMAGE_OK,
IMAGE_KEEP,
IMAGE_WAIT,
EMPTY_TITLE,
EMPTY_CAPTION,
FILE_NAME_EXISTS,
NO_CATEGORY_SELECTED,
IMAGE_GEOLOCATION_DIFFERENT

View file

@ -0,0 +1,44 @@
package fr.free.nrw.commons.wikidata;
import org.wikipedia.csrf.CsrfTokenClient;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.upload.WikiBaseInterface;
import fr.free.nrw.commons.utils.ConfigUtils;
import io.reactivex.Observable;
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
/**
* Wikibase Client for calling WikiBase APIs
*/
@Singleton
public class WikiBaseClient {
private final WikiBaseInterface wikiBaseInterface;
private final CsrfTokenClient csrfTokenClient;
@Inject
public WikiBaseClient(WikiBaseInterface wikiBaseInterface,
@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) {
this.wikiBaseInterface = wikiBaseInterface;
this.csrfTokenClient = csrfTokenClient;
}
public Observable<Boolean> postEditEntity(String fileEntityId, String data) {
try {
return wikiBaseInterface.postEditEntity(fileEntityId, csrfTokenClient.getTokenBlocking(), data)
.map(response -> (response.getSuccessVal() == 1));
} catch (Throwable throwable) {
return Observable.just(false);
}
}
public Observable<Long> getFileEntityId(String fileName) {
return wikiBaseInterface.getFileEntityId(fileName)
.map(response -> (long) (response.query().pages().get(0).pageId()));
}
}

View file

@ -3,15 +3,30 @@ package fr.free.nrw.commons.wikidata;
import android.annotation.SuppressLint;
import android.content.Context;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.wikipedia.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.Service;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.upload.mediaDetails.CaptionInterface;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
@ -30,21 +45,37 @@ public class WikidataEditService {
private final Context context;
private final WikidataEditListener wikidataEditListener;
private final JsonKvStore directKvStore;
private final CaptionInterface captionInterface;
private final WikiBaseClient wikiBaseClient;
private final WikidataClient wikidataClient;
private final MediaClient mediaClient;
private final CsrfTokenClient csrfTokenClient;
private final Service service;
@Inject
WikidataEditService(Context context,
WikidataEditListener wikidataEditListener,
@Named("default_preferences") JsonKvStore directKvStore,
WikidataClient wikidataClient) {
public WikidataEditService(Context context,
WikidataEditListener wikidataEditListener,
MediaClient mediaClient,
@Named("default_preferences") JsonKvStore directKvStore,
WikiBaseClient wikiBaseClient,
CaptionInterface captionInterface,
WikidataClient wikidataClient,
@Named("commons-csrf") CsrfTokenClient csrfTokenClient,
@Named("commons-service") Service service) {
this.context = context;
this.wikidataEditListener = wikidataEditListener;
this.directKvStore = directKvStore;
this.captionInterface = captionInterface;
this.wikiBaseClient = wikiBaseClient;
this.mediaClient = mediaClient;
this.wikidataClient = wikidataClient;
this.csrfTokenClient = csrfTokenClient;
this.service = service;
}
/**
* Create a P18 claim and log the edit with custom tag
*
* @param wikidataEntityId
* @param fileName
*/
@ -65,8 +96,11 @@ public class WikidataEditService {
}
editWikidataProperty(wikidataEntityId, fileName);
//editWikiBaseDepictsProperty(wikidataEntityId, fileName);
}
/**
* Edits the wikidata entity by adding the P18 property to it.
* Adding the P18 edit requires calling the wikidata API to create a claim against the entity
@ -97,6 +131,81 @@ public class WikidataEditService {
});
}
/**
* Edits the wikibase entity by adding DEPICTS property.
* Adding DEPICTS property requires call to the wikibase API to set tag against the entity.
*
* @param wikidataEntityId
* @param fileName
*/
@SuppressLint("CheckResult")
private void editWikiBaseDepictsProperty(String wikidataEntityId, String fileName) {
wikiBaseClient.getFileEntityId(fileName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(fileEntityId -> {
if (fileEntityId != null) {
Timber.d("EntityId for image was received successfully: %s", fileEntityId);
addDepictsProperty(wikidataEntityId, fileEntityId.toString());
} else {
Timber.d("Error acquiring EntityId for image: %s", fileName);
}
}, throwable -> {
Timber.e(throwable, "Error occurred while getting EntityID to set DEPICTS property");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
}
@SuppressLint("CheckResult")
private void addDepictsProperty(String entityId, String fileEntityId) {
if (ConfigUtils.isBetaFlavour()) {
entityId = "Q10"; // Wikipedia:Sandbox (Q10)
}
JsonObject value = new JsonObject();
value.addProperty("entity-type", "item");
value.addProperty("numeric-id", entityId.replace("Q", ""));
value.addProperty("id", entityId);
JsonObject dataValue = new JsonObject();
dataValue.add("value", value);
dataValue.addProperty("type", "wikibase-entityid");
JsonObject mainSnak = new JsonObject();
mainSnak.addProperty("snaktype", "value");
mainSnak.addProperty("property", BuildConfig.DEPICTS_PROPERTY);
mainSnak.add("datavalue", dataValue);
JsonObject claim = new JsonObject();
claim.add("mainsnak", mainSnak);
claim.addProperty("type", "statement");
claim.addProperty("rank", "preferred");
JsonArray claims = new JsonArray();
claims.add(claim);
JsonObject jsonData = new JsonObject();
jsonData.add("claims", claims);
String data = jsonData.toString();
Observable.defer((Callable<ObservableSource<Boolean>>) () ->
wikiBaseClient.postEditEntity("M" + fileEntityId, data))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(success -> {
if (success)
Timber.d("DEPICTS property was set successfully for %s", fileEntityId);
else
Timber.d("Unable to set DEPICTS property for %s", fileEntityId);
},
throwable -> {
Timber.e(throwable, "Error occurred while setting DEPICTS property");
ViewUtil.showLongToast(context, throwable.toString());
});
}
private void handleClaimResult(String wikidataEntityId, String revisionId) {
if (revisionId != null) {
if (wikidataEditListener != null) {
@ -109,13 +218,14 @@ public class WikidataEditService {
}
}
/**
* Show a success toast when the edit is made successfully
*/
private void showSuccessToast() {
String title = directKvStore.getString("Title", "");
String caption = directKvStore.getString("Title", "");
String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
String successMessage = String.format(Locale.getDefault(), successStringTemplate, title);
@SuppressLint({"StringFormatInvalid", "LocalSuppress"}) String successMessage = String.format(Locale.getDefault(), successStringTemplate, caption);
ViewUtil.showLongToast(context, successMessage);
}
@ -131,4 +241,79 @@ public class WikidataEditService {
Timber.d("Wikidata property name is %s", fileName);
return fileName;
}
}
/**
* Adding captions as labels after image is successfully uploaded
*/
@SuppressLint("CheckResult")
public void createLabelforWikidataEntity(String wikiDataEntityId, String fileName, Map<String, String> captions) {
Observable.fromCallable(() -> wikiBaseClient.getFileEntityId(fileName))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(fileEntityId -> {
if (fileEntityId != null) {
for (Map.Entry<String, String> entry : captions.entrySet()) {
Map<String, String> caption = new HashMap<>();
caption.put(entry.getKey(), entry.getValue());
try {
wikidataAddLabels(wikiDataEntityId, fileEntityId.toString(), caption);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
} else {
Timber.d("Error acquiring EntityId for image");
}
}, throwable -> {
Timber.e(throwable, "Error occurred while getting EntityID for the file");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
}
/**
* Adds label to Wikidata using the fileEntityId and the edit token, obtained from csrfTokenClient
*
* @param wikiDataEntityId entityId for the current contribution
* @param fileEntityId
* @param caption
*/
@SuppressLint("CheckResult")
private void wikidataAddLabels(String wikiDataEntityId, String fileEntityId, Map<String, String> caption) throws Throwable {
Observable.fromCallable(() -> {
try {
return csrfTokenClient.getTokenBlocking();
} catch (Throwable throwable) {
throwable.printStackTrace();
return null;
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(editToken -> {
if (editToken != null) {
Observable.fromCallable(() -> captionInterface.addLabelstoWikidata(fileEntityId, editToken, caption.get(0), caption))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(revisionId ->
{
if (revisionId != null) {
Timber.d("Caption successfully set, revision id = %s", revisionId);
} else {
Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId);
}
},
throwable -> {
Timber.e(throwable, "Error occurred while setting Captions");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
}else {
Timber.d("Error acquiring EntityId for image");
}
}, throwable -> {
Timber.e(throwable, "Error occurred while getting EntityID for the File");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
}
}

View file

@ -0,0 +1,42 @@
package fr.free.nrw.commons.wikidata.model;
/**
* Model class for Depiction item returned from API after calling searchForDepicts
*/
public class DepictSearchItem {
private final String id;
private final String pageid;
private final String url;
private final String label;
private final String description;
public DepictSearchItem(String id, String pageid, String url, String label, String description) {
this.id = id;
this.pageid = pageid;
this.url = url;
this.label = label;
this.description = description;
}
public String getId() {
return id;
}
public String getPageid() {
return pageid;
}
public String getUrl() {
return url;
}
public String getLabel() {
return label;
}
public String getDescription() {
return description;
}
}

View file

@ -0,0 +1,24 @@
package fr.free.nrw.commons.wikidata.model;
import java.util.List;
/**
* Model class for API response obtained from search for depictions
*/
public class DepictSearchResponse {
private final List<DepictSearchItem> search;
/**
* Constructor to initialise value of the search object
*/
public DepictSearchResponse(List<DepictSearchItem> search) {
this.search = search;
}
/**
* @return List<DepictSearchItem> for the DepictSearchResponse
*/
public List<DepictSearchItem> getSearch() {
return search;
}
}

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/primaryDarkColor">
<include layout="@layout/toolbar"/>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar"
android:background="?attr/tabBackground"
app:tabIndicatorColor="?attr/tabIndicatorColor"
app:tabMode="scrollable"
app:tabSelectedTextColor="?attr/tabSelectedTextColor"
app:tabTextColor="?attr/tabTextColor" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mediaContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar_layout"
android:orientation="horizontal"
android:visibility="gone" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar_layout" />
</RelativeLayout>
<include layout="@layout/drawer_view" />
</androidx.drawerlayout.widget.DrawerLayout>

Some files were not shown because too many files have changed in this diff Show more