#3222 Merge Structured Data branch into master (#3553)

* #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>

* #3482 Use Room in Structured Data branch - remove unused code (#3483)

* #3482 Use Room in Structured Data branch - remove unused code

* #3482 Use Room in Structured Data branch - fix unit test compilation

* #3482 Use Room in Structured Data branch - add kdoc

* #3490 Depiction Search in upload shows No Results before it gets results (#3491)

* #3482 Use Room in Structured Data branch - remove unused code

* #3482 Use Room in Structured Data branch - fix unit test compilation

* #3490 Depiction Search in upload shows No Results before it gets results - stop showing error on subscription

* #3490 Depiction Search in upload shows No Results before it gets results - update test cases

* make labels nullable too

* fix unit test compilation

* #3222 remove lingering reference to depiction content provider

* Fix Crash

* #3222 Merge master into Structured Data branch, fix conflicts - review fixes

* Fix method invocations

* #3529 Captions/depictions are not saved to Commons (#3574)

* #3529 Captions/depictions are not saved to Commons - make copy of list of depictionEntityIds - uncomment editBaseDepictsProperty - refactor upload related classes

* #3529 Captions/depictions are not saved to Commons - fix wrong ArrayList usage

* #3529 Captions/depictions are not saved to Commons - fix test

* #3503 Remove Title/Caption From MediaUploadDetail and only use Caption/Description pairs  (#3578)

* #3529 Captions/depictions are not saved to Commons - make copy of list of depictionEntityIds - uncomment editBaseDepictsProperty - refactor upload related classes

* #3529 Captions/depictions are not saved to Commons - fix wrong ArrayList usage

* #3529 Captions/depictions are not saved to Commons - fix test

* #3503 Remove Title/Caption From MediaUploadDetail and only use Caption/Description pairs - replace title with the first MediaDetail

* #3503 Remove Title/Caption From MediaUploadDetail and only use Caption/Description pairs - restore button disabling

* #3503 Remove Title/Caption From MediaUploadDetail and only use Caption/Description pairs - fix nearby place

