#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">
<option name="reportWhenNoStatementFollow" value="true" />
</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="FieldMayBeFinal" 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')
// Utils
implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
implementation 'in.yuvi:http.fluent:1.3'
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 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
@ -44,6 +43,7 @@ dependencies {
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
implementation 'com.karumi:dexter:5.0.0'
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
// Logging
@ -53,7 +53,7 @@ dependencies {
api('com.github.tony19:logback-android-classic:1.1.1-6') {
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
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
@ -65,7 +65,7 @@ dependencies {
//Mocking
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
testImplementation 'org.mockito:mockito-inline:2.8.47'
testImplementation 'org.mockito:mockito-inline:2.13.0'
testImplementation 'org.mockito:mockito-core:2.23.0'
testImplementation "org.powermock:powermock-module-junit4:2.0.0-beta.5"
testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5"
@ -108,9 +108,10 @@ dependencies {
//Room
implementation "androidx.room:room-runtime:$ROOM_VERSION"
kapt "androidx.room:room-compiler:$ROOM_VERSION" // For Kotlin use kapt instead of annotationProcessor
implementation 'com.squareup.retrofit2:retrofit:2.7.1'
implementation "androidx.room:room-ktx:$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"
// 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_HOST", "\"https://commons.wikimedia.org/w/api.php\""
buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json\""
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
@ -229,6 +231,7 @@ android {
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
buildConfigField "String", "TEST_USERNAME", "\"" + System.getenv("test_user_name") + "\""
buildConfigField "String", "TEST_PASSWORD", "\"" + System.getenv("test_user_password") + "\""
buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\""
dimension 'tier'
}
@ -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_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\""
buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns_beta_active.json\""
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
@ -261,6 +265,7 @@ android {
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
buildConfigField "String", "TEST_USERNAME", "\"" + System.getenv("test_user_name") + "\""
buildConfigField "String", "TEST_PASSWORD", "\"" + System.getenv("test_user_password") + "\""
buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\""
dimension 'tier'
}

View file

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

View file

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

View file

@ -119,6 +119,11 @@
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".depictions.WikidataItemDetailsActivity"
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".explore.categories.ExploreActivity"
android:label="@string/title_activity_explore"

View file

@ -1,62 +1,5 @@
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 org.acra.ReportField.ANDROID_VERSION;
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.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(
buildConfigClass = BuildConfig.class,
resReportSendSuccessToast = R.string.crash_dialog_ok_toast,
@ -120,8 +114,7 @@ public class CommonsApplication extends Application {
return languageLookUpTable;
}
@Inject
AppDatabase appDatabase;
@Inject ContributionDao contributionDao;
/**
* Used to declare and initialize various components and dependencies
@ -299,7 +292,7 @@ public class CommonsApplication extends Application {
CategoryDao.Table.onDelete(db);
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);
BookmarkLocationsDao.Table.onDelete(db);
}

View file

@ -3,31 +3,24 @@ package fr.free.nrw.commons;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.gallery.ExtMetadata;
import org.wikipedia.gallery.ImageInfo;
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
public class Media implements Parcelable {
@ -35,32 +28,42 @@ public class Media implements Parcelable {
// Primary metadata fields
@Nullable
public Uri localUri;
public String thumbUrl;
public String imageUrl;
public String filename;
public String description; // monolingual description on input...
public String discussion;
long dataLength;
public Date dateCreated;
@Nullable public Date dateUploaded;
public int width;
public int height;
public String license;
public String licenseUrl;
public String creator;
public ArrayList<String> categories; // as loaded at runtime?
public boolean requestedDeletion;
public HashMap<String, String> descriptions; // multilingual descriptions as loaded
public HashMap<String, String> tags = new HashMap<>();
@Nullable public LatLng coordinates;
private Uri localUri;
private String thumbUrl;
private String imageUrl;
private String filename;
private String thumbnailTitle;
/*
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
* This is a replacement of the previously used titles for images (titles were not multilingual)
* Also now captions replace the previous convention of using title for filename
*/
private String caption;
private String description; // monolingual description on input...
private String discussion;
private long dataLength;
private Date dateCreated;
@Nullable private Date dateUploaded;
private String license;
private String licenseUrl;
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
*/
protected Media() {
this.categories = new ArrayList<>();
this.descriptions = new HashMap<>();
public Media() {
}
/**
@ -69,7 +72,6 @@ public class Media implements Parcelable {
* @param filename Media filename
*/
public Media(String filename) {
this();
this.filename = filename;
}
@ -84,9 +86,9 @@ public class Media implements Parcelable {
* @param dateUploaded Media date uploaded
* @param creator Media creator
*/
public Media(Uri localUri, String imageUrl, String filename, String description,
public Media(Uri localUri, String imageUrl, String filename,
String description,
long dataLength, Date dateCreated, Date dateUploaded, String creator) {
this();
this.localUri = localUri;
this.thumbUrl = imageUrl;
this.imageUrl = imageUrl;
@ -96,8 +98,17 @@ public class Media implements Parcelable {
this.dateCreated = dateCreated;
this.dateUploaded = dateUploaded;
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) {
ImageInfo imageInfo = page.imageInfo();
if (imageInfo == null) {
return null;
return new Media(); // null is not allowed
}
ExtMetadata metadata = imageInfo.getMetadata();
if (metadata == null) {
@ -138,12 +149,14 @@ public class Media implements Parcelable {
media.setThumbUrl(imageInfo.getThumbUrl());
}
media.setPageId(String.valueOf(page.pageId()));
String language = Locale.getDefault().getLanguage();
if (StringUtils.isBlank(language)) {
language = "default";
}
media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription()));
media.setDescription(metadata.imageDescription());
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories()));
String latitude = metadata.getGpsLatitude();
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() {
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
* @return Media title
@ -202,6 +210,21 @@ public class Media implements Parcelable {
return filename != null ? getPageTitle().getDisplayTextWithoutNamespace().replaceFirst("[.][^.]+$", "") : "";
}
/**
* Set Caption(if available) as the thumbnail title of the image
*/
public void setThumbnailTitle(String title) {
this.thumbnailTitle = title;
}
/**
* @return title to be shown on image thumbnail
* If caption is available for the image then it returns caption else filename
*/
public String getThumbnailTitle() {
return thumbnailTitle != null? thumbnailTitle : getDisplayTitle();
}
/**
* Gets file page title
* @return New media page title
@ -268,6 +291,24 @@ public class Media implements Parcelable {
return description;
}
/**
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
* This is a replacement of the previously used titles for images (titles were not multilingual)
* Also now captions replace the previous convention of using title for filename
*
* @return caption
*/
public String getCaption() {
return caption;
}
/**
* @return depictions associated with the current media
*/
public Depictions getDepiction() {
return depictions;
}
/**
* Sets the file description.
* @param description the new description of the file
@ -334,38 +375,6 @@ public class Media implements Parcelable {
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.
* @return license as a String
@ -417,8 +426,8 @@ public class Media implements Parcelable {
* @return file categories as an ArrayList of Strings
*/
@SuppressWarnings("unchecked")
public ArrayList<String> getCategories() {
return (ArrayList<String>) categories.clone(); // feels dirty
public List<String> getCategories() {
return categories;
}
/**
@ -429,38 +438,7 @@ public class Media implements Parcelable {
* @param categories file categories as a list of Strings
*/
public void setCategories(List<String> categories) {
this.categories.clear();
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 "";
}
this.categories = categories;
}
@Nullable private static Date safeParseDate(String dateStr) {
@ -473,16 +451,17 @@ public class Media implements Parcelable {
/**
* Set requested deletion to true
* @param requestedDeletion
*/
public void setRequestedDeletion(){
requestedDeletion = true;
public void setRequestedDeletion(boolean requestedDeletion){
this.requestedDeletion = requestedDeletion;
}
/**
* Get the value of requested deletion
* @return boolean requestedDeletion
*/
public boolean getRequestedDeletion(){
public boolean isRequestedDeletion(){
return requestedDeletion;
}
@ -495,6 +474,42 @@ public class Media implements Parcelable {
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
public int describeContents() {
@ -513,20 +528,20 @@ public class Media implements Parcelable {
dest.writeString(this.thumbUrl);
dest.writeString(this.imageUrl);
dest.writeString(this.filename);
dest.writeString(this.thumbnailTitle);
dest.writeString(this.caption);
dest.writeString(this.description);
dest.writeString(this.discussion);
dest.writeLong(this.dataLength);
dest.writeLong(this.dateCreated != null ? this.dateCreated.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.licenseUrl);
dest.writeString(this.creator);
dest.writeString(this.pageId);
dest.writeStringList(this.categories);
dest.writeParcelable(this.depictions, flags);
dest.writeByte(this.requestedDeletion ? (byte) 1 : (byte) 0);
dest.writeSerializable(this.descriptions);
dest.writeSerializable(this.tags);
dest.writeParcelable(this.coordinates, flags);
}
@ -535,6 +550,8 @@ public class Media implements Parcelable {
this.thumbUrl = in.readString();
this.imageUrl = in.readString();
this.filename = in.readString();
this.thumbnailTitle = in.readString();
this.caption = in.readString();
this.description = in.readString();
this.discussion = in.readString();
this.dataLength = in.readLong();
@ -542,15 +559,15 @@ public class Media implements Parcelable {
this.dateCreated = tmpDateCreated == -1 ? null : new Date(tmpDateCreated);
long tmpDateUploaded = in.readLong();
this.dateUploaded = tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded);
this.width = in.readInt();
this.height = in.readInt();
this.license = in.readString();
this.licenseUrl = 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.descriptions = (HashMap<String, String>) in.readSerializable();
this.tags = (HashMap<String, String>) in.readSerializable();
this.coordinates = in.readParcelable(LatLng.class.getClassLoader());
}

View file

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

View file

@ -1,6 +1,11 @@
package fr.free.nrw.commons;
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.IOException;
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;
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.upload.GpsCategoryModel;
import fr.free.nrw.commons.utils.StringSortingUtils;
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;
/**
@ -27,19 +23,19 @@ public class CategoriesModel{
private final CategoryClient categoryClient;
private final CategoryDao categoryDao;
private final JsonKvStore directKvStore;
private final GpsCategoryModel gpsCategoryModel;
private HashMap<String, ArrayList<String>> categoriesCache;
private List<CategoryItem> selectedCategories;
@Inject GpsCategoryModel gpsCategoryModel;
@Inject
public CategoriesModel(CategoryClient categoryClient,
CategoryDao categoryDao,
@Named("default_preferences") JsonKvStore directKvStore) {
@Named("default_preferences") JsonKvStore directKvStore,
final GpsCategoryModel gpsCategoryModel) {
this.categoryClient = categoryClient;
this.categoryDao = categoryDao;
this.directKvStore = directKvStore;
this.categoriesCache = new HashMap<>();
this.gpsCategoryModel = gpsCategoryModel;
this.selectedCategories = new ArrayList<>();
}
@ -94,10 +90,6 @@ public class CategoriesModel{
categoryDao.save(category);
}
boolean cacheContainsKey(String term) {
return categoriesCache.containsKey(term);
}
//endregion
/**
* Regional category search
@ -108,20 +100,18 @@ public class CategoriesModel{
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 (TextUtils.isEmpty(term)) {
Observable<CategoryItem> categoryItemObservable = gpsCategories()
.concatWith(titleCategories(imageTitleList));
Observable<CategoryItem> categoryItemObservable =
Observable.concat(gpsCategories(), titleCategories(imageTitleList));
if (hasDirectCategories()) {
categoryItemObservable.concatWith(directCategories().concatWith(recentCategories()));
return Observable.concat(
categoryItemObservable,
directCategories(),
recentCategories()
);
}
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
//term passed as lower case to make search case-insensitive(taking only lower case for everything)
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
* @return
@ -256,7 +237,6 @@ public class CategoriesModel{
* Cleanup the existing in memory cache's
*/
public void cleanUp() {
this.categoriesCache.clear();
this.selectedCategories.clear();
}
}

View file

@ -1,5 +1,9 @@
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.os.Bundle;
import android.view.LayoutInflater;
@ -12,15 +16,7 @@ import android.widget.ListAdapter;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
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.ButterKnife;
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.disposables.CompositeDisposable;
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 static android.view.View.GONE;
import static android.view.View.VISIBLE;
/**
* Displays images for a particular category with load more on scrolling incorporated
*/
public class CategoryImagesListFragment extends DaggerFragment {
private static int TIMEOUT_SECONDS = 15;
/**
* counts the total number of items loaded from the API
*/
private int mediaSize = 0;
private GridViewAdapter gridAdapter;
@ -256,6 +257,38 @@ public class CategoryImagesListFragment extends DaggerFragment {
progressBar.setVisibility(GONE);
isLoading = false;
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.TextView;
import androidx.annotation.Nullable;
import com.facebook.drawee.view.SimpleDraweeView;
import java.util.ArrayList;
@ -55,7 +57,7 @@ public class GridViewAdapter extends ArrayAdapter {
data = new ArrayList<>();
return false;
}
if (data.size() <= 0) {
if (data.isEmpty()) {
return false;
}
String fileName = data.get(0).getFilename();
@ -86,12 +88,22 @@ public class GridViewAdapter extends ArrayAdapter {
SimpleDraweeView imageView = convertView.findViewById(R.id.categoryImageView);
TextView fileName = convertView.findViewById(R.id.categoryImageTitle);
TextView author = convertView.findViewById(R.id.categoryImageAuthor);
fileName.setText(item.getDisplayTitle());
fileName.setText(item.getThumbnailTitle());
setAuthorView(item, author);
imageView.setImageURI(item.getThumbUrl());
return convertView;
}
/**
* @return the Media item at the given position
*/
@Nullable
@Override
public Media getItem(int position) {
return data.get(position);
}
/**
* Shows author information if its present
* @param item

View file

@ -1,256 +1,156 @@
package fr.free.nrw.commons.contributions;
import android.content.Context;
import android.net.Uri;
import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.StringDef;
import androidx.room.Entity;
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.filepicker.UploadableFile;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.utils.ConfigUtils;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import fr.free.nrw.commons.upload.WikidataPlace;
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")
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
public static final int STATE_COMPLETED = -1;
public static final int STATE_FAILED = 1;
public static final int STATE_QUEUED = 2;
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)
@NonNull
public long _id;
public Uri contentUri;
public String source;
public String editSummary;
public int state;
public long transferred;
public String decimalCoords;
public boolean isMultiple;
public String wikiDataEntityId;
public String wikiItemName;
private String p18Value;
public Uri contentProviderUri;
public String dateCreatedSource;
private long _id;
private int state;
private long transferred;
private String decimalCoords;
private String dateCreatedSource;
private WikidataPlace wikidataPlace;
/**
* Each depiction loaded in depictions activity is associated with a wikidata entity id,
* this Id is in turn used to upload depictions to wikibase
*/
private List<DepictedItem> depictedItems = new ArrayList<>();
private String mimeType;
/**
* 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,
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() {
}
public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength,
Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords, int state) {
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
this.decimalCoords = decimalCoords;
this.editSummary = editSummary;
this.dateCreatedSource = "";
this.state=state;
public Contribution(final UploadItem item, final SessionManager sessionManager,
final List<DepictedItem> depictedItems, final List<String> categories) {
super(item.getMediaUri(),
item.getFileName(),
UploadMediaDetail.formatList(item.getUploadMediaDetails()),
sessionManager.getAuthorName(),
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(String dateCreatedSource) {
public void setDateCreatedSource(final String dateCreatedSource) {
this.dateCreatedSource = dateCreatedSource;
}
public boolean getMultiple() {
return isMultiple;
}
public void setMultiple(boolean multiple) {
isMultiple = multiple;
public String getDateCreatedSource() {
return dateCreatedSource;
}
public long getTransferred() {
return transferred;
}
public void setTransferred(long transferred) {
public void setTransferred(final long 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() {
return state;
}
public void setState(int state) {
public void setState(final int 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) {
StringBuilder buffer = new StringBuilder();
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();
if (!StringUtils.isBlank(templatizedCreatedDate)) {
buffer.append("|date=").append(templatizedCreatedDate);
public void setWikidataPlace(final WikidataPlace wikidataPlace) {
this.wikidataPlace = wikidataPlace;
}
buffer.append("}}").append("\n");
//Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null
if (decimalCoords != null) {
buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n");
public WikidataPlace getWikidataPlace() {
return wikidataPlace;
}
buffer.append("== {{int:license-header}} ==\n")
.append(licenseTemplateFor(getLicense())).append("\n\n")
.append("{{Uploaded from Mobile|platform=Android|version=")
.append(ConfigUtils.getVersionNameWithSha(applicationContext)).append("}}\n");
if(categories!=null&&categories.size()!=0) {
for (int i = 0; i < categories.size(); i++) {
String category = categories.get(i);
buffer.append("\n[[Category:").append(category).append("]]");
public long get_id() {
return _id;
}
public void set_id(final long _id) {
this._id = _id;
}
else
buffer.append("{{subst:unc}}");
return buffer.toString();
public String getDecimalCoords() {
return decimalCoords;
}
public void setDecimalCoords(final String decimalCoords) {
this.decimalCoords = decimalCoords;
}
public void setDepictedItems(final List<DepictedItem> depictedItems) {
this.depictedItems = depictedItems;
}
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
* @return
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
* This is a replacement of the previously used titles for images (titles were not multilingual)
* Also now captions replace the previous convention of using title for filename
*
* key of the HashMap is the language and value is the caption in the corresponding language
*
* returns list of captions stored in hashmap
*/
private String getTemplatizedCreatedDate() {
if (dateCreated != null) {
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 "";
public Map<String, String> getCaptions() {
return captions;
}
@Override
public void setFilename(String filename) {
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;
public void setCaptions(Map<String, String> captions) {
this.captions = captions;
}
@Override
@ -259,48 +159,34 @@ public class Contribution extends Media {
}
@Override
public void writeToParcel(Parcel dest, int flags) {
public void writeToParcel(final Parcel dest, final int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(this._id);
dest.writeParcelable(this.contentUri, flags);
dest.writeString(this.source);
dest.writeString(this.editSummary);
dest.writeInt(this.state);
dest.writeLong(this.transferred);
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);
dest.writeLong(_id);
dest.writeInt(state);
dest.writeLong(transferred);
dest.writeString(decimalCoords);
dest.writeString(dateCreatedSource);
dest.writeSerializable((HashMap) captions);
}
protected Contribution(Parcel in) {
protected Contribution(final Parcel in) {
super(in);
this._id = in.readLong();
this.contentUri = in.readParcelable(Uri.class.getClassLoader());
this.source = in.readString();
this.editSummary = in.readString();
this.state = in.readInt();
this.transferred = in.readLong();
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();
_id = in.readLong();
state = in.readInt();
transferred = in.readLong();
decimalCoords = in.readString();
dateCreatedSource = in.readString();
captions = (HashMap<String, String>) in.readSerializable();
}
public static final Creator<Contribution> CREATOR = new Creator<Contribution>() {
@Override
public Contribution createFromParcel(Parcel source) {
public Contribution createFromParcel(final Parcel source) {
return new Contribution(source);
}
@Override
public Contribution[] newArray(int size) {
public Contribution[] newArray(final int size) {
return new Contribution[size];
}
};

View file

@ -1,19 +1,13 @@
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.app.Activity;
import android.content.Context;
import android.content.Intent;
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.filepicker.DefaultCallback;
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.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA;
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_SOURCE;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
@Singleton
public class ContributionController {
@ -109,7 +102,7 @@ public class ContributionController {
@Override
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);
}
});
@ -125,11 +118,9 @@ public class ContributionController {
* Attaches place object for nearby uploads
*/
private Intent handleImagesPicked(Context context,
List<UploadableFile> imagesFiles,
String source) {
List<UploadableFile> imagesFiles) {
Intent shareIntent = new Intent(context, UploadActivity.class);
shareIntent.setAction(ACTION_INTERNAL_UPLOADS);
shareIntent.putExtra(EXTRA_SOURCE, source);
shareIntent.putParcelableArrayListExtra(EXTRA_FILES, new ArrayList<>(imagesFiles));
Place place = defaultKvStore.getJson(PLACE_OBJECT, Place.class);
if (place != null) {
@ -139,13 +130,4 @@ public class ContributionController {
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.Query;
import androidx.room.Transaction;
import androidx.room.Update;
import io.reactivex.disposables.Disposable;
import java.util.List;
import io.reactivex.Completable;
import io.reactivex.Single;
import java.util.List;
@Dao
public abstract class ContributionDao {
@ -40,9 +37,6 @@ public abstract class ContributionDao {
@Delete
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")
public abstract List<Contribution> getContributionWithTitle(String fileName);

View file

@ -1,33 +1,34 @@
package fr.free.nrw.commons.contributions;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.Nullable;
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.ButterKnife;
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.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.concurrent.TimeUnit;
import timber.log.Timber;
public class ContributionViewHolder extends RecyclerView.ViewHolder {
private static final long TIMEOUT_SECONDS = 15;
private final Callback callback;
@BindView(R.id.contributionImage)
SimpleDraweeView imageView;
@ -37,20 +38,26 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.contributionProgress) ProgressBar progressView;
@BindView(R.id.failed_image_options) LinearLayout failedImageOptions;
private int position;
private Contribution contribution;
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);
this.mediaClient = mediaClient;
ButterKnife.bind(this, parent);
this.callback=callback;
}
public void init(int position, Contribution contribution) {
this.contribution = contribution;
fetchAndDisplayCaption(contribution);
this.position = position;
String imageSource = chooseImageSource(contribution.thumbUrl, contribution.getLocalUri());
String imageSource = chooseImageSource(contribution.getThumbUrl(), contribution.getLocalUri());
if (!TextUtils.isEmpty(imageSource)) {
final ImageRequest imageRequest =
ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
@ -58,7 +65,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
.build();
imageView.setImageRequest(imageRequest);
}
titleView.setText(contribution.getDisplayTitle());
seqNumView.setText(String.valueOf(position + 1));
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
* null, moves to local uri and if both are null return null

View file

@ -1,5 +1,9 @@
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.content.ComponentName;
import android.content.Context;
@ -12,21 +16,12 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
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.ButterKnife;
import fr.free.nrw.commons.HandlerService;
@ -60,12 +55,11 @@ import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
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
extends CommonsDaggerSupportFragment
implements
@ -221,7 +215,7 @@ public class ContributionsFragment
@Override
public void fetchMediaUriFor(Contribution contribution) {
Timber.d("Fetching thumbnail for %s", contribution.filename);
Timber.d("Fetching thumbnail for %s", contribution.getFilename());
contributionsPresenter.fetchMediaDetails(contribution);
}
});

View file

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

View file

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

View file

@ -1,45 +1,30 @@
package fr.free.nrw.commons.contributions;
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.Nullable;
import androidx.lifecycle.LifecycleOwner;
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.Media;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
import fr.free.nrw.commons.db.AppDatabase;
import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.mwapi.UserClient;
import fr.free.nrw.commons.utils.ExecutorUtils;
import fr.free.nrw.commons.utils.NetworkUtils;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
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 static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
/**
* The presenter class for Contributions
*/
@ -105,12 +90,7 @@ public class ContributionsPresenter implements UserActionListener {
.observeOn(mainThreadScheduler)
.doOnNext(mwQueryLogEvent -> Timber.d("Received image %s", mwQueryLogEvent.title()))
.filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted()).doOnNext(mwQueryLogEvent -> Timber.d("Image %s passed filters", mwQueryLogEvent.title()))
.map(image -> {
Contribution contribution = new Contribution(null, null, image.title(),
"", -1, image.date(), image.date(), user,
"", "", STATE_COMPLETED);
return contribution;
})
.map(image -> new Contribution(image, user))
.toList()
.subscribe(this::saveContributionsToDB, error -> {
Timber.e("Failed to fetch contributions: %s", error.getMessage());
@ -197,11 +177,11 @@ public class ContributionsPresenter implements UserActionListener {
@Override
public void fetchMediaDetails(Contribution contribution) {
compositeDisposable.add(mediaDataExtractor
.getMediaFromFileName(contribution.filename)
.getMediaFromFileName(contribution.getFilename())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> {
contribution.thumbUrl=media.thumbUrl;
contribution.setThumbUrl(media.getThumbUrl());
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;
import android.net.Uri;
import androidx.room.TypeConverter;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.wikipedia.json.GsonUtil;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.location.LatLng;
import 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 static Gson getGson() {
@ -44,33 +44,74 @@ public class Converters {
}
@TypeConverter
public static String listObjectToString(ArrayList<String> objectList) {
return objectList == null ? null : getGson().toJson(objectList);
public static String listObjectToString(List<String> objectList) {
return writeObjectToString(objectList);
}
@TypeConverter
public static ArrayList<String> stringToArrayListObject(String objectList) {
return objectList == null ? null : getGson().fromJson(objectList,new TypeToken<ArrayList<String>>(){}.getType());
public static List<String> stringToListObject(String objectList) {
return readObjectWithTypeToken(objectList, new TypeToken<List<String>>() {});
}
@TypeConverter
public static String mapObjectToString(HashMap<String,String> objectList) {
return objectList == null ? null : getGson().toJson(objectList);
public static String mapObjectToString(Map<String,String> objectList) {
return writeObjectToString(objectList);
}
@TypeConverter
public static HashMap<String,String> stringToMap(String objectList) {
return objectList == null ? null : getGson().fromJson(objectList,new TypeToken<HashMap<String,String>>(){}.getType());
public static Map<String,String> stringToMap(String objectList) {
return readObjectWithTypeToken(objectList, new TypeToken<Map<String,String>>(){});
}
@TypeConverter
public static String latlngObjectToString(LatLng latlng) {
return latlng == null ? null : getGson().toJson(latlng);
return writeObjectToString(latlng);
}
@TypeConverter
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;
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_DELETE;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
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.Media;
import fr.free.nrw.commons.R;
@ -29,10 +19,16 @@ import io.reactivex.Single;
import io.reactivex.SingleSource;
import io.reactivex.android.schedulers.AndroidSchedulers;
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 static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_DELETE;
/**
* Refactored async task to Rx
*/
@ -104,7 +100,7 @@ public class DeleteHelper {
}
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 -> {
if (result) {
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.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.explore.categories.ExploreActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
@ -60,6 +61,9 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract CategoryDetailsActivity bindCategoryDetailsActivity();
@ContributesAndroidInjector
abstract WikidataItemDetailsActivity bindDepictionDetailsActivity();
@ContributesAndroidInjector
abstract ExploreActivity bindExploreActivity();

View file

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

View file

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

View file

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

View file

@ -1,24 +1,8 @@
package fr.free.nrw.commons.di;
import android.content.Context;
import androidx.annotation.NonNull;
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.Provides;
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.category.CategoryInterface;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaDetailInterface;
import fr.free.nrw.commons.media.MediaInterface;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.mwapi.UserInterface;
import fr.free.nrw.commons.review.ReviewInterface;
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 java.io.File;
import java.util.concurrent.TimeUnit;
import javax.inject.Named;
import javax.inject.Singleton;
import okhttp3.Cache;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
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;
@Module
@ -72,7 +70,7 @@ public class NetworkingModule {
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(message -> {
Timber.tag("OkHttp").v(message);
});
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
httpLoggingInterceptor.level(BuildConfig.DEBUG ? Level.BODY: Level.BASIC);
return httpLoggingInterceptor;
}
@ -86,7 +84,6 @@ public class NetworkingModule {
toolsForgeUrl,
WIKIDATA_SPARQL_QUERY_URL,
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,
BuildConfig.WIKIMEDIA_API_HOST,
gson);
}
@ -133,6 +130,7 @@ public class NetworkingModule {
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.
* @return returns a singleton Gson instance
@ -163,6 +161,18 @@ public class NetworkingModule {
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
@Singleton
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);
}
@Provides
@Singleton
public MediaDetailInterface providesMediaDetailInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikisite) {
return ServiceFactory.get(commonsWikisite, BuildConfig.COMMONS_URL, MediaDetailInterface.class);
}
@Provides
@Singleton
public CategoryInterface provideCategoryInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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;
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.content.res.Configuration;
import android.os.Bundle;
@ -8,23 +12,12 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
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.ButterKnife;
import com.pedrogomez.renderers.RVRendererAdapter;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
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 io.reactivex.android.schedulers.AndroidSchedulers;
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 static android.view.View.GONE;
import static android.view.View.VISIBLE;
/**
* Displays the image search screen.
*/
@ -67,6 +63,11 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
@Named("default_preferences")
JsonKvStore defaultKvStore;
/**
* A variable to store number of list items for whom API has been called to fetch captions
*/
private int mediaSize = 0;
private RVRendererAdapter<Media> imagesAdapter;
private List<Media> queryList = new ArrayList<>();
@ -101,7 +102,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
ButterKnife.bind(this, rootView);
if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
if (getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
imagesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
}
else{
@ -199,6 +200,35 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
imagesAdapter.addAll(mediaList);
imagesAdapter.notifyDataSetChanged();
((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();
}
}
@ -221,7 +251,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
private void initErrorView() {
progressBar.setVisibility(GONE);
imagesNotFoundView.setVisibility(VISIBLE);
imagesNotFoundView.setText(getString(R.string.images_not_found));
imagesNotFoundView.setText(getString(R.string.images_not_found,query));
}
/**

View file

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

View file

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

View file

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

View file

@ -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 org.wikipedia.dataclient.mwapi.MwQueryResponse;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import io.reactivex.Observable;
import io.reactivex.Single;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import io.reactivex.Observable;
import io.reactivex.Single;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.wikidata.Entities;
import org.wikipedia.wikidata.Entities.Entity;
import org.wikipedia.wikidata.Entities.Label;
import timber.log.Timber;
/**
@ -28,13 +28,17 @@ import timber.log.Timber;
public class MediaClient {
private final MediaInterface mediaInterface;
private final MediaDetailInterface mediaDetailInterface;
//OkHttpJsonApiClient used JsonKvStore for this. I don't know why.
private Map<String, Map<String, String>> continuationStore;
public static final String NO_CAPTION = "No caption";
private static final String NO_DEPICTION = "No depiction";
@Inject
public MediaClient(MediaInterface mediaInterface) {
public MediaClient(MediaInterface mediaInterface, MediaDetailInterface mediaDetailInterface) {
this.mediaInterface = mediaInterface;
this.mediaDetailInterface = mediaDetailInterface;
this.continuationStore = new HashMap<>();
}
@ -88,7 +92,7 @@ public class MediaClient {
*/
public Single<List<Media>> getMediaListFromSearch(String keyword) {
return responseToMediaList(
continuationStore.containsKey("search_" + keyword) ?
continuationStore.containsKey("search_" + keyword) && (continuationStore.get("search_" + keyword) != null) ?
mediaInterface.getMediaListFromSearch(keyword, 10, continuationStore.get("search_" + keyword)) : //if true
mediaInterface.getMediaListFromSearch(keyword, 10, Collections.emptyMap()), //if false
"search_" + keyword);
@ -152,6 +156,7 @@ public class MediaClient {
.single(Media.EMPTY);
}
@NonNull
public Single<String> getPageHtml(String title){
return mediaInterface.getPageHtml(title)
@ -160,4 +165,62 @@ public class MediaClient {
.map(MwParseResult::text)
.first("");
}
/**
* @return caption for image using wikibaseIdentifier
*/
public Single<String> getCaptionByWikibaseIdentifier(String wikibaseIdentifier) {
return mediaDetailInterface.getCaptionForImage(Locale.getDefault().getLanguage(), wikibaseIdentifier)
.map(mediaDetailResponse -> {
if (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;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.annotation.SuppressLint;
import android.graphics.drawable.Animatable;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Animatable;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
@ -22,28 +25,17 @@ import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
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 androidx.annotation.Nullable;
import butterknife.BindView;
import butterknife.ButterKnife;
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.MediaDataExtractor;
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.delete.DeleteHelper;
import fr.free.nrw.commons.delete.ReasonBuilder;
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.ui.widget.CompatTextView;
import fr.free.nrw.commons.ui.widget.HtmlTextView;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.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 static android.view.View.GONE;
import static android.view.View.VISIBLE;
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private boolean editable;
@ -108,6 +105,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
LinearLayout imageSpacer;
@BindView(R.id.mediaDetailTitle)
TextView title;
@BindView(R.id.caption_layout)
LinearLayout captionLayout;
@BindView(R.id.depicts_layout)
LinearLayout depictsLayout;
@BindView(R.id.media_detail_caption)
TextView mediaCaption;
@BindView(R.id.mediaDetailDesc)
HtmlTextView desc;
@BindView(R.id.mediaDetailAuthor)
@ -126,6 +129,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
LinearLayout nominatedForDeletion;
@BindView(R.id.mediaDetailCategoryContainer)
LinearLayout categoryContainer;
@BindView(R.id.media_detail_depiction_container)
LinearLayout depictionContainer;
@BindView(R.id.authorLinearLayout)
LinearLayout authorLayout;
@BindView(R.id.nominateDeletion)
@ -134,8 +139,15 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
ScrollView scrollView;
private ArrayList<String> categoryNames;
/**
* Depicts is a feature part of Structured data. Multiple Depictions can be added for an image just like categories.
* However unlike categories depictions is multi-lingual
* Ex: key: en value: monument
*/
private Depictions depictions;
private boolean categoriesLoaded = false;
private boolean categoriesPresent = false;
private boolean depictionLoaded = false;
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
private ViewTreeObserver.OnScrollChangedListener scrollListener;
@ -243,7 +255,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
desc.setHtmlText(media.getDescription());
license.setText(media.getLicense());
Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename())
Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename(), media.getPageId())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::setTextFields);
@ -318,18 +330,32 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
coordinates.setText(prettyCoordinates(media));
uploadedDate.setText(prettyUploadedDate(media));
mediaDiscussion.setText(prettyDiscussion(media));
if (prettyCaption(media).equals(getContext().getString(R.string.detail_caption_empty))) {
captionLayout.setVisibility(GONE);
} else mediaCaption.setText(prettyCaption(media));
categoryNames.clear();
categoryNames.addAll(media.getCategories());
depictions=media.getDepiction();
depictionLoaded = true;
categoriesLoaded = true;
categoriesPresent = (categoryNames.size() > 0);
if (!categoriesPresent) {
// Stick in a filler element.
categoryNames.add(getString(R.string.detail_panel_cats_none));
}
rebuildCatList();
if(depictions != null) {
rebuildDepictionList();
}
else depictsLayout.setVisibility(GONE);
if (media.getCreator() == null || media.getCreator().equals("")) {
authorLayout.setVisibility(GONE);
} else {
@ -339,6 +365,21 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
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)
public void onMediaDetailLicenceClicked(){
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) {
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false);
final CompatTextView textView = item.findViewById(R.id.mediaDetailCategoryItemText);
@ -534,9 +595,24 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
image.setAlpha(1.0f - scrollPercentage);
}
/**
* Returns captions for media details
*
* @param media object of class media
* @return caption as string
*/
private String prettyCaption(Media media) {
String caption = media.getCaption().trim();
if (caption.equals("")) {
return getString(R.string.detail_caption_empty);
} else {
return caption;
}
}
private String prettyDescription(Media media) {
// @todo use UI language when multilingual descs are available
String desc = media.getDescription(locale.getLanguage()).trim();
String desc = media.getDescription();
if (desc.equals("")) {
return getString(R.string.detail_description_empty);
} else {
@ -582,7 +658,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
private void checkDeletion(Media media){
if (media.getRequestedDeletion()){
if (media.isRequestedDeletion()){
delete.setVisibility(GONE);
nominatedForDeletion.setVisibility(VISIBLE);
} 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.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;

View file

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

View file

@ -1,38 +1,34 @@
package fr.free.nrw.commons.mwapi;
import android.text.TextUtils;
import androidx.annotation.NonNull;
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.FeedbackResponse;
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.nearby.Place;
import fr.free.nrw.commons.nearby.model.NearbyResponse;
import fr.free.nrw.commons.nearby.model.NearbyResultItem;
import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.wikidata.model.GetWikidataEditCountResponse;
import io.reactivex.Observable;
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.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.jetbrains.annotations.NotNull;
import timber.log.Timber;
/**
@ -40,14 +36,12 @@ import timber.log.Timber;
*/
@Singleton
public class OkHttpJsonApiClient {
private static final String THUMB_SIZE = "640";
private final OkHttpClient okHttpClient;
private final HttpUrl wikiMediaToolforgeUrl;
private final String sparqlQueryUrl;
private final String campaignsUrl;
private final String commonsBaseUrl;
private Gson gson;
private final Gson gson;
@Inject
@ -55,13 +49,11 @@ public class OkHttpJsonApiClient {
HttpUrl wikiMediaToolforgeUrl,
String sparqlQueryUrl,
String campaignsUrl,
String commonsBaseUrl,
Gson gson) {
this.okHttpClient = okHttpClient;
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
this.sparqlQueryUrl = sparqlQueryUrl;
this.campaignsUrl = campaignsUrl;
this.commonsBaseUrl = commonsBaseUrl;
this.gson = gson;
}
@ -122,7 +114,8 @@ public class OkHttpJsonApiClient {
if (json == null) {
return 0;
}
GetWikidataEditCountResponse countResponse = gson.fromJson(json, GetWikidataEditCountResponse.class);
GetWikidataEditCountResponse countResponse = gson
.fromJson(json, GetWikidataEditCountResponse.class);
if (null != countResponse) {
return countResponse.getWikidataEditCount();
}
@ -132,15 +125,16 @@ public class OkHttpJsonApiClient {
}
/**
* This takes userName as input, which is then used to fetch the feedback/achievements
* statistics using OkHttp and JavaRx. This function return JSONObject
* 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");
wikiMediaToolforgeUrl + (ConfigUtils.isBetaFlavour() ? "/feedback.py?labs=commonswiki"
: "/feedback.py");
return Single.fromCallable(() -> {
String url = String.format(
Locale.ENGLISH,
@ -171,13 +165,13 @@ public class OkHttpJsonApiClient {
});
}
public Observable<List<Place>> getNearbyPlaces(LatLng cur, String lang, double radius) throws IOException {
public Observable<List<Place>> getNearbyPlaces(LatLng cur, String language, double radius) throws IOException {
String wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq");
String query = wikidataQuery
.replace("${RAD}", String.format(Locale.ROOT, "%.2f", radius))
.replace("${LAT}", String.format(Locale.ROOT, "%.4f", cur.getLatitude()))
.replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.getLongitude()))
.replace("${LANG}", lang);
.replace("${LANG}", language);
HttpUrl.Builder urlBuilder = HttpUrl
.parse(sparqlQueryUrl)
@ -208,6 +202,48 @@ public class OkHttpJsonApiClient {
});
}
/**
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
* bridge -> suspended bridge, aqueduct, etc
*/
public Observable<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)
@ -223,25 +259,4 @@ public class OkHttpJsonApiClient {
return null;
});
}
/**
* Whenever imageInfo is fetched, these common properties can be specified for the API call
* https://www.mediawiki.org/wiki/API:Imageinfo
*
* @param builder
* @return
*/
private HttpUrl.Builder appendMediaProperties(HttpUrl.Builder builder) {
builder.addQueryParameter("prop", "imageinfo")
.addQueryParameter("iiprop", "url|extmetadata")
.addQueryParameter("iiurlwidth", THUMB_SIZE)
.addQueryParameter("iiextmetadatafilter", "DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl");
String language = Locale.getDefault().getLanguage();
if (!StringUtils.isBlank(language)) {
builder.addQueryParameter("iiextmetadatalanguage", language);
}
return builder;
}
}

View file

@ -20,6 +20,8 @@ import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadController;
import fr.free.nrw.commons.upload.UploadModel;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import fr.free.nrw.commons.upload.structure.depictions.DepictModel;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import io.reactivex.Observable;
import io.reactivex.Single;
@ -34,16 +36,17 @@ public class UploadRemoteDataSource {
private UploadModel uploadModel;
private UploadController uploadController;
private CategoriesModel categoriesModel;
private DepictModel depictModel;
private NearbyPlaces nearbyPlaces;
@Inject
public UploadRemoteDataSource(UploadModel uploadModel, UploadController uploadController,
CategoriesModel categoriesModel,
NearbyPlaces nearbyPlaces) {
CategoriesModel categoriesModel, NearbyPlaces nearbyPlaces, DepictModel depictModel) {
this.uploadModel = uploadModel;
this.uploadController = uploadController;
this.categoriesModel = categoriesModel;
this.nearbyPlaces = nearbyPlaces;
this.depictModel = depictModel;
}
/**
@ -80,19 +83,13 @@ public class UploadRemoteDataSource {
uploadController.prepareService();
}
/**
* Clean up the UploadController
*/
public void cleanup() {
uploadController.cleanup();
}
/**
* 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
categoriesModel.cleanUp();
depictModel.cleanUp();
}
/**
@ -167,13 +164,12 @@ public class UploadRemoteDataSource {
*
* @param uploadableFile
* @param place
* @param source
* @param similarImageInterface
* @return
*/
public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place,
String source, SimilarImageInterface similarImageInterface) {
return uploadModel.preProcessImage(uploadableFile, place, source, similarImageInterface);
SimilarImageInterface similarImageInterface) {
return uploadModel.preProcessImage(uploadableFile, place, similarImageInterface);
}
/**
@ -204,6 +200,32 @@ public class UploadRemoteDataSource {
}
}
/**
* 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.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import io.reactivex.Observable;
import io.reactivex.Single;
@ -71,7 +73,7 @@ public class UploadRepository {
*/
public void cleanup() {
localDataSource.cleanUp();
remoteDataSource.clearSelectedCategories();
remoteDataSource.cleanUp();
}
/**
@ -174,14 +176,12 @@ public class UploadRepository {
*
* @param uploadableFile
* @param place
* @param source
* @param similarImageInterface
* @return
*/
public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place,
String source, SimilarImageInterface similarImageInterface) {
return remoteDataSource
.preProcessImage(uploadableFile, place, source, similarImageInterface);
SimilarImageInterface similarImageInterface) {
return remoteDataSource.preProcessImage(uploadableFile, place, similarImageInterface);
}
/**
@ -263,6 +263,31 @@ public class UploadRepository {
localDataSource.setSelectedLicense(licenseName);
}
public void onDepictItemClicked(DepictedItem depictedItem) {
remoteDataSource.onDepictedItemClicked(depictedItem);
}
/**
* Fetches and returns the selected depictions for the current upload
*
* @return
*/
public List<DepictedItem> getSelectedDepictions() {
return remoteDataSource.getSelectedDepictions();
}
/**
* Search all depictions from
*
* @param query
* @return
*/
public Observable<DepictedItem> searchAllEntities(String query) {
return remoteDataSource.searchAllEntities(query);
}
/**
* Returns nearest place matching the passed latitude and longitude
* @param decLatitude

View file

@ -5,29 +5,39 @@ import android.content.Context
import android.net.Uri
import androidx.exifinterface.media.ExifInterface
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.mwapi.CategoryApi
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.settings.Prefs
import fr.free.nrw.commons.upload.structure.depictions.DepictModel
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.util.*
import javax.inject.Inject
import javax.inject.Named
/**
* 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(
private val context: Context,
private val contentResolver: ContentResolver,
private val cacheController: CacheController,
private val gpsCategoryModel: GpsCategoryModel,
private val depictsModel: DepictModel,
@param:Named("default_preferences") private val defaultKvStore: JsonKvStore,
private val apiCall: CategoryApi
private val apiCall: CategoryApi,
private val okHttpJsonApiClient: OkHttpJsonApiClient
) {
private val compositeDisposable = CompositeDisposable()
@ -57,7 +67,7 @@ class FileProcessor @Inject constructor(
similarImageInterface
)
} else {
useImageCoords(originalImageCoordinates)
prePopulateCategoriesAndDepictionsBy(originalImageCoordinates)
}
return originalImageCoordinates
}
@ -146,7 +156,7 @@ class FileProcessor @Inject constructor(
private fun readImageCoordinates(file: File) =
try {
ImageCoordinates(contentResolver.openInputStream(Uri.fromFile(file)))
ImageCoordinates(contentResolver.openInputStream(Uri.fromFile(file))!!)
} catch (e: IOException) {
Timber.e(e)
try {
@ -163,13 +173,8 @@ class FileProcessor @Inject constructor(
*
* @param imageCoordinates
*/
fun useImageCoords(imageCoordinates: ImageCoordinates) {
fun prePopulateCategoriesAndDepictionsBy(imageCoordinates: ImageCoordinates) {
requireNotNull(imageCoordinates.decimalCoords)
cacheController.setQtPoint(imageCoordinates.decLongitude, imageCoordinates.decLatitude)
val displayCatList = cacheController.findCategory()
// If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories
if (displayCatList.isEmpty()) {
compositeDisposable.add(
apiCall.request(imageCoordinates.decimalCoords)
.subscribeOn(Schedulers.io())
@ -182,10 +187,30 @@ class FileProcessor @Inject constructor(
}
)
)
Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList)
} else {
Timber.d("Cache found, setting categoryList in model to %s", displayCatList)
gpsCategoryModel.categoryList = displayCatList
}
compositeDisposable.add(
suggestNearbyDepictions(imageCoordinates)
)
}
private val radiiProgressionInMetres =
(DEFAULT_SUGGESTION_RADIUS_IN_METRES..MAX_SUGGESTION_RADIUS_IN_METRES step RADIUS_STEP_SIZE_IN_METRES)
private fun suggestNearbyDepictions(imageCoordinates: ImageCoordinates): Disposable {
return Observable.fromIterable(radiiProgressionInMetres.map { it / 1000.0 })
.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) }
)
}
}

View file

@ -33,8 +33,4 @@ public class GpsCategoryModel {
clear();
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
import androidx.exifinterface.media.ExifInterface
import fr.free.nrw.commons.location.LatLng
import timber.log.Timber
import java.io.IOException
import java.io.InputStream
@ -22,7 +23,6 @@ class ImageCoordinates internal constructor(exif: ExifInterface?) {
* Construct from a stream.
*/
internal constructor(stream: InputStream) : this(ExifInterface(stream))
/**
* Construct from the 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)
internal constructor(path: String) : this(ExifInterface(path))
init {
//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
@ -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
*

View file

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

View file

@ -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;
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.annotation.SuppressLint;
import android.app.ProgressDialog;
@ -10,7 +14,6 @@ import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.cardview.widget.CardView;
import androidx.fragment.app.Fragment;
@ -20,14 +23,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter;
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.ButterKnife;
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.SessionManager;
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.filepicker.UploadableFile;
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.theme.BaseActivity;
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
import fr.free.nrw.commons.upload.depicts.DepictsFragment;
import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback;
@ -52,12 +47,13 @@ import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
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 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 {
@Inject
ContributionController contributionController;
@ -102,11 +98,10 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
private UploadImageAdapter uploadImagesAdapter;
private List<Fragment> fragments;
private UploadCategoriesFragment uploadCategoriesFragment;
private DepictsFragment depictsFragment;
private MediaLicenseFragment mediaLicenseFragment;
private ThumbnailsAdapter thumbnailsAdapter;
private String source;
private Place place;
private List<UploadableFile> uploadableFiles = Collections.emptyList();
private int currentSelectedPosition = 0;
@ -288,6 +283,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@ -321,7 +317,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
fragments = new ArrayList<>();
for (UploadableFile uploadableFile : uploadableFiles) {
UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment();
uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, source, place);
uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, place);
uploadMediaDetailFragment.setCallback(new UploadMediaDetailFragmentCallback() {
@Override
public void deletePictureAtIndex(int index) {
@ -359,10 +355,13 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
uploadCategoriesFragment = new UploadCategoriesFragment();
uploadCategoriesFragment.setCallback(this);
depictsFragment = new DepictsFragment();
depictsFragment.setCallback(this);
mediaLicenseFragment = new MediaLicenseFragment();
mediaLicenseFragment.setCallback(this);
fragments.add(depictsFragment);
fragments.add(uploadCategoriesFragment);
fragments.add(mediaLicenseFragment);
@ -378,16 +377,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
private void receiveInternalSharedItems() {
Intent intent = getIntent();
if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
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);
Timber.d("Received intent %s with action %s", intent.toString(), intent.getAction());
uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES);
Timber.i("Received multiple upload %s", uploadableFiles.size());

View file

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

View file

@ -13,17 +13,8 @@ import android.net.Uri;
import android.os.IBinder;
import android.provider.MediaStore;
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.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution;
@ -34,23 +25,26 @@ import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
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;
@Singleton
public class UploadController {
private UploadService uploadService;
private SessionManager sessionManager;
private Context context;
private JsonKvStore store;
public interface ContributionUploadProgress {
void onUploadStarted(Contribution contribution);
}
private final SessionManager sessionManager;
private final Context context;
private final JsonKvStore store;
@Inject
public UploadController(SessionManager sessionManager,
Context context,
JsonKvStore store) {
public UploadController(final SessionManager sessionManager,
final Context context,
final JsonKvStore store) {
this.sessionManager = sessionManager;
this.context = context;
this.store = store;
@ -59,13 +53,13 @@ public class UploadController {
private boolean isUploadServiceConnected;
public ServiceConnection uploadServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder binder) {
public void onServiceConnected(final ComponentName componentName, final IBinder binder) {
uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder).getService();
isUploadServiceConnected = true;
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
public void onServiceDisconnected(final ComponentName componentName) {
// this should never happen
isUploadServiceConnected = false;
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.
*/
public void prepareService() {
Intent uploadServiceIntent = new Intent(context, UploadService.class);
final Intent uploadServiceIntent = new Intent(context, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
context.startService(uploadServiceIntent);
context.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
@ -96,28 +90,18 @@ public class UploadController {
*
* @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")
private void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
public void startUpload(final Contribution contribution) {
//Set creator, desc, and license
// If author name is enabled and set, use it
if (store.getBoolean("useAuthorName", false)) {
String authorName = store.getString("authorName", "");
final String authorName = store.getString("authorName", "");
contribution.setCreator(authorName);
}
if (TextUtils.isEmpty(contribution.getCreator())) {
Account currentAccount = sessionManager.getCurrentAccount();
final Account currentAccount = sessionManager.getCurrentAccount();
if (currentAccount == null) {
Timber.d("Current account is null");
ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in));
@ -131,23 +115,23 @@ public class UploadController {
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);
uploadTask(contribution, onComplete);
uploadTask(contribution);
}
/**
* Initiates the upload task
* @param contribution
* @param onComplete
* @return
*/
private Disposable uploadTask(Contribution contribution, ContributionUploadProgress onComplete) {
return Single.fromCallable(() -> makeUpload(contribution))
private Disposable uploadTask(final Contribution contribution) {
return Single.just(contribution)
.map(this::buildUpload)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(finalContribution -> onUploadCompleted(finalContribution, onComplete));
.subscribe(this::upload);
}
/**
@ -155,71 +139,76 @@ public class UploadController {
* @param contribution
* @return
*/
private Contribution makeUpload(Contribution contribution) {
long length;
ContentResolver contentResolver = context.getContentResolver();
private Contribution buildUpload(final Contribution contribution) {
final 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 {
if (contribution.getDataLength() <= 0) {
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");
if (assetFileDescriptor != null) {
length = assetFileDescriptor.getLength();
if (length == -1) {
// Let us find out the long way!
length = countBytes(contentResolver
.openInputStream(contribution.getLocalUri()));
}
contribution.setDataLength(length);
final long length = assetFileDescriptor.getLength();
return length != -1 ? length
: countBytes(contentResolver.openInputStream(contribution.getLocalUri()));
}
}
} catch (IOException | NullPointerException | SecurityException e) {
} catch (final IOException | NullPointerException | SecurityException e) {
Timber.e(e, "Exception occurred while uploading image");
}
String mimeType = (String) contribution.getTag("mimeType");
boolean imagePrefix = false;
if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
mimeType = contentResolver.getType(contribution.getLocalUri());
return contribution.getDataLength();
}
if (mimeType != null) {
contribution.setTag("mimeType", mimeType);
imagePrefix = mimeType.startsWith("image/");
Timber.d("MimeType is: %s", mimeType);
}
if (imagePrefix && contribution.getDateCreated() == null) {
private Date resolveDateTakenOrNow(final ContentResolver contentResolver, final Media contribution) {
Timber.d("local uri %s", contribution.getLocalUri());
Cursor cursor = contentResolver.query(contribution.getLocalUri(),
new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null);
try(final Cursor cursor = dateTakenCursor(contentResolver, contribution)) {
if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) {
cursor.moveToFirst();
Date dateCreated = new Date(cursor.getLong(0));
Date epochStart = new Date(0);
if (dateCreated.equals(epochStart) || dateCreated.before(epochStart)) {
// 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());
final Date dateCreated = new Date(cursor.getLong(0));
if (dateCreated.after(new Date(0))) {
return dateCreated;
}
}
return contribution;
return new Date();
}
}
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
* @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
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}
* @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;
BufferedInputStream bis = new BufferedInputStream(stream);
final BufferedInputStream bis = new BufferedInputStream(stream);
while (bis.read() != -1) {
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.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatEditText;
import androidx.appcompat.widget.AppCompatSpinner;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.AbstractTextWatcher;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import timber.log.Timber;
public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewHolder> {
public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDetailAdapter.ViewHolder> {
private List<Description> descriptions;
private List<UploadMediaDetail> uploadMediaDetails;
private Callback callback;
private EventListener eventListener;
private HashMap<AdapterView, String> selectedLanguages;
private String savedLanguageValue;
private final String savedLanguageValue;
public DescriptionsAdapter(String savedLanguageValue) {
descriptions = new ArrayList<>();
public UploadMediaDetailAdapter(String savedLanguageValue) {
uploadMediaDetails = new ArrayList<>();
selectedLanguages = new HashMap<>();
this.savedLanguageValue = savedLanguageValue;
}
@ -45,8 +43,12 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
this.callback = callback;
}
public void setItems(List<Description> descriptions) {
this.descriptions = descriptions;
public void setEventListener(EventListener eventListener) {
this.eventListener = eventListener;
}
public void setItems(List<UploadMediaDetail> uploadMediaDetails) {
this.uploadMediaDetails = uploadMediaDetails;
selectedLanguages = new HashMap<>();
notifyDataSetChanged();
}
@ -60,12 +62,12 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.init(position);
holder.bind(position);
}
@Override
public int getItemCount() {
return descriptions.size();
return uploadMediaDetails.size();
}
/**
@ -73,13 +75,13 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
*
* @return List of descriptions
*/
public List<Description> getDescriptions() {
return descriptions;
public List<UploadMediaDetail> getUploadMediaDetails() {
return uploadMediaDetails;
}
public void addDescription(Description description) {
this.descriptions.add(description);
notifyItemInserted(descriptions.size());
public void addDescription(UploadMediaDetail uploadMediaDetail) {
this.uploadMediaDetails.add(uploadMediaDetail);
notifyItemInserted(uploadMediaDetails.size());
}
public class ViewHolder extends RecyclerView.ViewHolder {
@ -91,21 +93,43 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
@BindView(R.id.description_item_edit_text)
AppCompatEditText descItemEditText;
@BindView(R.id.caption_item_edit_text)
AppCompatEditText captionItemEditText;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
Timber.i("descItemEditText:" + descItemEditText);
}
public void init(int position) {
Description description = descriptions.get(position);
Timber.d("Description is " + description);
if (!TextUtils.isEmpty(description.getDescriptionText())) {
descItemEditText.setText(description.getDescriptionText());
} else {
descItemEditText.setText("");
}
public void bind(int position) {
UploadMediaDetail uploadMediaDetail = uploadMediaDetails.get(position);
Timber.d("UploadMediaDetail is " + uploadMediaDetail);
captionItemEditText.setText(uploadMediaDetail.getCaptionText());
descItemEditText.setText(uploadMediaDetail.getDescriptionText());
captionItemEditText.addTextChangedListener(new AbstractTextWatcher(
value -> {
if (position == 0) {
eventListener.onPrimaryCaptionTextChange(value.length() != 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(),
null);
descItemEditText.setOnTouchListener((v, event) -> {
@ -122,18 +146,23 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
});
} else {
captionItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
}
captionItemEditText.addTextChangedListener(new AbstractTextWatcher(
captionText -> uploadMediaDetails.get(position).setCaptionText(captionText)));
initLanguageSpinner(position, uploadMediaDetail);
descItemEditText.addTextChangedListener(new AbstractTextWatcher(
descriptionText -> descriptions.get(position).setDescriptionText(descriptionText)));
initLanguageSpinner(position, description);
descriptionText -> uploadMediaDetails.get(position).setDescriptionText(descriptionText)));
initLanguageSpinner(position, uploadMediaDetail);
//If the description was manually added by the user, it deserves focus, if not, let the user decide
if (description.isManuallyAdded()) {
descItemEditText.requestFocus();
if (uploadMediaDetail.isManuallyAdded()) {
captionItemEditText.requestFocus();
} else {
descItemEditText.clearFocus();
captionItemEditText.clearFocus();
}
}
@ -142,7 +171,7 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
* @param position
* @param description
*/
private void initLanguageSpinner(int position, Description description) {
private void initLanguageSpinner(int position, UploadMediaDetail description) {
SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(
spinnerDescriptionLanguages.getContext(),
selectedLanguages
@ -205,6 +234,10 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
void showAlert(int mediaDetailDescription, int descriptionInfo);
}
public interface EventListener {
void onPrimaryCaptionTextChange(boolean isNotEmpty);
}
/**
* converts dp to pixel
* @param dp

View file

@ -4,21 +4,20 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
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.contributions.Contribution;
import fr.free.nrw.commons.filepicker.MimeTypeMapWrapper;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.ImageUtils;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.subjects.BehaviorSubject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
@ -26,6 +25,7 @@ import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.jetbrains.annotations.NotNull;
import timber.log.Timber;
@Singleton
@ -36,22 +36,23 @@ public class UploadModel {
private final Context context;
private String license;
private final Map<String, String> licensesByName;
private List<UploadItem> items = new ArrayList<>();
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private final List<UploadItem> items = new ArrayList<>();
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
private SessionManager sessionManager;
private FileProcessor fileProcessor;
private final SessionManager sessionManager;
private final FileProcessor fileProcessor;
private final ImageProcessingService imageProcessingService;
private List<String> selectedCategories;
private List<String> selectedCategories = new ArrayList<>();
private List<DepictedItem> selectedDepictions = new ArrayList<>();
@Inject
UploadModel(@Named("licenses") List<String> licenses,
@Named("default_preferences") JsonKvStore store,
@Named("licenses_by_name") Map<String, String> licensesByName,
Context context,
SessionManager sessionManager,
FileProcessor fileProcessor,
ImageProcessingService imageProcessingService) {
UploadModel(@Named("licenses") final List<String> licenses,
@Named("default_preferences") final JsonKvStore store,
@Named("licenses_by_name") final Map<String, String> licensesByName,
final Context context,
final SessionManager sessionManager,
final FileProcessor fileProcessor,
final ImageProcessingService imageProcessingService) {
this.licenses = licenses;
this.store = store;
this.license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
@ -68,39 +69,33 @@ public class UploadModel {
public void cleanUp() {
compositeDisposable.clear();
fileProcessor.cleanup();
this.items.clear();
if (this.selectedCategories != null) {
this.selectedCategories.clear();
}
items.clear();
selectedCategories.clear();
selectedDepictions.clear();
}
public void setSelectedCategories(List<String> selectedCategories) {
if (null == selectedCategories) {
selectedCategories = new ArrayList<>();
}
this.selectedCategories = selectedCategories;
}
/**
* pre process a one item at a time
*/
public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile,
Place place,
String source,
SimilarImageInterface similarImageInterface) {
return Observable.just(getUploadItem(uploadableFile, place, source, similarImageInterface));
public Observable<UploadItem> preProcessImage(final UploadableFile uploadableFile,
final Place place,
final SimilarImageInterface similarImageInterface) {
return Observable.just(
createAndAddUploadItem(uploadableFile, place, similarImageInterface));
}
public Single<Integer> getImageQuality(UploadItem uploadItem) {
public Single<Integer> getImageQuality(final UploadItem uploadItem) {
return imageProcessingService.validateImage(uploadItem);
}
private UploadItem getUploadItem(UploadableFile uploadableFile,
Place place,
String source,
SimilarImageInterface similarImageInterface) {
UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile
private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile,
final Place place,
final SimilarImageInterface similarImageInterface) {
final UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile
.getFileCreatedDate(context);
long fileCreatedDate = -1;
String createdTimestampSource = "";
@ -109,19 +104,14 @@ public class UploadModel {
createdTimestampSource = dateTimeWithSource.getSource();
}
Timber.d("File created date is %d", fileCreatedDate);
ImageCoordinates imageCoordinates = fileProcessor
final ImageCoordinates imageCoordinates = fileProcessor
.processFileCoordinates(similarImageInterface, uploadableFile.getFilePath());
UploadItem uploadItem = new UploadItem(uploadableFile.getContentUri(),
final UploadItem uploadItem = new UploadItem(uploadableFile.getContentUri(),
Uri.parse(uploadableFile.getFilePath()),
uploadableFile.getMimeType(context), source, imageCoordinates, place, fileCreatedDate,
uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate,
createdTimestampSource);
if (place != null) {
uploadItem.title.setTitleText(place.name);
if(uploadItem.descriptions.isEmpty()) {
uploadItem.descriptions.add(new Description());
}
uploadItem.descriptions.get(0).setDescriptionText(place.getLongDescription());
uploadItem.descriptions.get(0).setLanguageCode("en");
uploadItem.getUploadMediaDetails().set(0, new UploadMediaDetail(place));
}
if (!items.contains(uploadItem)) {
items.add(uploadItem);
@ -145,7 +135,7 @@ public class UploadModel {
return license;
}
public void setSelectedLicense(String licenseName) {
public void setSelectedLicense(final String licenseName) {
this.license = licensesByName.get(licenseName);
store.putString(Prefs.DEFAULT_LICENSE, license);
}
@ -153,26 +143,8 @@ public class UploadModel {
public Observable<Contribution> buildContributions() {
return Observable.fromIterable(items).map(item ->
{
Contribution contribution = new Contribution(item.mediaUri, null,
item.getFileName(),
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());
final Contribution contribution = new Contribution(
item, sessionManager, newListOf(selectedDepictions), newListOf(selectedCategories));
Timber.d("Created timestamp while building contribution is %s, %s",
item.getCreatedTimestamp(),
new Date(item.getCreatedTimestamp()));
@ -185,8 +157,8 @@ public class UploadModel {
});
}
public void deletePicture(String filePath) {
Iterator<UploadItem> iterator = items.iterator();
public void deletePicture(final String filePath) {
final Iterator<UploadItem> iterator = items.iterator();
while (iterator.hasNext()) {
if (iterator.next().mediaUri.toString().contains(filePath)) {
iterator.remove();
@ -202,51 +174,58 @@ public class UploadModel {
return items;
}
public void updateUploadItem(int index, UploadItem uploadItem) {
UploadItem uploadItem1 = items.get(index);
uploadItem1.setDescriptions(uploadItem.descriptions);
uploadItem1.setTitle(uploadItem.title);
public void updateUploadItem(final int index, final UploadItem uploadItem) {
final UploadItem uploadItem1 = items.get(index);
uploadItem1.setMediaDetails(uploadItem.uploadMediaDetails);
}
public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) {
fileProcessor.useImageCoords(imageCoordinates);
public void onDepictItemClicked(DepictedItem depictedItem) {
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);
}
public List<DepictedItem> getSelectedDepictions() {
return selectedDepictions;
}
@SuppressWarnings("WeakerAccess")
public static class UploadItem {
private final Uri originalContentUri;
private final Uri mediaUri;
private final String mimeType;
private final String source;
private ImageCoordinates gpsCoords;
public void setGpsCoords(ImageCoordinates gpsCoords) {
this.gpsCoords = gpsCoords;
}
private Title title;
private List<Description> descriptions;
private Place place;
private long createdTimestamp;
private String createdTimestampSource;
private BehaviorSubject<Integer> imageQuality;
private List<UploadMediaDetail> uploadMediaDetails;
private final Place place;
private final long createdTimestamp;
private final String createdTimestampSource;
private final BehaviorSubject<Integer> imageQuality;
@SuppressLint("CheckResult")
UploadItem(Uri originalContentUri,
Uri mediaUri, String mimeType, String source, ImageCoordinates gpsCoords,
Place place,
long createdTimestamp,
String createdTimestampSource) {
UploadItem(final Uri originalContentUri,
final Uri mediaUri, final String mimeType,
final ImageCoordinates gpsCoords,
final Place place,
final long createdTimestamp,
final String createdTimestampSource) {
this.originalContentUri = originalContentUri;
this.createdTimestampSource = createdTimestampSource;
title = new Title();
descriptions = new ArrayList<>();
uploadMediaDetails = new ArrayList<>(Arrays.asList(new UploadMediaDetail()));
this.place = place;
this.mediaUri = mediaUri;
this.mimeType = mimeType;
this.source = source;
this.gpsCoords = gpsCoords;
this.createdTimestamp = createdTimestamp;
imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT);
@ -256,26 +235,18 @@ public class UploadModel {
return createdTimestampSource;
}
public String getSource() {
return source;
}
public ImageCoordinates getGpsCoords() {
return gpsCoords;
}
public List<Description> getDescriptions() {
return descriptions;
public List<UploadMediaDetail> getUploadMediaDetails() {
return uploadMediaDetails;
}
public long getCreatedTimestamp() {
return createdTimestamp;
}
public Title getTitle() {
return title;
}
public Uri getMediaUri() {
return mediaUri;
}
@ -284,29 +255,16 @@ public class UploadModel {
return this.imageQuality.getValue();
}
public void setImageQuality(int imageQuality) {
public void setImageQuality(final int 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() {
return place;
}
public void setTitle(Title title) {
this.title = title;
}
public void setDescriptions(List<Description> descriptions) {
this.descriptions = descriptions;
public void setMediaDetails(final List<UploadMediaDetail> uploadMediaDetails) {
this.uploadMediaDetails = uploadMediaDetails;
}
public Uri getContentUri() {
@ -314,7 +272,7 @@ public class UploadModel {
}
@Override
public boolean equals(@Nullable Object obj) {
public boolean equals(@Nullable final Object obj) {
if (!(obj instanceof UploadItem)) {
return false;
}
@ -326,6 +284,21 @@ public class UploadModel {
public int 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 fr.free.nrw.commons.upload.categories.CategoriesContract;
import fr.free.nrw.commons.upload.categories.CategoriesPresenter;
import fr.free.nrw.commons.upload.depicts.DepictsContract;
import fr.free.nrw.commons.upload.depicts.DepictsPresenter;
import fr.free.nrw.commons.upload.license.MediaLicenseContract;
import fr.free.nrw.commons.upload.license.MediaLicensePresenter;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract;
@ -33,4 +35,9 @@ public abstract class UploadModule {
UploadMediaPresenter
presenter);
@Binds
public abstract DepictsContract.UserActionListener bindsDepictsPresenter(
DepictsPresenter
presenter
);
}

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
@ -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
view.showHideTopCard(false);
}
//Ask the repository to delete the picture
repository.deletePicture(uploadableFiles.get(index).getFilePath());
if (uploadableFiles.size() == 1) {
view.showMessage(R.string.upload_cancelled);

View file

@ -2,4 +2,16 @@ package fr.free.nrw.commons.upload
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.net.Uri;
import android.os.Bundle;
import androidx.core.app.NotificationCompat;
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.CommonsApplication;
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 io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.SingleObserver;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.io.IOException;
import java.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;
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 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";
@Inject WikidataEditService wikidataEditService;
@Inject SessionManager sessionManager;
@ -152,7 +146,6 @@ public class UploadService extends HandlerService<Contribution> {
@Override
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) {
case ACTION_UPLOAD_FILE:
@ -169,7 +162,7 @@ public class UploadService extends HandlerService<Contribution> {
.subscribeOn(ioThreadScheduler)
.observeOn(mainThreadScheduler)
.subscribe(aLong->{
contribution._id = aLong;
contribution.set_id(aLong);
UploadService.super.queue(what, contribution);
}, Throwable::printStackTrace));
break;
@ -252,12 +245,7 @@ public class UploadService extends HandlerService<Contribution> {
Timber.d("Stash upload response 1 is %s", uploadStash.toString());
String resultStatus = uploadStash.getResult();
if (!resultStatus.equals("Success")) {
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution);
return Observable.never();
} else {
if (uploadStash.isSuccessful()) {
Timber.d("making sure of uniqueness of name: %s", filename);
String uniqueFilename = findUniqueFilename(filename);
unfinishedUploads.add(uniqueFilename);
@ -266,24 +254,48 @@ public class UploadService extends HandlerService<Contribution> {
contribution,
uniqueFilename,
uploadStash.getFilekey());
} else {
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution);
return Observable.never();
}
})
.subscribe(uploadResult -> {
.subscribe(
uploadResult -> onUpload(contribution, notificationTag, uploadResult),
throwable -> {
Timber.w(throwable, "Exception during upload");
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
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);
String resultStatus = uploadResult.getResult();
if (!resultStatus.equals("Success")) {
if (uploadResult.isSuccessful()) {
onSuccessfulUpload(contribution, uploadResult);
} else {
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution);
} else {
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);
}
}
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()
@ -294,12 +306,6 @@ public class UploadService extends HandlerService<Contribution> {
.observeOn(mainThreadScheduler)
.subscribe());
}
}, throwable -> {
Timber.w(throwable, "Exception during upload");
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
showFailedNotification(contribution);
});
}
@SuppressLint("StringFormatInvalid")
@SuppressWarnings("deprecation")

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 View {
interface View {
void showProgress(boolean shouldShow);
@ -20,16 +20,13 @@ public interface CategoriesContract {
void setCategories(List<CategoryItem> categories);
void addCategory(CategoryItem category);
void goToNextScreen();
void showNoCategorySelected();
void setSelectedCategories(List<CategoryItem> selectedCategories);
}
public interface UserActionListener extends BasePresenter<View> {
interface UserActionListener extends BasePresenter<View> {
void searchForCategories(String query);

View file

@ -1,15 +1,9 @@
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 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.category.CategoryItem;
import fr.free.nrw.commons.repository.UploadRepository;
@ -18,11 +12,14 @@ import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
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 static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
/**
* The presenter class for UploadCategoriesFragment
*/
@ -86,6 +83,7 @@ public class CategoriesPresenter implements CategoriesContract.UserActionListene
)
.filter(categoryItem -> !repository.containsYear(categoryItem.getName()))
.distinct();
if(!TextUtils.isEmpty(query)) {
distinctCategoriesObservable=distinctCategoriesObservable.sorted(repository.sortBySimilarity(query));
}
@ -114,8 +112,9 @@ public class CategoriesPresenter implements CategoriesContract.UserActionListene
private List<String> getImageTitleList() {
List<String> titleList = new ArrayList<>();
for (UploadItem item : repository.getUploads()) {
if (item.getTitle().isSet()) {
titleList.add(item.getTitle().toString());
final String captionText = item.getUploadMediaDetails().get(0).getCaptionText();
if (!TextUtils.isEmpty(captionText)) {
titleList.add(captionText);
}
}
return titleList;

View file

@ -6,27 +6,18 @@ 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 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.category.CategoryClickedListener;
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 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;
public class UploadCategoriesFragment extends UploadBaseFragment implements CategoriesContract.View,
@ -54,7 +49,6 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
@Inject
CategoriesContract.UserActionListener presenter;
private RVRendererAdapter<CategoryItem> adapter;
private List<String> mediaTitleList=new ArrayList<>();
private Disposable subscribe;
private List<CategoryItem> categories;
private boolean isVisible;
@ -64,10 +58,6 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
super.onCreate(savedInstanceState);
}
public void setMediaTitleList(List<String> mediaTitleList) {
this.mediaTitleList = mediaTitleList;
}
@Nullable
@Override
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
public void goToNextScreen() {
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)
public void onNextButtonClicked() {
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 android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
@ -24,7 +19,6 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.github.chrisbanes.photoview.PhotoView;
import com.jakewharton.rxbinding2.widget.RxTextView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
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.nearby.Place;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.Description;
import fr.free.nrw.commons.upload.DescriptionsAdapter;
import fr.free.nrw.commons.upload.ImageCoordinates;
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.UploadMediaDetail;
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter;
import fr.free.nrw.commons.upload.UploadModel;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.disposables.Disposable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -53,8 +45,10 @@ import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import timber.log.Timber;
//import fr.free.nrw.commons.upload.DescriptionsAdapter;
public class UploadMediaDetailFragment extends UploadBaseFragment implements
UploadMediaDetailsContract.View {
UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener {
@BindView(R.id.tv_title)
TextView tvTitle;
@ -64,8 +58,6 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
AppCompatImageButton ibExpandCollapse;
@BindView(R.id.ll_container_media_detail)
LinearLayout llContainerMediaDetail;
@BindView(R.id.et_title)
EditText etTitle;
@BindView(R.id.rv_descriptions)
RecyclerView rvDescriptions;
@BindView(R.id.backgroundImage)
@ -74,12 +66,12 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
AppCompatButton btnNext;
@BindView(R.id.btn_previous)
AppCompatButton btnPrevious;
private DescriptionsAdapter descriptionsAdapter;
private UploadMediaDetailAdapter uploadMediaDetailAdapter;
@BindView(R.id.btn_copy_prev_title_desc)
AppCompatButton btnCopyPreviousTitleDesc;
private UploadModel.UploadItem uploadItem;
private List<Description> descriptions;
private List<UploadMediaDetail> descriptions;
@Inject
UploadMediaDetailsContract.UserActionListener presenter;
@ -89,10 +81,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
JsonKvStore defaultKvStore;
private UploadableFile uploadableFile;
private String source;
private Place place;
private Title title;
private boolean isExpanded = true;
private UploadMediaDetailFragmentCallback callback;
@ -106,9 +96,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
super.onCreate(savedInstanceState);
}
public void setImageTobeUploaded(UploadableFile uploadableFile, String source, Place place) {
public void setImageTobeUploaded(UploadableFile uploadableFile, Place place) {
this.uploadableFile = uploadableFile;
this.source = source;
this.place = place;
}
@ -129,25 +118,9 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
private void init() {
tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1,
callback.getTotalNumberOfSteps()));
title = new Title();
initRecyclerView();
initPresenter();
Disposable disposable = RxTextView.textChanges(etTitle)
.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);
presenter.receiveImage(uploadableFile, place);
if (callback.getIndexInViewFlipper(this) == 0) {
btnPrevious.setEnabled(false);
@ -166,36 +139,6 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
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() {
descriptionsAdapter = new DescriptionsAdapter(defaultKvStore.getString(Prefs.KEY_LANGUAGE_VALUE, ""));
descriptionsAdapter.setCallback(this::showInfoAlert);
uploadMediaDetailAdapter = new UploadMediaDetailAdapter(defaultKvStore.getString(Prefs.KEY_LANGUAGE_VALUE, ""));
uploadMediaDetailAdapter.setCallback(this::showInfoAlert);
uploadMediaDetailAdapter.setEventListener(this);
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)
public void onNextButtonClicked() {
uploadItem.setDescriptions(descriptionsAdapter.getDescriptions());
uploadItem.setMediaDetails(uploadMediaDetailAdapter.getUploadMediaDetails());
presenter.verifyImageQuality(uploadItem);
}
@ -248,9 +192,9 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
@OnClick(R.id.btn_add_description)
public void onButtonAddDescriptionClicked() {
Description description = new Description();
description.setManuallyAdded(true);//This was manually added by the user
descriptionsAdapter.addDescription(description);
UploadMediaDetail uploadMediaDetail = new UploadMediaDetail();
uploadMediaDetail.setManuallyAdded(true);//This was manually added by the user
uploadMediaDetailAdapter.addDescription(uploadMediaDetail);
}
@Override
@ -279,11 +223,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
@Override
public void onImageProcessed(UploadItem uploadItem, Place place) {
this.uploadItem = uploadItem;
if (uploadItem.getTitle() != null) {
etTitle.setText(uploadItem.getTitle().toString());
}
descriptions = uploadItem.getDescriptions();
descriptions = uploadItem.getUploadMediaDetails();
photoViewBackgroundImage.setImageURI(uploadItem.getMediaUri());
setDescriptionsInAdapter(descriptions);
}
@ -302,11 +242,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
getString(R.string.upload_nearby_place_found_description),
place.getName()),
() -> {
etTitle.setText(place.getName());
Description description = new Description();
description.setLanguageCode("en");
description.setDescriptionText(place.getLongDescription());
descriptions = Arrays.asList(description);
descriptions = new ArrayList<>(Arrays.asList(new UploadMediaDetail(place)));
setDescriptionsInAdapter(descriptions);
},
() -> {
@ -376,9 +312,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
}
@Override
public void setTitleAndDescription(String title, List<Description> descriptions) {
etTitle.setText(title);
setDescriptionsInAdapter(descriptions);
public void setCaptionsAndDescriptions(List<UploadMediaDetail> uploadMediaDetails) {
setDescriptionsInAdapter(uploadMediaDetails);
}
private void deleteThisPicture() {
@ -412,6 +347,13 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
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 {
@ -424,15 +366,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
presenter.fetchPreviousTitleAndDescription(callback.getIndexInViewFlipper(this));
}
private void setDescriptionsInAdapter(List<Description> descriptions) {
if (descriptions == null) {
descriptions = new ArrayList<>();
}
if (descriptions.size() == 0) {
descriptionsAdapter.addDescription(new Description());
} else {
descriptionsAdapter.setItems(descriptions);
private void setDescriptionsInAdapter(List<UploadMediaDetail> uploadMediaDetails){
uploadMediaDetailAdapter.setItems(uploadMediaDetails);
}
}
}

View file

@ -1,15 +1,13 @@
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.contributions.Contribution;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.upload.Description;
import fr.free.nrw.commons.upload.ImageCoordinates;
import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadMediaDetail;
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
@ -36,13 +34,12 @@ public interface UploadMediaDetailsContract {
void showMapWithImageCoordinates(boolean shouldShow);
void setTitleAndDescription(String title, List<Description> descriptions);
void setCaptionsAndDescriptions(List<UploadMediaDetail> uploadMediaDetails);
}
interface UserActionListener extends BasePresenter<View> {
void receiveImage(UploadableFile uploadableFile, @Contribution.FileSource String source,
Place place);
void receiveImage(UploadableFile uploadableFile, Place place);
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.MAIN_THREAD;
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE;
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION;
import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
@ -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
*
* @param uploadableFile
* @param source
* @param place
*/
@Override
public void receiveImage(UploadableFile uploadableFile, String source, Place place) {
public void receiveImage(UploadableFile uploadableFile, Place place) {
view.showProgress(true);
Disposable uploadItemDisposable = repository
.preProcessImage(uploadableFile, place, source, this)
.preProcessImage(uploadableFile, place, this)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.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
*/
@ -151,7 +149,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
public void fetchPreviousTitleAndDescription(int indexInViewFlipper) {
UploadItem previousUploadItem = repository.getPreviousUploadItem(indexInViewFlipper);
if (null != previousUploadItem) {
view.setTitleAndDescription(previousUploadItem.getTitle().getTitleText(), previousUploadItem.getDescriptions());
view.setCaptionsAndDescriptions(previousUploadItem.getUploadMediaDetails());
} else {
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
*/
@ -188,9 +186,9 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
}
switch (errorCode) {
case EMPTY_TITLE:
Timber.d("Title is empty. Showing toast");
view.showMessage(R.string.add_title_toast, R.color.color_error);
case EMPTY_CAPTION:
Timber.d("Captions are empty. Showing toast");
view.showMessage(R.string.add_caption_toast, R.color.color_error);
break;
case FILE_NAME_EXISTS:
Timber.d("Trying to show duplicate picture popup");

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