* fix thumbnail issue 3526 (#3617)

* #3222 Merge master into Structured Data branch, fix conflicts - fix bad merge

* #3529 Captions/depictions are not saved to Commons (#3588)

* #3529 Captions/depictions are not saved to Commons - update flow to update appropriate data

* #3529 Captions/depictions are not saved to Commons - fix invoking of setlabel

* #3529 Captions/depictions are not saved to Commons - fix unit tests

* #3529 Captions/depictions are not saved to Commons - use constant for @Named

* #3529 Captions/depictions are not saved to Commons - remove captions interface

* #3529 Captions/depictions are not saved to Commons - delete unused Contribution fields - enforce Single Responsibility by using PageContentsCreator

* #3529 Captions/depictions are not saved to Commons - prefix id with M - remove language from url and only add from Field

* #3529 Captions/depictions are not saved to Commons - make edits of depictions and captions sequential

* #3529 Captions/depictions are not saved to Commons - remove unused model fields

* #3529 Captions/depictions are not saved to Commons - weaken type of categories - copy list on Contribution creation

* #3529 Captions/depictions are not saved to Commons - mark Media fields private - weaken types - remove partly implemented fields

* #3529 Captions/depictions are not saved to Commons - add semi colon

* #3529 Captions/depictions are not saved to Commons - fix test

* Fix issue 3526 Unlike "Items" tab, "child classes" tab does not display description nor image thumbnail (#3619)

* fix thumbnail issue 3526

* Fix Description issue 3526

* revert changes on this file, not finished with it yet

* Fix Description for Child and Parent classes - issue 3526

* Remove conflict text in file

* Remove retrofit.HEAD import

* Incorporated review comments

* Fix issue 3137 (#3637)

* Fix issue 3137

* Remove import Timber

* Remove unnecessary space

* #3222 Merge master into Structured Data branch, fix conflicts - revert logging

* Fix build

* #3661 No Depictions Selected Dialog has reversed buttons - fix button order

* Revert "#3661 No Depictions Selected Dialog has reversed buttons - fix button order"

This reverts commit d8f9809584.

* #3222 Merge master into Structured Data branch, fix conflicts - remove unused methods/fields

* #3661 No Depictions Selected Dialog has reversed buttons - fix button order (#3662)

* #3653 Many Mnull requests - stop requesting captions for null ids (#3657)

* #3653 Many Mnull requests - stop requesting captions for null ids

* #3653 Many Mnull requests - move log line

* #3633 [structured-data branch] In depictions selection screen, suggest nearby items  (#3650)

* #3633 [structured-data branch] In depictions selection screen, suggest nearby items - for empty search terms show nearby items for depictions

* #3633 [structured-data branch] In depictions selection screen, suggest nearby items - use linear radii progression to search for places

* #3666 Crash when uploading on structured-data branch - revert cleanup of UploadController (#3670)

* #3222 Merge Structured Data branch into master - fix caption rendering in new UI

* #3222 Merge Structured Data branch into master - upgrade retrofit + okhttp

* #3664 Stop using JsonObject on StructuredData (#3672)

* #3664 Stop using JsonObject on StructuredData - remove usage in Media classes - remove from depicts client - create partial network models

* #3664 Stop using JsonObject on StructuredData - allow partial mapping of polymorphic models by returning null in typeadapter

* #3664 Stop using JsonObject on StructuredData - use models for editing depicts property

* #3664 Stop using JsonObject on StructuredData - use models for sparql parent query

* #3664 Stop using JsonObject on StructuredData - fix unit test compilation

* #3664 Stop using JsonObject on StructuredData - unify sparql responses

* #3664 Stop using JsonObject on StructuredData - minor cleanup of misnamed/unused/too broad visibility

* #3664 Stop using JsonObject on StructuredData - share variable names and logic for the Sarql queries

* #3664 Stop using JsonObject on StructuredData - add error logging

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>
Co-authored-by: vvijayalakshmi21 <34595292+vvijayalakshmi21@users.noreply.github.com>
This commit is contained in:
Seán Mac Gillicuddy 2020-04-21 17:34:53 +01:00 committed by GitHub
parent 22c20687f3
commit 0f906b20c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
168 changed files with 7463 additions and 2123 deletions

View file

@ -7,6 +7,7 @@
<inspection_tool class="ConfusingElse" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="ConfusingElse" enabled="true" level="WARNING" enabled_by_default="true">
<option name="reportWhenNoStatementFollow" value="true" /> <option name="reportWhenNoStatementFollow" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="ControlFlowStatementWithoutBraces" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="DefaultNotLastCaseInSwitch" enabled="true" level="WARNING" enabled_by_default="true" /> <inspection_tool class="DefaultNotLastCaseInSwitch" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="FieldMayBeFinal" enabled="true" level="WARNING" enabled_by_default="true" /> <inspection_tool class="FieldMayBeFinal" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true">

View file

@ -19,10 +19,9 @@ dependencies {
implementation project(':wikimedia-data-client') implementation project(':wikimedia-data-client')
// Utils // Utils
implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
implementation 'in.yuvi:http.fluent:1.3' implementation 'in.yuvi:http.fluent:1.3'
implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.squareup.okhttp3:okhttp:4.2.0' implementation 'com.squareup.okhttp3:okhttp:4.5.0'
implementation 'com.squareup.okio:okio:2.2.2' implementation 'com.squareup.okio:okio:2.2.2'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.3' implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
@ -44,6 +43,7 @@ dependencies {
implementation 'com.dinuscxj:circleprogressbar:1.1.1' implementation 'com.dinuscxj:circleprogressbar:1.1.1'
implementation 'com.karumi:dexter:5.0.0' implementation 'com.karumi:dexter:5.0.0'
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
// Logging // Logging
@ -53,7 +53,7 @@ dependencies {
api('com.github.tony19:logback-android-classic:1.1.1-6') { api('com.github.tony19:logback-android-classic:1.1.1-6') {
exclude group: 'com.google.android', module: 'android' exclude group: 'com.google.android', module: 'android'
} }
implementation "com.squareup.okhttp3:logging-interceptor:4.2.0" implementation "com.squareup.okhttp3:logging-interceptor:4.5.0"
// Dependency injector // Dependency injector
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION" implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
@ -65,7 +65,7 @@ dependencies {
//Mocking //Mocking
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' 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.mockito:mockito-core:2.23.0'
testImplementation "org.powermock:powermock-module-junit4:2.0.0-beta.5" testImplementation "org.powermock:powermock-module-junit4:2.0.0-beta.5"
testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5" testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5"
@ -108,9 +108,10 @@ dependencies {
//Room //Room
implementation "androidx.room:room-runtime:$ROOM_VERSION" implementation "androidx.room:room-runtime:$ROOM_VERSION"
kapt "androidx.room:room-compiler:$ROOM_VERSION" // For Kotlin use kapt instead of annotationProcessor implementation "androidx.room:room-ktx:$ROOM_VERSION"
implementation 'com.squareup.retrofit2:retrofit:2.7.1'
implementation "androidx.room:room-rxjava2:$ROOM_VERSION" implementation "androidx.room:room-rxjava2:$ROOM_VERSION"
kapt "androidx.room:room-compiler:$ROOM_VERSION" // For Kotlin use kapt instead of annotationProcessor
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
testImplementation "androidx.arch.core:core-testing:2.1.0" testImplementation "androidx.arch.core:core-testing:2.1.0"
// Pref // Pref
@ -208,6 +209,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_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", "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_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_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json\"" 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\"" buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
@ -229,6 +231,7 @@ android {
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\"" buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
buildConfigField "String", "TEST_USERNAME", "\"" + System.getenv("test_user_name") + "\"" buildConfigField "String", "TEST_USERNAME", "\"" + System.getenv("test_user_name") + "\""
buildConfigField "String", "TEST_PASSWORD", "\"" + System.getenv("test_user_password") + "\"" buildConfigField "String", "TEST_PASSWORD", "\"" + System.getenv("test_user_password") + "\""
buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\""
dimension 'tier' dimension 'tier'
} }
@ -240,6 +243,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_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", "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_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_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", "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\"" buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
@ -261,6 +265,7 @@ android {
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\"" buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
buildConfigField "String", "TEST_USERNAME", "\"" + System.getenv("test_user_name") + "\"" buildConfigField "String", "TEST_USERNAME", "\"" + System.getenv("test_user_name") + "\""
buildConfigField "String", "TEST_PASSWORD", "\"" + System.getenv("test_user_password") + "\"" buildConfigField "String", "TEST_PASSWORD", "\"" + System.getenv("test_user_password") + "\""
buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\""
dimension 'tier' 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 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.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4 import androidx.test.runner.AndroidJUnit4
import fr.free.nrw.commons.upload.UploadActivity 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.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -16,4 +25,25 @@ class UploadActivityTest {
fun orientationChange() { fun orientationChange() {
UITestHelper.changeOrientation(activityRule) 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:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.MainActivity" /> android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".depictions.WikidataItemDetailsActivity"
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.MainActivity" />
<activity <activity
android:name=".explore.categories.ExploreActivity" android:name=".explore.categories.ExploreActivity"
android:label="@string/title_activity_explore" android:label="@string/title_activity_explore"
@ -178,7 +183,7 @@
android:authorities="${applicationId}.categories.contentprovider" android:authorities="${applicationId}.categories.contentprovider"
android:exported="false" android:exported="false"
android:label="@string/provider_categories" android:label="@string/provider_categories"
android:syncable="false" /> android:syncable="false" />
<provider <provider
android:name=".explore.recentsearches.RecentSearchesContentProvider" android:name=".explore.recentsearches.RecentSearchesContentProvider"

View file

@ -1,62 +1,5 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.Build;
import android.os.Process;
import android.util.Log;
import androidx.annotation.NonNull;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.core.ImagePipelineConfig;
import com.mapbox.mapboxsdk.Mapbox;
import com.squareup.leakcanary.LeakCanary;
import com.squareup.leakcanary.RefWatcher;
import io.reactivex.Completable;
import org.acra.ACRA;
import org.acra.annotation.AcraCore;
import org.acra.annotation.AcraDialog;
import org.acra.annotation.AcraMailSender;
import org.acra.data.StringFormat;
import org.wikipedia.AppAdapter;
import org.wikipedia.language.AppLanguageLookUpTable;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
import fr.free.nrw.commons.category.CategoryDao;
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler;
import fr.free.nrw.commons.concurrency.ThreadPoolService;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.db.AppDatabase;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.kvstore.JsonKvStore;
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.utils.ConfigUtils;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.internal.functions.Functions;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static fr.free.nrw.commons.data.DBOpenHelper.CONTRIBUTIONS_TABLE; import static fr.free.nrw.commons.data.DBOpenHelper.CONTRIBUTIONS_TABLE;
import static org.acra.ReportField.ANDROID_VERSION; import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE; import static org.acra.ReportField.APP_VERSION_CODE;
@ -65,6 +8,57 @@ import static org.acra.ReportField.PHONE_MODEL;
import static org.acra.ReportField.STACK_TRACE; import static org.acra.ReportField.STACK_TRACE;
import static org.acra.ReportField.USER_COMMENT; import static org.acra.ReportField.USER_COMMENT;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import android.os.Process;
import android.util.Log;
import androidx.annotation.NonNull;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.core.ImagePipelineConfig;
import com.mapbox.mapboxsdk.Mapbox;
import com.squareup.leakcanary.LeakCanary;
import com.squareup.leakcanary.RefWatcher;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
import fr.free.nrw.commons.category.CategoryDao;
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler;
import fr.free.nrw.commons.concurrency.ThreadPoolService;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.kvstore.JsonKvStore;
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.utils.ConfigUtils;
import io.reactivex.Completable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.internal.functions.Functions;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import org.acra.ACRA;
import org.acra.annotation.AcraCore;
import org.acra.annotation.AcraDialog;
import org.acra.annotation.AcraMailSender;
import org.acra.data.StringFormat;
import org.wikipedia.AppAdapter;
import org.wikipedia.language.AppLanguageLookUpTable;
import timber.log.Timber;
@AcraCore( @AcraCore(
buildConfigClass = BuildConfig.class, buildConfigClass = BuildConfig.class,
resReportSendSuccessToast = R.string.crash_dialog_ok_toast, resReportSendSuccessToast = R.string.crash_dialog_ok_toast,
@ -120,8 +114,7 @@ public class CommonsApplication extends Application {
return languageLookUpTable; return languageLookUpTable;
} }
@Inject @Inject ContributionDao contributionDao;
AppDatabase appDatabase;
/** /**
* Used to declare and initialize various components and dependencies * Used to declare and initialize various components and dependencies
@ -299,7 +292,7 @@ public class CommonsApplication extends Application {
CategoryDao.Table.onDelete(db); CategoryDao.Table.onDelete(db);
dbOpenHelper.deleteTable(db,CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions dbOpenHelper.deleteTable(db,CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions
appDatabase.getContributionDao().deleteAll(); contributionDao.deleteAll();
BookmarkPicturesDao.Table.onDelete(db); BookmarkPicturesDao.Table.onDelete(db);
BookmarkLocationsDao.Table.onDelete(db); BookmarkLocationsDao.Table.onDelete(db);
} }

View file

@ -3,31 +3,24 @@ package fr.free.nrw.commons;
import android.net.Uri; import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.PrimaryKey; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.media.Depictions;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.mwapi.MwQueryPage; import org.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.gallery.ExtMetadata; import org.wikipedia.gallery.ExtMetadata;
import org.wikipedia.gallery.ImageInfo; import org.wikipedia.gallery.ImageInfo;
import org.wikipedia.page.PageTitle; import org.wikipedia.page.PageTitle;
import java.text.ParseException;
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 fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
@Entity @Entity
public class Media implements Parcelable { public class Media implements Parcelable {
@ -35,32 +28,42 @@ public class Media implements Parcelable {
// Primary metadata fields // Primary metadata fields
@Nullable @Nullable
public Uri localUri; private Uri localUri;
public String thumbUrl; private String thumbUrl;
public String imageUrl; private String imageUrl;
public String filename; private String filename;
public String description; // monolingual description on input... private String thumbnailTitle;
public String discussion; /*
long dataLength; * Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
public Date dateCreated; * This is a replacement of the previously used titles for images (titles were not multilingual)
@Nullable public Date dateUploaded; * Also now captions replace the previous convention of using title for filename
public int width; */
public int height; private String caption;
public String license; private String description; // monolingual description on input...
public String licenseUrl; private String discussion;
public String creator; private long dataLength;
public ArrayList<String> categories; // as loaded at runtime? private Date dateCreated;
public boolean requestedDeletion; @Nullable private Date dateUploaded;
public HashMap<String, String> descriptions; // multilingual descriptions as loaded private String license;
public HashMap<String, String> tags = new HashMap<>(); private String licenseUrl;
@Nullable public LatLng coordinates; private String creator;
/**
* Wikibase Identifier associated with media files
*/
private String pageId;
private List<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
*/
private Depictions depictions;
private boolean requestedDeletion;
@Nullable private LatLng coordinates;
/** /**
* Provides local constructor * Provides local constructor
*/ */
protected Media() { public Media() {
this.categories = new ArrayList<>();
this.descriptions = new HashMap<>();
} }
/** /**
@ -69,7 +72,6 @@ public class Media implements Parcelable {
* @param filename Media filename * @param filename Media filename
*/ */
public Media(String filename) { public Media(String filename) {
this();
this.filename = filename; this.filename = filename;
} }
@ -84,9 +86,9 @@ public class Media implements Parcelable {
* @param dateUploaded Media date uploaded * @param dateUploaded Media date uploaded
* @param creator Media creator * @param creator Media creator
*/ */
public Media(Uri localUri, String imageUrl, String filename, String description, public Media(Uri localUri, String imageUrl, String filename,
long dataLength, Date dateCreated, Date dateUploaded, String creator) { String description,
this(); long dataLength, Date dateCreated, Date dateUploaded, String creator) {
this.localUri = localUri; this.localUri = localUri;
this.thumbUrl = imageUrl; this.thumbUrl = imageUrl;
this.imageUrl = imageUrl; this.imageUrl = imageUrl;
@ -96,8 +98,17 @@ public class Media implements Parcelable {
this.dateCreated = dateCreated; this.dateCreated = dateCreated;
this.dateUploaded = dateUploaded; this.dateUploaded = dateUploaded;
this.creator = creator; this.creator = creator;
this.categories = new ArrayList<>(); }
this.descriptions = new HashMap<>();
public Media(Uri localUri, String filename,
String description, String creator, List<String> categories) {
this(localUri,null, filename,
description, -1, null, new Date(), creator);
this.categories = categories;
}
public Media(String title, Date date, String user) {
this(null, null, title, "", -1, date, date, user);
} }
/** /**
@ -112,7 +123,7 @@ public class Media implements Parcelable {
public static Media from(MwQueryPage page) { public static Media from(MwQueryPage page) {
ImageInfo imageInfo = page.imageInfo(); ImageInfo imageInfo = page.imageInfo();
if (imageInfo == null) { if (imageInfo == null) {
return null; return new Media(); // null is not allowed
} }
ExtMetadata metadata = imageInfo.getMetadata(); ExtMetadata metadata = imageInfo.getMetadata();
if (metadata == null) { if (metadata == null) {
@ -127,7 +138,7 @@ public class Media implements Parcelable {
Media media = new Media(null, Media media = new Media(null,
imageInfo.getOriginalUrl(), imageInfo.getOriginalUrl(),
page.title(), page.title(),
"", "",
0, 0,
safeParseDate(metadata.dateTime()), safeParseDate(metadata.dateTime()),
safeParseDate(metadata.dateTime()), safeParseDate(metadata.dateTime()),
@ -138,12 +149,14 @@ public class Media implements Parcelable {
media.setThumbUrl(imageInfo.getThumbUrl()); media.setThumbUrl(imageInfo.getThumbUrl());
} }
media.setPageId(String.valueOf(page.pageId()));
String language = Locale.getDefault().getLanguage(); String language = Locale.getDefault().getLanguage();
if (StringUtils.isBlank(language)) { if (StringUtils.isBlank(language)) {
language = "default"; language = "default";
} }
media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription())); media.setDescription(metadata.imageDescription());
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories())); media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories()));
String latitude = metadata.getGpsLatitude(); String latitude = metadata.getGpsLatitude();
String longitude = metadata.getGpsLongitude(); String longitude = metadata.getGpsLongitude();
@ -172,28 +185,23 @@ public class Media implements Parcelable {
} }
} }
/**
* @return pageId for the current media object*/
public String getPageId() {
return pageId;
}
/**
*sets pageId for the current media object
*/
public void setPageId(String pageId) {
this.pageId = pageId;
}
public String getThumbUrl() { public String getThumbUrl() {
return thumbUrl; return thumbUrl;
} }
/**
* Gets tag of media
* @param key Media key
* @return Media tag
*/
public Object getTag(String key) {
return tags.get(key);
}
/**
* Modifies( or creates a) tag of media
* @param key Media key
* @param value Media value
*/
public void setTag(String key, String value) {
tags.put(key, value);
}
/** /**
* Gets media display title * Gets media display title
* @return Media title * @return Media title
@ -202,6 +210,21 @@ public class Media implements Parcelable {
return filename != null ? getPageTitle().getDisplayTextWithoutNamespace().replaceFirst("[.][^.]+$", "") : ""; 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 * Gets file page title
* @return New media page title * @return New media page title
@ -268,6 +291,24 @@ public class Media implements Parcelable {
return description; 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 Depictions getDepiction() {
return depictions;
}
/** /**
* Sets the file description. * Sets the file description.
* @param description the new description of the file * @param description the new description of the file
@ -334,38 +375,6 @@ public class Media implements Parcelable {
this.creator = creator; this.creator = creator;
} }
/**
* Gets the width of the media.
* @return file width as an int
*/
public int getWidth() {
return width;
}
/**
* Sets the width of the media.
* @param width file width as an int
*/
public void setWidth(int width) {
this.width = width;
}
/**
* Gets the height of the media.
* @return file height as an int
*/
public int getHeight() {
return height;
}
/**
* Sets the height of the media.
* @param height file height as an int
*/
public void setHeight(int height) {
this.height = height;
}
/** /**
* Gets the license name of the file. * Gets the license name of the file.
* @return license as a String * @return license as a String
@ -417,8 +426,8 @@ public class Media implements Parcelable {
* @return file categories as an ArrayList of Strings * @return file categories as an ArrayList of Strings
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public ArrayList<String> getCategories() { public List<String> getCategories() {
return (ArrayList<String>) categories.clone(); // feels dirty return categories;
} }
/** /**
@ -429,38 +438,7 @@ public class Media implements Parcelable {
* @param categories file categories as a list of Strings * @param categories file categories as a list of Strings
*/ */
public void setCategories(List<String> categories) { public void setCategories(List<String> categories) {
this.categories.clear(); this.categories = categories;
this.categories.addAll(categories);
}
/**
* Modifies (or sets) media descriptions
* @param descriptions Media descriptions
*/
void setDescriptions(Map<String, String> descriptions) {
this.descriptions.clear();
this.descriptions.putAll(descriptions);
}
/**
* Gets media description in preferred language
* @param preferredLanguage Language preferred
* @return Description in preferred language
*/
public String getDescription(String preferredLanguage) {
if (descriptions.containsKey(preferredLanguage)) {
// See if the requested language is there.
return descriptions.get(preferredLanguage);
} else if (descriptions.containsKey("en")) {
// Ah, English. Language of the world, until the Chinese crush us.
return descriptions.get("en");
} else if (descriptions.containsKey("default")) {
// No languages marked...
return descriptions.get("default");
} else {
// FIXME: return the first available non-English description?
return "";
}
} }
@Nullable private static Date safeParseDate(String dateStr) { @Nullable private static Date safeParseDate(String dateStr) {
@ -473,16 +451,17 @@ public class Media implements Parcelable {
/** /**
* Set requested deletion to true * Set requested deletion to true
* @param requestedDeletion
*/ */
public void setRequestedDeletion(){ public void setRequestedDeletion(boolean requestedDeletion){
requestedDeletion = true; this.requestedDeletion = requestedDeletion;
} }
/** /**
* Get the value of requested deletion * Get the value of requested deletion
* @return boolean requestedDeletion * @return boolean requestedDeletion
*/ */
public boolean getRequestedDeletion(){ public boolean isRequestedDeletion(){
return requestedDeletion; return requestedDeletion;
} }
@ -495,6 +474,42 @@ public class Media implements Parcelable {
this.license = 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;
}
/* Sets depictions for the current media obtained fro Wikibase API*/
public void setDepictions(Depictions depictions) {
this.depictions = depictions;
}
public void setLocalUri(@Nullable final Uri localUri) {
this.localUri = localUri;
}
public void setImageUrl(final String imageUrl) {
this.imageUrl = imageUrl;
}
public void setDateUploaded(@Nullable final Date dateUploaded) {
this.dateUploaded = dateUploaded;
}
public void setLicenseUrl(final String licenseUrl) {
this.licenseUrl = licenseUrl;
}
public Depictions getDepictions() {
return depictions;
}
@Override @Override
public int describeContents() { public int describeContents() {
@ -513,20 +528,20 @@ public class Media implements Parcelable {
dest.writeString(this.thumbUrl); dest.writeString(this.thumbUrl);
dest.writeString(this.imageUrl); dest.writeString(this.imageUrl);
dest.writeString(this.filename); dest.writeString(this.filename);
dest.writeString(this.thumbnailTitle);
dest.writeString(this.caption);
dest.writeString(this.description); dest.writeString(this.description);
dest.writeString(this.discussion); dest.writeString(this.discussion);
dest.writeLong(this.dataLength); dest.writeLong(this.dataLength);
dest.writeLong(this.dateCreated != null ? this.dateCreated.getTime() : -1); dest.writeLong(this.dateCreated != null ? this.dateCreated.getTime() : -1);
dest.writeLong(this.dateUploaded != null ? this.dateUploaded.getTime() : -1); dest.writeLong(this.dateUploaded != null ? this.dateUploaded.getTime() : -1);
dest.writeInt(this.width);
dest.writeInt(this.height);
dest.writeString(this.license); dest.writeString(this.license);
dest.writeString(this.licenseUrl); dest.writeString(this.licenseUrl);
dest.writeString(this.creator); dest.writeString(this.creator);
dest.writeString(this.pageId);
dest.writeStringList(this.categories); dest.writeStringList(this.categories);
dest.writeParcelable(this.depictions, flags);
dest.writeByte(this.requestedDeletion ? (byte) 1 : (byte) 0); dest.writeByte(this.requestedDeletion ? (byte) 1 : (byte) 0);
dest.writeSerializable(this.descriptions);
dest.writeSerializable(this.tags);
dest.writeParcelable(this.coordinates, flags); dest.writeParcelable(this.coordinates, flags);
} }
@ -535,6 +550,8 @@ public class Media implements Parcelable {
this.thumbUrl = in.readString(); this.thumbUrl = in.readString();
this.imageUrl = in.readString(); this.imageUrl = in.readString();
this.filename = in.readString(); this.filename = in.readString();
this.thumbnailTitle = in.readString();
this.caption = in.readString();
this.description = in.readString(); this.description = in.readString();
this.discussion = in.readString(); this.discussion = in.readString();
this.dataLength = in.readLong(); this.dataLength = in.readLong();
@ -542,15 +559,15 @@ public class Media implements Parcelable {
this.dateCreated = tmpDateCreated == -1 ? null : new Date(tmpDateCreated); this.dateCreated = tmpDateCreated == -1 ? null : new Date(tmpDateCreated);
long tmpDateUploaded = in.readLong(); long tmpDateUploaded = in.readLong();
this.dateUploaded = tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded); this.dateUploaded = tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded);
this.width = in.readInt();
this.height = in.readInt();
this.license = in.readString(); this.license = in.readString();
this.licenseUrl = in.readString(); this.licenseUrl = in.readString();
this.creator = in.readString(); this.creator = in.readString();
this.categories = in.createStringArrayList(); this.pageId = in.readString();
final ArrayList<String> list = new ArrayList<>();
in.readStringList(list);
this.categories=list;
in.readParcelable(Depictions.class.getClassLoader());
this.requestedDeletion = in.readByte() != 0; this.requestedDeletion = in.readByte() != 0;
this.descriptions = (HashMap<String, String>) in.readSerializable();
this.tags = (HashMap<String, String>) in.readSerializable();
this.coordinates = in.readParcelable(LatLng.class.getClassLoader()); this.coordinates = in.readParcelable(LatLng.class.getClassLoader());
} }

View file

@ -1,12 +1,14 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import androidx.core.text.HtmlCompat; import androidx.core.text.HtmlCompat;
import fr.free.nrw.commons.media.Depictions;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.Single; import io.reactivex.Single;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.jetbrains.annotations.NotNull;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -17,30 +19,61 @@ import timber.log.Timber;
*/ */
@Singleton @Singleton
public class MediaDataExtractor { public class MediaDataExtractor {
private final MediaClient mediaClient;
private final MediaClient mediaClient;
@Inject @Inject
public MediaDataExtractor(MediaClient mediaClient) { public MediaDataExtractor(final MediaClient mediaClient) {
this.mediaClient = mediaClient; this.mediaClient = mediaClient;
} }
/** /**
* Simplified method to extract all details required to show media details. * 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 * @param filename for which the details are to be fetched
* @return full Media object with all details including deletion status and talk page * @return full Media object with all details including deletion status and talk page
*/ */
public Single<Media> fetchMediaDetails(String filename) { public Single<Media> fetchMediaDetails(final String filename, final String pageId) {
Single<Media> mediaSingle = getMediaFromFileName(filename); return Single.zip(getMediaFromFileName(filename),
Single<Boolean> pageExistsSingle = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + filename); mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + filename),
Single<String> discussionSingle = getDiscussion(filename); getDiscussion(filename),
return Single.zip(mediaSingle, pageExistsSingle, discussionSingle, (media, deletionStatus, discussion) -> { pageId != null ? getCaption(PAGE_ID_PREFIX + pageId)
media.setDiscussion(discussion); : Single.just(MediaClient.NO_CAPTION),
if (deletionStatus) { getDepictions(filename),
media.setRequestedDeletion(); this::combineToMedia);
} }
return media;
}); @NotNull
private Media combineToMedia(final Media media, final Boolean deletionStatus, final String discussion,
final String caption, final Depictions depictions) {
media.setDiscussion(discussion);
media.setCaption(caption);
media.setDepictions(depictions);
if (deletionStatus) {
media.setRequestedDeletion(true);
}
return media;
}
/**
* 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(final String wikibaseIdentifier) {
return mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier);
}
/**
* Fetch depictions from the MediaWiki API
* @param filename the filename we will return the caption for
* @return Depictions
*/
private Single<Depictions> getDepictions(final String filename) {
return mediaClient.getDepictions(filename)
.doOnError(throwable -> Timber.e(throwable, "error while fetching depictions"));
} }
/** /**
@ -48,7 +81,7 @@ public class MediaDataExtractor {
* @param filename Eg. File:Test.jpg * @param filename Eg. File:Test.jpg
* @return return data rich Media object * @return return data rich Media object
*/ */
public Single<Media> getMediaFromFileName(String filename) { public Single<Media> getMediaFromFileName(final String filename) {
return mediaClient.getMedia(filename); return mediaClient.getMedia(filename);
} }
@ -57,7 +90,7 @@ public class MediaDataExtractor {
* @param filename * @param filename
* @return * @return
*/ */
private Single<String> getDiscussion(String filename) { private Single<String> getDiscussion(final String filename) {
return mediaClient.getPageHtml(filename.replace("File", "File talk")) return mediaClient.getPageHtml(filename.replace("File", "File talk"))
.map(discussion -> HtmlCompat.fromHtml(discussion, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()) .map(discussion -> HtmlCompat.fromHtml(discussion, HtmlCompat.FROM_HTML_MODE_LEGACY).toString())
.onErrorReturn(throwable -> { .onErrorReturn(throwable -> {

View file

@ -1,6 +1,11 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import okhttp3.logging.HttpLoggingInterceptor.Level;
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
import org.wikipedia.dataclient.okhttp.HttpStatusException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import okhttp3.Cache; import okhttp3.Cache;

View file

@ -1,81 +0,0 @@
package fr.free.nrw.commons.caching;
import com.github.varunpant.quadtree.Point;
import com.github.varunpant.quadtree.QuadTree;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import timber.log.Timber;
@Singleton
public class CacheController {
private final QuadTree<List<String>> quadTree;
private double x, y;
private double xMinus, xPlus, yMinus, yPlus;
private static final int EARTH_RADIUS = 6378137;
@Inject
public CacheController(QuadTree quadTree) {
this.quadTree = quadTree;
}
public void setQtPoint(double decLongitude, double decLatitude) {
x = decLongitude;
y = decLatitude;
Timber.d("New QuadTree created");
Timber.d("X (longitude) value: %f, Y (latitude) value: %f", x, y);
}
public List<String> findCategory() {
Point<List<String>>[] pointsFound;
//Convert decLatitude and decLongitude to a coordinate offset range
convertCoordRange();
pointsFound = quadTree.searchWithin(xMinus, yMinus, xPlus, yPlus);
List<String> displayCatList = new ArrayList<>();
Timber.d("Points found in quadtree: %s", Arrays.toString(pointsFound));
if (pointsFound.length != 0) {
Timber.d("Entering for loop");
for (Point<List<String>> point : pointsFound) {
Timber.d("Nearby point: %s", point);
displayCatList = point.getValue();
Timber.d("Nearby cat: %s", point.getValue());
}
Timber.d("Categories found in cache: %s", displayCatList);
} else {
Timber.d("No categories found in cache");
}
return displayCatList;
}
//Based on algorithm at http://gis.stackexchange.com/questions/2951/algorithm-for-offsetting-a-latitude-longitude-by-some-amount-of-meters
private void convertCoordRange() {
//Position, decimal degrees
double lat = y;
double lon = x;
//offsets in meters
double offset = 100;
//Coordinate offsets in radians
double dLat = offset / EARTH_RADIUS;
double dLon = offset / (EARTH_RADIUS * Math.cos(Math.PI * lat / 180));
//OffsetPosition, decimal degrees
yPlus = lat + dLat * 180 / Math.PI;
yMinus = lat - dLat * 180 / Math.PI;
xPlus = lon + dLon * 180 / Math.PI;
xMinus = lon - dLon * 180 / Math.PI;
Timber.d("Search within: xMinus=%s, yMinus=%s, xPlus=%s, yPlus=%s",
xMinus, yMinus, xPlus, yPlus);
}
}

View file

@ -1,21 +1,17 @@
package fr.free.nrw.commons.category; package fr.free.nrw.commons.category;
import android.text.TextUtils; 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 javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.upload.GpsCategoryModel; import fr.free.nrw.commons.upload.GpsCategoryModel;
import fr.free.nrw.commons.utils.StringSortingUtils; import fr.free.nrw.commons.utils.StringSortingUtils;
import io.reactivex.Observable; import io.reactivex.Observable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -27,19 +23,19 @@ public class CategoriesModel{
private final CategoryClient categoryClient; private final CategoryClient categoryClient;
private final CategoryDao categoryDao; private final CategoryDao categoryDao;
private final JsonKvStore directKvStore; private final JsonKvStore directKvStore;
private final GpsCategoryModel gpsCategoryModel;
private HashMap<String, ArrayList<String>> categoriesCache;
private List<CategoryItem> selectedCategories; private List<CategoryItem> selectedCategories;
@Inject GpsCategoryModel gpsCategoryModel;
@Inject @Inject
public CategoriesModel(CategoryClient categoryClient, public CategoriesModel(CategoryClient categoryClient,
CategoryDao categoryDao, CategoryDao categoryDao,
@Named("default_preferences") JsonKvStore directKvStore) { @Named("default_preferences") JsonKvStore directKvStore,
final GpsCategoryModel gpsCategoryModel) {
this.categoryClient = categoryClient; this.categoryClient = categoryClient;
this.categoryDao = categoryDao; this.categoryDao = categoryDao;
this.directKvStore = directKvStore; this.directKvStore = directKvStore;
this.categoriesCache = new HashMap<>(); this.gpsCategoryModel = gpsCategoryModel;
this.selectedCategories = new ArrayList<>(); this.selectedCategories = new ArrayList<>();
} }
@ -94,10 +90,6 @@ public class CategoriesModel{
categoryDao.save(category); categoryDao.save(category);
} }
boolean cacheContainsKey(String term) {
return categoriesCache.containsKey(term);
}
//endregion
/** /**
* Regional category search * Regional category search
@ -108,20 +100,18 @@ public class CategoriesModel{
public Observable<CategoryItem> searchAll(String term, List<String> imageTitleList) { public Observable<CategoryItem> searchAll(String term, List<String> imageTitleList) {
//If query text is empty, show him category based on gps and title and recent searches //If query text is empty, show him category based on gps and title and recent searches
if (TextUtils.isEmpty(term)) { if (TextUtils.isEmpty(term)) {
Observable<CategoryItem> categoryItemObservable = gpsCategories() Observable<CategoryItem> categoryItemObservable =
.concatWith(titleCategories(imageTitleList)); Observable.concat(gpsCategories(), titleCategories(imageTitleList));
if (hasDirectCategories()) { if (hasDirectCategories()) {
categoryItemObservable.concatWith(directCategories().concatWith(recentCategories())); return Observable.concat(
categoryItemObservable,
directCategories(),
recentCategories()
);
} }
return categoryItemObservable; return categoryItemObservable;
} }
//if user types in something that is in cache, return cached category
if (cacheContainsKey(term)) {
return Observable.fromIterable(getCachedCategories(term))
.map(name -> new CategoryItem(name, false));
}
//otherwise, search API for matching categories //otherwise, search API for matching categories
//term passed as lower case to make search case-insensitive(taking only lower case for everything) //term passed as lower case to make search case-insensitive(taking only lower case for everything)
return categoryClient return categoryClient
@ -130,15 +120,6 @@ public class CategoriesModel{
} }
/**
* Returns cached categories
* @param term
* @return
*/
private ArrayList<String> getCachedCategories(String term) {
return categoriesCache.get(term);
}
/** /**
* Returns if we have a category in DirectKV Store * Returns if we have a category in DirectKV Store
* @return * @return
@ -256,7 +237,6 @@ public class CategoriesModel{
* Cleanup the existing in memory cache's * Cleanup the existing in memory cache's
*/ */
public void cleanUp() { public void cleanUp() {
this.categoriesCache.clear();
this.selectedCategories.clear(); this.selectedCategories.clear();
} }
} }

View file

@ -1,5 +1,9 @@
package fr.free.nrw.commons.category; package fr.free.nrw.commons.category;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -12,15 +16,7 @@ import android.widget.ListAdapter;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import dagger.android.support.DaggerFragment; import dagger.android.support.DaggerFragment;
@ -33,17 +29,22 @@ import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber; import timber.log.Timber;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
/** /**
* Displays images for a particular category with load more on scrolling incorporated * Displays images for a particular category with load more on scrolling incorporated
*/ */
public class CategoryImagesListFragment extends DaggerFragment { public class CategoryImagesListFragment extends DaggerFragment {
private static int TIMEOUT_SECONDS = 15; private static int TIMEOUT_SECONDS = 15;
/**
* counts the total number of items loaded from the API
*/
private int mediaSize = 0;
private GridViewAdapter gridAdapter; private GridViewAdapter gridAdapter;
@ -256,6 +257,38 @@ public class CategoryImagesListFragment extends DaggerFragment {
progressBar.setVisibility(GONE); progressBar.setVisibility(GONE);
isLoading = false; isLoading = false;
statusTextView.setVisibility(GONE); statusTextView.setVisibility(GONE);
for (Media m : collection) {
final String pageId = m.getPageId();
if (pageId != null) {
replaceTitlesWithCaptions(PAGE_ID_PREFIX + pageId, 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.ArrayAdapter;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.Nullable;
import com.facebook.drawee.view.SimpleDraweeView; import com.facebook.drawee.view.SimpleDraweeView;
import java.util.ArrayList; import java.util.ArrayList;
@ -55,7 +57,7 @@ public class GridViewAdapter extends ArrayAdapter {
data = new ArrayList<>(); data = new ArrayList<>();
return false; return false;
} }
if (data.size() <= 0) { if (data.isEmpty()) {
return false; return false;
} }
String fileName = data.get(0).getFilename(); String fileName = data.get(0).getFilename();
@ -86,12 +88,22 @@ public class GridViewAdapter extends ArrayAdapter {
SimpleDraweeView imageView = convertView.findViewById(R.id.categoryImageView); SimpleDraweeView imageView = convertView.findViewById(R.id.categoryImageView);
TextView fileName = convertView.findViewById(R.id.categoryImageTitle); TextView fileName = convertView.findViewById(R.id.categoryImageTitle);
TextView author = convertView.findViewById(R.id.categoryImageAuthor); TextView author = convertView.findViewById(R.id.categoryImageAuthor);
fileName.setText(item.getDisplayTitle()); fileName.setText(item.getThumbnailTitle());
setAuthorView(item, author); setAuthorView(item, author);
imageView.setImageURI(item.getThumbUrl()); imageView.setImageURI(item.getThumbUrl());
return convertView; 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 * Shows author information if its present
* @param item * @param item

View file

@ -1,36 +1,22 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import android.content.Context;
import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.StringDef;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
import org.apache.commons.lang3.StringUtils;
import java.lang.annotation.Retention;
import java.util.Date;
import java.util.Locale;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import fr.free.nrw.commons.upload.WikidataPlace;
import static java.lang.annotation.RetentionPolicy.SOURCE; import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.wikipedia.dataclient.mwapi.MwQueryLogEvent;
@Entity(tableName = "contribution") @Entity(tableName = "contribution")
public class Contribution extends Media { public class Contribution extends Media {
//{{According to Exif data|2009-01-09}}
private static final String TEMPLATE_DATE_ACC_TO_EXIF = "{{According to Exif data|%s}}";
//2009-01-09 9 January 2009
private static final String TEMPLATE_DATA_OTHER_SOURCE = "%s";
// No need to be bitwise - they're mutually exclusive // No need to be bitwise - they're mutually exclusive
public static final int STATE_COMPLETED = -1; public static final int STATE_COMPLETED = -1;
@ -38,219 +24,133 @@ public class Contribution extends Media {
public static final int STATE_QUEUED = 2; public static final int STATE_QUEUED = 2;
public static final int STATE_IN_PROGRESS = 3; public static final int STATE_IN_PROGRESS = 3;
@Retention(SOURCE)
@StringDef({SOURCE_CAMERA, SOURCE_GALLERY, SOURCE_EXTERNAL})
public @interface FileSource {}
public static final String SOURCE_CAMERA = "camera";
public static final String SOURCE_GALLERY = "gallery";
public static final String SOURCE_EXTERNAL = "external";
@PrimaryKey (autoGenerate = true) @PrimaryKey (autoGenerate = true)
@NonNull private long _id;
public long _id; private int state;
public Uri contentUri; private long transferred;
public String source; private String decimalCoords;
public String editSummary; private String dateCreatedSource;
public int state; private WikidataPlace wikidataPlace;
public long transferred; /**
public String decimalCoords; * Each depiction loaded in depictions activity is associated with a wikidata entity id,
public boolean isMultiple; * this Id is in turn used to upload depictions to wikibase
public String wikiDataEntityId; */
public String wikiItemName; private List<DepictedItem> depictedItems = new ArrayList<>();
private String p18Value; private String mimeType;
public Uri contentProviderUri; /**
public String dateCreatedSource; * 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>"
*/
private Map<String, String> captions = new HashMap<>();
public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength, public Contribution() {
Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) {
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
this.decimalCoords = decimalCoords;
this.editSummary = editSummary;
this.dateCreatedSource = "";
} }
public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength, public Contribution(final UploadItem item, final SessionManager sessionManager,
Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords, int state) { final List<DepictedItem> depictedItems, final List<String> categories) {
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator); super(item.getMediaUri(),
this.decimalCoords = decimalCoords; item.getFileName(),
this.editSummary = editSummary; UploadMediaDetail.formatList(item.getUploadMediaDetails()),
this.dateCreatedSource = ""; sessionManager.getAuthorName(),
this.state=state; categories);
captions = UploadMediaDetail.formatCaptions(item.getUploadMediaDetails());
decimalCoords = item.getGpsCoords().getDecimalCoords();
dateCreatedSource = "";
this.depictedItems = depictedItems;
wikidataPlace = WikidataPlace.from(item.getPlace());
} }
public Contribution(final MwQueryLogEvent queryLogEvent, final String user) {
super(queryLogEvent.title(),queryLogEvent.date(), user);
decimalCoords = "";
dateCreatedSource = "";
state = STATE_COMPLETED;
}
public void setDateCreatedSource(final String dateCreatedSource) {
public void setDateCreatedSource(String dateCreatedSource) {
this.dateCreatedSource = dateCreatedSource; this.dateCreatedSource = dateCreatedSource;
} }
public boolean getMultiple() { public String getDateCreatedSource() {
return isMultiple; return dateCreatedSource;
}
public void setMultiple(boolean multiple) {
isMultiple = multiple;
} }
public long getTransferred() { public long getTransferred() {
return transferred; return transferred;
} }
public void setTransferred(long transferred) { public void setTransferred(final long transferred) {
this.transferred = transferred; this.transferred = transferred;
} }
public String getEditSummary() {
return editSummary != null ? editSummary : CommonsApplication.DEFAULT_EDIT_SUMMARY;
}
public Uri getContentUri() {
return contentUri;
}
public void setContentUri(Uri contentUri) {
this.contentUri = contentUri;
}
public int getState() { public int getState() {
return state; return state;
} }
public void setState(int state) { public void setState(final int state) {
this.state = state; this.state = state;
} }
public void setDateUploaded(Date date) { /**
this.dateUploaded = date; * @return array list of entityids for the depictions
*/
public List<DepictedItem> getDepictedItems() {
return depictedItems;
} }
public String getPageContents(Context applicationContext) { public void setWikidataPlace(final WikidataPlace wikidataPlace) {
StringBuilder buffer = new StringBuilder(); this.wikidataPlace = wikidataPlace;
buffer }
.append("== {{int:filedesc}} ==\n")
.append("{{Information\n")
.append("|description=").append(getDescription()).append("\n")
.append("|source=").append("{{own}}\n")
.append("|author=[[User:").append(creator).append("|").append(creator).append("]]\n");
String templatizedCreatedDate = getTemplatizedCreatedDate(); public WikidataPlace getWikidataPlace() {
if (!StringUtils.isBlank(templatizedCreatedDate)) { return wikidataPlace;
buffer.append("|date=").append(templatizedCreatedDate); }
}
buffer.append("}}").append("\n"); public long get_id() {
return _id;
}
//Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null public void set_id(final long _id) {
if (decimalCoords != null) { this._id = _id;
buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n"); }
}
buffer.append("== {{int:license-header}} ==\n") public String getDecimalCoords() {
.append(licenseTemplateFor(getLicense())).append("\n\n") return decimalCoords;
.append("{{Uploaded from Mobile|platform=Android|version=") }
.append(ConfigUtils.getVersionNameWithSha(applicationContext)).append("}}\n");
if(categories!=null&&categories.size()!=0) { public void setDecimalCoords(final String decimalCoords) {
for (int i = 0; i < categories.size(); i++) { this.decimalCoords = decimalCoords;
String category = categories.get(i); }
buffer.append("\n[[Category:").append(category).append("]]");
} public void setDepictedItems(final List<DepictedItem> depictedItems) {
} this.depictedItems = depictedItems;
else }
buffer.append("{{subst:unc}}");
return buffer.toString(); public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public String getMimeType() {
return mimeType;
} }
/** /**
* Returns upload date in either TEMPLATE_DATE_ACC_TO_EXIF or TEMPLATE_DATA_OTHER_SOURCE * Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
* @return * 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
*/ */
private String getTemplatizedCreatedDate() { public Map<String, String> getCaptions() {
if (dateCreated != null) { return captions;
java.text.SimpleDateFormat dateFormat = new java.text.SimpleDateFormat("yyyy-MM-dd");
if (UploadableFile.DateTimeWithSource.EXIF_SOURCE.equals(dateCreatedSource)) {
return String.format(Locale.ENGLISH, TEMPLATE_DATE_ACC_TO_EXIF, dateFormat.format(dateCreated)) + "\n";
} else {
return String.format(Locale.ENGLISH, TEMPLATE_DATA_OTHER_SOURCE, dateFormat.format(dateCreated)) + "\n";
}
}
return "";
} }
@Override public void setCaptions(Map<String, String> captions) {
public void setFilename(String filename) { this.captions = captions;
this.filename = filename;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public Contribution() {
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
@NonNull
private String licenseTemplateFor(String license) {
switch (license) {
case Prefs.Licenses.CC_BY_3:
return "{{self|cc-by-3.0}}";
case Prefs.Licenses.CC_BY_4:
return "{{self|cc-by-4.0}}";
case Prefs.Licenses.CC_BY_SA_3:
return "{{self|cc-by-sa-3.0}}";
case Prefs.Licenses.CC_BY_SA_4:
return "{{self|cc-by-sa-4.0}}";
case Prefs.Licenses.CC0:
return "{{self|cc-zero}}";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
public String getWikiDataEntityId() {
return wikiDataEntityId;
}
public String getWikiItemName() {
return wikiItemName;
}
/**
* When the corresponding wikidata entity is known as in case of nearby uploads, it can be set
* using the setter method
* @param wikiDataEntityId wikiDataEntityId
*/
public void setWikiDataEntityId(String wikiDataEntityId) {
this.wikiDataEntityId = wikiDataEntityId;
}
public void setWikiItemName(String wikiItemName) {
this.wikiItemName = wikiItemName;
}
public String getP18Value() {
return p18Value;
}
/**
* When the corresponding image property of wiki entity is known as in case of nearby uploads,
* it can be set using the setter method
* @param p18Value p18 value, image property of the wikidata item
*/
public void setP18Value(String p18Value) {
this.p18Value = p18Value;
}
public void setContentProviderUri(Uri contentProviderUri) {
this.contentProviderUri = contentProviderUri;
} }
@Override @Override
@ -259,48 +159,34 @@ public class Contribution extends Media {
} }
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(final Parcel dest, final int flags) {
super.writeToParcel(dest, flags); super.writeToParcel(dest, flags);
dest.writeLong(this._id); dest.writeLong(_id);
dest.writeParcelable(this.contentUri, flags); dest.writeInt(state);
dest.writeString(this.source); dest.writeLong(transferred);
dest.writeString(this.editSummary); dest.writeString(decimalCoords);
dest.writeInt(this.state); dest.writeString(dateCreatedSource);
dest.writeLong(this.transferred); dest.writeSerializable((HashMap) captions);
dest.writeString(this.decimalCoords);
dest.writeByte(this.isMultiple ? (byte) 1 : (byte) 0);
dest.writeString(this.wikiDataEntityId);
dest.writeString(this.wikiItemName);
dest.writeString(this.p18Value);
dest.writeParcelable(this.contentProviderUri, flags);
dest.writeString(this.dateCreatedSource);
} }
protected Contribution(Parcel in) { protected Contribution(final Parcel in) {
super(in); super(in);
this._id = in.readLong(); _id = in.readLong();
this.contentUri = in.readParcelable(Uri.class.getClassLoader()); state = in.readInt();
this.source = in.readString(); transferred = in.readLong();
this.editSummary = in.readString(); decimalCoords = in.readString();
this.state = in.readInt(); dateCreatedSource = in.readString();
this.transferred = in.readLong(); captions = (HashMap<String, String>) in.readSerializable();
this.decimalCoords = in.readString();
this.isMultiple = in.readByte() != 0;
this.wikiDataEntityId = in.readString();
this.wikiItemName = in.readString();
this.p18Value = in.readString();
this.contentProviderUri = in.readParcelable(Uri.class.getClassLoader());
this.dateCreatedSource = in.readString();
} }
public static final Creator<Contribution> CREATOR = new Creator<Contribution>() { public static final Creator<Contribution> CREATOR = new Creator<Contribution>() {
@Override @Override
public Contribution createFromParcel(Parcel source) { public Contribution createFromParcel(final Parcel source) {
return new Contribution(source); return new Contribution(source);
} }
@Override @Override
public Contribution[] newArray(int size) { public Contribution[] newArray(final int size) {
return new Contribution[size]; return new Contribution[size];
} }
}; };

View file

@ -1,19 +1,13 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
import android.Manifest; import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.filepicker.DefaultCallback; import fr.free.nrw.commons.filepicker.DefaultCallback;
import fr.free.nrw.commons.filepicker.FilePicker; import fr.free.nrw.commons.filepicker.FilePicker;
@ -23,12 +17,11 @@ import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.upload.UploadActivity; import fr.free.nrw.commons.upload.UploadActivity;
import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import java.util.ArrayList;
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA; import java.util.List;
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY; import javax.inject.Inject;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES; import javax.inject.Named;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_SOURCE; import javax.inject.Singleton;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
@Singleton @Singleton
public class ContributionController { public class ContributionController {
@ -109,7 +102,7 @@ public class ContributionController {
@Override @Override
public void onImagesPicked(@NonNull List<UploadableFile> imagesFiles, FilePicker.ImageSource source, int type) { public void onImagesPicked(@NonNull List<UploadableFile> imagesFiles, FilePicker.ImageSource source, int type) {
Intent intent = handleImagesPicked(activity, imagesFiles, getSourceFromImageSource(source)); Intent intent = handleImagesPicked(activity, imagesFiles);
activity.startActivity(intent); activity.startActivity(intent);
} }
}); });
@ -125,11 +118,9 @@ public class ContributionController {
* Attaches place object for nearby uploads * Attaches place object for nearby uploads
*/ */
private Intent handleImagesPicked(Context context, private Intent handleImagesPicked(Context context,
List<UploadableFile> imagesFiles, List<UploadableFile> imagesFiles) {
String source) {
Intent shareIntent = new Intent(context, UploadActivity.class); Intent shareIntent = new Intent(context, UploadActivity.class);
shareIntent.setAction(ACTION_INTERNAL_UPLOADS); shareIntent.setAction(ACTION_INTERNAL_UPLOADS);
shareIntent.putExtra(EXTRA_SOURCE, source);
shareIntent.putParcelableArrayListExtra(EXTRA_FILES, new ArrayList<>(imagesFiles)); shareIntent.putParcelableArrayListExtra(EXTRA_FILES, new ArrayList<>(imagesFiles));
Place place = defaultKvStore.getJson(PLACE_OBJECT, Place.class); Place place = defaultKvStore.getJson(PLACE_OBJECT, Place.class);
if (place != null) { if (place != null) {
@ -139,13 +130,4 @@ public class ContributionController {
return shareIntent; return shareIntent;
} }
/**
* Get image upload source
*/
private String getSourceFromImageSource(FilePicker.ImageSource source) {
if (source.equals(FilePicker.ImageSource.CAMERA_IMAGE)) {
return SOURCE_CAMERA;
}
return SOURCE_GALLERY;
}
} }

View file

@ -7,13 +7,10 @@ import androidx.room.Insert;
import androidx.room.OnConflictStrategy; import androidx.room.OnConflictStrategy;
import androidx.room.Query; import androidx.room.Query;
import androidx.room.Transaction; import androidx.room.Transaction;
import androidx.room.Update; import androidx.room.Update;
import io.reactivex.disposables.Disposable;
import java.util.List;
import io.reactivex.Completable; import io.reactivex.Completable;
import io.reactivex.Single; import io.reactivex.Single;
import java.util.List;
@Dao @Dao
public abstract class ContributionDao { public abstract class ContributionDao {
@ -40,9 +37,6 @@ public abstract class ContributionDao {
@Delete @Delete
public abstract Single<Integer> delete(Contribution contribution); public abstract Single<Integer> delete(Contribution contribution);
@Query("SELECT * from contribution WHERE contentProviderUri=:uri")
public abstract List<Contribution> getContributionWithUri(String uri);
@Query("SELECT * from contribution WHERE filename=:fileName") @Query("SELECT * from contribution WHERE filename=:fileName")
public abstract List<Contribution> getContributionWithTitle(String fileName); public abstract List<Contribution> getContributionWithTitle(String fileName);

View file

@ -1,33 +1,34 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import android.graphics.Color; import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.view.SimpleDraweeView;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
import com.facebook.drawee.view.SimpleDraweeView;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback; import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import java.util.HashMap; import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.Random; import java.util.Random;
import java.util.concurrent.TimeUnit;
import timber.log.Timber;
public class ContributionViewHolder extends RecyclerView.ViewHolder { public class ContributionViewHolder extends RecyclerView.ViewHolder {
private static final long TIMEOUT_SECONDS = 15;
private final Callback callback; private final Callback callback;
@BindView(R.id.contributionImage) @BindView(R.id.contributionImage)
SimpleDraweeView imageView; SimpleDraweeView imageView;
@ -37,20 +38,26 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.contributionProgress) ProgressBar progressView; @BindView(R.id.contributionProgress) ProgressBar progressView;
@BindView(R.id.failed_image_options) LinearLayout failedImageOptions; @BindView(R.id.failed_image_options) LinearLayout failedImageOptions;
private int position; private int position;
private Contribution contribution; private Contribution contribution;
private Random random = new Random(); private Random random = new Random();
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private final MediaClient mediaClient;
ContributionViewHolder(View parent, Callback callback) { ContributionViewHolder(View parent, Callback callback,
MediaClient mediaClient) {
super(parent); super(parent);
this.mediaClient = mediaClient;
ButterKnife.bind(this, parent); ButterKnife.bind(this, parent);
this.callback=callback; this.callback=callback;
} }
public void init(int position, Contribution contribution) { public void init(int position, Contribution contribution) {
this.contribution = contribution; this.contribution = contribution;
fetchAndDisplayCaption(contribution);
this.position = position; this.position = position;
String imageSource = chooseImageSource(contribution.thumbUrl, contribution.getLocalUri()); String imageSource = chooseImageSource(contribution.getThumbUrl(), contribution.getLocalUri());
if (!TextUtils.isEmpty(imageSource)) { if (!TextUtils.isEmpty(imageSource)) {
final ImageRequest imageRequest = final ImageRequest imageRequest =
ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource)) ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
@ -58,7 +65,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
.build(); .build();
imageView.setImageRequest(imageRequest); imageView.setImageRequest(imageRequest);
} }
titleView.setText(contribution.getDisplayTitle());
seqNumView.setText(String.valueOf(position + 1)); seqNumView.setText(String.valueOf(position + 1));
seqNumView.setVisibility(View.VISIBLE); seqNumView.setVisibility(View.VISIBLE);
@ -97,6 +103,38 @@ 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(Contribution contribution) {
if ((contribution.getState() != Contribution.STATE_COMPLETED)) {
titleView.setText(contribution.getDisplayTitle());
} else {
final String pageId = contribution.getPageId();
if (pageId != null) {
Timber.d("Fetching caption for %s", contribution.getFilename());
String wikibaseMediaId = PAGE_ID_PREFIX
+ pageId; // 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(MediaClient.NO_CAPTION)) {
titleView.setText(subscriber);
} else {
titleView.setText(contribution.getDisplayTitle());
}
}));
} else {
titleView.setText(contribution.getDisplayTitle());
}
}
}
/** /**
* Returns the image source for the image view, first preference is given to thumbUrl if that is * Returns the image source for the image view, first preference is given to thumbUrl if that is
* null, moves to local uri and if both are null return null * null, moves to local uri and if both are null return null

View file

@ -1,5 +1,9 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
import android.Manifest; import android.Manifest;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
@ -12,21 +16,12 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener; import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
import fr.free.nrw.commons.MediaDataExtractor;
import io.reactivex.disposables.Disposable;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.HandlerService;
@ -60,12 +55,11 @@ import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber; import timber.log.Timber;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
public class ContributionsFragment public class ContributionsFragment
extends CommonsDaggerSupportFragment extends CommonsDaggerSupportFragment
implements implements
@ -221,7 +215,7 @@ public class ContributionsFragment
@Override @Override
public void fetchMediaUriFor(Contribution contribution) { public void fetchMediaUriFor(Contribution contribution) {
Timber.d("Fetching thumbnail for %s", contribution.filename); Timber.d("Fetching thumbnail for %s", contribution.getFilename());
contributionsPresenter.fetchMediaDetails(contribution); contributionsPresenter.fetchMediaDetails(contribution);
} }
}); });

View file

@ -1,29 +1,28 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.media.MediaClient;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import fr.free.nrw.commons.R;
/** /**
* Represents The View Adapter for the List of Contributions * Represents The View Adapter for the List of Contributions
*/ */
public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionViewHolder> { public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionViewHolder> {
private Callback callback; private Callback callback;
private final MediaClient mediaClient;
private List<Contribution> contributions; private List<Contribution> contributions;
public ContributionsListAdapter(Callback callback) { public ContributionsListAdapter(Callback callback,
MediaClient mediaClient) {
this.callback = callback; this.callback = callback;
this.mediaClient = mediaClient;
contributions = new ArrayList<>(); contributions = new ArrayList<>();
} }
@ -36,7 +35,7 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
public ContributionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public ContributionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ContributionViewHolder viewHolder = new ContributionViewHolder( ContributionViewHolder viewHolder = new ContributionViewHolder(
LayoutInflater.from(parent.getContext()) LayoutInflater.from(parent.getContext())
.inflate(R.layout.layout_contribution, parent, false), callback); .inflate(R.layout.layout_contribution, parent, false), callback, mediaClient);
return viewHolder; return viewHolder;
} }
@ -63,7 +62,7 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
return contributions.get(position)._id; return contributions.get(position).get_id();
} }
public interface Callback { public interface Callback {

View file

@ -1,5 +1,8 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -10,31 +13,24 @@ import android.view.animation.AnimationUtils;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.LayoutManager; import androidx.recyclerview.widget.RecyclerView.LayoutManager;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback; import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaClient;
import static android.view.View.GONE; import java.util.ArrayList;
import static android.view.View.VISIBLE; import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
/** /**
* Created by root on 01.06.2018. * Created by root on 01.06.2018.
@ -60,6 +56,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
@Inject @Named("default_preferences") JsonKvStore kvStore; @Inject @Named("default_preferences") JsonKvStore kvStore;
@Inject ContributionController controller; @Inject ContributionController controller;
@Inject MediaClient mediaClient;
private Animation fab_close; private Animation fab_close;
private Animation fab_open; private Animation fab_open;
@ -89,7 +86,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
} }
private void initAdapter() { private void initAdapter() {
adapter = new ContributionsListAdapter(callback); adapter = new ContributionsListAdapter(callback, mediaClient);
adapter.setHasStableIds(true); adapter.setHasStableIds(true);
} }

View file

@ -1,45 +1,30 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import android.content.Context; import android.content.Context;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import fr.free.nrw.commons.MediaDataExtractor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener; import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
import fr.free.nrw.commons.db.AppDatabase; import fr.free.nrw.commons.db.AppDatabase;
import fr.free.nrw.commons.di.CommonsApplicationModule; import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.mwapi.UserClient; import fr.free.nrw.commons.mwapi.UserClient;
import fr.free.nrw.commons.utils.ExecutorUtils;
import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.NetworkUtils;
import io.reactivex.Observable;
import io.reactivex.Scheduler; import io.reactivex.Scheduler;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber; import timber.log.Timber;
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
/** /**
* The presenter class for Contributions * The presenter class for Contributions
*/ */
@ -105,12 +90,7 @@ public class ContributionsPresenter implements UserActionListener {
.observeOn(mainThreadScheduler) .observeOn(mainThreadScheduler)
.doOnNext(mwQueryLogEvent -> Timber.d("Received image %s", mwQueryLogEvent.title())) .doOnNext(mwQueryLogEvent -> Timber.d("Received image %s", mwQueryLogEvent.title()))
.filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted()).doOnNext(mwQueryLogEvent -> Timber.d("Image %s passed filters", mwQueryLogEvent.title())) .filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted()).doOnNext(mwQueryLogEvent -> Timber.d("Image %s passed filters", mwQueryLogEvent.title()))
.map(image -> { .map(image -> new Contribution(image, user))
Contribution contribution = new Contribution(null, null, image.title(),
"", -1, image.date(), image.date(), user,
"", "", STATE_COMPLETED);
return contribution;
})
.toList() .toList()
.subscribe(this::saveContributionsToDB, error -> { .subscribe(this::saveContributionsToDB, error -> {
Timber.e("Failed to fetch contributions: %s", error.getMessage()); Timber.e("Failed to fetch contributions: %s", error.getMessage());
@ -197,11 +177,11 @@ public class ContributionsPresenter implements UserActionListener {
@Override @Override
public void fetchMediaDetails(Contribution contribution) { public void fetchMediaDetails(Contribution contribution) {
compositeDisposable.add(mediaDataExtractor compositeDisposable.add(mediaDataExtractor
.getMediaFromFileName(contribution.filename) .getMediaFromFileName(contribution.getFilename())
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> { .subscribe(media -> {
contribution.thumbUrl=media.thumbUrl; contribution.setThumbUrl(media.getThumbUrl());
updateContribution(contribution); updateContribution(contribution);
})); }));
} }

View file

@ -1,14 +0,0 @@
package fr.free.nrw.commons.db;
import androidx.room.Database;
import androidx.room.RoomDatabase;
import androidx.room.TypeConverters;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionDao;
@Database(entities = {Contribution.class}, version = 1, exportSchema = false)
@TypeConverters({Converters.class})
abstract public class AppDatabase extends RoomDatabase {
public abstract ContributionDao getContributionDao();
}

View file

@ -0,0 +1,17 @@
package fr.free.nrw.commons.db
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.contributions.ContributionDao
/**
* The database for accessing the respective DAOs
*
*/
@Database(entities = [Contribution::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun contributionDao(): ContributionDao
}

View file

@ -1,22 +1,22 @@
package fr.free.nrw.commons.db; package fr.free.nrw.commons.db;
import android.net.Uri; import android.net.Uri;
import androidx.room.TypeConverter; import androidx.room.TypeConverter;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; 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.CommonsApplication;
import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.media.Depictions;
import fr.free.nrw.commons.upload.WikidataPlace;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* This class supplies converters to write/read types to/from the database.
*/
public class Converters { public class Converters {
public static Gson getGson() { public static Gson getGson() {
@ -44,33 +44,74 @@ public class Converters {
} }
@TypeConverter @TypeConverter
public static String listObjectToString(ArrayList<String> objectList) { public static String listObjectToString(List<String> objectList) {
return objectList == null ? null : getGson().toJson(objectList); return writeObjectToString(objectList);
} }
@TypeConverter @TypeConverter
public static ArrayList<String> stringToArrayListObject(String objectList) { public static List<String> stringToListObject(String objectList) {
return objectList == null ? null : getGson().fromJson(objectList,new TypeToken<ArrayList<String>>(){}.getType()); return readObjectWithTypeToken(objectList, new TypeToken<List<String>>() {});
} }
@TypeConverter @TypeConverter
public static String mapObjectToString(HashMap<String,String> objectList) { public static String mapObjectToString(Map<String,String> objectList) {
return objectList == null ? null : getGson().toJson(objectList); return writeObjectToString(objectList);
} }
@TypeConverter @TypeConverter
public static HashMap<String,String> stringToMap(String objectList) { public static Map<String,String> stringToMap(String objectList) {
return objectList == null ? null : getGson().fromJson(objectList,new TypeToken<HashMap<String,String>>(){}.getType()); return readObjectWithTypeToken(objectList, new TypeToken<Map<String,String>>(){});
} }
@TypeConverter @TypeConverter
public static String latlngObjectToString(LatLng latlng) { public static String latlngObjectToString(LatLng latlng) {
return latlng == null ? null : getGson().toJson(latlng); return writeObjectToString(latlng);
} }
@TypeConverter @TypeConverter
public static LatLng stringToLatLng(String objectList) { public static LatLng stringToLatLng(String objectList) {
return objectList == null ? null : getGson().fromJson(objectList,LatLng.class); return readObjectFromString(objectList,LatLng.class);
} }
@TypeConverter
public static String wikidataPlaceToString(WikidataPlace wikidataPlace) {
return writeObjectToString(wikidataPlace);
}
@TypeConverter
public static WikidataPlace stringToWikidataPlace(String wikidataPlace) {
return readObjectFromString(wikidataPlace, WikidataPlace.class);
}
@TypeConverter
public static String depictionListToString(List<DepictedItem> depictedItems) {
return writeObjectToString(depictedItems);
}
@TypeConverter
public static List<DepictedItem> stringToList(String depictedItems) {
return readObjectWithTypeToken(depictedItems, new TypeToken<List<DepictedItem>>() {});
}
@TypeConverter
public static String depictionsToString(Depictions depictedItems) {
return writeObjectToString(depictedItems);
}
@TypeConverter
public static Depictions stringToDepictions(String depictedItems) {
return readObjectFromString(depictedItems, Depictions.class);
}
private static String writeObjectToString(Object object) {
return object == null ? null : getGson().toJson(object);
}
private static<T> T readObjectFromString(String objectAsString, Class<T> clazz) {
return objectAsString == null ? null : getGson().fromJson(objectAsString, clazz);
}
private static <T> T readObjectWithTypeToken(String objectList, TypeToken<T> typeToken) {
return objectList == null ? null : getGson().fromJson(objectList, typeToken.getType());
}
} }

View file

@ -1,22 +1,12 @@
package fr.free.nrw.commons.delete; package fr.free.nrw.commons.delete;
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_DELETE;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Locale;
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.BuildConfig;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
@ -29,10 +19,16 @@ import io.reactivex.Single;
import io.reactivex.SingleSource; import io.reactivex.SingleSource;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Locale;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import timber.log.Timber; import timber.log.Timber;
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_DELETE;
/** /**
* Refactored async task to Rx * Refactored async task to Rx
*/ */
@ -104,7 +100,7 @@ public class DeleteHelper {
} }
String creatorName = creator.replace(" (page does not exist)", ""); String creatorName = creator.replace(" (page does not exist)", "");
return pageEditClient.prependEdit(media.filename, fileDeleteString + "\n", summary) return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary)
.flatMap(result -> { .flatMap(result -> {
if (result) { if (result) {
return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary); return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);

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 caption, 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,267 @@
package fr.free.nrw.commons.depictions.Media;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
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 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.GridViewAdapter;
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import java.util.List;
import javax.inject.Inject;
import timber.log.Timber;
/**
* Fragment for showing image list after selected an item from SearchActivity In Explore
*/
public class DepictedImagesFragment extends DaggerFragment implements DepictedImagesContract.View {
public static final String PAGE_ID_PREFIX = "M";
@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 caption, int position) {
if (!caption.trim().equals(getString(R.string.detail_caption_empty))) {
gridAdapter.getItem(position).setThumbnailTitle(caption);
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 (RuntimeException e) {
Timber.e(e);
}
}
progressBar.setVisibility(GONE);
isLoading = false;
statusTextView.setVisibility(GONE);
for (Media media : collection) {
final String pageId = media.getPageId();
if (pageId != null) {
presenter.replaceTitlesWithCaptions(PAGE_ID_PREFIX + pageId, mediaSize++);
}
}
}
}

View file

@ -0,0 +1,159 @@
package fr.free.nrw.commons.depictions.Media;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
import android.annotation.SuppressLint;
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 java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
/**
* 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);
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, 0)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.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, queryList.size())
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.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)
.subscribe(caption -> {
view.handleLabelforImage(caption, position);
}));
}
/**
* add items to query list
*/
@Override
public void addItemsToQueryList(List<Media> collection) {
queryList.addAll(collection);
}
}

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 mediaDetailPagerFragment;
/**
* 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 (mediaDetailPagerFragment !=null){
mediaDetailPagerFragment.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 (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
// set isFeaturedImage true for featured images, to include author field on media detail
mediaDetailPagerFragment = new MediaDetailPagerFragment(false, true);
FragmentManager supportFragmentManager = getSupportFragmentManager();
supportFragmentManager
.beginTransaction()
.replace(R.id.mediaContainer, mediaDetailPagerFragment)
.addToBackStack(null)
.commit();
supportFragmentManager.executePendingTransactions();
}
mediaDetailPagerFragment.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.getName());
intent.putExtra("entityId", depictedItem.getId());
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

@ -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,190 @@
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.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,158 @@
package fr.free.nrw.commons.depictions.subClass;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
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.disposables.CompositeDisposable;
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 timber.log.Timber;
/**
* 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.getId(), size++);
}
}
}
/**
* Logs and handles API error scenario
*/
private void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading queried depictions");
view.initErrorView();
view.showSnackbar();
}
}

View file

@ -0,0 +1,26 @@
package fr.free.nrw.commons.depictions.subClass.models
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
data class SparqlResponse(val results: Result) {
fun toDepictedItems() =
results.bindings.map {
DepictedItem(
it.itemLabel.value,
it.itemDescription?.value ?: "",
"",
false,
it.item.value.substringAfterLast("/")
)
}
}
data class Result(val bindings: List<Binding>)
data class Binding(
val item: SparqInfo,
val itemLabel: SparqInfo,
val itemDescription: SparqInfo? = null
)
data class SparqInfo(val type: String, val value: String)

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.CategoryDetailsActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.MainActivity; 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.SearchActivity;
import fr.free.nrw.commons.explore.categories.ExploreActivity; import fr.free.nrw.commons.explore.categories.ExploreActivity;
import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.notification.NotificationActivity;
@ -60,6 +61,9 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract CategoryDetailsActivity bindCategoryDetailsActivity(); abstract CategoryDetailsActivity bindCategoryDetailsActivity();
@ContributesAndroidInjector
abstract WikidataItemDetailsActivity bindDepictionDetailsActivity();
@ContributesAndroidInjector @ContributesAndroidInjector
abstract ExploreActivity bindExploreActivity(); 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.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionViewHolder; import fr.free.nrw.commons.contributions.ContributionViewHolder;
import fr.free.nrw.commons.contributions.ContributionsModule; 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.nearby.PlaceRenderer;
import fr.free.nrw.commons.review.ReviewController; import fr.free.nrw.commons.review.ReviewController;
import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.settings.SettingsFragment;
@ -33,7 +35,7 @@ import fr.free.nrw.commons.widget.PicOfDayAppWidget;
ActivityBuilderModule.class, ActivityBuilderModule.class,
FragmentBuilderModule.class, FragmentBuilderModule.class,
ServiceBuilderModule.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> { public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
void inject(CommonsApplication application); void inject(CommonsApplication application);

View file

@ -7,7 +7,6 @@ import android.content.Context;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import androidx.collection.LruCache; import androidx.collection.LruCache;
import androidx.room.Room; import androidx.room.Room;
import com.github.varunpant.quadtree.QuadTree;
import com.google.gson.Gson; import com.google.gson.Gson;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
@ -50,7 +49,6 @@ public class CommonsApplicationModule {
private Context applicationContext; private Context applicationContext;
public static final String IO_THREAD="io_thread"; public static final String IO_THREAD="io_thread";
public static final String MAIN_THREAD="main_thread"; public static final String MAIN_THREAD="main_thread";
private AppDatabase appDatabase;
public CommonsApplicationModule(Context applicationContext) { public CommonsApplicationModule(Context applicationContext) {
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
@ -105,6 +103,11 @@ public class CommonsApplicationModule {
return context.getContentResolver().acquireContentProviderClient(BuildConfig.CATEGORY_AUTHORITY); return context.getContentResolver().acquireContentProviderClient(BuildConfig.CATEGORY_AUTHORITY);
} }
/**
* This method is used to provide instance of DepictsContentProviderClient
* @param context context
* @return DepictsContentProviderClient*/
/** /**
* This method is used to provide instance of RecentSearchContentProviderClient * This method is used to provide instance of RecentSearchContentProviderClient
* which provides content of Recent Searches from database * which provides content of Recent Searches from database
@ -218,26 +221,15 @@ public class CommonsApplicationModule {
return Objects.toString(AppAdapter.get().getUserName(), ""); return Objects.toString(AppAdapter.get().getUserName(), "");
} }
/**
* Provides quad tree
*
* @return
*/
@Provides
public QuadTree providesQuadTres() {
return new QuadTree<>(-180, -90, +180, +90);
}
@Provides @Provides
@Singleton @Singleton
public AppDatabase provideAppDataBase() { public AppDatabase provideAppDataBase() {
appDatabase=Room.databaseBuilder(applicationContext, AppDatabase.class, "commons_room.db").build(); return Room.databaseBuilder(applicationContext, AppDatabase.class, "commons_room.db").build();
return appDatabase;
} }
@Provides @Provides
public ContributionDao providesContributionsDao() { public ContributionDao providesContributionsDao(AppDatabase appDatabase) {
return appDatabase.getContributionDao(); return appDatabase.contributionDao();
} }
@Provides @Provides

View file

@ -13,19 +13,18 @@ import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
* then that must be mentioned here to inject the dependencies * then that must be mentioned here to inject the dependencies
*/ */
@Module @Module
@SuppressWarnings({"WeakerAccess", "unused"}) @SuppressWarnings({ "WeakerAccess", "unused" })
public abstract class ContentProviderBuilderModule { public abstract class ContentProviderBuilderModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract CategoryContentProvider bindCategoryContentProvider(); abstract CategoryContentProvider bindCategoryContentProvider();
@ContributesAndroidInjector @ContributesAndroidInjector
abstract RecentSearchesContentProvider bindRecentSearchesContentProvider(); abstract RecentSearchesContentProvider bindRecentSearchesContentProvider();
@ContributesAndroidInjector @ContributesAndroidInjector
abstract BookmarkPicturesContentProvider bindBookmarkContentProvider(); abstract BookmarkPicturesContentProvider bindBookmarkContentProvider();
@ContributesAndroidInjector
abstract BookmarkLocationsContentProvider bindBookmarkLocationContentProvider();
@ContributesAndroidInjector
abstract BookmarkLocationsContentProvider bindBookmarkLocationContentProvider();
} }

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

View file

@ -1,24 +1,8 @@
package fr.free.nrw.commons.di; package fr.free.nrw.commons.di;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.gson.Gson; import com.google.gson.Gson;
import org.wikipedia.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.json.GsonUtil;
import org.wikipedia.login.LoginClient;
import java.io.File;
import java.util.concurrent.TimeUnit;
import javax.inject.Named;
import javax.inject.Singleton;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
@ -26,16 +10,30 @@ import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.actions.PageEditInterface; import fr.free.nrw.commons.actions.PageEditInterface;
import fr.free.nrw.commons.category.CategoryInterface; import fr.free.nrw.commons.category.CategoryInterface;
import fr.free.nrw.commons.kvstore.JsonKvStore; 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.media.MediaInterface;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.mwapi.UserInterface; import fr.free.nrw.commons.mwapi.UserInterface;
import fr.free.nrw.commons.review.ReviewInterface; import fr.free.nrw.commons.review.ReviewInterface;
import fr.free.nrw.commons.upload.UploadInterface; import fr.free.nrw.commons.upload.UploadInterface;
import fr.free.nrw.commons.upload.WikiBaseInterface;
import fr.free.nrw.commons.upload.depicts.DepictsInterface;
import fr.free.nrw.commons.wikidata.WikidataInterface; import fr.free.nrw.commons.wikidata.WikidataInterface;
import java.io.File;
import java.util.concurrent.TimeUnit;
import javax.inject.Named;
import javax.inject.Singleton;
import okhttp3.Cache; import okhttp3.Cache;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor; import okhttp3.logging.HttpLoggingInterceptor;
import okhttp3.logging.HttpLoggingInterceptor.Level;
import org.wikipedia.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.json.GsonUtil;
import org.wikipedia.login.LoginClient;
import timber.log.Timber; import timber.log.Timber;
@Module @Module
@ -72,7 +70,7 @@ public class NetworkingModule {
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(message -> { HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(message -> {
Timber.tag("OkHttp").v(message); Timber.tag("OkHttp").v(message);
}); });
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); httpLoggingInterceptor.level(BuildConfig.DEBUG ? Level.BODY: Level.BASIC);
return httpLoggingInterceptor; return httpLoggingInterceptor;
} }
@ -86,8 +84,7 @@ public class NetworkingModule {
toolsForgeUrl, toolsForgeUrl,
WIKIDATA_SPARQL_QUERY_URL, WIKIDATA_SPARQL_QUERY_URL,
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL, BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,
BuildConfig.WIKIMEDIA_API_HOST, gson);
gson);
} }
@Named(NAMED_COMMONS_CSRF) @Named(NAMED_COMMONS_CSRF)
@ -133,6 +130,7 @@ public class NetworkingModule {
return new WikiSite(BuildConfig.WIKIDATA_URL); return new WikiSite(BuildConfig.WIKIDATA_URL);
} }
/** /**
* Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere. * 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 * @return returns a singleton Gson instance
@ -163,6 +161,18 @@ public class NetworkingModule {
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class); return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.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 @Provides
@Singleton @Singleton
public UploadInterface provideUploadInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { public UploadInterface provideUploadInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
@ -198,6 +208,12 @@ public class NetworkingModule {
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, MediaInterface.class); return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, MediaInterface.class);
} }
@Provides
@Singleton
public MediaDetailInterface providesMediaDetailInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikisite) {
return ServiceFactory.get(commonsWikisite, BuildConfig.COMMONS_URL, MediaDetailInterface.class);
}
@Provides @Provides
@Singleton @Singleton
public CategoryInterface provideCategoryInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { 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.R;
import fr.free.nrw.commons.category.CategoryImagesCallback; import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.explore.categories.SearchCategoryFragment; 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.images.SearchImageFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment; import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment;
@ -50,6 +51,7 @@ public class SearchActivity extends NavigationBaseActivity
private SearchImageFragment searchImageFragment; private SearchImageFragment searchImageFragment;
private SearchCategoryFragment searchCategoryFragment; private SearchCategoryFragment searchCategoryFragment;
private SearchDepictionsFragment searchDepictionsFragment;
private RecentSearchesFragment recentSearchesFragment; private RecentSearchesFragment recentSearchesFragment;
private FragmentManager supportFragmentManager; private FragmentManager supportFragmentManager;
private MediaDetailPagerFragment mediaDetails; private MediaDetailPagerFragment mediaDetails;
@ -68,6 +70,7 @@ public class SearchActivity extends NavigationBaseActivity
setSearchHistoryFragment(); setSearchHistoryFragment();
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(viewPagerAdapter); viewPager.setAdapter(viewPagerAdapter);
viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive
tabLayout.setupWithViewPager(viewPager); tabLayout.setupWithViewPager(viewPager);
setTabs(); setTabs();
searchView.setQueryHint(getString(R.string.search_commons)); searchView.setQueryHint(getString(R.string.search_commons));
@ -93,11 +96,14 @@ public class SearchActivity extends NavigationBaseActivity
List<Fragment> fragmentList = new ArrayList<>(); List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>(); List<String> titleList = new ArrayList<>();
searchImageFragment = new SearchImageFragment(); searchImageFragment = new SearchImageFragment();
searchDepictionsFragment = new SearchDepictionsFragment();
searchCategoryFragment= new SearchCategoryFragment(); searchCategoryFragment= new SearchCategoryFragment();
fragmentList.add(searchImageFragment); fragmentList.add(searchImageFragment);
titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase()); titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase());
fragmentList.add(searchCategoryFragment); fragmentList.add(searchCategoryFragment);
titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase()); 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.setTabData(fragmentList, titleList);
viewPagerAdapter.notifyDataSetChanged(); viewPagerAdapter.notifyDataSetChanged();
@ -112,6 +118,11 @@ public class SearchActivity extends NavigationBaseActivity
viewPager.setVisibility(View.VISIBLE); viewPager.setVisibility(View.VISIBLE);
tabLayout.setVisibility(View.VISIBLE); tabLayout.setVisibility(View.VISIBLE);
searchHistoryContainer.setVisibility(View.GONE); searchHistoryContainer.setVisibility(View.GONE);
if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) {
searchDepictionsFragment.updateDepictionList(query.toString());
}
if (FragmentUtils.isFragmentUIActive(searchImageFragment)) { if (FragmentUtils.isFragmentUIActive(searchImageFragment)) {
searchImageFragment.updateImageList(query.toString()); searchImageFragment.updateImageList(query.toString());
} }
@ -119,6 +130,7 @@ public class SearchActivity extends NavigationBaseActivity
if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) { if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) {
searchCategoryFragment.updateCategoryList(query.toString()); searchCategoryFragment.updateCategoryList(query.toString());
} }
}else { }else {
//Open RecentSearchesFragment //Open RecentSearchesFragment
recentSearchesFragment.updateRecentSearches(); 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() { private void initErrorView() {
progressBar.setVisibility(GONE); progressBar.setVisibility(GONE);
categoriesNotFoundView.setVisibility(VISIBLE); 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,175 @@
package fr.free.nrw.commons.explore.depictions;
import androidx.annotation.Nullable;
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 fr.free.nrw.commons.wikidata.WikidataProperties;
import io.reactivex.Observable;
import io.reactivex.Single;
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.List;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.wikipedia.wikidata.DataValue.DataValueString;
import org.wikipedia.wikidata.Statement_partial;
/**
* 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(DepictedItem::new);
}
/**
* 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(claimsResponse -> {
final List<Statement_partial> imageClaim = claimsResponse.getClaims()
.get(WikidataProperties.IMAGE.getPropertyName());
if (imageClaim != null) {
final DataValueString dataValue = (DataValueString) imageClaim
.get(0)
.getMainSnak()
.getDataValue();
return getThumbnailUrl((dataValue.getValue()));
}
return NO_DEPICTED_IMAGE;
})
.singleOrError();
}
/**
* @return list of images for a particular depict entity
*/
public Observable<List<Media>> fetchImagesForDepictedItem(String query, 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(),
"",
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,237 @@
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() {
isLoading = false;
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(boolean isLoading) {
depictionNotFound.setVisibility(GONE);
bottomProgressBar.setVisibility(View.VISIBLE);
progressBar.setVisibility(GONE);
this.isLoading = isLoading;
}
@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(boolean isLoading);
/**
* 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,180 @@
package fr.free.nrw.commons.explore.depictions;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
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 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 timber.log.Timber;
/**
* 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(true);
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");
view.initErrorView();
view.showSnackbar();
}
/**
* 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.getId(), 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.getName());
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

@ -1,5 +1,9 @@
package fr.free.nrw.commons.explore.images; package fr.free.nrw.commons.explore.images;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
@ -8,23 +12,12 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.pedrogomez.renderers.RVRendererAdapter;
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 butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import com.pedrogomez.renderers.RVRendererAdapter;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
@ -37,11 +30,14 @@ import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
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 timber.log.Timber; import timber.log.Timber;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
/** /**
* Displays the image search screen. * Displays the image search screen.
*/ */
@ -67,6 +63,11 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
@Named("default_preferences") @Named("default_preferences")
JsonKvStore defaultKvStore; 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 RVRendererAdapter<Media> imagesAdapter;
private List<Media> queryList = new ArrayList<>(); private List<Media> queryList = new ArrayList<>();
@ -101,7 +102,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false); View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
ButterKnife.bind(this, rootView); 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())); imagesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
} }
else{ else{
@ -198,10 +199,39 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
progressBar.setVisibility(GONE); progressBar.setVisibility(GONE);
imagesAdapter.addAll(mediaList); imagesAdapter.addAll(mediaList);
imagesAdapter.notifyDataSetChanged(); imagesAdapter.notifyDataSetChanged();
((SearchActivity) getContext()).viewPagerNotifyDataSetChanged(); ((SearchActivity)getContext()).viewPagerNotifyDataSetChanged();
for (Media m : mediaList) {
final String pageId = m.getPageId();
if (pageId != null) {
replaceTitlesWithCaptions(PAGE_ID_PREFIX + pageId, 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 position) {
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(subscriber -> {
handleLabelforImage(subscriber, position);
}));
}
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 * Logs and handles API error scenario
* @param throwable * @param throwable
@ -221,7 +251,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
private void initErrorView() { private void initErrorView() {
progressBar.setVisibility(GONE); progressBar.setVisibility(GONE);
imagesNotFoundView.setVisibility(VISIBLE); 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 @Override
public void render() { public void render() {
Media item = getContent(); Media item = getContent();
tvImageName.setText(item.getDisplayTitle()); tvImageName.setText(item.getThumbnailTitle());
browseImage.setImageURI(item.getThumbUrl()); browseImage.setImageURI(item.getThumbUrl());
setAuthorView(item, categoryImageAuthor); 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

@ -0,0 +1,30 @@
package fr.free.nrw.commons.media
import android.os.Parcelable
import androidx.annotation.WorkerThread
import fr.free.nrw.commons.wikidata.WikidataProperties.DEPICTS
import kotlinx.android.parcel.Parcelize
import org.wikipedia.wikidata.DataValue.DataValueEntityId
import org.wikipedia.wikidata.Entities
import java.util.*
@Parcelize
data class Depictions(val depictions: List<IdAndLabel>) : Parcelable {
companion object {
@JvmStatic
@WorkerThread
fun from(entities: Entities, mediaClient: MediaClient) =
Depictions(
entities.first?.statements
?.getOrElse(DEPICTS.propertyName, { emptyList() })
?.map { statement ->
(statement.mainSnak.dataValue as DataValueEntityId).value.id
}
?.map { id -> IdAndLabel(id, fetchLabel(mediaClient, id)) }
?: emptyList()
)
private fun fetchLabel(mediaClient: MediaClient, id: String) =
mediaClient.getLabelForDepiction(id, Locale.getDefault().language).blockingGet()
}
}

View file

@ -0,0 +1,14 @@
package fr.free.nrw.commons.media
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import org.wikipedia.wikidata.Entities
@Parcelize
data class IdAndLabel(val entityId: String, val entityLabel: String) : Parcelable {
constructor(entityId: String, entities: MutableMap<String, Entities.Entity>) : this(
entityId,
entities.values.first().labels().values.first().value()
)
}

View file

@ -2,23 +2,23 @@ package fr.free.nrw.commons.media;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import fr.free.nrw.commons.Media;
import org.wikipedia.dataclient.mwapi.MwQueryResponse; import fr.free.nrw.commons.utils.CommonsDateUtil;
import io.reactivex.Observable;
import io.reactivex.Single;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import fr.free.nrw.commons.Media; import org.wikipedia.wikidata.Entities;
import fr.free.nrw.commons.utils.CommonsDateUtil; import org.wikipedia.wikidata.Entities.Entity;
import io.reactivex.Observable; import org.wikipedia.wikidata.Entities.Label;
import io.reactivex.Single;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -28,13 +28,17 @@ import timber.log.Timber;
public class MediaClient { public class MediaClient {
private final MediaInterface mediaInterface; private final MediaInterface mediaInterface;
private final MediaDetailInterface mediaDetailInterface;
//OkHttpJsonApiClient used JsonKvStore for this. I don't know why. //OkHttpJsonApiClient used JsonKvStore for this. I don't know why.
private Map<String, Map<String, String>> continuationStore; private Map<String, Map<String, String>> continuationStore;
public static final String NO_CAPTION = "No caption";
private static final String NO_DEPICTION = "No depiction";
@Inject @Inject
public MediaClient(MediaInterface mediaInterface) { public MediaClient(MediaInterface mediaInterface, MediaDetailInterface mediaDetailInterface) {
this.mediaInterface = mediaInterface; this.mediaInterface = mediaInterface;
this.mediaDetailInterface = mediaDetailInterface;
this.continuationStore = new HashMap<>(); this.continuationStore = new HashMap<>();
} }
@ -88,7 +92,7 @@ public class MediaClient {
*/ */
public Single<List<Media>> getMediaListFromSearch(String keyword) { public Single<List<Media>> getMediaListFromSearch(String keyword) {
return responseToMediaList( 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, continuationStore.get("search_" + keyword)) : //if true
mediaInterface.getMediaListFromSearch(keyword, 10, Collections.emptyMap()), //if false mediaInterface.getMediaListFromSearch(keyword, 10, Collections.emptyMap()), //if false
"search_" + keyword); "search_" + keyword);
@ -152,6 +156,7 @@ public class MediaClient {
.single(Media.EMPTY); .single(Media.EMPTY);
} }
@NonNull @NonNull
public Single<String> getPageHtml(String title){ public Single<String> getPageHtml(String title){
return mediaInterface.getPageHtml(title) return mediaInterface.getPageHtml(title)
@ -160,4 +165,62 @@ public class MediaClient {
.map(MwParseResult::text) .map(MwParseResult::text)
.first(""); .first("");
} }
}
/**
* @return caption for image using wikibaseIdentifier
*/
public Single<String> getCaptionByWikibaseIdentifier(String wikibaseIdentifier) {
return mediaDetailInterface.getCaptionForImage(Locale.getDefault().getLanguage(), wikibaseIdentifier)
.map(mediaDetailResponse -> {
if (isSuccess(mediaDetailResponse)) {
for (Entity wikibaseItem : mediaDetailResponse.entities().values()) {
for (Label label : wikibaseItem.labels().values()) {
return label.value();
}
}
}
return NO_CAPTION;
})
.singleOrError();
}
private boolean isSuccess(Entities response) {
return response != null && response.getSuccess() == 1 && response.entities() != null;
}
/**
* 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<Depictions> getDepictions(String filename) {
return mediaDetailInterface.fetchEntitiesByFileName(Locale.getDefault().getLanguage(), filename)
.map(entities -> Depictions.from(entities, this))
.singleOrError();
}
/**
* Gets labels for Depictions using Entity Id from MediaWikiAPI
*
* @param entityId EntityId (Ex: Q81566) of the depict entity
* @return label
*/
public Single<String> getLabelForDepiction(String entityId, String language) {
return mediaDetailInterface.getEntity(entityId, language)
.map(entities -> {
if (isSuccess(entities)) {
for (Entity entity : entities.entities().values()) {
for (Label label : entity.labels().values()) {
return label.value();
}
}
}
throw new RuntimeException("failed getEntities");
})
.singleOrError();
}
}

View file

@ -1,10 +1,13 @@
package fr.free.nrw.commons.media; package fr.free.nrw.commons.media;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.graphics.drawable.Animatable;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Intent;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Animatable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
@ -22,28 +25,17 @@ import android.widget.ScrollView;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.controller.ControllerListener;
import com.facebook.drawee.view.SimpleDraweeView;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.util.DateUtil;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import javax.inject.Inject;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
import androidx.annotation.Nullable; import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.controller.ControllerListener;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.SimpleDraweeView;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor; import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
@ -53,19 +45,24 @@ import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.contributions.ContributionsFragment; import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.delete.DeleteHelper; import fr.free.nrw.commons.delete.DeleteHelper;
import fr.free.nrw.commons.delete.ReasonBuilder; 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.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.ui.widget.CompatTextView; import fr.free.nrw.commons.ui.widget.CompatTextView;
import fr.free.nrw.commons.ui.widget.HtmlTextView; 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 fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.util.DateUtil;
import timber.log.Timber; import timber.log.Timber;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
public class MediaDetailFragment extends CommonsDaggerSupportFragment { public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private boolean editable; private boolean editable;
@ -108,6 +105,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
LinearLayout imageSpacer; LinearLayout imageSpacer;
@BindView(R.id.mediaDetailTitle) @BindView(R.id.mediaDetailTitle)
TextView title; 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) @BindView(R.id.mediaDetailDesc)
HtmlTextView desc; HtmlTextView desc;
@BindView(R.id.mediaDetailAuthor) @BindView(R.id.mediaDetailAuthor)
@ -126,6 +129,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
LinearLayout nominatedForDeletion; LinearLayout nominatedForDeletion;
@BindView(R.id.mediaDetailCategoryContainer) @BindView(R.id.mediaDetailCategoryContainer)
LinearLayout categoryContainer; LinearLayout categoryContainer;
@BindView(R.id.media_detail_depiction_container)
LinearLayout depictionContainer;
@BindView(R.id.authorLinearLayout) @BindView(R.id.authorLinearLayout)
LinearLayout authorLayout; LinearLayout authorLayout;
@BindView(R.id.nominateDeletion) @BindView(R.id.nominateDeletion)
@ -134,8 +139,15 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
ScrollView scrollView; ScrollView scrollView;
private ArrayList<String> categoryNames; 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 Depictions depictions;
private boolean categoriesLoaded = false; private boolean categoriesLoaded = false;
private boolean categoriesPresent = false; private boolean categoriesPresent = false;
private boolean depictionLoaded = false;
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once! private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
private ViewTreeObserver.OnScrollChangedListener scrollListener; private ViewTreeObserver.OnScrollChangedListener scrollListener;
@ -243,7 +255,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
desc.setHtmlText(media.getDescription()); desc.setHtmlText(media.getDescription());
license.setText(media.getLicense()); license.setText(media.getLicense());
Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename()) Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename(), media.getPageId())
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(this::setTextFields); .subscribe(this::setTextFields);
@ -318,18 +330,32 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
coordinates.setText(prettyCoordinates(media)); coordinates.setText(prettyCoordinates(media));
uploadedDate.setText(prettyUploadedDate(media)); uploadedDate.setText(prettyUploadedDate(media));
mediaDiscussion.setText(prettyDiscussion(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.clear();
categoryNames.addAll(media.getCategories()); categoryNames.addAll(media.getCategories());
depictions=media.getDepiction();
depictionLoaded = true;
categoriesLoaded = true; categoriesLoaded = true;
categoriesPresent = (categoryNames.size() > 0); categoriesPresent = (categoryNames.size() > 0);
if (!categoriesPresent) { if (!categoriesPresent) {
// Stick in a filler element. // Stick in a filler element.
categoryNames.add(getString(R.string.detail_panel_cats_none)); categoryNames.add(getString(R.string.detail_panel_cats_none));
} }
rebuildCatList(); rebuildCatList();
if(depictions != null) {
rebuildDepictionList();
}
else depictsLayout.setVisibility(GONE);
if (media.getCreator() == null || media.getCreator().equals("")) { if (media.getCreator() == null || media.getCreator().equals("")) {
authorLayout.setVisibility(GONE); authorLayout.setVisibility(GONE);
} else { } else {
@ -339,6 +365,21 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
checkDeletion(media); checkDeletion(media);
} }
/**
* Populates media details fragment with depiction list
*/
private void rebuildDepictionList() {
depictionContainer.removeAllViews();
for (IdAndLabel depiction : depictions.getDepictions()) {
depictionContainer.addView(
buildDepictLabel(
depiction.getEntityLabel(),
depiction.getEntityId(),
depictionContainer
));
}
}
@OnClick(R.id.mediaDetailLicense) @OnClick(R.id.mediaDetailLicense)
public void onMediaDetailLicenceClicked(){ public void onMediaDetailLicenceClicked(){
String url = media.getLicenseUrl(); String url = media.getLicenseUrl();
@ -505,6 +546,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_category_item, depictionContainer, false);
final CompatTextView textView = item.findViewById(R.id.mediaDetailCategoryItemText);
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.getName());
intent.putExtra("entityId", depictedItem.getId());
getContext().startActivity(intent);
});
}
return item;
}
private View buildCatLabel(final String catName, ViewGroup categoryContainer) { private View buildCatLabel(final String catName, ViewGroup categoryContainer) {
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false); final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false);
final CompatTextView textView = item.findViewById(R.id.mediaDetailCategoryItemText); final CompatTextView textView = item.findViewById(R.id.mediaDetailCategoryItemText);
@ -534,9 +595,24 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
image.setAlpha(1.0f - scrollPercentage); 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) { private String prettyDescription(Media media) {
// @todo use UI language when multilingual descs are available // @todo use UI language when multilingual descs are available
String desc = media.getDescription(locale.getLanguage()).trim(); String desc = media.getDescription();
if (desc.equals("")) { if (desc.equals("")) {
return getString(R.string.detail_description_empty); return getString(R.string.detail_description_empty);
} else { } else {
@ -582,7 +658,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
} }
private void checkDeletion(Media media){ private void checkDeletion(Media media){
if (media.getRequestedDeletion()){ if (media.isRequestedDeletion()){
delete.setVisibility(GONE); delete.setVisibility(GONE);
nominatedForDeletion.setVisibility(VISIBLE); nominatedForDeletion.setVisibility(VISIBLE);
} else if (!isCategoryImage) { } else if (!isCategoryImage) {

View file

@ -0,0 +1,37 @@
package fr.free.nrw.commons.media;
import io.reactivex.Observable;
import org.wikipedia.wikidata.Entities;
import retrofit2.http.GET;
import retrofit2.http.Query;
/**
* Interface for interacting with Commons Structured Data related APIs
*/
public interface MediaDetailInterface {
/**
* Fetches entity 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&sites=commonswiki")
Observable<Entities> fetchEntitiesByFileName(@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<Entities> getEntity(@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<Entities> getCaptionForImage(@Query("languages") String language, @Query("ids") String wikibaseIdentifier);
}

View file

@ -13,6 +13,7 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter; import androidx.fragment.app.FragmentStatePagerAdapter;

View file

@ -4,6 +4,7 @@ import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import java.util.Map; import java.util.Map;
import fr.free.nrw.commons.depictions.models.DepictionResponse;
import io.reactivex.Observable; import io.reactivex.Observable;
import retrofit2.http.GET; import retrofit2.http.GET;
import retrofit2.http.Query; import retrofit2.http.Query;
@ -83,4 +84,23 @@ public interface MediaInterface {
@GET("w/api.php?format=json&action=parse&prop=text") @GET("w/api.php?format=json&action=parse&prop=text")
Observable<MwParseResponse> getPageHtml(@Query("page") String title); 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

@ -1,38 +1,34 @@
package fr.free.nrw.commons.mwapi; package fr.free.nrw.commons.mwapi;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.gson.Gson; import com.google.gson.Gson;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.achievements.FeaturedImages; import fr.free.nrw.commons.achievements.FeaturedImages;
import fr.free.nrw.commons.achievements.FeedbackResponse; import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.campaigns.CampaignResponseDTO; import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import fr.free.nrw.commons.depictions.subClass.models.SparqlResponse;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.model.NearbyResponse; import fr.free.nrw.commons.nearby.model.NearbyResponse;
import fr.free.nrw.commons.nearby.model.NearbyResultItem; import fr.free.nrw.commons.nearby.model.NearbyResultItem;
import fr.free.nrw.commons.upload.FileUtils; 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.utils.ConfigUtils;
import fr.free.nrw.commons.wikidata.model.GetWikidataEditCountResponse; import fr.free.nrw.commons.wikidata.model.GetWikidataEditCountResponse;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Singleton;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
import org.jetbrains.annotations.NotNull;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -40,208 +36,227 @@ import timber.log.Timber;
*/ */
@Singleton @Singleton
public class OkHttpJsonApiClient { public class OkHttpJsonApiClient {
private static final String THUMB_SIZE = "640";
private final OkHttpClient okHttpClient; private final OkHttpClient okHttpClient;
private final HttpUrl wikiMediaToolforgeUrl; private final HttpUrl wikiMediaToolforgeUrl;
private final String sparqlQueryUrl; private final String sparqlQueryUrl;
private final String campaignsUrl; private final String campaignsUrl;
private final String commonsBaseUrl; private final Gson gson;
private Gson gson;
@Inject @Inject
public OkHttpJsonApiClient(OkHttpClient okHttpClient, public OkHttpJsonApiClient(OkHttpClient okHttpClient,
HttpUrl wikiMediaToolforgeUrl, HttpUrl wikiMediaToolforgeUrl,
String sparqlQueryUrl, String sparqlQueryUrl,
String campaignsUrl, String campaignsUrl,
String commonsBaseUrl, Gson gson) {
Gson gson) { this.okHttpClient = okHttpClient;
this.okHttpClient = okHttpClient; this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl; this.sparqlQueryUrl = sparqlQueryUrl;
this.sparqlQueryUrl = sparqlQueryUrl; this.campaignsUrl = campaignsUrl;
this.campaignsUrl = campaignsUrl; this.gson = gson;
this.commonsBaseUrl = commonsBaseUrl; }
this.gson = gson;
@NonNull
public Single<Integer> getUploadCount(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
urlBuilder
.addPathSegments("uploadsbyuser.py")
.addQueryParameter("user", userName);
if (ConfigUtils.isBetaFlavour()) {
urlBuilder.addQueryParameter("labs", "commonswiki");
} }
@NonNull Request request = new Request.Builder()
public Single<Integer> getUploadCount(String userName) { .url(urlBuilder.build())
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder(); .build();
urlBuilder
.addPathSegments("uploadsbyuser.py")
.addQueryParameter("user", userName);
if (ConfigUtils.isBetaFlavour()) { return Single.fromCallable(() -> {
urlBuilder.addQueryParameter("labs", "commonswiki"); Response response = okHttpClient.newCall(request).execute();
if (response != null && response.isSuccessful()) {
ResponseBody responseBody = response.body();
if (null != responseBody) {
String responseBodyString = responseBody.string().trim();
if (!TextUtils.isEmpty(responseBodyString)) {
try {
return Integer.parseInt(responseBodyString);
} catch (NumberFormatException e) {
Timber.e(e);
}
}
}
}
return 0;
});
}
@NonNull
public Single<Integer> getWikidataEdits(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
urlBuilder
.addPathSegments("wikidataedits.py")
.addQueryParameter("user", userName);
if (ConfigUtils.isBetaFlavour()) {
urlBuilder.addQueryParameter("labs", "commonswiki");
}
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return Single.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null &&
response.isSuccessful() && response.body() != null) {
String json = response.body().string();
if (json == null) {
return 0;
}
GetWikidataEditCountResponse countResponse = gson
.fromJson(json, GetWikidataEditCountResponse.class);
if (null != countResponse) {
return countResponse.getWikidataEditCount();
}
}
return 0;
});
}
/**
* This takes userName as input, which is then used to fetch the feedback/achievements statistics
* using OkHttp and JavaRx. This function return JSONObject
*
* @param userName MediaWiki user name
* @return
*/
public Single<FeedbackResponse> getAchievements(String userName) {
final String fetchAchievementUrlTemplate =
wikiMediaToolforgeUrl + (ConfigUtils.isBetaFlavour() ? "/feedback.py?labs=commonswiki"
: "/feedback.py");
return Single.fromCallable(() -> {
String url = String.format(
Locale.ENGLISH,
fetchAchievementUrlTemplate,
userName);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
Timber.d("Response for achievements is %s", json);
try {
return gson.fromJson(json, FeedbackResponse.class);
} catch (Exception e) {
return new FeedbackResponse(0, 0, 0, new FeaturedImages(0, 0), 0, "");
} }
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return Single.fromCallable(() -> { }
Response response = okHttpClient.newCall(request).execute(); return null;
if (response != null && response.isSuccessful()) { });
ResponseBody responseBody = response.body(); }
if (null != responseBody) {
String responseBodyString = responseBody.string().trim();
if (!TextUtils.isEmpty(responseBodyString)) {
try {
return Integer.parseInt(responseBodyString);
} catch (NumberFormatException e) {
Timber.e(e);
}
}
}
}
return 0;
});
}
@NonNull public Observable<List<Place>> getNearbyPlaces(LatLng cur, String language, double radius) throws IOException {
public Single<Integer> getWikidataEdits(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
urlBuilder
.addPathSegments("wikidataedits.py")
.addQueryParameter("user", userName);
if (ConfigUtils.isBetaFlavour()) {
urlBuilder.addQueryParameter("labs", "commonswiki");
}
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return Single.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null &&
response.isSuccessful() && response.body() != null) {
String json = response.body().string();
if (json == null) {
return 0;
}
GetWikidataEditCountResponse countResponse = gson.fromJson(json, GetWikidataEditCountResponse.class);
if (null != countResponse) {
return countResponse.getWikidataEditCount();
}
}
return 0;
});
}
/**
* This takes userName as input, which is then used to fetch the feedback/achievements
* statistics using OkHttp and JavaRx. This function return JSONObject
*
* @param userName MediaWiki user name
* @return
*/
public Single<FeedbackResponse> getAchievements(String userName) {
final String fetchAchievementUrlTemplate =
wikiMediaToolforgeUrl + (ConfigUtils.isBetaFlavour() ? "/feedback.py?labs=commonswiki" : "/feedback.py");
return Single.fromCallable(() -> {
String url = String.format(
Locale.ENGLISH,
fetchAchievementUrlTemplate,
userName);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
Timber.d("Response for achievements is %s", json);
try {
return gson.fromJson(json, FeedbackResponse.class);
} catch (Exception e) {
return new FeedbackResponse(0, 0, 0, new FeaturedImages(0, 0), 0, "");
}
}
return null;
});
}
public Observable<List<Place>> getNearbyPlaces(LatLng cur, String lang, double radius) throws IOException {
String wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq"); String wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq");
String query = wikidataQuery String query = wikidataQuery
.replace("${RAD}", String.format(Locale.ROOT, "%.2f", radius)) .replace("${RAD}", String.format(Locale.ROOT, "%.2f", radius))
.replace("${LAT}", String.format(Locale.ROOT, "%.4f", cur.getLatitude())) .replace("${LAT}", String.format(Locale.ROOT, "%.4f", cur.getLatitude()))
.replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.getLongitude())) .replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.getLongitude()))
.replace("${LANG}", lang); .replace("${LANG}", language);
HttpUrl.Builder urlBuilder = HttpUrl HttpUrl.Builder urlBuilder = HttpUrl
.parse(sparqlQueryUrl) .parse(sparqlQueryUrl)
.newBuilder() .newBuilder()
.addQueryParameter("query", query) .addQueryParameter("query", query)
.addQueryParameter("format", "json"); .addQueryParameter("format", "json");
Request request = new Request.Builder() Request request = new Request.Builder()
.url(urlBuilder.build()) .url(urlBuilder.build())
.build(); .build();
return Observable.fromCallable(() -> { return Observable.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute(); Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) { if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string(); String json = response.body().string();
if (json == null) { if (json == null) {
return new ArrayList<>(); return new ArrayList<>();
}
NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class);
List<NearbyResultItem> bindings = nearbyResponse.getResults().getBindings();
List<Place> places = new ArrayList<>();
for (NearbyResultItem item : bindings) {
places.add(Place.from(item));
}
return places;
}
return new ArrayList<>();
});
}
public Single<CampaignResponseDTO> getCampaigns() {
return Single.fromCallable(() -> {
Request request = new Request.Builder().url(campaignsUrl)
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
return gson.fromJson(json, CampaignResponseDTO.class);
}
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);
} }
NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class);
List<NearbyResultItem> bindings = nearbyResponse.getResults().getBindings();
List<Place> places = new ArrayList<>();
for (NearbyResultItem item : bindings) {
places.add(Place.from(item));
}
return places;
}
return new ArrayList<>();
});
}
return builder; /**
} * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
* bridge -> suspended bridge, aqueduct, etc
*/
public Observable<List<DepictedItem>> getChildQIDs(String qid) throws IOException {
return depictedItemsFrom(sparqlQuery(qid, "/queries/subclasses_query.rq"));
}
/**
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
* bridge -> suspended bridge, aqueduct, etc
*/
public Observable<List<DepictedItem>> getParentQIDs(String qid) throws IOException {
return depictedItemsFrom(sparqlQuery(qid, "/queries/parentclasses_query.rq"));
}
private Observable<List<DepictedItem>> depictedItemsFrom(Request request) {
return Observable.fromCallable(() -> {
try (ResponseBody body = okHttpClient.newCall(request).execute().body()) {
return gson.fromJson(body.string(), SparqlResponse.class).toDepictedItems();
}catch (Exception e) {
Timber.e(e);
return new ArrayList<DepictedItem>();
}
}).doOnError(Timber::e);
}
@NotNull
private Request sparqlQuery(String qid, String fileName) throws IOException {
String query = FileUtils.readFromResource(fileName).
replace("${QID}", qid)
.replace("${LANG}", "\"" + Locale.getDefault().getLanguage() + "\"");
HttpUrl.Builder urlBuilder = HttpUrl
.parse(sparqlQueryUrl)
.newBuilder()
.addQueryParameter("query", query)
.addQueryParameter("format", "json");
return new Request.Builder()
.url(urlBuilder.build())
.build();
}
public Single<CampaignResponseDTO> getCampaigns() {
return Single.fromCallable(() -> {
Request request = new Request.Builder().url(campaignsUrl)
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
return gson.fromJson(json, CampaignResponseDTO.class);
}
return null;
});
}
} }

View file

@ -20,6 +20,8 @@ import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadController; import fr.free.nrw.commons.upload.UploadController;
import fr.free.nrw.commons.upload.UploadModel; import fr.free.nrw.commons.upload.UploadModel;
import fr.free.nrw.commons.upload.UploadModel.UploadItem; 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 io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
@ -34,16 +36,17 @@ public class UploadRemoteDataSource {
private UploadModel uploadModel; private UploadModel uploadModel;
private UploadController uploadController; private UploadController uploadController;
private CategoriesModel categoriesModel; private CategoriesModel categoriesModel;
private DepictModel depictModel;
private NearbyPlaces nearbyPlaces; private NearbyPlaces nearbyPlaces;
@Inject @Inject
public UploadRemoteDataSource(UploadModel uploadModel, UploadController uploadController, public UploadRemoteDataSource(UploadModel uploadModel, UploadController uploadController,
CategoriesModel categoriesModel, CategoriesModel categoriesModel, NearbyPlaces nearbyPlaces, DepictModel depictModel) {
NearbyPlaces nearbyPlaces) {
this.uploadModel = uploadModel; this.uploadModel = uploadModel;
this.uploadController = uploadController; this.uploadController = uploadController;
this.categoriesModel = categoriesModel; this.categoriesModel = categoriesModel;
this.nearbyPlaces = nearbyPlaces; this.nearbyPlaces = nearbyPlaces;
this.depictModel = depictModel;
} }
/** /**
@ -80,19 +83,13 @@ public class UploadRemoteDataSource {
uploadController.prepareService(); uploadController.prepareService();
} }
/**
* Clean up the UploadController
*/
public void cleanup() {
uploadController.cleanup();
}
/** /**
* Clean up the selected categories * Clean up the selected categories
*/ */
public void clearSelectedCategories(){ public void cleanUp(){
//This needs further refactoring, this should not be here, right now the structure wont suppoort rhis //This needs further refactoring, this should not be here, right now the structure wont suppoort rhis
categoriesModel.cleanUp(); categoriesModel.cleanUp();
depictModel.cleanUp();
} }
/** /**
@ -167,13 +164,12 @@ public class UploadRemoteDataSource {
* *
* @param uploadableFile * @param uploadableFile
* @param place * @param place
* @param source
* @param similarImageInterface * @param similarImageInterface
* @return * @return
*/ */
public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place, public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place,
String source, SimilarImageInterface similarImageInterface) { SimilarImageInterface similarImageInterface) {
return uploadModel.preProcessImage(uploadableFile, place, source, similarImageInterface); return uploadModel.preProcessImage(uploadableFile, place, similarImageInterface);
} }
/** /**
@ -204,7 +200,33 @@ public class UploadRemoteDataSource {
} }
} }
public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) { /**
uploadModel.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex); * handles category selection/unselection
} * @param depictedItem
*/
public void onDepictedItemClicked(DepictedItem depictedItem) {
uploadModel.onDepictItemClicked(depictedItem);
}
/**
* returns the list of selected depictions
* @return
*/
public List<DepictedItem> getSelectedDepictions() {
return uploadModel.getSelectedDepictions();
}
/**
* get all depictions
*/
public Observable<DepictedItem> searchAllEntities(String query) {
return depictModel.searchAllEntities(query);
}
public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) {
uploadModel.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex);
}
} }

View file

@ -13,6 +13,8 @@ import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.upload.SimilarImageInterface; import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadModel.UploadItem; import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
@ -71,7 +73,7 @@ public class UploadRepository {
*/ */
public void cleanup() { public void cleanup() {
localDataSource.cleanUp(); localDataSource.cleanUp();
remoteDataSource.clearSelectedCategories(); remoteDataSource.cleanUp();
} }
/** /**
@ -174,14 +176,12 @@ public class UploadRepository {
* *
* @param uploadableFile * @param uploadableFile
* @param place * @param place
* @param source
* @param similarImageInterface * @param similarImageInterface
* @return * @return
*/ */
public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place, public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place,
String source, SimilarImageInterface similarImageInterface) { SimilarImageInterface similarImageInterface) {
return remoteDataSource return remoteDataSource.preProcessImage(uploadableFile, place, similarImageInterface);
.preProcessImage(uploadableFile, place, source, similarImageInterface);
} }
/** /**
@ -263,6 +263,31 @@ public class UploadRepository {
localDataSource.setSelectedLicense(licenseName); 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
* @return
*/
public Observable<DepictedItem> searchAllEntities(String query) {
return remoteDataSource.searchAllEntities(query);
}
/** /**
* Returns nearest place matching the passed latitude and longitude * Returns nearest place matching the passed latitude and longitude
* @param decLatitude * @param decLatitude
@ -273,7 +298,7 @@ public class UploadRepository {
return remoteDataSource.getNearbyPlaces(decLatitude, decLongitude); return remoteDataSource.getNearbyPlaces(decLatitude, decLongitude);
} }
public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) { public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) {
remoteDataSource.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex); remoteDataSource.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex);
} }
} }

View file

@ -5,29 +5,39 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import fr.free.nrw.commons.R import fr.free.nrw.commons.R
import fr.free.nrw.commons.caching.CacheController
import fr.free.nrw.commons.kvstore.JsonKvStore import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.mwapi.CategoryApi import fr.free.nrw.commons.mwapi.CategoryApi
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.settings.Prefs import fr.free.nrw.commons.settings.Prefs
import fr.free.nrw.commons.upload.structure.depictions.DepictModel
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Named import javax.inject.Named
/** /**
* Processing of the image filePath that is about to be uploaded via ShareActivity is done here * Processing of the image filePath that is about to be uploaded via ShareActivity is done here
*/ */
private const val DEFAULT_SUGGESTION_RADIUS_IN_METRES = 100
private const val MAX_SUGGESTION_RADIUS_IN_METRES = 1000
private const val RADIUS_STEP_SIZE_IN_METRES = 100
private const val MIN_NEARBY_RESULTS = 5
class FileProcessor @Inject constructor( class FileProcessor @Inject constructor(
private val context: Context, private val context: Context,
private val contentResolver: ContentResolver, private val contentResolver: ContentResolver,
private val cacheController: CacheController,
private val gpsCategoryModel: GpsCategoryModel, private val gpsCategoryModel: GpsCategoryModel,
private val depictsModel: DepictModel,
@param:Named("default_preferences") private val defaultKvStore: JsonKvStore, @param:Named("default_preferences") private val defaultKvStore: JsonKvStore,
private val apiCall: CategoryApi private val apiCall: CategoryApi,
private val okHttpJsonApiClient: OkHttpJsonApiClient
) { ) {
private val compositeDisposable = CompositeDisposable() private val compositeDisposable = CompositeDisposable()
@ -57,7 +67,7 @@ class FileProcessor @Inject constructor(
similarImageInterface similarImageInterface
) )
} else { } else {
useImageCoords(originalImageCoordinates) prePopulateCategoriesAndDepictionsBy(originalImageCoordinates)
} }
return originalImageCoordinates return originalImageCoordinates
} }
@ -146,7 +156,7 @@ class FileProcessor @Inject constructor(
private fun readImageCoordinates(file: File) = private fun readImageCoordinates(file: File) =
try { try {
ImageCoordinates(contentResolver.openInputStream(Uri.fromFile(file))) ImageCoordinates(contentResolver.openInputStream(Uri.fromFile(file))!!)
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
try { try {
@ -163,29 +173,44 @@ class FileProcessor @Inject constructor(
* *
* @param imageCoordinates * @param imageCoordinates
*/ */
fun useImageCoords(imageCoordinates: ImageCoordinates) { fun prePopulateCategoriesAndDepictionsBy(imageCoordinates: ImageCoordinates) {
requireNotNull(imageCoordinates.decimalCoords) requireNotNull(imageCoordinates.decimalCoords)
cacheController.setQtPoint(imageCoordinates.decLongitude, imageCoordinates.decLatitude) compositeDisposable.add(
val displayCatList = cacheController.findCategory() apiCall.request(imageCoordinates.decimalCoords)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(
{ gpsCategoryModel.categoryList = it },
{
Timber.e(it)
gpsCategoryModel.clear()
}
)
)
// If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories compositeDisposable.add(
if (displayCatList.isEmpty()) { suggestNearbyDepictions(imageCoordinates)
compositeDisposable.add( )
apiCall.request(imageCoordinates.decimalCoords) }
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io()) private val radiiProgressionInMetres =
.subscribe( (DEFAULT_SUGGESTION_RADIUS_IN_METRES..MAX_SUGGESTION_RADIUS_IN_METRES step RADIUS_STEP_SIZE_IN_METRES)
{ gpsCategoryModel.categoryList = it },
{ private fun suggestNearbyDepictions(imageCoordinates: ImageCoordinates): Disposable {
Timber.e(it) return Observable.fromIterable(radiiProgressionInMetres.map { it / 1000.0 })
gpsCategoryModel.clear() .concatMap {
} okHttpJsonApiClient.getNearbyPlaces(
) imageCoordinates.latLng,
Locale.getDefault().language,
it
)
}
.subscribeOn(Schedulers.io())
.filter { it.size >= MIN_NEARBY_RESULTS }
.take(1)
.subscribe(
{ depictsModel.nearbyPlaces = it },
{ Timber.e(it) }
) )
Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList)
} else {
Timber.d("Cache found, setting categoryList in model to %s", displayCatList)
gpsCategoryModel.categoryList = displayCatList
}
} }
} }

View file

@ -33,8 +33,4 @@ public class GpsCategoryModel {
clear(); clear();
categorySet.addAll(categoryList != null ? categoryList : new ArrayList<>()); categorySet.addAll(categoryList != null ? categoryList : new ArrayList<>());
} }
public void add(String categoryString) {
categorySet.add(categoryString);
}
} }

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.upload package fr.free.nrw.commons.upload
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import fr.free.nrw.commons.location.LatLng
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -22,7 +23,6 @@ class ImageCoordinates internal constructor(exif: ExifInterface?) {
* Construct from a stream. * Construct from a stream.
*/ */
internal constructor(stream: InputStream) : this(ExifInterface(stream)) internal constructor(stream: InputStream) : this(ExifInterface(stream))
/** /**
* Construct from the file path of the image. * Construct from the file path of the image.
* @param path file path of the image * @param path file path of the image
@ -30,8 +30,6 @@ class ImageCoordinates internal constructor(exif: ExifInterface?) {
@Throws(IOException::class) @Throws(IOException::class)
internal constructor(path: String) : this(ExifInterface(path)) internal constructor(path: String) : this(ExifInterface(path))
init { init {
//If image has no EXIF data and user has enabled GPS setting, get user's location //If image has no EXIF data and user has enabled GPS setting, get user's location
//Always return null as a temporary fix for #1599 //Always return null as a temporary fix for #1599
@ -55,6 +53,8 @@ class ImageCoordinates internal constructor(exif: ExifInterface?) {
} }
} }
val latLng: LatLng? get() = LatLng(decLatitude, decLongitude, -1.0f)
/** /**
* Convert a string to an accurate Degree * Convert a string to an accurate Degree
* *

View file

@ -1,6 +1,6 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
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.FILE_NAME_EXISTS;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
@ -11,6 +11,7 @@ import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.ImageUtilsWrapper; import fr.free.nrw.commons.utils.ImageUtilsWrapper;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -39,6 +40,7 @@ public class ImageProcessingService {
this.mediaClient = mediaClient; this.mediaClient = mediaClient;
} }
/** /**
* Check image quality before upload - checks duplicate image - checks dark image - checks * Check image quality before upload - checks duplicate image - checks dark image - checks
* geolocation for image - check for valid title * geolocation for image - check for valid title
@ -88,18 +90,18 @@ public class ImageProcessingService {
/** /**
* Checks item title * Checks item caption
* - empty title * - empty caption
* - existing title * - existing caption
* *
* @param uploadItem * @param uploadItem
* @return * @return
*/ */
private Single<Integer> validateItemTitle(UploadModel.UploadItem uploadItem) { private Single<Integer> validateItemTitle(UploadModel.UploadItem uploadItem) {
Timber.d("Checking for image title %s", uploadItem.getTitle()); Timber.d("Checking for image title %s", uploadItem.getUploadMediaDetails());
Title title = uploadItem.getTitle(); List<UploadMediaDetail> captions = uploadItem.getUploadMediaDetails();
if (title.isEmpty()) { if (captions.isEmpty()) {
return Single.just(EMPTY_TITLE); return Single.just(EMPTY_CAPTION);
} }
return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.getFileName()) return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.getFileName())

View file

@ -0,0 +1,109 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import androidx.annotation.NonNull;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.filepicker.UploadableFile.DateTimeWithSource;
import fr.free.nrw.commons.settings.Prefs.Licenses;
import fr.free.nrw.commons.utils.ConfigUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
class PageContentsCreator {
//{{According to Exif data|2009-01-09}}
private static final String TEMPLATE_DATE_ACC_TO_EXIF = "{{According to Exif data|%s}}";
//2009-01-09 9 January 2009
private static final String TEMPLATE_DATA_OTHER_SOURCE = "%s";
private final Context context;
@Inject
public PageContentsCreator(Context context) {
this.context = context;
}
public String createFrom(Contribution contribution) {
StringBuilder buffer = new StringBuilder();
buffer
.append("== {{int:filedesc}} ==\n")
.append("{{Information\n")
.append("|description=").append(contribution.getDescription()).append("\n")
.append("|source=").append("{{own}}\n")
.append("|author=[[User:").append(contribution.getCreator()).append("|")
.append(contribution.getCreator()).append("]]\n");
String templatizedCreatedDate = getTemplatizedCreatedDate(
contribution.getDateCreated(), contribution.getDateCreatedSource());
if (!StringUtils.isBlank(templatizedCreatedDate)) {
buffer.append("|date=").append(templatizedCreatedDate);
}
buffer.append("}}").append("\n");
//Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null
final String decimalCoords = contribution.getDecimalCoords();
if (decimalCoords != null) {
buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n");
}
buffer.append("== {{int:license-header}} ==\n")
.append(licenseTemplateFor(contribution.getLicense())).append("\n\n")
.append("{{Uploaded from Mobile|platform=Android|version=")
.append(ConfigUtils.getVersionNameWithSha(context)).append("}}\n");
final List<String> categories = contribution.getCategories();
if (categories != null && categories.size() != 0) {
for (int i = 0; i < categories.size(); i++) {
buffer.append("\n[[Category:").append(categories.get(i)).append("]]");
}
} else {
buffer.append("{{subst:unc}}");
}
return buffer.toString();
}
/**
* Returns upload date in either TEMPLATE_DATE_ACC_TO_EXIF or TEMPLATE_DATA_OTHER_SOURCE
*
* @param dateCreated
* @param dateCreatedSource
* @return
*/
private String getTemplatizedCreatedDate(Date dateCreated, String dateCreatedSource) {
if (dateCreated != null) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
return String.format(Locale.ENGLISH,
isExif(dateCreatedSource) ? TEMPLATE_DATE_ACC_TO_EXIF : TEMPLATE_DATA_OTHER_SOURCE,
dateFormat.format(dateCreated)
) + "\n";
}
return "";
}
private boolean isExif(String dateCreatedSource) {
return DateTimeWithSource.EXIF_SOURCE.equals(dateCreatedSource);
}
@NonNull
private String licenseTemplateFor(String license) {
switch (license) {
case Licenses.CC_BY_3:
return "{{self|cc-by-3.0}}";
case Licenses.CC_BY_4:
return "{{self|cc-by-4.0}}";
case Licenses.CC_BY_SA_3:
return "{{self|cc-by-sa-3.0}}";
case Licenses.CC_BY_SA_4:
return "{{self|cc-by-sa-4.0}}";
case Licenses.CC0:
return "{{self|cc-zero}}";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
}

View file

@ -1,29 +0,0 @@
package fr.free.nrw.commons.upload
import android.text.TextUtils
class Title {
private var titleText: String? = null
var isSet = false
override fun toString(): String {
if (titleText == null) {
return ""
} else {
return titleText!!
}
}
fun setTitleText(titleText: String?) {
this.titleText=titleText?.trim()
if (!TextUtils.isEmpty(titleText)) {
isSet = true
}
}
val isEmpty: Boolean
get() = titleText == null || titleText!!.isEmpty()
fun getTitleText(): String? {
return titleText
}
}

View file

@ -1,5 +1,9 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.ProgressDialog; import android.app.ProgressDialog;
@ -10,7 +14,6 @@ import android.widget.ImageButton;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.cardview.widget.CardView; import androidx.cardview.widget.CardView;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@ -20,14 +23,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
@ -36,7 +31,6 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.category.CategoriesModel; import fr.free.nrw.commons.category.CategoriesModel;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
@ -44,6 +38,7 @@ import fr.free.nrw.commons.mwapi.UserClient;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.theme.BaseActivity; import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment; 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.license.MediaLicenseFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment; import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback; import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback;
@ -52,12 +47,13 @@ import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber; import timber.log.Timber;
import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
public class UploadActivity extends BaseActivity implements UploadContract.View, UploadBaseFragment.Callback { public class UploadActivity extends BaseActivity implements UploadContract.View, UploadBaseFragment.Callback {
@Inject @Inject
ContributionController contributionController; ContributionController contributionController;
@ -102,11 +98,10 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
private UploadImageAdapter uploadImagesAdapter; private UploadImageAdapter uploadImagesAdapter;
private List<Fragment> fragments; private List<Fragment> fragments;
private UploadCategoriesFragment uploadCategoriesFragment; private UploadCategoriesFragment uploadCategoriesFragment;
private DepictsFragment depictsFragment;
private MediaLicenseFragment mediaLicenseFragment; private MediaLicenseFragment mediaLicenseFragment;
private ThumbnailsAdapter thumbnailsAdapter; private ThumbnailsAdapter thumbnailsAdapter;
private String source;
private Place place; private Place place;
private List<UploadableFile> uploadableFiles = Collections.emptyList(); private List<UploadableFile> uploadableFiles = Collections.emptyList();
private int currentSelectedPosition = 0; private int currentSelectedPosition = 0;
@ -288,6 +283,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
} }
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
@ -321,7 +317,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
fragments = new ArrayList<>(); fragments = new ArrayList<>();
for (UploadableFile uploadableFile : uploadableFiles) { for (UploadableFile uploadableFile : uploadableFiles) {
UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment(); UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment();
uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, source, place); uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, place);
uploadMediaDetailFragment.setCallback(new UploadMediaDetailFragmentCallback() { uploadMediaDetailFragment.setCallback(new UploadMediaDetailFragmentCallback() {
@Override @Override
public void deletePictureAtIndex(int index) { public void deletePictureAtIndex(int index) {
@ -359,10 +355,13 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
uploadCategoriesFragment = new UploadCategoriesFragment(); uploadCategoriesFragment = new UploadCategoriesFragment();
uploadCategoriesFragment.setCallback(this); uploadCategoriesFragment.setCallback(this);
depictsFragment = new DepictsFragment();
depictsFragment.setCallback(this);
mediaLicenseFragment = new MediaLicenseFragment(); mediaLicenseFragment = new MediaLicenseFragment();
mediaLicenseFragment.setCallback(this); mediaLicenseFragment.setCallback(this);
fragments.add(depictsFragment);
fragments.add(uploadCategoriesFragment); fragments.add(uploadCategoriesFragment);
fragments.add(mediaLicenseFragment); fragments.add(mediaLicenseFragment);
@ -378,16 +377,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
private void receiveInternalSharedItems() { private void receiveInternalSharedItems() {
Intent intent = getIntent(); Intent intent = getIntent();
if (intent.hasExtra(UploadService.EXTRA_SOURCE)) { Timber.d("Received intent %s with action %s", intent.toString(), intent.getAction());
source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
} else {
source = Contribution.SOURCE_EXTERNAL;
}
Timber.d("Received intent %s with action %s and from source %s",
intent.toString(),
intent.getAction(),
source);
uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES); uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES);
Timber.i("Received multiple upload %s", uploadableFiles.size()); Timber.i("Received multiple upload %s", uploadableFiles.size());

View file

@ -1,35 +1,36 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import fr.free.nrw.commons.CommonsApplication;
import org.wikipedia.csrf.CsrfTokenClient;
import java.io.File;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.upload.UploadService.NotificationUpdateProgressListener; import fr.free.nrw.commons.upload.UploadService.NotificationUpdateProgressListener;
import io.reactivex.Observable; import io.reactivex.Observable;
import java.io.File;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import org.wikipedia.csrf.CsrfTokenClient;
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
@Singleton @Singleton
public class UploadClient { public class UploadClient {
private final UploadInterface uploadInterface; private final UploadInterface uploadInterface;
private final CsrfTokenClient csrfTokenClient; private final CsrfTokenClient csrfTokenClient;
private final PageContentsCreator pageContentsCreator;
@Inject @Inject
public UploadClient(UploadInterface uploadInterface, @Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) { public UploadClient(UploadInterface uploadInterface,
@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient,
PageContentsCreator pageContentsCreator) {
this.uploadInterface = uploadInterface; this.uploadInterface = uploadInterface;
this.csrfTokenClient = csrfTokenClient; this.csrfTokenClient = csrfTokenClient;
this.pageContentsCreator = pageContentsCreator;
} }
Observable<UploadResult> uploadFileToStash(Context context, String filename, File file, Observable<UploadResult> uploadFileToStash(Context context, String filename, File file,
@ -61,8 +62,8 @@ public class UploadClient {
try { try {
return uploadInterface return uploadInterface
.uploadFileFromStash(csrfTokenClient.getTokenBlocking(), .uploadFileFromStash(csrfTokenClient.getTokenBlocking(),
contribution.getPageContents(context), pageContentsCreator.createFrom(contribution),
contribution.getEditSummary(), CommonsApplication.DEFAULT_EDIT_SUMMARY,
uniqueFileName, uniqueFileName,
fileKey).map(uploadResponse -> uploadResponse.getUpload()); fileKey).map(uploadResponse -> uploadResponse.getUpload());
} catch (Throwable throwable) { } catch (Throwable throwable) {

View file

@ -13,17 +13,8 @@ import android.net.Uri;
import android.os.IBinder; import android.os.IBinder;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.text.TextUtils; import android.text.TextUtils;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
@ -34,23 +25,26 @@ import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import javax.inject.Inject;
import javax.inject.Singleton;
import timber.log.Timber; import timber.log.Timber;
@Singleton @Singleton
public class UploadController { public class UploadController {
private UploadService uploadService; private UploadService uploadService;
private SessionManager sessionManager; private final SessionManager sessionManager;
private Context context; private final Context context;
private JsonKvStore store; private final JsonKvStore store;
public interface ContributionUploadProgress {
void onUploadStarted(Contribution contribution);
}
@Inject @Inject
public UploadController(SessionManager sessionManager, public UploadController(final SessionManager sessionManager,
Context context, final Context context,
JsonKvStore store) { final JsonKvStore store) {
this.sessionManager = sessionManager; this.sessionManager = sessionManager;
this.context = context; this.context = context;
this.store = store; this.store = store;
@ -59,13 +53,13 @@ public class UploadController {
private boolean isUploadServiceConnected; private boolean isUploadServiceConnected;
public ServiceConnection uploadServiceConnection = new ServiceConnection() { public ServiceConnection uploadServiceConnection = new ServiceConnection() {
@Override @Override
public void onServiceConnected(ComponentName componentName, IBinder binder) { public void onServiceConnected(final ComponentName componentName, final IBinder binder) {
uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder).getService(); uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder).getService();
isUploadServiceConnected = true; isUploadServiceConnected = true;
} }
@Override @Override
public void onServiceDisconnected(ComponentName componentName) { public void onServiceDisconnected(final ComponentName componentName) {
// this should never happen // this should never happen
isUploadServiceConnected = false; isUploadServiceConnected = false;
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!")); Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
@ -76,7 +70,7 @@ public class UploadController {
* Prepares the upload service. * Prepares the upload service.
*/ */
public void prepareService() { public void prepareService() {
Intent uploadServiceIntent = new Intent(context, UploadService.class); final Intent uploadServiceIntent = new Intent(context, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
context.startService(uploadServiceIntent); context.startService(uploadServiceIntent);
context.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE); context.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
@ -96,28 +90,18 @@ public class UploadController {
* *
* @param contribution the contribution object * @param contribution the contribution object
*/ */
public void startUpload(Contribution contribution) {
startUpload(contribution, c -> {});
}
/**
* Starts a new upload task.
*
* @param contribution the contribution object
* @param onComplete the progress tracker
*/
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
private void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) { public void startUpload(final Contribution contribution) {
//Set creator, desc, and license //Set creator, desc, and license
// If author name is enabled and set, use it // If author name is enabled and set, use it
if (store.getBoolean("useAuthorName", false)) { if (store.getBoolean("useAuthorName", false)) {
String authorName = store.getString("authorName", ""); final String authorName = store.getString("authorName", "");
contribution.setCreator(authorName); contribution.setCreator(authorName);
} }
if (TextUtils.isEmpty(contribution.getCreator())) { if (TextUtils.isEmpty(contribution.getCreator())) {
Account currentAccount = sessionManager.getCurrentAccount(); final Account currentAccount = sessionManager.getCurrentAccount();
if (currentAccount == null) { if (currentAccount == null) {
Timber.d("Current account is null"); Timber.d("Current account is null");
ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in)); ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in));
@ -131,23 +115,23 @@ public class UploadController {
contribution.setDescription(""); contribution.setDescription("");
} }
String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); final String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
contribution.setLicense(license); contribution.setLicense(license);
uploadTask(contribution, onComplete); uploadTask(contribution);
} }
/** /**
* Initiates the upload task * Initiates the upload task
* @param contribution * @param contribution
* @param onComplete
* @return * @return
*/ */
private Disposable uploadTask(Contribution contribution, ContributionUploadProgress onComplete) { private Disposable uploadTask(final Contribution contribution) {
return Single.fromCallable(() -> makeUpload(contribution)) return Single.just(contribution)
.map(this::buildUpload)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(finalContribution -> onUploadCompleted(finalContribution, onComplete)); .subscribe(this::upload);
} }
/** /**
@ -155,71 +139,76 @@ public class UploadController {
* @param contribution * @param contribution
* @return * @return
*/ */
private Contribution makeUpload(Contribution contribution) { private Contribution buildUpload(final Contribution contribution) {
long length; final ContentResolver contentResolver = context.getContentResolver();
ContentResolver contentResolver = context.getContentResolver();
contribution.setDataLength(resolveDataLength(contentResolver, contribution));
final String mimeType = resolveMimeType(contentResolver, contribution);
if (mimeType != null) {
Timber.d("MimeType is: %s", mimeType);
contribution.setMimeType(mimeType);
if(mimeType.startsWith("image/") && contribution.getDateCreated() == null){
contribution.setDateCreated(resolveDateTakenOrNow(contentResolver, contribution));
}
}
return contribution;
}
private String resolveMimeType(final ContentResolver contentResolver, final Contribution contribution) {
final String mimeType = contribution.getMimeType();
if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
return contentResolver.getType(contribution.getLocalUri());
}
return mimeType;
}
private long resolveDataLength(final ContentResolver contentResolver, final Media contribution) {
try { try {
if (contribution.getDataLength() <= 0) { if (contribution.getDataLength() <= 0) {
Timber.d("UploadController/doInBackground, contribution.getLocalUri():%s", contribution.getLocalUri()); Timber.d("UploadController/doInBackground, contribution.getLocalUri():%s", contribution.getLocalUri());
AssetFileDescriptor assetFileDescriptor = contentResolver final AssetFileDescriptor assetFileDescriptor = contentResolver
.openAssetFileDescriptor(Uri.fromFile(new File(contribution.getLocalUri().getPath())), "r"); .openAssetFileDescriptor(Uri.fromFile(new File(contribution.getLocalUri().getPath())), "r");
if (assetFileDescriptor != null) { if (assetFileDescriptor != null) {
length = assetFileDescriptor.getLength(); final long length = assetFileDescriptor.getLength();
if (length == -1) { return length != -1 ? length
// Let us find out the long way! : countBytes(contentResolver.openInputStream(contribution.getLocalUri()));
length = countBytes(contentResolver
.openInputStream(contribution.getLocalUri()));
}
contribution.setDataLength(length);
} }
} }
} catch (IOException | NullPointerException | SecurityException e) { } catch (final IOException | NullPointerException | SecurityException e) {
Timber.e(e, "Exception occurred while uploading image"); Timber.e(e, "Exception occurred while uploading image");
} }
return contribution.getDataLength();
}
String mimeType = (String) contribution.getTag("mimeType"); private Date resolveDateTakenOrNow(final ContentResolver contentResolver, final Media contribution) {
boolean imagePrefix = false; Timber.d("local uri %s", contribution.getLocalUri());
try(final Cursor cursor = dateTakenCursor(contentResolver, contribution)) {
if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
mimeType = contentResolver.getType(contribution.getLocalUri());
}
if (mimeType != null) {
contribution.setTag("mimeType", mimeType);
imagePrefix = mimeType.startsWith("image/");
Timber.d("MimeType is: %s", mimeType);
}
if (imagePrefix && contribution.getDateCreated() == null) {
Timber.d("local uri %s", contribution.getLocalUri());
Cursor cursor = contentResolver.query(contribution.getLocalUri(),
new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null);
if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) { if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) {
cursor.moveToFirst(); cursor.moveToFirst();
Date dateCreated = new Date(cursor.getLong(0)); final Date dateCreated = new Date(cursor.getLong(0));
Date epochStart = new Date(0); if (dateCreated.after(new Date(0))) {
if (dateCreated.equals(epochStart) || dateCreated.before(epochStart)) { return dateCreated;
// If date is incorrect (1st second of unix time) then set it to the current date
dateCreated = new Date();
} }
contribution.setDateCreated(dateCreated);
cursor.close();
} else {
contribution.setDateCreated(new Date());
} }
return new Date();
} }
return contribution; }
private Cursor dateTakenCursor(final ContentResolver contentResolver, final Media contribution) {
return contentResolver.query(contribution.getLocalUri(),
new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null);
} }
/** /**
* When the contribution object is completely formed, the item is queued to the upload service * When the contribution object is completely formed, the item is queued to the upload service
* @param contribution * @param contribution
* @param onComplete
*/ */
private void onUploadCompleted(Contribution contribution, ContributionUploadProgress onComplete) { private void upload(final Contribution contribution) {
//Starts the upload. If commented out, user can proceed to next Fragment but upload doesn't happen //Starts the upload. If commented out, user can proceed to next Fragment but upload doesn't happen
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, contribution); uploadService.queue(UploadService.ACTION_UPLOAD_FILE, contribution);
onComplete.onUploadStarted(contribution);
} }
@ -230,9 +219,9 @@ public class UploadController {
* @return the number of bytes in {@code stream} * @return the number of bytes in {@code stream}
* @throws IOException if an I/O error occurs * @throws IOException if an I/O error occurs
*/ */
private long countBytes(InputStream stream) throws IOException { private long countBytes(final InputStream stream) throws IOException {
long count = 0; long count = 0;
BufferedInputStream bis = new BufferedInputStream(stream); final BufferedInputStream bis = new BufferedInputStream(stream);
while (bis.read() != -1) { while (bis.read() != -1) {
count++; count++;
} }

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.getName());
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.getId(),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,62 @@
package fr.free.nrw.commons.upload
import fr.free.nrw.commons.nearby.Place
import java.util.*
/**
* Holds a description of an item being uploaded by [UploadActivity]
*/
data class UploadMediaDetail constructor(
/**
* @return The language code ie. "en" or "fr"
*/
/**
* @param languageCode The language code ie. "en" or "fr"
*/
var languageCode: String? = null,
var descriptionText: String = "",
var captionText: String = ""
) {
constructor(place: Place) : this(
Locale.getDefault().language,
place.longDescription,
place.name
)
/**
* @return the index of the language selected in a spinner with [SpinnerLanguagesAdapter]
*/
/**
* @param selectedLanguageIndex the index of the language selected in a spinner with [SpinnerLanguagesAdapter]
*/
var selectedLanguageIndex: Int = -1
/**
* returns if the description was added manually (by the user, or we have added it programaticallly)
* @return
*/
/**
* sets to true if the description was manually added by the user
* @param manuallyAdded
*/
var isManuallyAdded: Boolean = false
companion object {
/**
* Formatting captions to the Wikibase format for sending labels
* @param uploadMediaDetails list of media Details
*/
@JvmStatic
fun formatCaptions(uploadMediaDetails: List<UploadMediaDetail>) =
uploadMediaDetails.associate { it.languageCode to it.captionText }.filter { it.value.isNotBlank() }
/**
* 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}}
*/
@JvmStatic
fun formatList(descriptions: List<UploadMediaDetail>) =
descriptions.filter { it.descriptionText.isNotEmpty() }
.joinToString { "{{${it.languageCode}|1=${it.descriptionText}}}" }
}
}

View file

@ -10,33 +10,31 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener; import android.widget.AdapterView.OnItemSelectedListener;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatEditText; import androidx.appcompat.widget.AppCompatEditText;
import androidx.appcompat.widget.AppCompatSpinner; import androidx.appcompat.widget.AppCompatSpinner;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.AbstractTextWatcher; import fr.free.nrw.commons.utils.AbstractTextWatcher;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import timber.log.Timber; 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 Callback callback;
private EventListener eventListener;
private HashMap<AdapterView, String> selectedLanguages; private HashMap<AdapterView, String> selectedLanguages;
private String savedLanguageValue; private final String savedLanguageValue;
public DescriptionsAdapter(String savedLanguageValue) { public UploadMediaDetailAdapter(String savedLanguageValue) {
descriptions = new ArrayList<>(); uploadMediaDetails = new ArrayList<>();
selectedLanguages = new HashMap<>(); selectedLanguages = new HashMap<>();
this.savedLanguageValue = savedLanguageValue; this.savedLanguageValue = savedLanguageValue;
} }
@ -45,8 +43,12 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
this.callback = callback; this.callback = callback;
} }
public void setItems(List<Description> descriptions) { public void setEventListener(EventListener eventListener) {
this.descriptions = descriptions; this.eventListener = eventListener;
}
public void setItems(List<UploadMediaDetail> uploadMediaDetails) {
this.uploadMediaDetails = uploadMediaDetails;
selectedLanguages = new HashMap<>(); selectedLanguages = new HashMap<>();
notifyDataSetChanged(); notifyDataSetChanged();
} }
@ -60,12 +62,12 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
@Override @Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) { public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.init(position); holder.bind(position);
} }
@Override @Override
public int getItemCount() { public int getItemCount() {
return descriptions.size(); return uploadMediaDetails.size();
} }
/** /**
@ -73,13 +75,13 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
* *
* @return List of descriptions * @return List of descriptions
*/ */
public List<Description> getDescriptions() { public List<UploadMediaDetail> getUploadMediaDetails() {
return descriptions; return uploadMediaDetails;
} }
public void addDescription(Description description) { public void addDescription(UploadMediaDetail uploadMediaDetail) {
this.descriptions.add(description); this.uploadMediaDetails.add(uploadMediaDetail);
notifyItemInserted(descriptions.size()); notifyItemInserted(uploadMediaDetails.size());
} }
public class ViewHolder extends RecyclerView.ViewHolder { public class ViewHolder extends RecyclerView.ViewHolder {
@ -91,21 +93,43 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
@BindView(R.id.description_item_edit_text) @BindView(R.id.description_item_edit_text)
AppCompatEditText descItemEditText; AppCompatEditText descItemEditText;
@BindView(R.id.caption_item_edit_text)
AppCompatEditText captionItemEditText;
public ViewHolder(View itemView) { public ViewHolder(View itemView) {
super(itemView); super(itemView);
ButterKnife.bind(this, itemView); ButterKnife.bind(this, itemView);
Timber.i("descItemEditText:" + descItemEditText); Timber.i("descItemEditText:" + descItemEditText);
} }
public void init(int position) { public void bind(int position) {
Description description = descriptions.get(position); UploadMediaDetail uploadMediaDetail = uploadMediaDetails.get(position);
Timber.d("Description is " + description); Timber.d("UploadMediaDetail is " + uploadMediaDetail);
if (!TextUtils.isEmpty(description.getDescriptionText())) { captionItemEditText.setText(uploadMediaDetail.getCaptionText());
descItemEditText.setText(description.getDescriptionText()); descItemEditText.setText(uploadMediaDetail.getDescriptionText());
} else {
descItemEditText.setText(""); captionItemEditText.addTextChangedListener(new AbstractTextWatcher(
} value -> {
if (position == 0) {
eventListener.onPrimaryCaptionTextChange(value.length() != 0);
}
}));
if (position == 0) { 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(), descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(),
null); null);
descItemEditText.setOnTouchListener((v, event) -> { descItemEditText.setOnTouchListener((v, event) -> {
@ -122,18 +146,23 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
}); });
} else { } else {
captionItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
descItemEditText.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( descItemEditText.addTextChangedListener(new AbstractTextWatcher(
descriptionText -> descriptions.get(position).setDescriptionText(descriptionText))); descriptionText -> uploadMediaDetails.get(position).setDescriptionText(descriptionText)));
initLanguageSpinner(position, description); initLanguageSpinner(position, uploadMediaDetail);
//If the description was manually added by the user, it deserves focus, if not, let the user decide //If the description was manually added by the user, it deserves focus, if not, let the user decide
if (description.isManuallyAdded()) { if (uploadMediaDetail.isManuallyAdded()) {
descItemEditText.requestFocus(); captionItemEditText.requestFocus();
} else { } else {
descItemEditText.clearFocus(); captionItemEditText.clearFocus();
} }
} }
@ -142,7 +171,7 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
* @param position * @param position
* @param description * @param description
*/ */
private void initLanguageSpinner(int position, Description description) { private void initLanguageSpinner(int position, UploadMediaDetail description) {
SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter( SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(
spinnerDescriptionLanguages.getContext(), spinnerDescriptionLanguages.getContext(),
selectedLanguages selectedLanguages
@ -205,6 +234,10 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
void showAlert(int mediaDetailDescription, int descriptionInfo); void showAlert(int mediaDetailDescription, int descriptionInfo);
} }
public interface EventListener {
void onPrimaryCaptionTextChange(boolean isNotEmpty);
}
/** /**
* converts dp to pixel * converts dp to pixel
* @param dp * @param dp

View file

@ -4,21 +4,20 @@ import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.filepicker.MimeTypeMapWrapper;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.ImageUtils;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.subjects.BehaviorSubject; import io.reactivex.subjects.BehaviorSubject;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@ -26,6 +25,7 @@ import java.util.Map;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.jetbrains.annotations.NotNull;
import timber.log.Timber; import timber.log.Timber;
@Singleton @Singleton
@ -36,22 +36,23 @@ public class UploadModel {
private final Context context; private final Context context;
private String license; private String license;
private final Map<String, String> licensesByName; private final Map<String, String> licensesByName;
private List<UploadItem> items = new ArrayList<>(); private final List<UploadItem> items = new ArrayList<>();
private CompositeDisposable compositeDisposable = new CompositeDisposable(); private final CompositeDisposable compositeDisposable = new CompositeDisposable();
private SessionManager sessionManager; private final SessionManager sessionManager;
private FileProcessor fileProcessor; private final FileProcessor fileProcessor;
private final ImageProcessingService imageProcessingService; private final ImageProcessingService imageProcessingService;
private List<String> selectedCategories; private List<String> selectedCategories = new ArrayList<>();
private List<DepictedItem> selectedDepictions = new ArrayList<>();
@Inject @Inject
UploadModel(@Named("licenses") List<String> licenses, UploadModel(@Named("licenses") final List<String> licenses,
@Named("default_preferences") JsonKvStore store, @Named("default_preferences") final JsonKvStore store,
@Named("licenses_by_name") Map<String, String> licensesByName, @Named("licenses_by_name") final Map<String, String> licensesByName,
Context context, final Context context,
SessionManager sessionManager, final SessionManager sessionManager,
FileProcessor fileProcessor, final FileProcessor fileProcessor,
ImageProcessingService imageProcessingService) { final ImageProcessingService imageProcessingService) {
this.licenses = licenses; this.licenses = licenses;
this.store = store; this.store = store;
this.license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); this.license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
@ -68,39 +69,33 @@ public class UploadModel {
public void cleanUp() { public void cleanUp() {
compositeDisposable.clear(); compositeDisposable.clear();
fileProcessor.cleanup(); fileProcessor.cleanup();
this.items.clear(); items.clear();
if (this.selectedCategories != null) { selectedCategories.clear();
this.selectedCategories.clear(); selectedDepictions.clear();
}
} }
public void setSelectedCategories(List<String> selectedCategories) { public void setSelectedCategories(List<String> selectedCategories) {
if (null == selectedCategories) {
selectedCategories = new ArrayList<>();
}
this.selectedCategories = selectedCategories; this.selectedCategories = selectedCategories;
} }
/** /**
* pre process a one item at a time * pre process a one item at a time
*/ */
public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, public Observable<UploadItem> preProcessImage(final UploadableFile uploadableFile,
Place place, final Place place,
String source, final SimilarImageInterface similarImageInterface) {
SimilarImageInterface similarImageInterface) { return Observable.just(
return Observable.just(getUploadItem(uploadableFile, place, source, similarImageInterface)); createAndAddUploadItem(uploadableFile, place, similarImageInterface));
} }
public Single<Integer> getImageQuality(UploadItem uploadItem) { public Single<Integer> getImageQuality(final UploadItem uploadItem) {
return imageProcessingService.validateImage(uploadItem); return imageProcessingService.validateImage(uploadItem);
} }
private UploadItem getUploadItem(UploadableFile uploadableFile, private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile,
Place place, final Place place,
String source, final SimilarImageInterface similarImageInterface) {
SimilarImageInterface similarImageInterface) { final UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile
UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile
.getFileCreatedDate(context); .getFileCreatedDate(context);
long fileCreatedDate = -1; long fileCreatedDate = -1;
String createdTimestampSource = ""; String createdTimestampSource = "";
@ -109,19 +104,14 @@ public class UploadModel {
createdTimestampSource = dateTimeWithSource.getSource(); createdTimestampSource = dateTimeWithSource.getSource();
} }
Timber.d("File created date is %d", fileCreatedDate); Timber.d("File created date is %d", fileCreatedDate);
ImageCoordinates imageCoordinates = fileProcessor final ImageCoordinates imageCoordinates = fileProcessor
.processFileCoordinates(similarImageInterface, uploadableFile.getFilePath()); .processFileCoordinates(similarImageInterface, uploadableFile.getFilePath());
UploadItem uploadItem = new UploadItem(uploadableFile.getContentUri(), final UploadItem uploadItem = new UploadItem(uploadableFile.getContentUri(),
Uri.parse(uploadableFile.getFilePath()), Uri.parse(uploadableFile.getFilePath()),
uploadableFile.getMimeType(context), source, imageCoordinates, place, fileCreatedDate, uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate,
createdTimestampSource); createdTimestampSource);
if (place != null) { if (place != null) {
uploadItem.title.setTitleText(place.name); uploadItem.getUploadMediaDetails().set(0, new UploadMediaDetail(place));
if(uploadItem.descriptions.isEmpty()) {
uploadItem.descriptions.add(new Description());
}
uploadItem.descriptions.get(0).setDescriptionText(place.getLongDescription());
uploadItem.descriptions.get(0).setLanguageCode("en");
} }
if (!items.contains(uploadItem)) { if (!items.contains(uploadItem)) {
items.add(uploadItem); items.add(uploadItem);
@ -145,7 +135,7 @@ public class UploadModel {
return license; return license;
} }
public void setSelectedLicense(String licenseName) { public void setSelectedLicense(final String licenseName) {
this.license = licensesByName.get(licenseName); this.license = licensesByName.get(licenseName);
store.putString(Prefs.DEFAULT_LICENSE, license); store.putString(Prefs.DEFAULT_LICENSE, license);
} }
@ -153,26 +143,8 @@ public class UploadModel {
public Observable<Contribution> buildContributions() { public Observable<Contribution> buildContributions() {
return Observable.fromIterable(items).map(item -> return Observable.fromIterable(items).map(item ->
{ {
Contribution contribution = new Contribution(item.mediaUri, null, final Contribution contribution = new Contribution(
item.getFileName(), item, sessionManager, newListOf(selectedDepictions), newListOf(selectedCategories));
Description.formatList(item.descriptions), -1,
null, null, sessionManager.getAuthorName(),
CommonsApplication.DEFAULT_EDIT_SUMMARY, item.gpsCoords.getDecimalCoords());
if (item.place != null) {
contribution.setWikiDataEntityId(item.place.getWikiDataEntityId());
contribution.setWikiItemName(item.place.getName());
// If item already has an image, we need to know it. We don't want to override existing image later
contribution.setP18Value(item.place.pic);
}
if (null == selectedCategories) {//Just a fail safe, this should never be null
selectedCategories = new ArrayList<>();
}
contribution.setCategories(selectedCategories);
contribution.setTag("mimeType", item.mimeType);
contribution.setSource(item.source);
contribution.setContentProviderUri(item.mediaUri);
contribution.setDateUploaded(new Date());
Timber.d("Created timestamp while building contribution is %s, %s", Timber.d("Created timestamp while building contribution is %s, %s",
item.getCreatedTimestamp(), item.getCreatedTimestamp(),
new Date(item.getCreatedTimestamp())); new Date(item.getCreatedTimestamp()));
@ -185,8 +157,8 @@ public class UploadModel {
}); });
} }
public void deletePicture(String filePath) { public void deletePicture(final String filePath) {
Iterator<UploadItem> iterator = items.iterator(); final Iterator<UploadItem> iterator = items.iterator();
while (iterator.hasNext()) { while (iterator.hasNext()) {
if (iterator.next().mediaUri.toString().contains(filePath)) { if (iterator.next().mediaUri.toString().contains(filePath)) {
iterator.remove(); iterator.remove();
@ -202,51 +174,58 @@ public class UploadModel {
return items; return items;
} }
public void updateUploadItem(int index, UploadItem uploadItem) { public void updateUploadItem(final int index, final UploadItem uploadItem) {
UploadItem uploadItem1 = items.get(index); final UploadItem uploadItem1 = items.get(index);
uploadItem1.setDescriptions(uploadItem.descriptions); uploadItem1.setMediaDetails(uploadItem.uploadMediaDetails);
uploadItem1.setTitle(uploadItem.title);
} }
public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) { public void onDepictItemClicked(DepictedItem depictedItem) {
fileProcessor.useImageCoords(imageCoordinates); if (depictedItem.isSelected()) {
selectedDepictions.add(depictedItem);
} else {
selectedDepictions.remove(depictedItem);
}
}
@NotNull
private <T> List<T> newListOf(final List<T> items) {
return items != null ? new ArrayList<>(items) : new ArrayList<>();
}
public void useSimilarPictureCoordinates(final ImageCoordinates imageCoordinates, final int uploadItemIndex) {
fileProcessor.prePopulateCategoriesAndDepictionsBy(imageCoordinates);
items.get(uploadItemIndex).setGpsCoords(imageCoordinates); items.get(uploadItemIndex).setGpsCoords(imageCoordinates);
} }
public List<DepictedItem> getSelectedDepictions() {
return selectedDepictions;
}
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public static class UploadItem { public static class UploadItem {
private final Uri originalContentUri; private final Uri originalContentUri;
private final Uri mediaUri; private final Uri mediaUri;
private final String mimeType; private final String mimeType;
private final String source;
private ImageCoordinates gpsCoords; private ImageCoordinates gpsCoords;
private List<UploadMediaDetail> uploadMediaDetails;
public void setGpsCoords(ImageCoordinates gpsCoords) { private final Place place;
this.gpsCoords = gpsCoords; private final long createdTimestamp;
} private final String createdTimestampSource;
private final BehaviorSubject<Integer> imageQuality;
private Title title;
private List<Description> descriptions;
private Place place;
private long createdTimestamp;
private String createdTimestampSource;
private BehaviorSubject<Integer> imageQuality;
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
UploadItem(Uri originalContentUri, UploadItem(final Uri originalContentUri,
Uri mediaUri, String mimeType, String source, ImageCoordinates gpsCoords, final Uri mediaUri, final String mimeType,
Place place, final ImageCoordinates gpsCoords,
long createdTimestamp, final Place place,
String createdTimestampSource) { final long createdTimestamp,
final String createdTimestampSource) {
this.originalContentUri = originalContentUri; this.originalContentUri = originalContentUri;
this.createdTimestampSource = createdTimestampSource; this.createdTimestampSource = createdTimestampSource;
title = new Title(); uploadMediaDetails = new ArrayList<>(Arrays.asList(new UploadMediaDetail()));
descriptions = new ArrayList<>();
this.place = place; this.place = place;
this.mediaUri = mediaUri; this.mediaUri = mediaUri;
this.mimeType = mimeType; this.mimeType = mimeType;
this.source = source;
this.gpsCoords = gpsCoords; this.gpsCoords = gpsCoords;
this.createdTimestamp = createdTimestamp; this.createdTimestamp = createdTimestamp;
imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT); imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT);
@ -256,26 +235,18 @@ public class UploadModel {
return createdTimestampSource; return createdTimestampSource;
} }
public String getSource() {
return source;
}
public ImageCoordinates getGpsCoords() { public ImageCoordinates getGpsCoords() {
return gpsCoords; return gpsCoords;
} }
public List<Description> getDescriptions() { public List<UploadMediaDetail> getUploadMediaDetails() {
return descriptions; return uploadMediaDetails;
} }
public long getCreatedTimestamp() { public long getCreatedTimestamp() {
return createdTimestamp; return createdTimestamp;
} }
public Title getTitle() {
return title;
}
public Uri getMediaUri() { public Uri getMediaUri() {
return mediaUri; return mediaUri;
} }
@ -284,29 +255,16 @@ public class UploadModel {
return this.imageQuality.getValue(); return this.imageQuality.getValue();
} }
public void setImageQuality(int imageQuality) { public void setImageQuality(final int imageQuality) {
this.imageQuality.onNext(imageQuality); this.imageQuality.onNext(imageQuality);
} }
public String getFileExt() {
return MimeTypeMapWrapper.getExtensionFromMimeType(mimeType);
}
public String getFileName() {
return title
!= null ? Utils.fixExtension(title.toString(), getFileExt()) : null;
}
public Place getPlace() { public Place getPlace() {
return place; return place;
} }
public void setTitle(Title title) { public void setMediaDetails(final List<UploadMediaDetail> uploadMediaDetails) {
this.title = title; this.uploadMediaDetails = uploadMediaDetails;
}
public void setDescriptions(List<Description> descriptions) {
this.descriptions = descriptions;
} }
public Uri getContentUri() { public Uri getContentUri() {
@ -314,7 +272,7 @@ public class UploadModel {
} }
@Override @Override
public boolean equals(@Nullable Object obj) { public boolean equals(@Nullable final Object obj) {
if (!(obj instanceof UploadItem)) { if (!(obj instanceof UploadItem)) {
return false; return false;
} }
@ -326,6 +284,21 @@ public class UploadModel {
public int hashCode() { public int hashCode() {
return mediaUri.hashCode(); return mediaUri.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();
}
public void setGpsCoords(final ImageCoordinates gpsCoords) {
this.gpsCoords = gpsCoords;
}
public String getMimeType() {
return mimeType;
}
}
} }

View file

@ -4,6 +4,8 @@ import dagger.Binds;
import dagger.Module; import dagger.Module;
import fr.free.nrw.commons.upload.categories.CategoriesContract; import fr.free.nrw.commons.upload.categories.CategoriesContract;
import fr.free.nrw.commons.upload.categories.CategoriesPresenter; 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.MediaLicenseContract;
import fr.free.nrw.commons.upload.license.MediaLicensePresenter; import fr.free.nrw.commons.upload.license.MediaLicensePresenter;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract; import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract;
@ -33,4 +35,9 @@ public abstract class UploadModule {
UploadMediaPresenter UploadMediaPresenter
presenter); 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 android.annotation.SuppressLint;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
@ -89,7 +90,6 @@ public class UploadPresenter implements UploadContract.UserActionListener {
if (index == uploadableFiles.size() - 1) {//If the next fragment to be shown is not one of the MediaDetailsFragment, lets hide the top card 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); view.showHideTopCard(false);
} }
//Ask the repository to delete the picture
repository.deletePicture(uploadableFiles.get(index).getFilePath()); repository.deletePicture(uploadableFiles.get(index).getFilePath());
if (uploadableFiles.size() == 1) { if (uploadableFiles.size() == 1) {
view.showMessage(R.string.upload_cancelled); view.showMessage(R.string.upload_cancelled);

View file

@ -2,4 +2,16 @@ package fr.free.nrw.commons.upload
import org.wikipedia.gallery.ImageInfo import org.wikipedia.gallery.ImageInfo
class UploadResult(val result: String, val filekey: String, val filename: String, val sessionkey: String, val imageinfo: ImageInfo) private const val RESULT_SUCCESS = "Success"
data class UploadResult(
val result: String,
val filekey: String,
val filename: String,
val sessionkey: String,
val imageinfo: ImageInfo
) {
fun isSuccessful(): Boolean = result == RESULT_SUCCESS
fun createCanonicalFileName() = "File:$filename"
}

View file

@ -7,20 +7,8 @@ import android.content.Intent;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.HandlerService;
@ -35,10 +23,17 @@ import fr.free.nrw.commons.utils.CommonsDateUtil;
import fr.free.nrw.commons.wikidata.WikidataEditService; import fr.free.nrw.commons.wikidata.WikidataEditService;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Scheduler; import io.reactivex.Scheduler;
import io.reactivex.SingleObserver;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber; import timber.log.Timber;
public class UploadService extends HandlerService<Contribution> { public class UploadService extends HandlerService<Contribution> {
@ -48,7 +43,6 @@ public class UploadService extends HandlerService<Contribution> {
public static final int ACTION_UPLOAD_FILE = 1; public static final int ACTION_UPLOAD_FILE = 1;
public static final String ACTION_START_SERVICE = EXTRA_PREFIX + ".upload"; public static final String ACTION_START_SERVICE = EXTRA_PREFIX + ".upload";
public static final String EXTRA_SOURCE = EXTRA_PREFIX + ".source";
public static final String EXTRA_FILES = EXTRA_PREFIX + ".files"; public static final String EXTRA_FILES = EXTRA_PREFIX + ".files";
@Inject WikidataEditService wikidataEditService; @Inject WikidataEditService wikidataEditService;
@Inject SessionManager sessionManager; @Inject SessionManager sessionManager;
@ -152,7 +146,6 @@ public class UploadService extends HandlerService<Contribution> {
@Override @Override
public void queue(int what, Contribution contribution) { public void queue(int what, Contribution contribution) {
Timber.d("Upload service queue has contribution with wiki data entity id as %s", contribution.getWikiDataEntityId());
switch (what) { switch (what) {
case ACTION_UPLOAD_FILE: case ACTION_UPLOAD_FILE:
@ -169,7 +162,7 @@ public class UploadService extends HandlerService<Contribution> {
.subscribeOn(ioThreadScheduler) .subscribeOn(ioThreadScheduler)
.observeOn(mainThreadScheduler) .observeOn(mainThreadScheduler)
.subscribe(aLong->{ .subscribe(aLong->{
contribution._id = aLong; contribution.set_id(aLong);
UploadService.super.queue(what, contribution); UploadService.super.queue(what, contribution);
}, Throwable::printStackTrace)); }, Throwable::printStackTrace));
break; break;
@ -252,55 +245,68 @@ public class UploadService extends HandlerService<Contribution> {
Timber.d("Stash upload response 1 is %s", uploadStash.toString()); Timber.d("Stash upload response 1 is %s", uploadStash.toString());
String resultStatus = uploadStash.getResult(); if (uploadStash.isSuccessful()) {
if (!resultStatus.equals("Success")) {
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution);
return Observable.never();
} else {
Timber.d("making sure of uniqueness of name: %s", filename); Timber.d("making sure of uniqueness of name: %s", filename);
String uniqueFilename = findUniqueFilename(filename); String uniqueFilename = findUniqueFilename(filename);
unfinishedUploads.add(uniqueFilename); unfinishedUploads.add(uniqueFilename);
return uploadClient.uploadFileFromStash( return uploadClient.uploadFileFromStash(
getApplicationContext(), getApplicationContext(),
contribution, contribution,
uniqueFilename, uniqueFilename,
uploadStash.getFilekey()); uploadStash.getFilekey());
} } else {
})
.subscribe(uploadResult -> {
Timber.d("Stash upload response 2 is %s", uploadResult.toString());
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
String resultStatus = uploadResult.getResult();
if (!resultStatus.equals("Success")) {
Timber.d("Contribution upload failed. Wikidata entity won't be edited"); Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution); showFailedNotification(contribution);
} else { return Observable.never();
String canonicalFilename = "File:" + uploadResult.getFilename();
Timber.d("Contribution upload success. Initiating Wikidata edit for"
+ " entity id %s if necessary (if P18 is null). P18 value is %s",
contribution.getWikiDataEntityId(), contribution.getP18Value());
wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), contribution.getWikiItemName(), canonicalFilename, contribution.getP18Value());
contribution.setFilename(canonicalFilename);
contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl());
contribution.setState(Contribution.STATE_COMPLETED);
contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatTimestamp()
.parse(uploadResult.getImageinfo().getTimestamp()));
compositeDisposable.add(contributionDao
.save(contribution)
.subscribeOn(ioThreadScheduler)
.observeOn(mainThreadScheduler)
.subscribe());
} }
}, throwable -> { })
.subscribe(
uploadResult -> onUpload(contribution, notificationTag, uploadResult),
throwable -> {
Timber.w(throwable, "Exception during upload"); Timber.w(throwable, "Exception during upload");
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS); notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
showFailedNotification(contribution); showFailedNotification(contribution);
}); });
} }
private void onUpload(Contribution contribution, String notificationTag,
UploadResult uploadResult) throws ParseException {
Timber.d("Stash upload response 2 is %s", uploadResult.toString());
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
if (uploadResult.isSuccessful()) {
onSuccessfulUpload(contribution, uploadResult);
} else {
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution);
}
}
private void onSuccessfulUpload(Contribution contribution, UploadResult uploadResult)
throws ParseException {
compositeDisposable
.add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution));
WikidataPlace wikidataPlace = contribution.getWikidataPlace();
if (wikidataPlace != null && wikidataPlace.getImageValue() == null) {
wikidataEditService.createImageClaim(wikidataPlace, uploadResult);
}
saveCompletedContribution(contribution, uploadResult);
}
private void saveCompletedContribution(Contribution contribution, UploadResult uploadResult) throws ParseException {
contribution.setFilename(uploadResult.createCanonicalFileName());
contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl());
contribution.setState(Contribution.STATE_COMPLETED);
contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatTimestamp()
.parse(uploadResult.getImageinfo().getTimestamp()));
compositeDisposable.add(contributionDao
.save(contribution)
.subscribeOn(ioThreadScheduler)
.observeOn(mainThreadScheduler)
.subscribe());
}
@SuppressLint("StringFormatInvalid") @SuppressLint("StringFormatInvalid")
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private void showFailedNotification(Contribution contribution) { private void showFailedNotification(Contribution contribution) {

View file

@ -0,0 +1,46 @@
package fr.free.nrw.commons.upload;
import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
import androidx.annotation.NonNull;
import io.reactivex.Observable;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.Query;
/**
* 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);
/**
* 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
*/
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=wbsetlabel")
Observable<MwPostResponse> addLabelstoWikidata(@Field("id") String fileEntityId,
@Field("token") String editToken,
@Field("language") String language,
@Field("value") String captionValue);
}

View file

@ -0,0 +1,6 @@
package fr.free.nrw.commons.upload
interface WikidataItem {
val id:String
val name:String
}

View file

@ -0,0 +1,21 @@
package fr.free.nrw.commons.upload
import android.os.Parcelable
import fr.free.nrw.commons.nearby.Place
import kotlinx.android.parcel.Parcelize
@Parcelize
internal data class WikidataPlace(override val id: String, override val name: String, val imageValue: String?) :
WikidataItem,Parcelable {
constructor(place: Place) : this(
place.wikiDataEntityId!!,
place.name,
place.pic.takeIf { it.isNotBlank() })
companion object {
@JvmStatic
fun from(place: Place?): WikidataPlace? {
return place?.let { WikidataPlace(it) }
}
}
}

View file

@ -10,7 +10,7 @@ import fr.free.nrw.commons.category.CategoryItem;
*/ */
public interface CategoriesContract { public interface CategoriesContract {
public interface View { interface View {
void showProgress(boolean shouldShow); void showProgress(boolean shouldShow);
@ -20,16 +20,13 @@ public interface CategoriesContract {
void setCategories(List<CategoryItem> categories); void setCategories(List<CategoryItem> categories);
void addCategory(CategoryItem category);
void goToNextScreen(); void goToNextScreen();
void showNoCategorySelected(); void showNoCategorySelected();
void setSelectedCategories(List<CategoryItem> selectedCategories);
} }
public interface UserActionListener extends BasePresenter<View> { interface UserActionListener extends BasePresenter<View> {
void searchForCategories(String query); void searchForCategories(String query);

View file

@ -1,15 +1,9 @@
package fr.free.nrw.commons.upload.categories; package fr.free.nrw.commons.upload.categories;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
import android.text.TextUtils; import android.text.TextUtils;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryItem; import fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.repository.UploadRepository; import fr.free.nrw.commons.repository.UploadRepository;
@ -18,11 +12,14 @@ import io.reactivex.Observable;
import io.reactivex.Scheduler; import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import timber.log.Timber; 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 UploadCategoriesFragment * The presenter class for UploadCategoriesFragment
*/ */
@ -86,9 +83,10 @@ public class CategoriesPresenter implements CategoriesContract.UserActionListene
) )
.filter(categoryItem -> !repository.containsYear(categoryItem.getName())) .filter(categoryItem -> !repository.containsYear(categoryItem.getName()))
.distinct(); .distinct();
if(!TextUtils.isEmpty(query)) {
distinctCategoriesObservable=distinctCategoriesObservable.sorted(repository.sortBySimilarity(query)); if(!TextUtils.isEmpty(query)) {
} distinctCategoriesObservable=distinctCategoriesObservable.sorted(repository.sortBySimilarity(query));
}
Disposable searchCategoriesDisposable = distinctCategoriesObservable Disposable searchCategoriesDisposable = distinctCategoriesObservable
.observeOn(mainThreadScheduler) .observeOn(mainThreadScheduler)
.subscribe( .subscribe(
@ -114,8 +112,9 @@ public class CategoriesPresenter implements CategoriesContract.UserActionListene
private List<String> getImageTitleList() { private List<String> getImageTitleList() {
List<String> titleList = new ArrayList<>(); List<String> titleList = new ArrayList<>();
for (UploadItem item : repository.getUploads()) { for (UploadItem item : repository.getUploads()) {
if (item.getTitle().isSet()) { final String captionText = item.getUploadMediaDetails().get(0).getCaptionText();
titleList.add(item.getTitle().toString()); if (!TextUtils.isEmpty(captionText)) {
titleList.add(captionText);
} }
} }
return titleList; return titleList;

View file

@ -6,27 +6,18 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.textfield.TextInputLayout;
import com.jakewharton.rxbinding2.view.RxView; import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxTextView; import com.jakewharton.rxbinding2.widget.RxTextView;
import com.pedrogomez.renderers.RVRendererAdapter; 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.R;
import fr.free.nrw.commons.category.CategoryClickedListener; import fr.free.nrw.commons.category.CategoryClickedListener;
import fr.free.nrw.commons.category.CategoryItem; import fr.free.nrw.commons.category.CategoryItem;
@ -35,6 +26,10 @@ import fr.free.nrw.commons.upload.UploadCategoriesAdapterFactory;
import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.DialogUtil;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import timber.log.Timber; import timber.log.Timber;
public class UploadCategoriesFragment extends UploadBaseFragment implements CategoriesContract.View, public class UploadCategoriesFragment extends UploadBaseFragment implements CategoriesContract.View,
@ -54,7 +49,6 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
@Inject @Inject
CategoriesContract.UserActionListener presenter; CategoriesContract.UserActionListener presenter;
private RVRendererAdapter<CategoryItem> adapter; private RVRendererAdapter<CategoryItem> adapter;
private List<String> mediaTitleList=new ArrayList<>();
private Disposable subscribe; private Disposable subscribe;
private List<CategoryItem> categories; private List<CategoryItem> categories;
private boolean isVisible; private boolean isVisible;
@ -64,10 +58,6 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
} }
public void setMediaTitleList(List<String> mediaTitleList) {
this.mediaTitleList = mediaTitleList;
}
@Nullable @Nullable
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@ -151,12 +141,6 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
} }
} }
@Override
public void addCategory(CategoryItem category) {
adapter.add(category);
adapter.notifyItemInserted(adapter.getItemCount());
}
@Override @Override
public void goToNextScreen() { public void goToNextScreen() {
callback.onNextButtonClicked(callback.getIndexInViewFlipper(this)); callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
@ -174,11 +158,6 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
} }
@Override
public void setSelectedCategories(List<CategoryItem> selectedCategories) {
}
@OnClick(R.id.btn_next) @OnClick(R.id.btn_next)
public void onNextButtonClicked() { public void onNextButtonClicked() {
presenter.verifyCategories(); presenter.verifyCategories();

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,195 @@
package fr.free.nrw.commons.upload.depicts;
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.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
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 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.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 java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
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_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 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.yes_submit),
getString(R.string.no_go_back),
this::goToNextScreen,
null
);
}
@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) {
presenter.searchForDepictions(query);
}
}

View file

@ -0,0 +1,28 @@
package fr.free.nrw.commons.upload.depicts;
import fr.free.nrw.commons.wikidata.model.DepictSearchResponse;
import io.reactivex.Observable;
import org.wikipedia.wikidata.ClaimsResponse;
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<ClaimsResponse> getImageForEntity(@Query("entity") String entityId);
}

View file

@ -0,0 +1,152 @@
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 fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.repository.UploadRepository;
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 io.reactivex.schedulers.Schedulers;
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 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<>();
Observable<DepictedItem> distinctDepictsObservable = Observable
.fromIterable(repository.getSelectedDepictions())
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.doOnSubscribe(disposable -> {
view.showProgress(true);
view.setDepictsList(null);
})
.observeOn(ioScheduler)
.concatWith(
repository.searchAllEntities(query)
)
.distinct();
Disposable searchDepictsDisposable = distinctDepictsObservable
.observeOn(mainThreadScheduler)
.subscribe(
e -> {
depictedItemList.add(e);
},
t -> {
view.showProgress(false);
view.showError(true);
Timber.e(t);
},
() -> {
view.showProgress(false);
if (depictedItemList.isEmpty()) {
view.showError(true);
} else {
view.showError(false);
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()) {
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);
}));
}
}

View file

@ -3,15 +3,10 @@ package fr.free.nrw.commons.upload.mediaDetails;
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult; import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -24,7 +19,6 @@ import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
import com.github.chrisbanes.photoview.PhotoView; import com.github.chrisbanes.photoview.PhotoView;
import com.jakewharton.rxbinding2.widget.RxTextView;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
@ -32,18 +26,16 @@ import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.settings.Prefs; 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.ImageCoordinates; import fr.free.nrw.commons.upload.ImageCoordinates;
import fr.free.nrw.commons.upload.SimilarImageDialogFragment; import fr.free.nrw.commons.upload.SimilarImageDialogFragment;
import fr.free.nrw.commons.upload.Title;
import fr.free.nrw.commons.upload.UploadBaseFragment; import fr.free.nrw.commons.upload.UploadBaseFragment;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter;
import fr.free.nrw.commons.upload.UploadModel; import fr.free.nrw.commons.upload.UploadModel;
import fr.free.nrw.commons.upload.UploadModel.UploadItem; import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.disposables.Disposable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -53,8 +45,10 @@ import javax.inject.Named;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import timber.log.Timber; import timber.log.Timber;
//import fr.free.nrw.commons.upload.DescriptionsAdapter;
public class UploadMediaDetailFragment extends UploadBaseFragment implements public class UploadMediaDetailFragment extends UploadBaseFragment implements
UploadMediaDetailsContract.View { UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener {
@BindView(R.id.tv_title) @BindView(R.id.tv_title)
TextView tvTitle; TextView tvTitle;
@ -64,8 +58,6 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
AppCompatImageButton ibExpandCollapse; AppCompatImageButton ibExpandCollapse;
@BindView(R.id.ll_container_media_detail) @BindView(R.id.ll_container_media_detail)
LinearLayout llContainerMediaDetail; LinearLayout llContainerMediaDetail;
@BindView(R.id.et_title)
EditText etTitle;
@BindView(R.id.rv_descriptions) @BindView(R.id.rv_descriptions)
RecyclerView rvDescriptions; RecyclerView rvDescriptions;
@BindView(R.id.backgroundImage) @BindView(R.id.backgroundImage)
@ -74,12 +66,12 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
AppCompatButton btnNext; AppCompatButton btnNext;
@BindView(R.id.btn_previous) @BindView(R.id.btn_previous)
AppCompatButton btnPrevious; AppCompatButton btnPrevious;
private DescriptionsAdapter descriptionsAdapter; private UploadMediaDetailAdapter uploadMediaDetailAdapter;
@BindView(R.id.btn_copy_prev_title_desc) @BindView(R.id.btn_copy_prev_title_desc)
AppCompatButton btnCopyPreviousTitleDesc; AppCompatButton btnCopyPreviousTitleDesc;
private UploadModel.UploadItem uploadItem; private UploadModel.UploadItem uploadItem;
private List<Description> descriptions; private List<UploadMediaDetail> descriptions;
@Inject @Inject
UploadMediaDetailsContract.UserActionListener presenter; UploadMediaDetailsContract.UserActionListener presenter;
@ -89,10 +81,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
JsonKvStore defaultKvStore; JsonKvStore defaultKvStore;
private UploadableFile uploadableFile; private UploadableFile uploadableFile;
private String source;
private Place place; private Place place;
private Title title;
private boolean isExpanded = true; private boolean isExpanded = true;
private UploadMediaDetailFragmentCallback callback; private UploadMediaDetailFragmentCallback callback;
@ -106,9 +96,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
} }
public void setImageTobeUploaded(UploadableFile uploadableFile, String source, Place place) { public void setImageTobeUploaded(UploadableFile uploadableFile, Place place) {
this.uploadableFile = uploadableFile; this.uploadableFile = uploadableFile;
this.source = source;
this.place = place; this.place = place;
} }
@ -129,25 +118,9 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
private void init() { private void init() {
tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1, tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1,
callback.getTotalNumberOfSteps())); callback.getTotalNumberOfSteps()));
title = new Title();
initRecyclerView(); initRecyclerView();
initPresenter(); initPresenter();
Disposable disposable = RxTextView.textChanges(etTitle) presenter.receiveImage(uploadableFile, place);
.subscribe(text -> {
if (!TextUtils.isEmpty(text)) {
btnNext.setEnabled(true);
btnNext.setClickable(true);
btnNext.setAlpha(1.0f);
title.setTitleText(text.toString());
uploadItem.setTitle(title);
} else {
btnNext.setAlpha(0.5f);
btnNext.setEnabled(false);
btnNext.setClickable(false);
}
});
compositeDisposable.add(disposable);
presenter.receiveImage(uploadableFile, source, place);
if (callback.getIndexInViewFlipper(this) == 0) { if (callback.getIndexInViewFlipper(this) == 0) {
btnPrevious.setEnabled(false); btnPrevious.setEnabled(false);
@ -166,36 +139,6 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
attachImageViewScaleChangeListener(); attachImageViewScaleChangeListener();
addEtTitleTouchListener();
}
/**
* Handles the drawable click listener for Edit Text
*/
private void addEtTitleTouchListener() {
etTitle.setOnTouchListener((v, event) -> {
//2 is for drawable right
float twelveDpInPixels = convertDpToPixel(12, getContext());
if (event.getAction() == MotionEvent.ACTION_UP && etTitle.getCompoundDrawables() != null
&& etTitle.getCompoundDrawables().length > 2 && etTitle
.getCompoundDrawables()[2].getBounds()
.contains((int) (etTitle.getWidth() - (event.getX() + twelveDpInPixels)),
(int) (event.getY() - twelveDpInPixels))) {
showInfoAlert(R.string.media_detail_title, R.string.title_info);
return true;
}
return false;
});
}
/**
* converts dp to pixel
* @param dp
* @param context
* @return
*/
private float convertDpToPixel(float dp, Context context) {
return dp * ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT);
} }
/** /**
@ -217,13 +160,14 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
} }
/** /**
* init the recycler veiw * init the description recycler veiw and caption recyclerview
*/ */
private void initRecyclerView() { private void initRecyclerView() {
descriptionsAdapter = new DescriptionsAdapter(defaultKvStore.getString(Prefs.KEY_LANGUAGE_VALUE, "")); uploadMediaDetailAdapter = new UploadMediaDetailAdapter(defaultKvStore.getString(Prefs.KEY_LANGUAGE_VALUE, ""));
descriptionsAdapter.setCallback(this::showInfoAlert); uploadMediaDetailAdapter.setCallback(this::showInfoAlert);
uploadMediaDetailAdapter.setEventListener(this);
rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext())); rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
rvDescriptions.setAdapter(descriptionsAdapter); rvDescriptions.setAdapter(uploadMediaDetailAdapter);
} }
/** /**
@ -237,7 +181,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
@OnClick(R.id.btn_next) @OnClick(R.id.btn_next)
public void onNextButtonClicked() { public void onNextButtonClicked() {
uploadItem.setDescriptions(descriptionsAdapter.getDescriptions()); uploadItem.setMediaDetails(uploadMediaDetailAdapter.getUploadMediaDetails());
presenter.verifyImageQuality(uploadItem); presenter.verifyImageQuality(uploadItem);
} }
@ -248,9 +192,9 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
@OnClick(R.id.btn_add_description) @OnClick(R.id.btn_add_description)
public void onButtonAddDescriptionClicked() { public void onButtonAddDescriptionClicked() {
Description description = new Description(); UploadMediaDetail uploadMediaDetail = new UploadMediaDetail();
description.setManuallyAdded(true);//This was manually added by the user uploadMediaDetail.setManuallyAdded(true);//This was manually added by the user
descriptionsAdapter.addDescription(description); uploadMediaDetailAdapter.addDescription(uploadMediaDetail);
} }
@Override @Override
@ -279,11 +223,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
@Override @Override
public void onImageProcessed(UploadItem uploadItem, Place place) { public void onImageProcessed(UploadItem uploadItem, Place place) {
this.uploadItem = uploadItem; this.uploadItem = uploadItem;
if (uploadItem.getTitle() != null) { descriptions = uploadItem.getUploadMediaDetails();
etTitle.setText(uploadItem.getTitle().toString());
}
descriptions = uploadItem.getDescriptions();
photoViewBackgroundImage.setImageURI(uploadItem.getMediaUri()); photoViewBackgroundImage.setImageURI(uploadItem.getMediaUri());
setDescriptionsInAdapter(descriptions); setDescriptionsInAdapter(descriptions);
} }
@ -302,11 +242,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
getString(R.string.upload_nearby_place_found_description), getString(R.string.upload_nearby_place_found_description),
place.getName()), place.getName()),
() -> { () -> {
etTitle.setText(place.getName()); descriptions = new ArrayList<>(Arrays.asList(new UploadMediaDetail(place)));
Description description = new Description();
description.setLanguageCode("en");
description.setDescriptionText(place.getLongDescription());
descriptions = Arrays.asList(description);
setDescriptionsInAdapter(descriptions); setDescriptionsInAdapter(descriptions);
}, },
() -> { () -> {
@ -376,9 +312,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
} }
@Override @Override
public void setTitleAndDescription(String title, List<Description> descriptions) { public void setCaptionsAndDescriptions(List<UploadMediaDetail> uploadMediaDetails) {
etTitle.setText(title); setDescriptionsInAdapter(uploadMediaDetails);
setDescriptionsInAdapter(descriptions);
} }
private void deleteThisPicture() { private void deleteThisPicture() {
@ -412,6 +347,13 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
uploadItem.getGpsCoords().getDecLongitude(), 0.0f)); uploadItem.getGpsCoords().getDecLongitude(), 0.0f));
} }
@Override
public void onPrimaryCaptionTextChange(boolean isNotEmpty) {
btnNext.setEnabled(isNotEmpty);
btnNext.setClickable(isNotEmpty);
btnNext.setAlpha(isNotEmpty ? 1.0f: 0.5f);
}
public interface UploadMediaDetailFragmentCallback extends Callback { public interface UploadMediaDetailFragmentCallback extends Callback {
@ -424,15 +366,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
presenter.fetchPreviousTitleAndDescription(callback.getIndexInViewFlipper(this)); presenter.fetchPreviousTitleAndDescription(callback.getIndexInViewFlipper(this));
} }
private void setDescriptionsInAdapter(List<Description> descriptions) { private void setDescriptionsInAdapter(List<UploadMediaDetail> uploadMediaDetails){
if (descriptions == null) { uploadMediaDetailAdapter.setItems(uploadMediaDetails);
descriptions = new ArrayList<>();
}
if (descriptions.size() == 0) {
descriptionsAdapter.addDescription(new Description());
} else {
descriptionsAdapter.setItems(descriptions);
}
} }
} }

View file

@ -1,15 +1,13 @@
package fr.free.nrw.commons.upload.mediaDetails; package fr.free.nrw.commons.upload.mediaDetails;
import fr.free.nrw.commons.upload.ImageCoordinates;
import java.util.List;
import fr.free.nrw.commons.BasePresenter; 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.filepicker.UploadableFile;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.upload.Description; import fr.free.nrw.commons.upload.ImageCoordinates;
import fr.free.nrw.commons.upload.SimilarImageInterface; import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.UploadModel.UploadItem; import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import java.util.List;
/** /**
* The contract with with UploadMediaDetails and its presenter would talk to each other * The contract with with UploadMediaDetails and its presenter would talk to each other
@ -36,13 +34,12 @@ public interface UploadMediaDetailsContract {
void showMapWithImageCoordinates(boolean shouldShow); void showMapWithImageCoordinates(boolean shouldShow);
void setTitleAndDescription(String title, List<Description> descriptions); void setCaptionsAndDescriptions(List<UploadMediaDetail> uploadMediaDetails);
} }
interface UserActionListener extends BasePresenter<View> { interface UserActionListener extends BasePresenter<View> {
void receiveImage(UploadableFile uploadableFile, @Contribution.FileSource String source, void receiveImage(UploadableFile uploadableFile, Place place);
Place place);
void verifyImageQuality(UploadItem uploadItem); void verifyImageQuality(UploadItem uploadItem);

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.upload.mediaDetails;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; 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.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.FILE_NAME_EXISTS;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP; import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
@ -64,16 +64,14 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
/** /**
* Receives the corresponding uploadable file, processes it and return the view with and uplaod item * Receives the corresponding uploadable file, processes it and return the view with and uplaod item
* * @param uploadableFile
* @param uploadableFile
* @param source
* @param place * @param place
*/ */
@Override @Override
public void receiveImage(UploadableFile uploadableFile, String source, Place place) { public void receiveImage(UploadableFile uploadableFile, Place place) {
view.showProgress(true); view.showProgress(true);
Disposable uploadItemDisposable = repository Disposable uploadItemDisposable = repository
.preProcessImage(uploadableFile, place, source, this) .preProcessImage(uploadableFile, place, this)
.subscribeOn(ioScheduler) .subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler) .observeOn(mainThreadScheduler)
.subscribe(uploadItem -> .subscribe(uploadItem ->
@ -143,7 +141,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 * @param indexInViewFlipper
*/ */
@ -151,7 +149,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
public void fetchPreviousTitleAndDescription(int indexInViewFlipper) { public void fetchPreviousTitleAndDescription(int indexInViewFlipper) {
UploadItem previousUploadItem = repository.getPreviousUploadItem(indexInViewFlipper); UploadItem previousUploadItem = repository.getPreviousUploadItem(indexInViewFlipper);
if (null != previousUploadItem) { if (null != previousUploadItem) {
view.setTitleAndDescription(previousUploadItem.getTitle().getTitleText(), previousUploadItem.getDescriptions()); view.setCaptionsAndDescriptions(previousUploadItem.getUploadMediaDetails());
} else { } else {
view.showMessage(R.string.previous_image_title_description_not_found, R.color.color_error); view.showMessage(R.string.previous_image_title_description_not_found, R.color.color_error);
} }
@ -176,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 * @param errorCode
*/ */
@ -188,9 +186,9 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
} }
switch (errorCode) { switch (errorCode) {
case EMPTY_TITLE: case EMPTY_CAPTION:
Timber.d("Title is empty. Showing toast"); Timber.d("Captions are empty. Showing toast");
view.showMessage(R.string.add_title_toast, R.color.color_error); view.showMessage(R.string.add_caption_toast, R.color.color_error);
break; break;
case FILE_NAME_EXISTS: case FILE_NAME_EXISTS:
Timber.d("Trying to show duplicate picture popup"); Timber.d("Trying to show duplicate picture popup");

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