mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
Issue-5662-kotlinstyle (#5833)
* *.kt: bulk correction of formatting using ktlint --format * *.kt: replace wildcard imports and second stage auto format ktlint --format * QuizQuestionTest.kt: modified property names to camel case to meet ktlint standard * LevelControllerTest.kt: modified property names to camel case to meet ktlint standard * QuizActivityUnitTest.kt: modified property names to camel case to meet ktlint standard * MediaDetailFragmentUnitTests.kt: modified property names to camel case to meet ktlint standard * UploadWorker.kt: modified property names to camel case to meet ktlint standard * UploadClient.kt: modified property names to camel case to meet ktlint standard * BasePagingPresenter.kt: modified property names to camel case to meet ktlint standard * DescriptionEditActivity.kt: modified property names to camel case to meet ktlint standard * OnSwipeTouchListener.kt: modified property names to camel case to meet ktlint standard * MediaDetailFragmentUnitTests.kt: corrected excessive line length to meet ktlint standard * DepictedItem.kt: corrected property name format and catch format to for ktlint standard * UploadCategoryAdapter.kt: corrected class definition format to meet ktlint standard * CustomSelectorActivity.kt: reformatted function names to first letter lowercase to meet ktlint standard * MediaDetailFragmentUnitTests.kt: fix string literal indentation to meet ktlint standard * NotForUploadDao.kt: file renamed to match class name, new file NotForUploadStatusDao.kt * UploadedDao.kt: file renamed to match class name, new file UploadedStatusDao.kt * Urls.kt: fixed excessive line length for ktLint standard * Snak_partial.kt & Statement_partial.kt: refactored to remove underscores in class names to meet ktLint standard * *.kt: fixed consecutive KDOC error for ktLint * PageableBaseDataSourceTest.kt & UploadPresenterTest.kt: fixed excessive line lengths to meet ktLint standard * CheckboxTriStatesTest.kt: renamed file to match class name to meet ktLint standard * .kt: resolved backing-property-naming error in ktLint, made matching properties public, matched names and refactored * TestConnectionFactory.kt: fixed property naming to adhere to ktLint standard
This commit is contained in:
parent
950539c55c
commit
2d82a430c4
405 changed files with 11032 additions and 9137 deletions
|
|
@ -25,7 +25,6 @@ import org.junit.runner.RunWith
|
|||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AboutActivityTest {
|
||||
|
||||
@get:Rule
|
||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(AboutActivity::class.java)
|
||||
|
||||
|
|
@ -36,7 +35,8 @@ class AboutActivityTest {
|
|||
device.setOrientationNatural()
|
||||
device.freezeRotation()
|
||||
Intents.init()
|
||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
Intents
|
||||
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||
}
|
||||
|
||||
|
|
@ -47,11 +47,12 @@ class AboutActivityTest {
|
|||
|
||||
@Test
|
||||
fun testBuildNumber() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.about_version))
|
||||
Espresso
|
||||
.onView(ViewMatchers.withId(R.id.about_version))
|
||||
.check(
|
||||
ViewAssertions.matches(
|
||||
withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha())
|
||||
)
|
||||
withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -61,8 +62,8 @@ class AboutActivityTest {
|
|||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.WEBSITE_URL)
|
||||
)
|
||||
IntentMatchers.hasData(Urls.WEBSITE_URL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -73,8 +74,8 @@ class AboutActivityTest {
|
|||
CoreMatchers.anyOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.FACEBOOK_WEB_URL),
|
||||
IntentMatchers.hasPackage(Urls.FACEBOOK_PACKAGE_NAME)
|
||||
)
|
||||
IntentMatchers.hasPackage(Urls.FACEBOOK_PACKAGE_NAME),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -84,8 +85,8 @@ class AboutActivityTest {
|
|||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.GITHUB_REPO_URL)
|
||||
)
|
||||
IntentMatchers.hasData(Urls.GITHUB_REPO_URL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -95,8 +96,8 @@ class AboutActivityTest {
|
|||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(BuildConfig.PRIVACY_POLICY_URL)
|
||||
)
|
||||
IntentMatchers.hasData(BuildConfig.PRIVACY_POLICY_URL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -108,8 +109,8 @@ class AboutActivityTest {
|
|||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData("${Urls.TRANSLATE_WIKI_URL}$langCode")
|
||||
)
|
||||
IntentMatchers.hasData("${Urls.TRANSLATE_WIKI_URL}$langCode"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -119,27 +120,30 @@ class AboutActivityTest {
|
|||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.CREDITS_URL)
|
||||
)
|
||||
IntentMatchers.hasData(Urls.CREDITS_URL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchUserGuide() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.about_user_guide)).perform(ViewActions.click())
|
||||
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.USER_GUIDE_URL)))
|
||||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.USER_GUIDE_URL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testLaunchAboutFaq() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.about_faq)).perform(ViewActions.click())
|
||||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(Urls.FAQ_URL)
|
||||
)
|
||||
IntentMatchers.hasData(Urls.FAQ_URL),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@ import fr.free.nrw.commons.auth.LoginActivity
|
|||
import fr.free.nrw.commons.auth.SignupActivity
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.junit.*
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LoginActivityTest {
|
||||
|
||||
@get:Rule
|
||||
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
||||
|
||||
|
|
@ -49,8 +51,8 @@ class LoginActivityTest {
|
|||
Intents.intended(
|
||||
CoreMatchers.allOf(
|
||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||
IntentMatchers.hasData(BuildConfig.FORGOT_PASSWORD_URL)
|
||||
)
|
||||
IntentMatchers.hasData(BuildConfig.FORGOT_PASSWORD_URL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -64,4 +66,4 @@ class LoginActivityTest {
|
|||
fun orientationChange() {
|
||||
UITestHelper.changeOrientation(activityRule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,20 +21,23 @@ import fr.free.nrw.commons.kvstore.JsonKvStore
|
|||
import fr.free.nrw.commons.notification.NotificationActivity
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.*
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MainActivityTest {
|
||||
|
||||
@get:Rule
|
||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
|
||||
|
||||
@get:Rule
|
||||
var mGrantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
|
||||
"android.permission.ACCESS_FINE_LOCATION"
|
||||
)
|
||||
var mGrantPermissionRule: GrantPermissionRule =
|
||||
GrantPermissionRule.grant(
|
||||
"android.permission.ACCESS_FINE_LOCATION",
|
||||
)
|
||||
|
||||
private val device: UiDevice =
|
||||
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
|
|
@ -48,7 +51,8 @@ class MainActivityTest {
|
|||
UITestHelper.loginUser()
|
||||
UITestHelper.skipWelcome()
|
||||
Intents.init()
|
||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
Intents
|
||||
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val storeName = context.packageName + "_preferences"
|
||||
|
|
@ -62,137 +66,149 @@ class MainActivityTest {
|
|||
|
||||
@Test
|
||||
fun testNearby() {
|
||||
Espresso.onView(
|
||||
Matchers.allOf(
|
||||
childAtPosition(
|
||||
Espresso
|
||||
.onView(
|
||||
Matchers.allOf(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0,
|
||||
),
|
||||
1,
|
||||
),
|
||||
1
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
ViewMatchers.isDisplayed()
|
||||
)
|
||||
).perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||
).perform(ViewActions.click())
|
||||
Espresso
|
||||
.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||
.check(matches(ViewMatchers.isDisplayed()))
|
||||
UITestHelper.sleep(10000)
|
||||
val actionMenuItemView2 = Espresso.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.list_sheet), ViewMatchers.withContentDescription("List"),
|
||||
childAtPosition(
|
||||
val actionMenuItemView2 =
|
||||
Espresso.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.list_sheet),
|
||||
ViewMatchers.withContentDescription("List"),
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.toolbar),
|
||||
1
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.toolbar),
|
||||
1,
|
||||
),
|
||||
0,
|
||||
),
|
||||
0
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
ViewMatchers.isDisplayed()
|
||||
)
|
||||
)
|
||||
actionMenuItemView2.perform(ViewActions.click())
|
||||
UITestHelper.sleep(1000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExplore() {
|
||||
Espresso.onView(
|
||||
Matchers.allOf(
|
||||
childAtPosition(
|
||||
Espresso
|
||||
.onView(
|
||||
Matchers.allOf(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0,
|
||||
),
|
||||
2,
|
||||
),
|
||||
2
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
ViewMatchers.isDisplayed()
|
||||
)
|
||||
).perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||
).perform(ViewActions.click())
|
||||
Espresso
|
||||
.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||
.check(matches(ViewMatchers.isDisplayed()))
|
||||
UITestHelper.sleep(1000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testContributions() {
|
||||
Espresso.onView(
|
||||
Matchers.allOf(
|
||||
childAtPosition(
|
||||
Espresso
|
||||
.onView(
|
||||
Matchers.allOf(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0,
|
||||
),
|
||||
0,
|
||||
),
|
||||
0
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
ViewMatchers.isDisplayed()
|
||||
)
|
||||
).perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||
).perform(ViewActions.click())
|
||||
Espresso
|
||||
.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||
.check(matches(ViewMatchers.isDisplayed()))
|
||||
Espresso.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.contributionImage),
|
||||
childAtPosition(
|
||||
Espresso
|
||||
.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.contributionImage),
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.contributionsList),
|
||||
0
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.contributionsList),
|
||||
0,
|
||||
),
|
||||
1,
|
||||
),
|
||||
1
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
ViewMatchers.isDisplayed()
|
||||
)
|
||||
).perform(ViewActions.click())
|
||||
val actionMenuItemView = Espresso.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.menu_bookmark_current_image),
|
||||
childAtPosition(
|
||||
).perform(ViewActions.click())
|
||||
val actionMenuItemView =
|
||||
Espresso.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.menu_bookmark_current_image),
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.toolbar),
|
||||
1
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.toolbar),
|
||||
1,
|
||||
),
|
||||
0,
|
||||
),
|
||||
0
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
ViewMatchers.isDisplayed()
|
||||
)
|
||||
)
|
||||
actionMenuItemView.perform(ViewActions.click())
|
||||
UITestHelper.sleep(3000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBookmarks() {
|
||||
Espresso.onView(
|
||||
Matchers.allOf(
|
||||
childAtPosition(
|
||||
Espresso
|
||||
.onView(
|
||||
Matchers.allOf(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0,
|
||||
),
|
||||
3,
|
||||
),
|
||||
3
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
ViewMatchers.isDisplayed()
|
||||
)
|
||||
).perform(ViewActions.click())
|
||||
).perform(ViewActions.click())
|
||||
UITestHelper.sleep(1000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNotifications() {
|
||||
Espresso.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.notifications),
|
||||
childAtPosition(
|
||||
Espresso
|
||||
.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.notifications),
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.toolbar),
|
||||
1
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.toolbar),
|
||||
1,
|
||||
),
|
||||
1,
|
||||
),
|
||||
1
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
ViewMatchers.isDisplayed()
|
||||
)
|
||||
).perform(ViewActions.click())
|
||||
).perform(ViewActions.click())
|
||||
Intents.intended(IntentMatchers.hasComponent(NotificationActivity::class.java.name))
|
||||
Espresso.pressBack()
|
||||
UITestHelper.sleep(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import android.app.Activity
|
|||
import android.app.Instrumentation
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.action.ViewActions.swipeRight
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
|
|
@ -26,7 +25,6 @@ import org.junit.runner.RunWith
|
|||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ProfileActivityTest {
|
||||
|
||||
@get:Rule
|
||||
var activityRule = IntentsTestRule(LoginActivity::class.java)
|
||||
|
||||
|
|
@ -38,7 +36,8 @@ class ProfileActivityTest {
|
|||
device.freezeRotation()
|
||||
UITestHelper.loginUser()
|
||||
UITestHelper.skipWelcome()
|
||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
Intents
|
||||
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||
}
|
||||
|
||||
|
|
@ -50,20 +49,19 @@ class ProfileActivityTest {
|
|||
childAtPosition(
|
||||
childAtPosition(
|
||||
withId(R.id.fragment_main_nav_tab_layout),
|
||||
0
|
||||
0,
|
||||
),
|
||||
4
|
||||
4,
|
||||
),
|
||||
ViewMatchers.isDisplayed()
|
||||
)
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
).perform(ViewActions.click())
|
||||
onView(Matchers.allOf(withId(R.id.more_profile))).perform(
|
||||
ViewActions.scrollTo(),
|
||||
ViewActions.click()
|
||||
ViewActions.click(),
|
||||
)
|
||||
device.swipe(1033,1346,531,1346,20)
|
||||
device.swipe(1033, 1346, 531, 1346, 20)
|
||||
UITestHelper.sleep(5000)
|
||||
Intents.intended(hasComponent(ProfileActivity::class.java.name))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import org.junit.runner.RunWith
|
|||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ReviewActivityTest {
|
||||
|
||||
@get:Rule
|
||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(ReviewActivity::class.java)
|
||||
|
||||
|
|
@ -17,5 +16,4 @@ class ReviewActivityTest {
|
|||
fun orientationChange() {
|
||||
UITestHelper.changeOrientation(activityRule)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import org.junit.runner.RunWith
|
|||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SearchActivityTest {
|
||||
|
||||
@get:Rule
|
||||
var activityRule = ActivityTestRule(SearchActivity::class.java)
|
||||
|
||||
|
|
@ -31,21 +30,22 @@ class SearchActivityTest {
|
|||
|
||||
@Test
|
||||
fun exploreActivityTest() {
|
||||
val searchAutoComplete = Espresso.onView(
|
||||
Matchers.allOf(
|
||||
UITestHelper.childAtPosition(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
|
||||
UITestHelper.childAtPosition(
|
||||
val searchAutoComplete =
|
||||
Espresso.onView(
|
||||
Matchers.allOf(
|
||||
UITestHelper.childAtPosition(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
|
||||
1
|
||||
)
|
||||
UITestHelper.childAtPosition(
|
||||
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
|
||||
1,
|
||||
),
|
||||
),
|
||||
0,
|
||||
),
|
||||
0
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
ViewMatchers.isDisplayed()
|
||||
)
|
||||
)
|
||||
searchAutoComplete.perform(ViewActions.replaceText("cat"), ViewActions.closeSoftKeyboard())
|
||||
UITestHelper.sleep(5000)
|
||||
device.swipe(1000, 1400, 500, 1400, 20)
|
||||
|
|
@ -56,4 +56,4 @@ class SearchActivityTest {
|
|||
device.swipe(800, 1400, 600, 1400, 20)
|
||||
UITestHelper.sleep(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import org.junit.runner.RunWith
|
|||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SettingsActivityLoggedInTest {
|
||||
|
||||
@get:Rule
|
||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
|
||||
|
||||
|
|
@ -35,31 +34,32 @@ class SettingsActivityLoggedInTest {
|
|||
device.freezeRotation()
|
||||
UITestHelper.loginUser()
|
||||
UITestHelper.skipWelcome()
|
||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
Intents
|
||||
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSettings() {
|
||||
Espresso.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withContentDescription("More"),
|
||||
UITestHelper.childAtPosition(
|
||||
Espresso
|
||||
.onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withContentDescription("More"),
|
||||
UITestHelper.childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0
|
||||
UITestHelper.childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0,
|
||||
),
|
||||
4,
|
||||
),
|
||||
4
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
ViewMatchers.isDisplayed()
|
||||
)
|
||||
).perform(ViewActions.click())
|
||||
).perform(ViewActions.click())
|
||||
Espresso.onView(Matchers.allOf(ViewMatchers.withId(R.id.more_settings))).perform(
|
||||
ViewActions.scrollTo(),
|
||||
ViewActions.click()
|
||||
ViewActions.click(),
|
||||
)
|
||||
Intents.intended(IntentMatchers.hasComponent(SettingsActivity::class.java.name))
|
||||
UITestHelper.sleep(1000)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import org.junit.runner.RunWith
|
|||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SettingsActivityTest {
|
||||
|
||||
private lateinit var defaultKvStore: JsonKvStore
|
||||
|
||||
@get:Rule
|
||||
|
|
@ -44,22 +43,24 @@ class SettingsActivityTest {
|
|||
fun useAuthorNameTogglesOn() {
|
||||
// Turn on "Use author name" preference if currently off
|
||||
if (!defaultKvStore.getBoolean("useAuthorName", false)) {
|
||||
Espresso.onView(
|
||||
allOf(
|
||||
withId(R.id.recycler_view),
|
||||
childAtPosition(withId(android.R.id.list_container), 0)
|
||||
Espresso
|
||||
.onView(
|
||||
allOf(
|
||||
withId(R.id.recycler_view),
|
||||
childAtPosition(withId(android.R.id.list_container), 0),
|
||||
),
|
||||
).perform(
|
||||
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(6, click()),
|
||||
)
|
||||
).perform(
|
||||
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(6, click())
|
||||
)
|
||||
}
|
||||
// Check authorName preference is enabled
|
||||
Espresso.onView(
|
||||
allOf(
|
||||
withId(R.id.recycler_view),
|
||||
childAtPosition(withId(android.R.id.list_container), 0)
|
||||
)
|
||||
).check(matches(isEnabled()))
|
||||
Espresso
|
||||
.onView(
|
||||
allOf(
|
||||
withId(R.id.recycler_view),
|
||||
childAtPosition(withId(android.R.id.list_container), 0),
|
||||
),
|
||||
).check(matches(isEnabled()))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -10,17 +10,20 @@ import androidx.test.espresso.action.ViewActions
|
|||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.hamcrest.*
|
||||
import org.hamcrest.BaseMatcher
|
||||
import org.hamcrest.Description
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.TypeSafeMatcher
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
class UITestHelper {
|
||||
companion object {
|
||||
fun skipWelcome() {
|
||||
try {
|
||||
onView(ViewMatchers.withId(R.id.button_ok))
|
||||
.perform(ViewActions.click())
|
||||
//Skip tutorial
|
||||
// Skip tutorial
|
||||
onView(ViewMatchers.withId(R.id.finishTutorialButton))
|
||||
.perform(ViewActions.click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
|
|
@ -29,27 +32,31 @@ class UITestHelper {
|
|||
|
||||
fun skipLogin() {
|
||||
try {
|
||||
//Skip Login
|
||||
val htmlTextView = onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.skip_login), ViewMatchers.withText("Skip"),
|
||||
ViewMatchers.isDisplayed()
|
||||
// Skip Login
|
||||
val htmlTextView =
|
||||
onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.skip_login),
|
||||
ViewMatchers.withText("Skip"),
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
)
|
||||
)
|
||||
htmlTextView.perform(ViewActions.click())
|
||||
|
||||
val appCompatButton = onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText("Yes"),
|
||||
childAtPosition(
|
||||
val appCompatButton =
|
||||
onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(android.R.id.button1),
|
||||
ViewMatchers.withText("Yes"),
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.buttonPanel),
|
||||
0
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.buttonPanel),
|
||||
0,
|
||||
),
|
||||
3,
|
||||
),
|
||||
3
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
appCompatButton.perform(ViewActions.scrollTo(), ViewActions.click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
|
|
@ -57,18 +64,18 @@ class UITestHelper {
|
|||
|
||||
fun loginUser() {
|
||||
try {
|
||||
//Perform Login
|
||||
// Perform Login
|
||||
sleep(3000)
|
||||
onView(ViewMatchers.withId(R.id.login_username))
|
||||
.perform(
|
||||
ViewActions.replaceText(getTestUsername()),
|
||||
ViewActions.closeSoftKeyboard()
|
||||
ViewActions.closeSoftKeyboard(),
|
||||
)
|
||||
sleep(2000)
|
||||
onView(ViewMatchers.withId(R.id.login_password))
|
||||
.perform(
|
||||
ViewActions.replaceText(getTestUserPassword()),
|
||||
ViewActions.closeSoftKeyboard()
|
||||
ViewActions.closeSoftKeyboard(),
|
||||
)
|
||||
sleep(2000)
|
||||
onView(ViewMatchers.withId(R.id.login_button))
|
||||
|
|
@ -76,7 +83,6 @@ class UITestHelper {
|
|||
sleep(10000)
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun logoutUser() {
|
||||
|
|
@ -87,36 +93,38 @@ class UITestHelper {
|
|||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||
0
|
||||
0,
|
||||
),
|
||||
4
|
||||
4,
|
||||
),
|
||||
ViewMatchers.isDisplayed()
|
||||
)
|
||||
ViewMatchers.isDisplayed(),
|
||||
),
|
||||
).perform(ViewActions.click())
|
||||
onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(R.id.more_logout), ViewMatchers.withText("Logout"),
|
||||
ViewMatchers.withId(R.id.more_logout),
|
||||
ViewMatchers.withText("Logout"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.scroll_view_more_bottom_sheet),
|
||||
0
|
||||
0,
|
||||
),
|
||||
6
|
||||
)
|
||||
)
|
||||
6,
|
||||
),
|
||||
),
|
||||
).perform(ViewActions.scrollTo(), ViewActions.click())
|
||||
onView(
|
||||
Matchers.allOf(
|
||||
ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText("Yes"),
|
||||
ViewMatchers.withId(android.R.id.button1),
|
||||
ViewMatchers.withText("Yes"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
ViewMatchers.withId(R.id.buttonPanel),
|
||||
0
|
||||
0,
|
||||
),
|
||||
3
|
||||
)
|
||||
)
|
||||
3,
|
||||
),
|
||||
),
|
||||
).perform(ViewActions.scrollTo(), ViewActions.click())
|
||||
sleep(5000)
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
|
|
@ -124,9 +132,9 @@ class UITestHelper {
|
|||
}
|
||||
|
||||
fun childAtPosition(
|
||||
parentMatcher: Matcher<View>, position: Int
|
||||
parentMatcher: Matcher<View>,
|
||||
position: Int,
|
||||
): Matcher<View> {
|
||||
|
||||
return object : TypeSafeMatcher<View>() {
|
||||
override fun describeTo(description: Description) {
|
||||
description.appendText("Child at position $position in parent ")
|
||||
|
|
@ -135,8 +143,9 @@ class UITestHelper {
|
|||
|
||||
public override fun matchesSafely(view: View): Boolean {
|
||||
val parent = view.parent
|
||||
return parent is ViewGroup && parentMatcher.matches(parent)
|
||||
&& view == parent.getChildAt(position)
|
||||
return parent is ViewGroup &&
|
||||
parentMatcher.matches(parent) &&
|
||||
view == parent.getChildAt(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -154,14 +163,18 @@ class UITestHelper {
|
|||
val username = BuildConfig.TEST_USERNAME
|
||||
if (StringUtils.isEmpty(username) || username == "null") {
|
||||
throw NotImplementedError("Configure your beta account's username")
|
||||
} else return username
|
||||
} else {
|
||||
return username
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTestUserPassword(): String {
|
||||
val password = BuildConfig.TEST_PASSWORD
|
||||
if (StringUtils.isEmpty(password) || password == "null") {
|
||||
throw NotImplementedError("Configure your beta account's password")
|
||||
} else return password
|
||||
} else {
|
||||
return password
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Activity> changeOrientation(activityRule: ActivityTestRule<T>) {
|
||||
|
|
@ -174,6 +187,7 @@ class UITestHelper {
|
|||
fun <T> first(matcher: Matcher<T>): Matcher<T>? {
|
||||
return object : BaseMatcher<T>() {
|
||||
var isFirst = true
|
||||
|
||||
override fun matches(item: Any): Boolean {
|
||||
if (isFirst && matcher.matches(item)) {
|
||||
isFirst = false
|
||||
|
|
@ -188,4 +202,4 @@ class UITestHelper {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import android.app.Activity
|
|||
import android.app.Instrumentation
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.*
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||
import androidx.test.espresso.action.ViewActions.replaceText
|
||||
import androidx.test.espresso.action.ViewActions.scrollTo
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
|
|
@ -28,7 +31,6 @@ import org.junit.runner.RunWith
|
|||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class UploadCancelledTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
var mActivityTestRule = ActivityTestRule(LoginActivity::class.java)
|
||||
|
|
@ -37,7 +39,7 @@ class UploadCancelledTest {
|
|||
@JvmField
|
||||
var mGrantPermissionRule: GrantPermissionRule =
|
||||
GrantPermissionRule.grant(
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
)
|
||||
|
||||
private val device: UiDevice =
|
||||
|
|
@ -48,14 +50,14 @@ class UploadCancelledTest {
|
|||
try {
|
||||
Intents.init()
|
||||
} catch (ex: IllegalStateException) {
|
||||
|
||||
}
|
||||
device.unfreezeRotation()
|
||||
device.setOrientationNatural()
|
||||
device.freezeRotation()
|
||||
UITestHelper.loginUser()
|
||||
UITestHelper.skipWelcome()
|
||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
Intents
|
||||
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||
}
|
||||
|
||||
|
|
@ -64,130 +66,137 @@ class UploadCancelledTest {
|
|||
try {
|
||||
Intents.release()
|
||||
} catch (ex: IllegalStateException) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uploadCancelledAfterLocationPickedTest() {
|
||||
|
||||
val bottomNavigationItemView = onView(
|
||||
allOf(
|
||||
childAtPosition(
|
||||
val bottomNavigationItemView =
|
||||
onView(
|
||||
allOf(
|
||||
childAtPosition(
|
||||
withId(R.id.fragment_main_nav_tab_layout),
|
||||
0
|
||||
childAtPosition(
|
||||
withId(R.id.fragment_main_nav_tab_layout),
|
||||
0,
|
||||
),
|
||||
1,
|
||||
),
|
||||
1
|
||||
isDisplayed(),
|
||||
),
|
||||
isDisplayed()
|
||||
)
|
||||
)
|
||||
bottomNavigationItemView.perform(click())
|
||||
|
||||
UITestHelper.sleep(12000)
|
||||
|
||||
val actionMenuItemView = onView(
|
||||
allOf(
|
||||
withId(R.id.list_sheet),
|
||||
childAtPosition(
|
||||
val actionMenuItemView =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.list_sheet),
|
||||
childAtPosition(
|
||||
withId(R.id.toolbar),
|
||||
1
|
||||
childAtPosition(
|
||||
withId(R.id.toolbar),
|
||||
1,
|
||||
),
|
||||
0,
|
||||
),
|
||||
0
|
||||
isDisplayed(),
|
||||
),
|
||||
isDisplayed()
|
||||
)
|
||||
)
|
||||
actionMenuItemView.perform(click())
|
||||
|
||||
val recyclerView = onView(
|
||||
allOf(
|
||||
withId(R.id.rv_nearby_list),
|
||||
val recyclerView =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.rv_nearby_list),
|
||||
),
|
||||
)
|
||||
)
|
||||
recyclerView.perform(
|
||||
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
|
||||
0,
|
||||
click()
|
||||
)
|
||||
click(),
|
||||
),
|
||||
)
|
||||
|
||||
val linearLayout3 = onView(
|
||||
allOf(
|
||||
withId(R.id.cameraButton),
|
||||
childAtPosition(
|
||||
allOf(
|
||||
withId(R.id.nearby_button_layout),
|
||||
val linearLayout3 =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.cameraButton),
|
||||
childAtPosition(
|
||||
allOf(
|
||||
withId(R.id.nearby_button_layout),
|
||||
),
|
||||
0,
|
||||
),
|
||||
0
|
||||
isDisplayed(),
|
||||
),
|
||||
isDisplayed()
|
||||
)
|
||||
)
|
||||
linearLayout3.perform(click())
|
||||
|
||||
val pasteSensitiveTextInputEditText = onView(
|
||||
allOf(
|
||||
withId(R.id.caption_item_edit_text),
|
||||
childAtPosition(
|
||||
val pasteSensitiveTextInputEditText =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.caption_item_edit_text),
|
||||
childAtPosition(
|
||||
withId(R.id.caption_item_edit_text_input_layout),
|
||||
0
|
||||
childAtPosition(
|
||||
withId(R.id.caption_item_edit_text_input_layout),
|
||||
0,
|
||||
),
|
||||
0,
|
||||
),
|
||||
0
|
||||
isDisplayed(),
|
||||
),
|
||||
isDisplayed()
|
||||
)
|
||||
)
|
||||
pasteSensitiveTextInputEditText.perform(replaceText("test"), closeSoftKeyboard())
|
||||
|
||||
val pasteSensitiveTextInputEditText2 = onView(
|
||||
allOf(
|
||||
withId(R.id.description_item_edit_text),
|
||||
childAtPosition(
|
||||
val pasteSensitiveTextInputEditText2 =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.description_item_edit_text),
|
||||
childAtPosition(
|
||||
withId(R.id.description_item_edit_text_input_layout),
|
||||
0
|
||||
childAtPosition(
|
||||
withId(R.id.description_item_edit_text_input_layout),
|
||||
0,
|
||||
),
|
||||
0,
|
||||
),
|
||||
0
|
||||
isDisplayed(),
|
||||
),
|
||||
isDisplayed()
|
||||
)
|
||||
)
|
||||
pasteSensitiveTextInputEditText2.perform(replaceText("test"), closeSoftKeyboard())
|
||||
|
||||
val appCompatButton2 = onView(
|
||||
allOf(
|
||||
withId(R.id.btn_next),
|
||||
childAtPosition(
|
||||
val appCompatButton2 =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.btn_next),
|
||||
childAtPosition(
|
||||
withId(R.id.ll_container_media_detail),
|
||||
2
|
||||
childAtPosition(
|
||||
withId(R.id.ll_container_media_detail),
|
||||
2,
|
||||
),
|
||||
1,
|
||||
),
|
||||
1
|
||||
isDisplayed(),
|
||||
),
|
||||
isDisplayed()
|
||||
)
|
||||
)
|
||||
appCompatButton2.perform(click())
|
||||
|
||||
val appCompatButton3 = onView(
|
||||
allOf(
|
||||
withId(android.R.id.button1),
|
||||
val appCompatButton3 =
|
||||
onView(
|
||||
allOf(
|
||||
withId(android.R.id.button1),
|
||||
),
|
||||
)
|
||||
)
|
||||
appCompatButton3.perform(scrollTo(), click())
|
||||
|
||||
Intents.intended(IntentMatchers.hasComponent(LocationPickerActivity::class.java.name))
|
||||
|
||||
val floatingActionButton3 = onView(
|
||||
allOf(
|
||||
withId(R.id.location_chosen_button),
|
||||
isDisplayed()
|
||||
val floatingActionButton3 =
|
||||
onView(
|
||||
allOf(
|
||||
withId(R.id.location_chosen_button),
|
||||
isDisplayed(),
|
||||
),
|
||||
)
|
||||
)
|
||||
UITestHelper.sleep(2000)
|
||||
floatingActionButton3.perform(click())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@ import androidx.test.espresso.intent.Intents.intended
|
|||
import androidx.test.espresso.intent.Intents.intending
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasType
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withParent
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
|
|
@ -29,21 +32,29 @@ import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
|
|||
import fr.free.nrw.commons.util.MyViewAction
|
||||
import fr.free.nrw.commons.utils.ConfigUtils
|
||||
import org.hamcrest.core.AllOf.allOf
|
||||
import org.junit.*
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
import java.util.Random
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class UploadTest {
|
||||
@get:Rule
|
||||
var permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION)!!
|
||||
var permissionRule =
|
||||
GrantPermissionRule.grant(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
)!!
|
||||
|
||||
@get:Rule
|
||||
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
||||
|
|
@ -61,7 +72,6 @@ class UploadTest {
|
|||
try {
|
||||
Intents.init()
|
||||
} catch (ex: IllegalStateException) {
|
||||
|
||||
}
|
||||
UITestHelper.loginUser()
|
||||
UITestHelper.skipWelcome()
|
||||
|
|
@ -94,14 +104,13 @@ class UploadTest {
|
|||
dismissWarning("Yes")
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||
.perform(replaceText(commonsFileName))
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.description_item_edit_text)))
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(5000)
|
||||
dismissWarning("Yes")
|
||||
|
|
@ -109,29 +118,30 @@ class UploadTest {
|
|||
UITestHelper.sleep(3000)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
||||
.perform(replaceText("Uploaded with Mobile/Android Tests"))
|
||||
.perform(replaceText("Uploaded with Mobile/Android Tests"))
|
||||
|
||||
UITestHelper.sleep(3000)
|
||||
|
||||
try {
|
||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
dismissWarning("Yes, Submit")
|
||||
|
||||
UITestHelper.sleep(500)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(10000)
|
||||
|
||||
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||
val fileUrl =
|
||||
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||
commonsFileName.replace(' ', '_') + ".jpg"
|
||||
Timber.i("File should be uploaded to $fileUrl")
|
||||
}
|
||||
|
|
@ -139,8 +149,8 @@ class UploadTest {
|
|||
private fun dismissWarning(warningText: String) {
|
||||
try {
|
||||
onView(withText(warningText))
|
||||
.check(matches(isDisplayed()))
|
||||
.perform(click())
|
||||
.check(matches(isDisplayed()))
|
||||
.perform(click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
}
|
||||
|
|
@ -167,10 +177,10 @@ class UploadTest {
|
|||
dismissWarning("Yes")
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||
.perform(replaceText(commonsFileName))
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(10000)
|
||||
dismissWarning("Yes")
|
||||
|
|
@ -178,29 +188,30 @@ class UploadTest {
|
|||
UITestHelper.sleep(3000)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
||||
.perform(replaceText("Test"))
|
||||
.perform(replaceText("Test"))
|
||||
|
||||
UITestHelper.sleep(3000)
|
||||
|
||||
try {
|
||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
dismissWarning("Yes, Submit")
|
||||
|
||||
UITestHelper.sleep(500)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(10000)
|
||||
|
||||
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||
val fileUrl =
|
||||
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||
commonsFileName.replace(' ', '_') + ".jpg"
|
||||
Timber.i("File should be uploaded to $fileUrl")
|
||||
}
|
||||
|
|
@ -227,23 +238,29 @@ class UploadTest {
|
|||
dismissWarningDialog()
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||
.perform(replaceText(commonsFileName))
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
onView(withId(R.id.rv_descriptions)).perform(
|
||||
RecyclerViewActions
|
||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(0,
|
||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))
|
||||
RecyclerViewActions
|
||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(
|
||||
0,
|
||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description"),
|
||||
),
|
||||
)
|
||||
|
||||
onView(withId(R.id.btn_add))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
onView(withId(R.id.rv_descriptions)).perform(
|
||||
RecyclerViewActions
|
||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(1,
|
||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description")))
|
||||
RecyclerViewActions
|
||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(
|
||||
1,
|
||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description"),
|
||||
),
|
||||
)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(5000)
|
||||
dismissWarning("Yes")
|
||||
|
|
@ -251,29 +268,30 @@ class UploadTest {
|
|||
UITestHelper.sleep(3000)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
||||
.perform(replaceText("Test"))
|
||||
.perform(replaceText("Test"))
|
||||
|
||||
UITestHelper.sleep(3000)
|
||||
|
||||
try {
|
||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
dismissWarning("Yes, Submit")
|
||||
|
||||
UITestHelper.sleep(500)
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
UITestHelper.sleep(10000)
|
||||
|
||||
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||
val fileUrl =
|
||||
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||
commonsFileName.replace(' ', '_') + ".jpg"
|
||||
Timber.i("File should be uploaded to $fileUrl")
|
||||
}
|
||||
|
|
@ -306,7 +324,6 @@ class UploadTest {
|
|||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -328,8 +345,8 @@ class UploadTest {
|
|||
private fun dismissWarningDialog() {
|
||||
try {
|
||||
onView(withText("Yes"))
|
||||
.check(matches(isDisplayed()))
|
||||
.perform(click())
|
||||
.check(matches(isDisplayed()))
|
||||
.perform(click())
|
||||
} catch (ignored: NoMatchingViewException) {
|
||||
}
|
||||
}
|
||||
|
|
@ -337,10 +354,10 @@ class UploadTest {
|
|||
private fun openGallery() {
|
||||
// Open FAB
|
||||
onView(allOf<View>(withId(R.id.fab_plus), isDisplayed()))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
|
||||
// Click gallery
|
||||
onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed()))
|
||||
.perform(click())
|
||||
.perform(click())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons
|
|||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
|
@ -22,7 +21,6 @@ import org.junit.runner.RunWith
|
|||
@LargeTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class WelcomeActivityTest {
|
||||
|
||||
@get:Rule
|
||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(WelcomeActivity::class.java)
|
||||
|
||||
|
|
@ -130,4 +128,4 @@ class WelcomeActivityTest {
|
|||
fun orientationChange() {
|
||||
UITestHelper.changeOrientation(activityRule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import org.junit.runner.RunWith
|
|||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PasteSensitiveTextInputEditTextTest {
|
||||
|
||||
private var context: Context? = null
|
||||
private var textView: PasteSensitiveTextInputEditText? = null
|
||||
|
||||
|
|
@ -23,9 +22,13 @@ class PasteSensitiveTextInputEditTextTest {
|
|||
|
||||
// this test has no real value, just % for test code coverage
|
||||
@Test
|
||||
fun extractFormattingAttributeSet(){
|
||||
val methodExtractFormattingAttribute = textView!!.javaClass.getDeclaredMethod(
|
||||
"extractFormattingAttribute", Context::class.java, AttributeSet::class.java)
|
||||
fun extractFormattingAttributeSet() {
|
||||
val methodExtractFormattingAttribute =
|
||||
textView!!.javaClass.getDeclaredMethod(
|
||||
"extractFormattingAttribute",
|
||||
Context::class.java,
|
||||
AttributeSet::class.java,
|
||||
)
|
||||
methodExtractFormattingAttribute.isAccessible = true
|
||||
methodExtractFormattingAttribute.invoke(textView, context, null)
|
||||
}
|
||||
|
|
@ -40,4 +43,4 @@ class PasteSensitiveTextInputEditTextTest {
|
|||
textView!!.setFormattingAllowed(false)
|
||||
Assert.assertFalse(fieldFormattingAllowed.getBoolean(textView))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,56 +9,58 @@ import org.hamcrest.Matcher
|
|||
|
||||
class MyViewAction {
|
||||
companion object {
|
||||
fun typeTextInChildViewWithId(id: Int, textToBeTyped: String): ViewAction {
|
||||
return object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? {
|
||||
return null
|
||||
}
|
||||
fun typeTextInChildViewWithId(
|
||||
id: Int,
|
||||
textToBeTyped: String,
|
||||
): ViewAction =
|
||||
object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? = null
|
||||
|
||||
override fun getDescription(): String {
|
||||
return "Click on a child view with specified id."
|
||||
}
|
||||
override fun getDescription(): String = "Click on a child view with specified id."
|
||||
|
||||
override fun perform(uiController: UiController, view: View) {
|
||||
override fun perform(
|
||||
uiController: UiController,
|
||||
view: View,
|
||||
) {
|
||||
val v = view.findViewById<View>(id) as EditText
|
||||
v.setText(textToBeTyped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectSpinnerItemInChildViewWithId(id: Int, position: Int): ViewAction {
|
||||
return object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? {
|
||||
return null
|
||||
}
|
||||
fun selectSpinnerItemInChildViewWithId(
|
||||
id: Int,
|
||||
position: Int,
|
||||
): ViewAction =
|
||||
object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? = null
|
||||
|
||||
override fun getDescription(): String {
|
||||
return "Click on a child view with specified id."
|
||||
}
|
||||
override fun getDescription(): String = "Click on a child view with specified id."
|
||||
|
||||
override fun perform(uiController: UiController, view: View) {
|
||||
override fun perform(
|
||||
uiController: UiController,
|
||||
view: View,
|
||||
) {
|
||||
val v = view.findViewById<View>(id) as AppCompatSpinner
|
||||
v.setSelection(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clickItemWithId(id: Int, position: Int): ViewAction {
|
||||
return object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? {
|
||||
return null
|
||||
}
|
||||
fun clickItemWithId(
|
||||
id: Int,
|
||||
position: Int,
|
||||
): ViewAction =
|
||||
object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View>? = null
|
||||
|
||||
override fun getDescription(): String {
|
||||
return "Click on a child view with specified id."
|
||||
}
|
||||
override fun getDescription(): String = "Click on a child view with specified id."
|
||||
|
||||
override fun perform(uiController: UiController, view: View) {
|
||||
override fun perform(
|
||||
uiController: UiController,
|
||||
view: View,
|
||||
) {
|
||||
val v = view.findViewById<View>(id) as View
|
||||
v.performClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,26 +39,25 @@ class BaseMarker {
|
|||
constructor() {
|
||||
}
|
||||
|
||||
fun fromResource(context: Context, drawableResId: Int) {
|
||||
fun fromResource(
|
||||
context: Context,
|
||||
drawableResId: Int,
|
||||
) {
|
||||
val drawable: Drawable = context.resources.getDrawable(drawableResId)
|
||||
icon = if (drawable is BitmapDrawable) {
|
||||
(drawable as BitmapDrawable).bitmap
|
||||
} else {
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
drawable.intrinsicWidth,
|
||||
drawable.intrinsicHeight, Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
bitmap
|
||||
}
|
||||
icon =
|
||||
if (drawable is BitmapDrawable) {
|
||||
(drawable as BitmapDrawable).bitmap
|
||||
} else {
|
||||
val bitmap =
|
||||
Bitmap.createBitmap(
|
||||
drawable.intrinsicWidth,
|
||||
drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888,
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
bitmap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ object BetaConstants {
|
|||
* production server where beta server does not work
|
||||
*/
|
||||
const val COMMONS_URL = "https://commons.wikimedia.org/"
|
||||
|
||||
/**
|
||||
* Commons production's depicts property which is used in beta for some specific GET calls on
|
||||
* production server where beta server does not work
|
||||
*/
|
||||
const val DEPICTS_PROPERTY = "P180"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,31 +3,31 @@ package fr.free.nrw.commons
|
|||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
class CameraPosition(val latitude: Double, val longitude: Double, val zoom: Double) : Parcelable {
|
||||
|
||||
class CameraPosition(
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val zoom: Double,
|
||||
) : Parcelable {
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readDouble(),
|
||||
parcel.readDouble(),
|
||||
parcel.readDouble()
|
||||
parcel.readDouble(),
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
override fun writeToParcel(
|
||||
parcel: Parcel,
|
||||
flags: Int,
|
||||
) {
|
||||
parcel.writeDouble(latitude)
|
||||
parcel.writeDouble(longitude)
|
||||
parcel.writeDouble(zoom)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
override fun describeContents(): Int = 0
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<CameraPosition> {
|
||||
override fun createFromParcel(parcel: Parcel): CameraPosition {
|
||||
return CameraPosition(parcel)
|
||||
}
|
||||
override fun createFromParcel(parcel: Parcel): CameraPosition = CameraPosition(parcel)
|
||||
|
||||
override fun newArray(size: Int): Array<CameraPosition?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
override fun newArray(size: Int): Array<CameraPosition?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ package fr.free.nrw.commons
|
|||
|
||||
import android.os.Parcelable
|
||||
import fr.free.nrw.commons.location.LatLng
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import fr.free.nrw.commons.wikidata.model.page.PageTitle
|
||||
import java.util.*
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
@Parcelize
|
||||
class Media constructor(
|
||||
|
|
@ -14,7 +16,6 @@ class Media constructor(
|
|||
*/
|
||||
var pageId: String = UUID.randomUUID().toString(),
|
||||
var thumbUrl: String? = null,
|
||||
|
||||
/**
|
||||
* Gets image URL
|
||||
* @return Image URL
|
||||
|
|
@ -26,16 +27,11 @@ class Media constructor(
|
|||
*/
|
||||
var filename: String? = null,
|
||||
/**
|
||||
* Gets the file description.
|
||||
* Gets or sets the file description.
|
||||
* @return file description as a string
|
||||
*/
|
||||
// monolingual description on input...
|
||||
/**
|
||||
* Sets the file description.
|
||||
* @param fallbackDescription the new description of the file
|
||||
*/
|
||||
var fallbackDescription: String? = null,
|
||||
|
||||
/**
|
||||
* Gets the upload date of the file.
|
||||
* Can be null.
|
||||
|
|
@ -43,28 +39,19 @@ class Media constructor(
|
|||
*/
|
||||
var dateUploaded: Date? = null,
|
||||
/**
|
||||
* Gets the license name of the file.
|
||||
* Gets or sets the license name of the file.
|
||||
* @return license as a String
|
||||
*/
|
||||
/**
|
||||
* Sets the license name of the file.
|
||||
*
|
||||
* @param license license name as a String
|
||||
*/
|
||||
var license: String? = null,
|
||||
var licenseUrl: String? = null,
|
||||
/**
|
||||
* Gets the name of the creator of the file.
|
||||
* Gets or sets the name of the creator of the file.
|
||||
* @return author name as a String
|
||||
*/
|
||||
/**
|
||||
* Sets the author name of the file.
|
||||
* @param author creator name as a string
|
||||
*/
|
||||
var author: String? = null,
|
||||
|
||||
var user:String?=null,
|
||||
|
||||
var user: String? = null,
|
||||
/**
|
||||
* Gets the categories the file falls under.
|
||||
* @return file categories as an ArrayList of Strings
|
||||
|
|
@ -83,23 +70,23 @@ class Media constructor(
|
|||
* Stores the mapping of category title to hidden attribute
|
||||
* Example: "Mountains" => false, "CC-BY-SA-2.0" => true
|
||||
*/
|
||||
var categoriesHiddenStatus: Map<String, Boolean> = emptyMap()
|
||||
var categoriesHiddenStatus: Map<String, Boolean> = emptyMap(),
|
||||
) : Parcelable {
|
||||
|
||||
constructor(
|
||||
captions: Map<String, String>,
|
||||
categories: List<String>?,
|
||||
filename: String?,
|
||||
fallbackDescription: String?,
|
||||
author: String?, user:String?
|
||||
author: String?,
|
||||
user: String?,
|
||||
) : this(
|
||||
filename = filename,
|
||||
fallbackDescription = fallbackDescription,
|
||||
dateUploaded = Date(),
|
||||
author = author,
|
||||
user=user,
|
||||
user = user,
|
||||
categories = categories,
|
||||
captions = captions
|
||||
captions = captions,
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
@ -108,10 +95,11 @@ class Media constructor(
|
|||
*/
|
||||
val displayTitle: String
|
||||
get() =
|
||||
if (filename != null)
|
||||
if (filename != null) {
|
||||
pageTitle.displayTextWithoutNamespace.replaceFirst("[.][^.]+$".toRegex(), "")
|
||||
else
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets file page title
|
||||
|
|
@ -127,9 +115,10 @@ class Media constructor(
|
|||
get() = String.format("[[%s|thumb|%s]]", filename, mostRelevantCaption)
|
||||
|
||||
val mostRelevantCaption: String
|
||||
get() = captions[Locale.getDefault().language]
|
||||
?: captions.values.firstOrNull()
|
||||
?: displayTitle
|
||||
get() =
|
||||
captions[Locale.getDefault().language]
|
||||
?: captions.values.firstOrNull()
|
||||
?: displayTitle
|
||||
|
||||
/**
|
||||
* Gets the categories the file falls under.
|
||||
|
|
@ -138,6 +127,8 @@ class Media constructor(
|
|||
var addedCategories: List<String>? = null
|
||||
// TODO added categories should be removed. It is added for a short fix. On category update,
|
||||
// categories should be re-fetched instead
|
||||
get() = field // getter
|
||||
set(value) { field = value } // setter
|
||||
get() = field // getter
|
||||
set(value) {
|
||||
field = value
|
||||
} // setter
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import androidx.core.text.HtmlCompat
|
||||
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
|
||||
import fr.free.nrw.commons.media.IdAndCaptions
|
||||
import fr.free.nrw.commons.media.MediaClient
|
||||
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
|
||||
import io.reactivex.Single
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
@ -17,42 +17,46 @@ import javax.inject.Singleton
|
|||
* to the media and may change due to editing.
|
||||
*/
|
||||
@Singleton
|
||||
class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClient) {
|
||||
class MediaDataExtractor
|
||||
@Inject
|
||||
constructor(
|
||||
private val mediaClient: MediaClient,
|
||||
) {
|
||||
fun fetchDepictionIdsAndLabels(media: Media) =
|
||||
mediaClient
|
||||
.getEntities(media.depictionIds)
|
||||
.map {
|
||||
it
|
||||
.entities()
|
||||
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
|
||||
}.map { it.map { (key, value) -> IdAndCaptions(key, value) } }
|
||||
.onErrorReturn { emptyList() }
|
||||
|
||||
fun fetchDepictionIdsAndLabels(media: Media) =
|
||||
mediaClient.getEntities(media.depictionIds)
|
||||
.map {
|
||||
it.entities()
|
||||
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
|
||||
}
|
||||
.map { it.map { (key, value) -> IdAndCaptions(key, value) } }
|
||||
.onErrorReturn { emptyList() }
|
||||
fun checkDeletionRequestExists(media: Media) = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
|
||||
|
||||
fun checkDeletionRequestExists(media: Media) =
|
||||
mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
|
||||
fun fetchDiscussion(media: Media) =
|
||||
mediaClient
|
||||
.getPageHtml(media.filename!!.replace("File", "File talk"))
|
||||
.map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() }
|
||||
.onErrorReturn {
|
||||
Timber.d("Error occurred while fetching discussion")
|
||||
""
|
||||
}
|
||||
|
||||
fun fetchDiscussion(media: Media) =
|
||||
mediaClient.getPageHtml(media.filename!!.replace("File", "File talk"))
|
||||
.map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() }
|
||||
.onErrorReturn {
|
||||
Timber.d("Error occurred while fetching discussion")
|
||||
""
|
||||
}
|
||||
fun refresh(media: Media): Single<Media> =
|
||||
Single.ambArray(
|
||||
mediaClient
|
||||
.getMediaById(PAGE_ID_PREFIX + media.pageId)
|
||||
.onErrorResumeNext { Single.never() },
|
||||
mediaClient
|
||||
.getMediaSuppressingErrors(media.filename)
|
||||
.onErrorResumeNext { Single.never() },
|
||||
)
|
||||
|
||||
fun refresh(media: Media): Single<Media> {
|
||||
return Single.ambArray(
|
||||
mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId)
|
||||
.onErrorResumeNext { Single.never() },
|
||||
mediaClient.getMediaSuppressingErrors(media.filename)
|
||||
.onErrorResumeNext { Single.never() }
|
||||
)
|
||||
fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title)
|
||||
|
||||
/**
|
||||
* Fetches wikitext from mediaClient
|
||||
*/
|
||||
fun getCurrentWikiText(title: String) = mediaClient.getCurrentWikiText(title)
|
||||
}
|
||||
|
||||
fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title);
|
||||
|
||||
/**
|
||||
* Fetches wikitext from mediaClient
|
||||
*/
|
||||
fun getCurrentWikiText(title: String) = mediaClient.getCurrentWikiText(title);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ internal object Urls {
|
|||
const val FAQ_URL = "https://github.com/commons-app/commons-app-documentation/blob/master/android/Frequently-Asked-Questions.md"
|
||||
const val PLAY_STORE_PREFIX = "market://details?id="
|
||||
const val PLAY_STORE_URL_PREFIX = "https://play.google.com/store/apps/details?id="
|
||||
const val TRANSLATE_WIKI_URL = "https://translatewiki.net/w/i.php?title=Special:Translate&group=commons-android-strings&filter=%21translated&action=translate&language="
|
||||
const val TRANSLATE_WIKI_URL =
|
||||
"https://translatewiki.net/w/i.php?title=Special:Translate" +
|
||||
"&group=commons-android-strings&filter=%21translated&action=translate&language="
|
||||
const val FACEBOOK_WEB_URL = "https://www.facebook.com/1921335171459985"
|
||||
const val FACEBOOK_APP_URL = "fb://page/1921335171459985"
|
||||
const val FACEBOOK_PACKAGE_NAME = "com.facebook.katana"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package fr.free.nrw.commons.actions
|
||||
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* This class acts as a Client to facilitate wiki page editing
|
||||
|
|
@ -15,9 +14,8 @@ import timber.log.Timber
|
|||
*/
|
||||
class PageEditClient(
|
||||
private val csrfTokenClient: CsrfTokenClient,
|
||||
private val pageEditInterface: PageEditInterface
|
||||
private val pageEditInterface: PageEditInterface,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Replace the content of a wiki page
|
||||
* @param pageTitle Title of the page to edit
|
||||
|
|
@ -25,12 +23,17 @@ class PageEditClient(
|
|||
* @param summary Edit summary
|
||||
* @return whether the edit was successful
|
||||
*/
|
||||
fun edit(pageTitle: String, text: String, summary: String): Observable<Boolean> {
|
||||
return try {
|
||||
pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking())
|
||||
fun edit(
|
||||
pageTitle: String,
|
||||
text: String,
|
||||
summary: String,
|
||||
): Observable<Boolean> =
|
||||
try {
|
||||
pageEditInterface
|
||||
.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking())
|
||||
.map { editResponse ->
|
||||
editResponse.edit()!!.editSucceeded()
|
||||
}
|
||||
editResponse.edit()!!.editSucceeded()
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
throw throwable
|
||||
|
|
@ -38,7 +41,6 @@ class PageEditClient(
|
|||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new page with the given title, text, and summary.
|
||||
|
|
@ -49,20 +51,25 @@ class PageEditClient(
|
|||
* @return An observable that emits true if the page creation succeeded, false otherwise.
|
||||
* @throws InvalidLoginTokenException If an invalid login token is encountered during the process.
|
||||
*/
|
||||
fun postCreate(pageTitle: String, text: String, summary: String): Observable<Boolean> {
|
||||
return try {
|
||||
pageEditInterface.postCreate(
|
||||
pageTitle,
|
||||
summary,
|
||||
text,
|
||||
"text/x-wiki",
|
||||
"wikitext",
|
||||
true,
|
||||
true,
|
||||
csrfTokenClient.getTokenBlocking()
|
||||
).map { editResponse ->
|
||||
editResponse.edit()!!.editSucceeded()
|
||||
}
|
||||
fun postCreate(
|
||||
pageTitle: String,
|
||||
text: String,
|
||||
summary: String,
|
||||
): Observable<Boolean> =
|
||||
try {
|
||||
pageEditInterface
|
||||
.postCreate(
|
||||
pageTitle,
|
||||
summary,
|
||||
text,
|
||||
"text/x-wiki",
|
||||
"wikitext",
|
||||
true,
|
||||
true,
|
||||
csrfTokenClient.getTokenBlocking(),
|
||||
).map { editResponse ->
|
||||
editResponse.edit()!!.editSucceeded()
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
throw throwable
|
||||
|
|
@ -70,7 +77,6 @@ class PageEditClient(
|
|||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append text to the end of a wiki page
|
||||
|
|
@ -79,9 +85,14 @@ class PageEditClient(
|
|||
* @param summary Edit summary
|
||||
* @return whether the edit was successful
|
||||
*/
|
||||
fun appendEdit(pageTitle: String, appendText: String, summary: String): Observable<Boolean> {
|
||||
return try {
|
||||
pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking())
|
||||
fun appendEdit(
|
||||
pageTitle: String,
|
||||
appendText: String,
|
||||
summary: String,
|
||||
): Observable<Boolean> =
|
||||
try {
|
||||
pageEditInterface
|
||||
.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking())
|
||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
|
|
@ -90,7 +101,6 @@ class PageEditClient(
|
|||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend text to the beginning of a wiki page
|
||||
|
|
@ -99,9 +109,14 @@ class PageEditClient(
|
|||
* @param summary Edit summary
|
||||
* @return whether the edit was successful
|
||||
*/
|
||||
fun prependEdit(pageTitle: String, prependText: String, summary: String): Observable<Boolean> {
|
||||
return try {
|
||||
pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking())
|
||||
fun prependEdit(
|
||||
pageTitle: String,
|
||||
prependText: String,
|
||||
summary: String,
|
||||
): Observable<Boolean> =
|
||||
try {
|
||||
pageEditInterface
|
||||
.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking())
|
||||
.map { editResponse -> editResponse.edit()?.editSucceeded() ?: false }
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
|
|
@ -110,8 +125,6 @@ class PageEditClient(
|
|||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Appends a new section to the wiki page
|
||||
|
|
@ -121,9 +134,15 @@ class PageEditClient(
|
|||
* @param summary Edit summary
|
||||
* @return whether the edit was successful
|
||||
*/
|
||||
fun createNewSection(pageTitle: String, sectionTitle: String, sectionText: String, summary: String): Observable<Boolean> {
|
||||
return try {
|
||||
pageEditInterface.postNewSection(pageTitle, summary, sectionTitle, sectionText, csrfTokenClient.getTokenBlocking())
|
||||
fun createNewSection(
|
||||
pageTitle: String,
|
||||
sectionTitle: String,
|
||||
sectionText: String,
|
||||
summary: String,
|
||||
): Observable<Boolean> =
|
||||
try {
|
||||
pageEditInterface
|
||||
.postNewSection(pageTitle, summary, sectionTitle, sectionText, csrfTokenClient.getTokenBlocking())
|
||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
|
|
@ -132,8 +151,6 @@ class PageEditClient(
|
|||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set new labels to Wikibase server of commons
|
||||
|
|
@ -143,12 +160,21 @@ class PageEditClient(
|
|||
* @param value label
|
||||
* @return 1 when the edit was successful
|
||||
*/
|
||||
fun setCaptions(summary: String, title: String,
|
||||
language: String, value: String) : Observable<Int>{
|
||||
return try {
|
||||
pageEditInterface.postCaptions(summary, title, language,
|
||||
value, csrfTokenClient.getTokenBlocking()
|
||||
).map { it.success }
|
||||
fun setCaptions(
|
||||
summary: String,
|
||||
title: String,
|
||||
language: String,
|
||||
value: String,
|
||||
): Observable<Int> =
|
||||
try {
|
||||
pageEditInterface
|
||||
.postCaptions(
|
||||
summary,
|
||||
title,
|
||||
language,
|
||||
value,
|
||||
csrfTokenClient.getTokenBlocking(),
|
||||
).map { it.success }
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
throw throwable
|
||||
|
|
@ -156,16 +182,20 @@ class PageEditClient(
|
|||
Observable.just(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whole WikiText of required file
|
||||
* @param title : Name of the file
|
||||
* @return Observable<MwQueryResult>
|
||||
*/
|
||||
fun getCurrentWikiText(title: String): Single<String?> {
|
||||
return pageEditInterface.getWikiText(title).map {
|
||||
it.query()?.pages()?.get(0)?.revisions()?.get(0)?.content()
|
||||
fun getCurrentWikiText(title: String): Single<String?> =
|
||||
pageEditInterface.getWikiText(title).map {
|
||||
it
|
||||
.query()
|
||||
?.pages()
|
||||
?.get(0)
|
||||
?.revisions()
|
||||
?.get(0)
|
||||
?.content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,15 @@ package fr.free.nrw.commons.actions
|
|||
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||
import fr.free.nrw.commons.wikidata.model.Entities
|
||||
import fr.free.nrw.commons.wikidata.model.edit.Edit
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import retrofit2.http.*
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
|
||||
/**
|
||||
* This interface facilitates wiki commons page editing services to the Networking module
|
||||
|
|
@ -33,7 +38,7 @@ interface PageEditInterface {
|
|||
@Field("summary") summary: String,
|
||||
@Field("text") text: String,
|
||||
// NOTE: This csrf shold always be sent as the last field of form data
|
||||
@Field("token") token: String
|
||||
@Field("token") token: String,
|
||||
): Observable<Edit>
|
||||
|
||||
/**
|
||||
|
|
@ -60,7 +65,7 @@ interface PageEditInterface {
|
|||
@Field("minor") minor: Boolean,
|
||||
@Field("recreate") recreate: Boolean,
|
||||
// NOTE: This csrf shold always be sent as the last field of form data
|
||||
@Field("token") token: String
|
||||
@Field("token") token: String,
|
||||
): Observable<Edit>
|
||||
|
||||
/**
|
||||
|
|
@ -79,7 +84,7 @@ interface PageEditInterface {
|
|||
@Field("title") title: String,
|
||||
@Field("summary") summary: String,
|
||||
@Field("appendtext") appendText: String,
|
||||
@Field("token") token: String
|
||||
@Field("token") token: String,
|
||||
): Observable<Edit>
|
||||
|
||||
/**
|
||||
|
|
@ -98,7 +103,7 @@ interface PageEditInterface {
|
|||
@Field("title") title: String,
|
||||
@Field("summary") summary: String,
|
||||
@Field("prependtext") prependText: String,
|
||||
@Field("token") token: String
|
||||
@Field("token") token: String,
|
||||
): Observable<Edit>
|
||||
|
||||
@FormUrlEncoded
|
||||
|
|
@ -109,7 +114,7 @@ interface PageEditInterface {
|
|||
@Field("summary") summary: String,
|
||||
@Field("sectiontitle") sectionTitle: String,
|
||||
@Field("text") sectionText: String,
|
||||
@Field("token") token: String
|
||||
@Field("token") token: String,
|
||||
): Observable<Edit>
|
||||
|
||||
@FormUrlEncoded
|
||||
|
|
@ -120,7 +125,7 @@ interface PageEditInterface {
|
|||
@Field("title") title: String,
|
||||
@Field("language") language: String,
|
||||
@Field("value") value: String,
|
||||
@Field("token") token: String
|
||||
@Field("token") token: String,
|
||||
): Observable<Entities>
|
||||
|
||||
/**
|
||||
|
|
@ -130,6 +135,6 @@ interface PageEditInterface {
|
|||
*/
|
||||
@GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=")
|
||||
fun getWikiText(
|
||||
@Query("titles") title: String
|
||||
@Query("titles") title: String,
|
||||
): Single<MwQueryResponse?>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
package fr.free.nrw.commons.actions
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
|
||||
import io.reactivex.Observable
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||
import fr.free.nrw.commons.auth.login.LoginFailedException
|
||||
import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
|
||||
import io.reactivex.Observable
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -15,34 +14,33 @@ import javax.inject.Singleton
|
|||
* Thanks are used by a user to show gratitude to another user for their contributions
|
||||
*/
|
||||
@Singleton
|
||||
class ThanksClient @Inject constructor(
|
||||
@param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
|
||||
private val service: ThanksInterface
|
||||
) {
|
||||
/**
|
||||
* Thanks a user for a particular revision
|
||||
* @param revisionId The revision ID the user would like to thank someone for
|
||||
* @return if thanks was successfully sent to intended recipient
|
||||
*/
|
||||
fun thank(revisionId: Long): Observable<Boolean> {
|
||||
return try {
|
||||
service.thank(
|
||||
revisionId.toString(), // Rev
|
||||
null, // Log
|
||||
csrfTokenClient.getTokenBlocking(), // Token
|
||||
CommonsApplication.getInstance().userAgent // Source
|
||||
).map {
|
||||
mwThankPostResponse -> mwThankPostResponse.result?.success == 1
|
||||
class ThanksClient
|
||||
@Inject
|
||||
constructor(
|
||||
@param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
|
||||
private val service: ThanksInterface,
|
||||
) {
|
||||
/**
|
||||
* Thanks a user for a particular revision
|
||||
* @param revisionId The revision ID the user would like to thank someone for
|
||||
* @return if thanks was successfully sent to intended recipient
|
||||
*/
|
||||
fun thank(revisionId: Long): Observable<Boolean> =
|
||||
try {
|
||||
service
|
||||
.thank(
|
||||
revisionId.toString(), // Rev
|
||||
null, // Log
|
||||
csrfTokenClient.getTokenBlocking(), // Token
|
||||
CommonsApplication.getInstance().userAgent, // Source
|
||||
).map { mwThankPostResponse ->
|
||||
mwThankPostResponse.result?.success == 1
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
Observable.error(throwable)
|
||||
} else {
|
||||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (throwable: Throwable) {
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
Observable.error(throwable)
|
||||
}
|
||||
else {
|
||||
Observable.just(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,6 @@ interface ThanksInterface {
|
|||
@Field("rev") rev: String?,
|
||||
@Field("log") log: String?,
|
||||
@Field("token") token: String,
|
||||
@Field("source") source: String?
|
||||
@Field("source") source: String?,
|
||||
): Observable<MwThankPostResponse?>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ package fr.free.nrw.commons.auth.csrf
|
|||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import fr.free.nrw.commons.auth.login.LoginClient
|
||||
import fr.free.nrw.commons.auth.login.LoginCallback
|
||||
import fr.free.nrw.commons.auth.login.LoginClient
|
||||
import fr.free.nrw.commons.auth.login.LoginFailedException
|
||||
import fr.free.nrw.commons.auth.login.LoginResult
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
|
|
@ -17,12 +17,11 @@ class CsrfTokenClient(
|
|||
private val sessionManager: SessionManager,
|
||||
private val csrfTokenInterface: CsrfTokenInterface,
|
||||
private val loginClient: LoginClient,
|
||||
private val logoutClient: LogoutClient
|
||||
private val logoutClient: LogoutClient,
|
||||
) {
|
||||
private var retries = 0
|
||||
private var csrfTokenCall: Call<MwQueryResponse?>? = null
|
||||
|
||||
|
||||
@Throws(Throwable::class)
|
||||
fun getTokenBlocking(): String {
|
||||
var token = ""
|
||||
|
|
@ -37,11 +36,20 @@ class CsrfTokenClient(
|
|||
}
|
||||
|
||||
// Get CSRFToken response off the main thread.
|
||||
val response = newSingleThreadExecutor().submit(Callable {
|
||||
csrfTokenInterface.getCsrfTokenCall().execute()
|
||||
}).get()
|
||||
val response =
|
||||
newSingleThreadExecutor()
|
||||
.submit(
|
||||
Callable {
|
||||
csrfTokenInterface.getCsrfTokenCall().execute()
|
||||
},
|
||||
).get()
|
||||
|
||||
if (response.body()?.query()?.csrfToken().isNullOrEmpty()) {
|
||||
if (response
|
||||
.body()
|
||||
?.query()
|
||||
?.csrfToken()
|
||||
.isNullOrEmpty()
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -51,9 +59,8 @@ class CsrfTokenClient(
|
|||
}
|
||||
break
|
||||
} catch (e: LoginFailedException) {
|
||||
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||
}
|
||||
catch (t: Throwable) {
|
||||
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||
} catch (t: Throwable) {
|
||||
Timber.w(t)
|
||||
}
|
||||
}
|
||||
|
|
@ -65,45 +72,65 @@ class CsrfTokenClient(
|
|||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun request(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> =
|
||||
requestToken(service, object : Callback {
|
||||
override fun success(token: String?) {
|
||||
if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) {
|
||||
retryWithLogin(cb) {
|
||||
InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||
fun request(
|
||||
service: CsrfTokenInterface,
|
||||
cb: Callback,
|
||||
): Call<MwQueryResponse?> =
|
||||
requestToken(
|
||||
service,
|
||||
object : Callback {
|
||||
override fun success(token: String?) {
|
||||
if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) {
|
||||
retryWithLogin(cb) {
|
||||
InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||
}
|
||||
} else {
|
||||
cb.success(token)
|
||||
}
|
||||
} else {
|
||||
cb.success(token)
|
||||
}
|
||||
}
|
||||
|
||||
override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught }
|
||||
override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught }
|
||||
|
||||
override fun twoFactorPrompt() = cb.twoFactorPrompt()
|
||||
})
|
||||
override fun twoFactorPrompt() = cb.twoFactorPrompt()
|
||||
},
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
fun requestToken(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> {
|
||||
fun requestToken(
|
||||
service: CsrfTokenInterface,
|
||||
cb: Callback,
|
||||
): Call<MwQueryResponse?> {
|
||||
val call = service.getCsrfTokenCall()
|
||||
call.enqueue(object : retrofit2.Callback<MwQueryResponse?> {
|
||||
override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) {
|
||||
if (call.isCanceled) {
|
||||
return
|
||||
call.enqueue(
|
||||
object : retrofit2.Callback<MwQueryResponse?> {
|
||||
override fun onResponse(
|
||||
call: Call<MwQueryResponse?>,
|
||||
response: Response<MwQueryResponse?>,
|
||||
) {
|
||||
if (call.isCanceled) {
|
||||
return
|
||||
}
|
||||
cb.success(response.body()!!.query()!!.csrfToken())
|
||||
}
|
||||
cb.success(response.body()!!.query()!!.csrfToken())
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<MwQueryResponse?>, t: Throwable) {
|
||||
if (call.isCanceled) {
|
||||
return
|
||||
override fun onFailure(
|
||||
call: Call<MwQueryResponse?>,
|
||||
t: Throwable,
|
||||
) {
|
||||
if (call.isCanceled) {
|
||||
return
|
||||
}
|
||||
cb.failure(t)
|
||||
}
|
||||
cb.failure(t)
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
return call
|
||||
}
|
||||
|
||||
private fun retryWithLogin(callback: Callback, caught: () -> Throwable?) {
|
||||
private fun retryWithLogin(
|
||||
callback: Callback,
|
||||
caught: () -> Throwable?,
|
||||
) {
|
||||
val userName = sessionManager.userName
|
||||
val password = sessionManager.password
|
||||
if (retries < MAX_RETRIES && !userName.isNullOrEmpty() && !password.isNullOrEmpty()) {
|
||||
|
|
@ -123,26 +150,31 @@ class CsrfTokenClient(
|
|||
username: String,
|
||||
password: String,
|
||||
callback: Callback,
|
||||
retryCallback: () -> Unit
|
||||
) = loginClient.request(username, password, object : LoginCallback {
|
||||
override fun success(loginResult: LoginResult) {
|
||||
if (loginResult.pass) {
|
||||
sessionManager.updateAccount(loginResult)
|
||||
retryCallback()
|
||||
} else {
|
||||
callback.failure(LoginFailedException(loginResult.message))
|
||||
retryCallback: () -> Unit,
|
||||
) = loginClient.request(
|
||||
username,
|
||||
password,
|
||||
object : LoginCallback {
|
||||
override fun success(loginResult: LoginResult) {
|
||||
if (loginResult.pass) {
|
||||
sessionManager.updateAccount(loginResult)
|
||||
retryCallback()
|
||||
} else {
|
||||
callback.failure(LoginFailedException(loginResult.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun twoFactorPrompt(caught: Throwable, token: String?) =
|
||||
callback.twoFactorPrompt()
|
||||
override fun twoFactorPrompt(
|
||||
caught: Throwable,
|
||||
token: String?,
|
||||
) = callback.twoFactorPrompt()
|
||||
|
||||
// Should not happen here, but call the callback just in case.
|
||||
override fun passwordResetPrompt(token: String?) =
|
||||
callback.failure(LoginFailedException("Logged in with temporary password."))
|
||||
// Should not happen here, but call the callback just in case.
|
||||
override fun passwordResetPrompt(token: String?) = callback.failure(LoginFailedException("Logged in with temporary password."))
|
||||
|
||||
override fun error(caught: Throwable) = callback.failure(caught)
|
||||
})
|
||||
override fun error(caught: Throwable) = callback.failure(caught)
|
||||
},
|
||||
)
|
||||
|
||||
private fun cancel() {
|
||||
loginClient.cancel()
|
||||
|
|
@ -154,7 +186,9 @@ class CsrfTokenClient(
|
|||
|
||||
interface Callback {
|
||||
fun success(token: String?)
|
||||
|
||||
fun failure(caught: Throwable?)
|
||||
|
||||
fun twoFactorPrompt()
|
||||
}
|
||||
|
||||
|
|
@ -166,5 +200,7 @@ class CsrfTokenClient(
|
|||
const val ANONYMOUS_TOKEN_MESSAGE = "App believes we're logged in, but got anonymous token."
|
||||
}
|
||||
}
|
||||
class InvalidLoginTokenException(message: String) : Exception(message)
|
||||
|
||||
class InvalidLoginTokenException(
|
||||
message: String,
|
||||
) : Exception(message)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ package fr.free.nrw.commons.auth.csrf
|
|||
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage
|
||||
import javax.inject.Inject
|
||||
|
||||
class LogoutClient @Inject constructor(private val store: CommonsCookieStorage) {
|
||||
fun logout() = store.clear()
|
||||
}
|
||||
class LogoutClient
|
||||
@Inject
|
||||
constructor(
|
||||
private val store: CommonsCookieStorage,
|
||||
) {
|
||||
fun logout() = store.clear()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@ package fr.free.nrw.commons.auth.login
|
|||
|
||||
interface LoginCallback {
|
||||
fun success(loginResult: LoginResult)
|
||||
fun twoFactorPrompt(caught: Throwable, token: String?)
|
||||
|
||||
fun twoFactorPrompt(
|
||||
caught: Throwable,
|
||||
token: String?,
|
||||
)
|
||||
|
||||
fun passwordResetPrompt(token: String?)
|
||||
|
||||
fun error(caught: Throwable)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import android.text.TextUtils
|
|||
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
|
||||
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
|
||||
import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
|
@ -16,7 +16,9 @@ import java.io.IOException
|
|||
/**
|
||||
* Responsible for making login related requests to the server.
|
||||
*/
|
||||
class LoginClient(private val loginInterface: LoginInterface) {
|
||||
class LoginClient(
|
||||
private val loginInterface: LoginInterface,
|
||||
) {
|
||||
private var tokenCall: Call<MwQueryResponse?>? = null
|
||||
private var loginCall: Call<LoginResponse?>? = null
|
||||
|
||||
|
|
@ -30,80 +32,116 @@ class LoginClient(private val loginInterface: LoginInterface) {
|
|||
|
||||
private fun getLoginToken() = loginInterface.getLoginToken()
|
||||
|
||||
fun request(userName: String, password: String, cb: LoginCallback) {
|
||||
fun request(
|
||||
userName: String,
|
||||
password: String,
|
||||
cb: LoginCallback,
|
||||
) {
|
||||
cancel()
|
||||
|
||||
tokenCall = getLoginToken()
|
||||
tokenCall!!.enqueue(object : Callback<MwQueryResponse?> {
|
||||
override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) {
|
||||
login(
|
||||
userName, password, null, null, response.body()!!.query()!!.loginToken(),
|
||||
userLanguage, cb
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<MwQueryResponse?>, caught: Throwable) {
|
||||
if (call.isCanceled) {
|
||||
return
|
||||
tokenCall!!.enqueue(
|
||||
object : Callback<MwQueryResponse?> {
|
||||
override fun onResponse(
|
||||
call: Call<MwQueryResponse?>,
|
||||
response: Response<MwQueryResponse?>,
|
||||
) {
|
||||
login(
|
||||
userName,
|
||||
password,
|
||||
null,
|
||||
null,
|
||||
response.body()!!.query()!!.loginToken(),
|
||||
userLanguage,
|
||||
cb,
|
||||
)
|
||||
}
|
||||
cb.error(caught)
|
||||
}
|
||||
})
|
||||
|
||||
override fun onFailure(
|
||||
call: Call<MwQueryResponse?>,
|
||||
caught: Throwable,
|
||||
) {
|
||||
if (call.isCanceled) {
|
||||
return
|
||||
}
|
||||
cb.error(caught)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun login(
|
||||
userName: String, password: String, retypedPassword: String?, twoFactorCode: String?,
|
||||
loginToken: String?, userLanguage: String, cb: LoginCallback
|
||||
userName: String,
|
||||
password: String,
|
||||
retypedPassword: String?,
|
||||
twoFactorCode: String?,
|
||||
loginToken: String?,
|
||||
userLanguage: String,
|
||||
cb: LoginCallback,
|
||||
) {
|
||||
this.userLanguage = userLanguage
|
||||
|
||||
loginCall = if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
|
||||
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
||||
} else {
|
||||
loginInterface.postLogIn(
|
||||
userName, password, retypedPassword, twoFactorCode, loginToken, userLanguage, true
|
||||
)
|
||||
}
|
||||
loginCall =
|
||||
if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
|
||||
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
||||
} else {
|
||||
loginInterface.postLogIn(
|
||||
userName,
|
||||
password,
|
||||
retypedPassword,
|
||||
twoFactorCode,
|
||||
loginToken,
|
||||
userLanguage,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
loginCall!!.enqueue(object : Callback<LoginResponse?> {
|
||||
override fun onResponse(
|
||||
call: Call<LoginResponse?>,
|
||||
response: Response<LoginResponse?>
|
||||
) {
|
||||
val loginResult = response.body()?.toLoginResult(password)
|
||||
if (loginResult != null) {
|
||||
if (loginResult.pass && !loginResult.userName.isNullOrEmpty()) {
|
||||
// The server could do some transformations on user names, e.g. on some
|
||||
// wikis is uppercases the first letter.
|
||||
getExtendedInfo(loginResult.userName, loginResult, cb)
|
||||
} else if ("UI" == loginResult.status) {
|
||||
when (loginResult) {
|
||||
is OAuthResult -> cb.twoFactorPrompt(
|
||||
LoginFailedException(loginResult.message),
|
||||
loginToken
|
||||
)
|
||||
loginCall!!.enqueue(
|
||||
object : Callback<LoginResponse?> {
|
||||
override fun onResponse(
|
||||
call: Call<LoginResponse?>,
|
||||
response: Response<LoginResponse?>,
|
||||
) {
|
||||
val loginResult = response.body()?.toLoginResult(password)
|
||||
if (loginResult != null) {
|
||||
if (loginResult.pass && !loginResult.userName.isNullOrEmpty()) {
|
||||
// The server could do some transformations on user names, e.g. on some
|
||||
// wikis is uppercases the first letter.
|
||||
getExtendedInfo(loginResult.userName, loginResult, cb)
|
||||
} else if ("UI" == loginResult.status) {
|
||||
when (loginResult) {
|
||||
is OAuthResult ->
|
||||
cb.twoFactorPrompt(
|
||||
LoginFailedException(loginResult.message),
|
||||
loginToken,
|
||||
)
|
||||
|
||||
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
|
||||
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
|
||||
|
||||
is LoginResult.Result -> cb.error(
|
||||
LoginFailedException(loginResult.message)
|
||||
)
|
||||
is LoginResult.Result ->
|
||||
cb.error(
|
||||
LoginFailedException(loginResult.message),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
cb.error(LoginFailedException(loginResult.message))
|
||||
}
|
||||
} else {
|
||||
cb.error(LoginFailedException(loginResult.message))
|
||||
cb.error(IOException("Login failed. Unexpected response."))
|
||||
}
|
||||
} else {
|
||||
cb.error(IOException("Login failed. Unexpected response."))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<LoginResponse?>, t: Throwable) {
|
||||
if (call.isCanceled) {
|
||||
return
|
||||
override fun onFailure(
|
||||
call: Call<LoginResponse?>,
|
||||
t: Throwable,
|
||||
) {
|
||||
if (call.isCanceled) {
|
||||
return
|
||||
}
|
||||
cb.error(t)
|
||||
}
|
||||
cb.error(t)
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun doLogin(
|
||||
|
|
@ -111,43 +149,65 @@ class LoginClient(private val loginInterface: LoginInterface) {
|
|||
password: String,
|
||||
twoFactorCode: String,
|
||||
userLanguage: String,
|
||||
loginCallback: LoginCallback
|
||||
loginCallback: LoginCallback,
|
||||
) {
|
||||
getLoginToken().enqueue(object :Callback<MwQueryResponse?>{
|
||||
override fun onResponse(
|
||||
call: Call<MwQueryResponse?>,
|
||||
response: Response<MwQueryResponse?>
|
||||
) = if (response.isSuccessful){
|
||||
val loginToken = response.body()?.query()?.loginToken()
|
||||
loginToken?.let {
|
||||
login(username, password, null, twoFactorCode, it, userLanguage, loginCallback)
|
||||
} ?: run {
|
||||
getLoginToken().enqueue(
|
||||
object : Callback<MwQueryResponse?> {
|
||||
override fun onResponse(
|
||||
call: Call<MwQueryResponse?>,
|
||||
response: Response<MwQueryResponse?>,
|
||||
) = if (response.isSuccessful) {
|
||||
val loginToken = response.body()?.query()?.loginToken()
|
||||
loginToken?.let {
|
||||
login(username, password, null, twoFactorCode, it, userLanguage, loginCallback)
|
||||
} ?: run {
|
||||
loginCallback.error(IOException("Failed to retrieve login token"))
|
||||
}
|
||||
} else {
|
||||
loginCallback.error(IOException("Failed to retrieve login token"))
|
||||
}
|
||||
} else {
|
||||
loginCallback.error(IOException("Failed to retrieve login token"))
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<MwQueryResponse?>, t: Throwable) {
|
||||
loginCallback.error(t)
|
||||
}
|
||||
})
|
||||
override fun onFailure(
|
||||
call: Call<MwQueryResponse?>,
|
||||
t: Throwable,
|
||||
) {
|
||||
loginCallback.error(t)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
fun loginBlocking(userName: String, password: String, twoFactorCode: String?) {
|
||||
fun loginBlocking(
|
||||
userName: String,
|
||||
password: String,
|
||||
twoFactorCode: String?,
|
||||
) {
|
||||
val tokenResponse = getLoginToken().execute()
|
||||
if (tokenResponse.body()?.query()?.loginToken().isNullOrEmpty()) {
|
||||
if (tokenResponse
|
||||
.body()
|
||||
?.query()
|
||||
?.loginToken()
|
||||
.isNullOrEmpty()
|
||||
) {
|
||||
throw IOException("Unexpected response when getting login token.")
|
||||
}
|
||||
|
||||
val loginToken = tokenResponse.body()?.query()?.loginToken()
|
||||
val tempLoginCall = if (twoFactorCode.isNullOrEmpty()) {
|
||||
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
||||
} else {
|
||||
loginInterface.postLogIn(
|
||||
userName, password, null, twoFactorCode, loginToken, userLanguage, true
|
||||
)
|
||||
}
|
||||
val tempLoginCall =
|
||||
if (twoFactorCode.isNullOrEmpty()) {
|
||||
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
||||
} else {
|
||||
loginInterface.postLogIn(
|
||||
userName,
|
||||
password,
|
||||
null,
|
||||
twoFactorCode,
|
||||
loginToken,
|
||||
userLanguage,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
val response = tempLoginCall.execute()
|
||||
val loginResponse = response.body() ?: throw IOException("Unexpected response when logging in.")
|
||||
|
|
@ -166,18 +226,23 @@ class LoginClient(private val loginInterface: LoginInterface) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getExtendedInfo(userName: String, loginResult: LoginResult, cb: LoginCallback) =
|
||||
loginInterface.getUserInfo(userName)
|
||||
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ response: MwQueryResponse? ->
|
||||
loginResult.userId = response?.query()?.userInfo()?.id() ?: 0
|
||||
loginResult.groups =
|
||||
response?.query()?.getUserResponse(userName)?.groups ?: emptySet()
|
||||
cb.success(loginResult)
|
||||
}, { caught: Throwable ->
|
||||
Timber.e(caught, "Login succeeded but getting group information failed. ")
|
||||
cb.error(caught)
|
||||
})
|
||||
private fun getExtendedInfo(
|
||||
userName: String,
|
||||
loginResult: LoginResult,
|
||||
cb: LoginCallback,
|
||||
) = loginInterface
|
||||
.getUserInfo(userName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ response: MwQueryResponse? ->
|
||||
loginResult.userId = response?.query()?.userInfo()?.id() ?: 0
|
||||
loginResult.groups =
|
||||
response?.query()?.getUserResponse(userName)?.groups ?: emptySet()
|
||||
cb.success(loginResult)
|
||||
}, { caught: Throwable ->
|
||||
Timber.e(caught, "Login succeeded but getting group information failed. ")
|
||||
cb.error(caught)
|
||||
})
|
||||
|
||||
fun cancel() {
|
||||
tokenCall?.let {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
package fr.free.nrw.commons.auth.login
|
||||
|
||||
class LoginFailedException(message: String?) : Throwable(message)
|
||||
class LoginFailedException(
|
||||
message: String?,
|
||||
) : Throwable(message)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package fr.free.nrw.commons.auth.login
|
||||
|
||||
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||
import io.reactivex.Observable
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import io.reactivex.Observable
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
|
|
@ -24,7 +24,7 @@ interface LoginInterface {
|
|||
@Field("password") pass: String?,
|
||||
@Field("logintoken") token: String?,
|
||||
@Field("uselang") userLanguage: String?,
|
||||
@Field("loginreturnurl") url: String?
|
||||
@Field("loginreturnurl") url: String?,
|
||||
): Call<LoginResponse?>
|
||||
|
||||
@Headers("Cache-Control: no-cache")
|
||||
|
|
@ -37,9 +37,11 @@ interface LoginInterface {
|
|||
@Field("OATHToken") twoFactorCode: String?,
|
||||
@Field("logintoken") token: String?,
|
||||
@Field("uselang") userLanguage: String?,
|
||||
@Field("logincontinue") loginContinue: Boolean
|
||||
@Field("logincontinue") loginContinue: Boolean,
|
||||
): Call<LoginResponse?>
|
||||
|
||||
@GET(MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate")
|
||||
fun getUserInfo(@Query("ususers") userName: String): Observable<MwQueryResponse?>
|
||||
}
|
||||
fun getUserInfo(
|
||||
@Query("ususers") userName: String,
|
||||
): Observable<MwQueryResponse?>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ class LoginResponse {
|
|||
@SerializedName("clientlogin")
|
||||
private val clientLogin: ClientLogin? = null
|
||||
|
||||
fun toLoginResult(password: String): LoginResult? {
|
||||
return clientLogin?.toLoginResult(password)
|
||||
}
|
||||
fun toLoginResult(password: String): LoginResult? = clientLogin?.toLoginResult(password)
|
||||
}
|
||||
|
||||
internal class ClientLogin {
|
||||
|
|
@ -39,7 +37,7 @@ internal class ClientLogin {
|
|||
}
|
||||
}
|
||||
} else if ("PASS" != status && "FAIL" != status) {
|
||||
//TODO: String resource -- Looks like needed for others in this class too
|
||||
// TODO: String resource -- Looks like needed for others in this class too
|
||||
userMessage = "An unknown error occurred."
|
||||
}
|
||||
return Result(status ?: "", userName, password, userMessage)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ sealed class LoginResult(
|
|||
val status: String,
|
||||
val userName: String?,
|
||||
val password: String?,
|
||||
val message: String?
|
||||
val message: String?,
|
||||
) {
|
||||
var userId = 0
|
||||
var groups = emptySet<String>()
|
||||
|
|
@ -14,20 +14,20 @@ sealed class LoginResult(
|
|||
status: String,
|
||||
userName: String?,
|
||||
password: String?,
|
||||
message: String?
|
||||
): LoginResult(status, userName, password, message)
|
||||
message: String?,
|
||||
) : LoginResult(status, userName, password, message)
|
||||
|
||||
class OAuthResult(
|
||||
status: String,
|
||||
userName: String?,
|
||||
password: String?,
|
||||
message: String?
|
||||
message: String?,
|
||||
) : LoginResult(status, userName, password, message)
|
||||
|
||||
class ResetPasswordResult(
|
||||
status: String,
|
||||
userName: String?,
|
||||
password: String?,
|
||||
message: String?
|
||||
message: String?,
|
||||
) : LoginResult(status, userName, password, message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,25 +15,34 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
|||
/**
|
||||
* Helps to inflate Wikidata Items into Items tab
|
||||
*/
|
||||
class BookmarkItemsAdapter (val list: List<DepictedItem>, val context: Context) :
|
||||
RecyclerView.Adapter<BookmarkItemsAdapter.BookmarkItemViewHolder>() {
|
||||
|
||||
class BookmarkItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
class BookmarkItemsAdapter(
|
||||
val list: List<DepictedItem>,
|
||||
val context: Context,
|
||||
) : RecyclerView.Adapter<BookmarkItemsAdapter.BookmarkItemViewHolder>() {
|
||||
class BookmarkItemViewHolder(
|
||||
itemView: View,
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
var depictsLabel: TextView = itemView.findViewById(R.id.depicts_label)
|
||||
var description: TextView = itemView.findViewById(R.id.description)
|
||||
var depictsImage: SimpleDraweeView = itemView.findViewById(R.id.depicts_image)
|
||||
var layout : ConstraintLayout = itemView.findViewById(R.id.layout_item)
|
||||
var layout: ConstraintLayout = itemView.findViewById(R.id.layout_item)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkItemViewHolder {
|
||||
val v: View = LayoutInflater.from(context)
|
||||
.inflate(R.layout.item_depictions, parent, false)
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): BookmarkItemViewHolder {
|
||||
val v: View =
|
||||
LayoutInflater
|
||||
.from(context)
|
||||
.inflate(R.layout.item_depictions, parent, false)
|
||||
return BookmarkItemViewHolder(v)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BookmarkItemViewHolder, position: Int) {
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: BookmarkItemViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
val depictedItem = list[position]
|
||||
holder.depictsLabel.text = depictedItem.name
|
||||
holder.description.text = depictedItem.description
|
||||
|
|
@ -48,7 +57,5 @@ class BookmarkItemsAdapter (val list: List<DepictedItem>, val context: Context)
|
|||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return list.size
|
||||
}
|
||||
}
|
||||
override fun getItemCount(): Int = list.size
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,25 +2,25 @@ package fr.free.nrw.commons.bookmarks.models
|
|||
|
||||
import android.net.Uri
|
||||
|
||||
class Bookmark(mediaName: String?, mediaCreator: String?,
|
||||
/**
|
||||
* Modifies the content URI - marking this bookmark as already saved in the database
|
||||
* @param contentUri the content URI
|
||||
*/
|
||||
var contentUri: Uri?) {
|
||||
class Bookmark(
|
||||
mediaName: String?,
|
||||
mediaCreator: String?,
|
||||
/**
|
||||
* Gets the content URI for this bookmark
|
||||
* Gets or Sets the content URI - marking this bookmark as already saved in the database
|
||||
* @return content URI
|
||||
* @param contentUri the content URI
|
||||
*/
|
||||
var contentUri: Uri?,
|
||||
) {
|
||||
/**
|
||||
* Gets the media name
|
||||
* @return the media name
|
||||
*/
|
||||
val mediaName: String = mediaName ?: ""
|
||||
|
||||
/**
|
||||
* Gets media creator
|
||||
* @return creator name
|
||||
*/
|
||||
val mediaCreator: String = mediaCreator ?: ""
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.google.gson.annotations.SerializedName
|
|||
class CampaignConfig {
|
||||
@SerializedName("showOnlyLiveCampaigns")
|
||||
private val showOnlyLiveCampaigns = false
|
||||
|
||||
@SerializedName("sortBy")
|
||||
private val sortBy: String? = null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import fr.free.nrw.commons.campaigns.models.Campaign
|
|||
class CampaignResponseDTO {
|
||||
@SerializedName("config")
|
||||
val campaignConfig: CampaignConfig? = null
|
||||
|
||||
@SerializedName("campaigns")
|
||||
val campaigns: List<Campaign>? = null
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ package fr.free.nrw.commons.campaigns.models
|
|||
/**
|
||||
* A data class to hold a campaign
|
||||
*/
|
||||
data class Campaign(var title: String? = null,
|
||||
var description: String? = null,
|
||||
var startDate: String? = null,
|
||||
var endDate: String? = null,
|
||||
var link: String? = null,
|
||||
var isWLMCampaign: Boolean = false)
|
||||
data class Campaign(
|
||||
var title: String? = null,
|
||||
var description: String? = null,
|
||||
var startDate: String? = null,
|
||||
var endDate: String? = null,
|
||||
var link: String? = null,
|
||||
var isWLMCampaign: Boolean = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,275 +8,287 @@ import fr.free.nrw.commons.utils.StringSortingUtils
|
|||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.Function4
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* The model class for categories in upload
|
||||
*/
|
||||
class CategoriesModel @Inject constructor(
|
||||
private val categoryClient: CategoryClient,
|
||||
private val categoryDao: CategoryDao,
|
||||
private val gpsCategoryModel: GpsCategoryModel
|
||||
) {
|
||||
private val selectedCategories: MutableList<CategoryItem> = mutableListOf()
|
||||
class CategoriesModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val categoryClient: CategoryClient,
|
||||
private val categoryDao: CategoryDao,
|
||||
private val gpsCategoryModel: GpsCategoryModel,
|
||||
) {
|
||||
private val selectedCategories: MutableList<CategoryItem> = mutableListOf()
|
||||
|
||||
/**
|
||||
* Existing categories which are selected
|
||||
*/
|
||||
private var selectedExistingCategories: MutableList<String> = mutableListOf()
|
||||
/**
|
||||
* Existing categories which are selected
|
||||
*/
|
||||
private var selectedExistingCategories: MutableList<String> = mutableListOf()
|
||||
|
||||
/**
|
||||
* Returns true if an item is considered to be a spammy category which should be ignored
|
||||
*
|
||||
* @param item a category item that needs to be validated to know if it is spammy or not
|
||||
* @return
|
||||
*/
|
||||
fun isSpammyCategory(item: String): Boolean {
|
||||
//Check for current and previous year to exclude these categories from removal
|
||||
val now = Calendar.getInstance()
|
||||
val curYear = now[Calendar.YEAR]
|
||||
val curYearInString = curYear.toString()
|
||||
val prevYear = curYear - 1
|
||||
val prevYearInString = prevYear.toString()
|
||||
Timber.d("Previous year: %s", prevYearInString)
|
||||
/**
|
||||
* Returns true if an item is considered to be a spammy category which should be ignored
|
||||
*
|
||||
* @param item a category item that needs to be validated to know if it is spammy or not
|
||||
* @return
|
||||
*/
|
||||
fun isSpammyCategory(item: String): Boolean {
|
||||
// Check for current and previous year to exclude these categories from removal
|
||||
val now = Calendar.getInstance()
|
||||
val curYear = now[Calendar.YEAR]
|
||||
val curYearInString = curYear.toString()
|
||||
val prevYear = curYear - 1
|
||||
val prevYearInString = prevYear.toString()
|
||||
Timber.d("Previous year: %s", prevYearInString)
|
||||
|
||||
val mentionsDecade = item.matches(".*0s.*".toRegex())
|
||||
val recentDecade = item.matches(".*20[0-2]0s.*".toRegex())
|
||||
val spammyCategory = item.matches("(.*)needing(.*)".toRegex())
|
||||
|| item.matches("(.*)taken on(.*)".toRegex())
|
||||
val mentionsDecade = item.matches(".*0s.*".toRegex())
|
||||
val recentDecade = item.matches(".*20[0-2]0s.*".toRegex())
|
||||
val spammyCategory =
|
||||
item.matches("(.*)needing(.*)".toRegex()) ||
|
||||
item.matches("(.*)taken on(.*)".toRegex())
|
||||
|
||||
// always skip irrelevant categories such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
||||
if (spammyCategory) {
|
||||
return true
|
||||
// always skip irrelevant categories such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
||||
if (spammyCategory) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (mentionsDecade) {
|
||||
// Check if the year in the form of XX(X)0s is recent/relevant, i.e. in the 2000s or 2010s/2020s as stated in Issue #1029
|
||||
// Example: "2020s" is OK, but "1920s" is not (and should be skipped)
|
||||
return !recentDecade
|
||||
} else {
|
||||
// If it is not an year in decade form (e.g. 19xxs/20xxs), then check if item contains a 4-digit year
|
||||
// anywhere within the string (.* is wildcard) (Issue #47)
|
||||
// And that item does not equal the current year or previous year
|
||||
return item.matches(".*(19|20)\\d{2}.*".toRegex()) &&
|
||||
!item.contains(curYearInString) &&
|
||||
!item.contains(prevYearInString)
|
||||
}
|
||||
}
|
||||
|
||||
if (mentionsDecade) {
|
||||
// Check if the year in the form of XX(X)0s is recent/relevant, i.e. in the 2000s or 2010s/2020s as stated in Issue #1029
|
||||
// Example: "2020s" is OK, but "1920s" is not (and should be skipped)
|
||||
return !recentDecade
|
||||
} else {
|
||||
// If it is not an year in decade form (e.g. 19xxs/20xxs), then check if item contains a 4-digit year
|
||||
// anywhere within the string (.* is wildcard) (Issue #47)
|
||||
// And that item does not equal the current year or previous year
|
||||
return item.matches(".*(19|20)\\d{2}.*".toRegex())
|
||||
&& !item.contains(curYearInString)
|
||||
&& !item.contains(prevYearInString)
|
||||
/**
|
||||
* Updates category count in category dao
|
||||
* @param item
|
||||
*/
|
||||
fun updateCategoryCount(item: CategoryItem) {
|
||||
var category = categoryDao.find(item.name)
|
||||
|
||||
// Newly used category...
|
||||
if (category == null) {
|
||||
category = Category(null, item.name, item.description, item.thumbnail, Date(), 0)
|
||||
}
|
||||
category.incTimesUsed()
|
||||
categoryDao.save(category)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates category count in category dao
|
||||
* @param item
|
||||
*/
|
||||
fun updateCategoryCount(item: CategoryItem) {
|
||||
var category = categoryDao.find(item.name)
|
||||
/**
|
||||
* Regional category search
|
||||
* @param term
|
||||
* @param imageTitleList
|
||||
* @return
|
||||
*/
|
||||
fun searchAll(
|
||||
term: String,
|
||||
imageTitleList: List<String>,
|
||||
selectedDepictions: List<DepictedItem>,
|
||||
): Observable<List<CategoryItem>> =
|
||||
suggestionsOrSearch(term, imageTitleList, selectedDepictions)
|
||||
.map { it.map { CategoryItem(it.name, it.description, it.thumbnail, false) } }
|
||||
|
||||
// Newly used category...
|
||||
if (category == null) {
|
||||
category = Category(null, item.name, item.description, item.thumbnail, Date(), 0)
|
||||
}
|
||||
category.incTimesUsed()
|
||||
categoryDao.save(category)
|
||||
}
|
||||
private fun suggestionsOrSearch(
|
||||
term: String,
|
||||
imageTitleList: List<String>,
|
||||
selectedDepictions: List<DepictedItem>,
|
||||
): Observable<List<CategoryItem>> =
|
||||
if (TextUtils.isEmpty(term)) {
|
||||
Observable.combineLatest(
|
||||
categoriesFromDepiction(selectedDepictions),
|
||||
gpsCategoryModel.categoriesFromLocation,
|
||||
titleCategories(imageTitleList),
|
||||
Observable.just(categoryDao.recentCategories(SEARCH_CATS_LIMIT)),
|
||||
Function4(::combine),
|
||||
)
|
||||
} else {
|
||||
categoryClient
|
||||
.searchCategoriesForPrefix(term, SEARCH_CATS_LIMIT)
|
||||
.map { it.sortedWith(StringSortingUtils.sortBySimilarity(term)) }
|
||||
.toObservable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Regional category search
|
||||
* @param term
|
||||
* @param imageTitleList
|
||||
* @return
|
||||
*/
|
||||
fun searchAll(
|
||||
term: String,
|
||||
imageTitleList: List<String>,
|
||||
selectedDepictions: List<DepictedItem>
|
||||
): Observable<List<CategoryItem>> {
|
||||
return suggestionsOrSearch(term, imageTitleList, selectedDepictions)
|
||||
.map { it.map { CategoryItem(it.name, it.description, it.thumbnail, false) } }
|
||||
}
|
||||
|
||||
private fun suggestionsOrSearch(
|
||||
term: String,
|
||||
imageTitleList: List<String>,
|
||||
selectedDepictions: List<DepictedItem>
|
||||
): Observable<List<CategoryItem>> {
|
||||
return if (TextUtils.isEmpty(term))
|
||||
Observable.combineLatest(
|
||||
categoriesFromDepiction(selectedDepictions),
|
||||
gpsCategoryModel.categoriesFromLocation,
|
||||
titleCategories(imageTitleList),
|
||||
Observable.just(categoryDao.recentCategories(SEARCH_CATS_LIMIT)),
|
||||
Function4(::combine)
|
||||
)
|
||||
else
|
||||
categoryClient.searchCategoriesForPrefix(term, SEARCH_CATS_LIMIT)
|
||||
.map { it.sortedWith(StringSortingUtils.sortBySimilarity(term)) }
|
||||
/**
|
||||
* Fetches details of every category associated with selected depictions, converts them into
|
||||
* CategoryItem and returns them in a list.
|
||||
*
|
||||
* @param selectedDepictions selected DepictItems
|
||||
* @return List of CategoryItem associated with selected depictions
|
||||
*/
|
||||
private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>): Observable<MutableList<CategoryItem>>? =
|
||||
Observable
|
||||
.fromIterable(
|
||||
selectedDepictions.map { it.commonsCategories }.flatten(),
|
||||
).map { categoryItem ->
|
||||
categoryClient
|
||||
.getCategoriesByName(
|
||||
categoryItem.name,
|
||||
categoryItem.name,
|
||||
SEARCH_CATS_LIMIT,
|
||||
).map {
|
||||
CategoryItem(
|
||||
it[0].name,
|
||||
it[0].description,
|
||||
it[0].thumbnail,
|
||||
it[0].isSelected,
|
||||
)
|
||||
}.blockingGet()
|
||||
}.toList()
|
||||
.toObservable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches details of every category associated with selected depictions, converts them into
|
||||
* CategoryItem and returns them in a list.
|
||||
*
|
||||
* @param selectedDepictions selected DepictItems
|
||||
* @return List of CategoryItem associated with selected depictions
|
||||
*/
|
||||
private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>):
|
||||
Observable<MutableList<CategoryItem>>? {
|
||||
return Observable.fromIterable(
|
||||
selectedDepictions.map { it.commonsCategories }.flatten())
|
||||
.map { categoryItem ->
|
||||
categoryClient.getCategoriesByName(categoryItem.name,
|
||||
categoryItem.name, SEARCH_CATS_LIMIT).map {
|
||||
/**
|
||||
* Fetches details of every category by their name, converts them into
|
||||
* CategoryItem and returns them in a list.
|
||||
*
|
||||
* @param categoryNames selected Categories
|
||||
* @return List of CategoryItem
|
||||
*/
|
||||
fun getCategoriesByName(categoryNames: List<String>): Observable<MutableList<CategoryItem>>? =
|
||||
Observable
|
||||
.fromIterable(categoryNames)
|
||||
.map { categoryName ->
|
||||
buildCategories(categoryName)
|
||||
}.filter { categoryItem ->
|
||||
categoryItem.name != "Hidden"
|
||||
}.toList()
|
||||
.toObservable()
|
||||
|
||||
CategoryItem(it[0].name, it[0].description,
|
||||
it[0].thumbnail, it[0].isSelected)
|
||||
/**
|
||||
* Fetches the categories and converts them into CategoryItem
|
||||
*/
|
||||
fun buildCategories(categoryName: String): CategoryItem =
|
||||
categoryClient
|
||||
.getCategoriesByName(
|
||||
categoryName,
|
||||
categoryName,
|
||||
SEARCH_CATS_LIMIT,
|
||||
).map {
|
||||
if (it.isNotEmpty()) {
|
||||
CategoryItem(
|
||||
it[0].name,
|
||||
it[0].description,
|
||||
it[0].thumbnail,
|
||||
it[0].isSelected,
|
||||
)
|
||||
} else {
|
||||
CategoryItem(
|
||||
"Hidden",
|
||||
"Hidden",
|
||||
"hidden",
|
||||
false,
|
||||
)
|
||||
}
|
||||
}.blockingGet()
|
||||
|
||||
}.blockingGet()
|
||||
}.toList().toObservable()
|
||||
}
|
||||
private fun combine(
|
||||
depictionCategories: List<CategoryItem>,
|
||||
locationCategories: List<CategoryItem>,
|
||||
titles: List<CategoryItem>,
|
||||
recents: List<CategoryItem>,
|
||||
) = depictionCategories + locationCategories + titles + recents
|
||||
|
||||
/**
|
||||
* Fetches details of every category by their name, converts them into
|
||||
* CategoryItem and returns them in a list.
|
||||
*
|
||||
* @param categoryNames selected Categories
|
||||
* @return List of CategoryItem
|
||||
*/
|
||||
fun getCategoriesByName(categoryNames: List<String>):
|
||||
Observable<MutableList<CategoryItem>>? {
|
||||
return Observable.fromIterable(categoryNames)
|
||||
.map { categoryName ->
|
||||
buildCategories(categoryName)
|
||||
}
|
||||
.filter { categoryItem ->
|
||||
categoryItem.name != "Hidden"
|
||||
}
|
||||
.toList().toObservable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the categories and converts them into CategoryItem
|
||||
*/
|
||||
fun buildCategories(categoryName: String): CategoryItem {
|
||||
return categoryClient.getCategoriesByName(categoryName,
|
||||
categoryName, SEARCH_CATS_LIMIT).map {
|
||||
if(it.isNotEmpty()) {
|
||||
CategoryItem(
|
||||
it[0].name, it[0].description,
|
||||
it[0].thumbnail, it[0].isSelected
|
||||
)
|
||||
} else {
|
||||
CategoryItem(
|
||||
"Hidden", "Hidden",
|
||||
"hidden", false
|
||||
)
|
||||
}
|
||||
}.blockingGet()
|
||||
}
|
||||
|
||||
private fun combine(
|
||||
depictionCategories: List<CategoryItem>,
|
||||
locationCategories: List<CategoryItem>,
|
||||
titles: List<CategoryItem>,
|
||||
recents: List<CategoryItem>
|
||||
) = depictionCategories + locationCategories + titles + recents
|
||||
|
||||
|
||||
/**
|
||||
* Returns title based categories
|
||||
* @param titleList
|
||||
* @return
|
||||
*/
|
||||
private fun titleCategories(titleList: List<String>) =
|
||||
if (titleList.isNotEmpty())
|
||||
Observable.combineLatest(titleList.map { getTitleCategories(it) }) { searchResults ->
|
||||
searchResults.map { it as List<CategoryItem> }.flatten()
|
||||
}
|
||||
else
|
||||
Observable.just(emptyList())
|
||||
|
||||
/**
|
||||
* Return category for single title
|
||||
* @param title
|
||||
* @return
|
||||
*/
|
||||
private fun getTitleCategories(title: String): Observable<List<CategoryItem>> {
|
||||
return categoryClient.searchCategories(title, SEARCH_CATS_LIMIT).toObservable()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles category item selection
|
||||
* @param item
|
||||
*/
|
||||
fun onCategoryItemClicked(item: CategoryItem, media: Media?) {
|
||||
if (media == null) {
|
||||
if (item.isSelected) {
|
||||
selectedCategories.add(item)
|
||||
updateCategoryCount(item)
|
||||
} else {
|
||||
selectedCategories.remove(item)
|
||||
}
|
||||
} else {
|
||||
if (item.isSelected) {
|
||||
if (media.categories?.contains(item.name) == true) {
|
||||
selectedExistingCategories.add(item.name)
|
||||
} else {
|
||||
selectedCategories.add(item)
|
||||
updateCategoryCount(item)
|
||||
/**
|
||||
* Returns title based categories
|
||||
* @param titleList
|
||||
* @return
|
||||
*/
|
||||
private fun titleCategories(titleList: List<String>) =
|
||||
if (titleList.isNotEmpty()) {
|
||||
Observable.combineLatest(titleList.map { getTitleCategories(it) }) { searchResults ->
|
||||
searchResults.map { it as List<CategoryItem> }.flatten()
|
||||
}
|
||||
} else {
|
||||
if (media.categories?.contains(item.name) == true) {
|
||||
selectedExistingCategories.remove(item.name)
|
||||
if (!media.categories?.contains(item.name)!!) {
|
||||
val categoriesList: MutableList<String> = ArrayList()
|
||||
categoriesList.add(item.name)
|
||||
categoriesList.addAll(media.categories!!)
|
||||
media.categories = categoriesList
|
||||
}
|
||||
Observable.just(emptyList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Return category for single title
|
||||
* @param title
|
||||
* @return
|
||||
*/
|
||||
private fun getTitleCategories(title: String): Observable<List<CategoryItem>> =
|
||||
categoryClient.searchCategories(title, SEARCH_CATS_LIMIT).toObservable()
|
||||
|
||||
/**
|
||||
* Handles category item selection
|
||||
* @param item
|
||||
*/
|
||||
fun onCategoryItemClicked(
|
||||
item: CategoryItem,
|
||||
media: Media?,
|
||||
) {
|
||||
if (media == null) {
|
||||
if (item.isSelected) {
|
||||
selectedCategories.add(item)
|
||||
updateCategoryCount(item)
|
||||
} else {
|
||||
selectedCategories.remove(item)
|
||||
}
|
||||
} else {
|
||||
if (item.isSelected) {
|
||||
if (media.categories?.contains(item.name) == true) {
|
||||
selectedExistingCategories.add(item.name)
|
||||
} else {
|
||||
selectedCategories.add(item)
|
||||
updateCategoryCount(item)
|
||||
}
|
||||
} else {
|
||||
if (media.categories?.contains(item.name) == true) {
|
||||
selectedExistingCategories.remove(item.name)
|
||||
if (!media.categories?.contains(item.name)!!) {
|
||||
val categoriesList: MutableList<String> = ArrayList()
|
||||
categoriesList.add(item.name)
|
||||
categoriesList.addAll(media.categories!!)
|
||||
media.categories = categoriesList
|
||||
}
|
||||
} else {
|
||||
selectedCategories.remove(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Selected Categories
|
||||
* @return
|
||||
*/
|
||||
fun getSelectedCategories(): List<CategoryItem> {
|
||||
return selectedCategories
|
||||
}
|
||||
/**
|
||||
* Get Selected Categories
|
||||
* @return
|
||||
*/
|
||||
fun getSelectedCategories(): List<CategoryItem> = selectedCategories
|
||||
|
||||
/**
|
||||
* Cleanup the existing in memory cache's
|
||||
*/
|
||||
fun cleanUp() {
|
||||
selectedCategories.clear()
|
||||
selectedExistingCategories.clear()
|
||||
}
|
||||
/**
|
||||
* Cleanup the existing in memory cache's
|
||||
*/
|
||||
fun cleanUp() {
|
||||
selectedCategories.clear()
|
||||
selectedExistingCategories.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SEARCH_CATS_LIMIT = 25
|
||||
}
|
||||
companion object {
|
||||
const val SEARCH_CATS_LIMIT = 25
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides selected existing categories
|
||||
*
|
||||
* @return selected existing categories
|
||||
*/
|
||||
fun getSelectedExistingCategories(): List<String> {
|
||||
return selectedExistingCategories
|
||||
}
|
||||
/**
|
||||
* Provides selected existing categories
|
||||
*
|
||||
* @return selected existing categories
|
||||
*/
|
||||
fun getSelectedExistingCategories(): List<String> = selectedExistingCategories
|
||||
|
||||
/**
|
||||
* Initialize existing categories
|
||||
*
|
||||
* @param selectedExistingCategories existing categories
|
||||
*/
|
||||
fun setSelectedExistingCategories(selectedExistingCategories: MutableList<String>) {
|
||||
this.selectedExistingCategories = selectedExistingCategories
|
||||
/**
|
||||
* Initialize existing categories
|
||||
*
|
||||
* @param selectedExistingCategories existing categories
|
||||
*/
|
||||
fun setSelectedExistingCategories(selectedExistingCategories: MutableList<String>) {
|
||||
this.selectedExistingCategories = selectedExistingCategories
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package fr.free.nrw.commons.category
|
||||
|
||||
import io.reactivex.Single
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||
import io.reactivex.Single
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -15,109 +15,123 @@ const val CATEGORY_NEEDING_CATEGORIES = "needing categories"
|
|||
* Category Client to handle custom calls to Commons MediaWiki APIs
|
||||
*/
|
||||
@Singleton
|
||||
class CategoryClient @Inject constructor(private val categoryInterface: CategoryInterface) :
|
||||
ContinuationClient<MwQueryResponse, CategoryItem>() {
|
||||
class CategoryClient
|
||||
@Inject
|
||||
constructor(
|
||||
private val categoryInterface: CategoryInterface,
|
||||
) : ContinuationClient<MwQueryResponse, CategoryItem>() {
|
||||
/**
|
||||
* Searches for categories containing the specified string.
|
||||
*
|
||||
* @param filter The string to be searched
|
||||
* @param itemLimit How many results are returned
|
||||
* @param offset Starts returning items from the nth result. If offset is 9, the response starts with the 9th item of the search result
|
||||
* @return
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun searchCategories(
|
||||
filter: String?,
|
||||
itemLimit: Int,
|
||||
offset: Int = 0,
|
||||
): Single<List<CategoryItem>> = responseMapper(categoryInterface.searchCategories(filter, itemLimit, offset))
|
||||
|
||||
/**
|
||||
* Searches for categories containing the specified string.
|
||||
*
|
||||
* @param filter The string to be searched
|
||||
* @param itemLimit How many results are returned
|
||||
* @param offset Starts returning items from the nth result. If offset is 9, the response starts with the 9th item of the search result
|
||||
* @return
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun searchCategories(filter: String?, itemLimit: Int, offset: Int = 0):
|
||||
Single<List<CategoryItem>> {
|
||||
return responseMapper(categoryInterface.searchCategories(filter, itemLimit, offset))
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for categories starting with the specified string.
|
||||
*
|
||||
* @param prefix The prefix to be searched
|
||||
* @param itemLimit How many results are returned
|
||||
* @param offset Starts returning items from the nth result. If offset is 9, the response starts with the 9th item of the search result
|
||||
* @return
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun searchCategoriesForPrefix(prefix: String?, itemLimit: Int, offset: Int = 0):
|
||||
Single<List<CategoryItem>> {
|
||||
return responseMapper(
|
||||
categoryInterface.searchCategoriesForPrefix(prefix, itemLimit, offset)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches categories starting and ending with a specified name.
|
||||
*
|
||||
* @param startingCategoryName Name of the category to start
|
||||
* @param endingCategoryName Name of the category to end
|
||||
* @param itemLimit How many categories to return
|
||||
* @param offset offset
|
||||
* @return MwQueryResponse
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun getCategoriesByName(startingCategoryName: String?, endingCategoryName: String?,
|
||||
itemLimit: Int, offset: Int = 0): Single<List<CategoryItem>> {
|
||||
return responseMapper(
|
||||
categoryInterface.getCategoriesByName(startingCategoryName, endingCategoryName,
|
||||
itemLimit, offset)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The method takes categoryName as input and returns a List of Subcategories
|
||||
* It uses the generator query API to get the subcategories in a category, 500 at a time.
|
||||
*
|
||||
* @param categoryName Category name as defined on commons
|
||||
* @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
|
||||
*/
|
||||
fun getSubCategoryList(categoryName: String): Single<List<CategoryItem>> {
|
||||
return continuationRequest(SUB_CATEGORY_CONTINUATION_PREFIX, categoryName) {
|
||||
categoryInterface.getSubCategoryList(
|
||||
categoryName, it
|
||||
/**
|
||||
* Searches for categories starting with the specified string.
|
||||
*
|
||||
* @param prefix The prefix to be searched
|
||||
* @param itemLimit How many results are returned
|
||||
* @param offset Starts returning items from the nth result. If offset is 9, the response starts with the 9th item of the search result
|
||||
* @return
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun searchCategoriesForPrefix(
|
||||
prefix: String?,
|
||||
itemLimit: Int,
|
||||
offset: Int = 0,
|
||||
): Single<List<CategoryItem>> =
|
||||
responseMapper(
|
||||
categoryInterface.searchCategoriesForPrefix(prefix, itemLimit, offset),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The method takes categoryName as input and returns a List of parent categories
|
||||
* It uses the generator query API to get the parent categories of a category, 500 at a time.
|
||||
*
|
||||
* @param categoryName Category name as defined on commons
|
||||
* @return
|
||||
*/
|
||||
fun getParentCategoryList(categoryName: String): Single<List<CategoryItem>> {
|
||||
return continuationRequest(PARENT_CATEGORY_CONTINUATION_PREFIX, categoryName) {
|
||||
categoryInterface.getParentCategoryList(categoryName, it)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Fetches categories starting and ending with a specified name.
|
||||
*
|
||||
* @param startingCategoryName Name of the category to start
|
||||
* @param endingCategoryName Name of the category to end
|
||||
* @param itemLimit How many categories to return
|
||||
* @param offset offset
|
||||
* @return MwQueryResponse
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun getCategoriesByName(
|
||||
startingCategoryName: String?,
|
||||
endingCategoryName: String?,
|
||||
itemLimit: Int,
|
||||
offset: Int = 0,
|
||||
): Single<List<CategoryItem>> =
|
||||
responseMapper(
|
||||
categoryInterface.getCategoriesByName(
|
||||
startingCategoryName,
|
||||
endingCategoryName,
|
||||
itemLimit,
|
||||
offset,
|
||||
),
|
||||
)
|
||||
|
||||
fun resetSubCategoryContinuation(category: String) {
|
||||
resetContinuation(SUB_CATEGORY_CONTINUATION_PREFIX, category)
|
||||
}
|
||||
|
||||
fun resetParentCategoryContinuation(category: String) {
|
||||
resetContinuation(PARENT_CATEGORY_CONTINUATION_PREFIX, category)
|
||||
}
|
||||
|
||||
override fun responseMapper(
|
||||
networkResult: Single<MwQueryResponse>,
|
||||
key: String?
|
||||
): Single<List<CategoryItem>> {
|
||||
return networkResult
|
||||
.map {
|
||||
handleContinuationResponse(it.continuation(), key)
|
||||
it.query()?.pages() ?: emptyList()
|
||||
/**
|
||||
* The method takes categoryName as input and returns a List of Subcategories
|
||||
* It uses the generator query API to get the subcategories in a category, 500 at a time.
|
||||
*
|
||||
* @param categoryName Category name as defined on commons
|
||||
* @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
|
||||
*/
|
||||
fun getSubCategoryList(categoryName: String): Single<List<CategoryItem>> =
|
||||
continuationRequest(SUB_CATEGORY_CONTINUATION_PREFIX, categoryName) {
|
||||
categoryInterface.getSubCategoryList(
|
||||
categoryName,
|
||||
it,
|
||||
)
|
||||
}
|
||||
.map {
|
||||
it.filter {
|
||||
page -> page.categoryInfo() == null || !page.categoryInfo().isHidden
|
||||
|
||||
/**
|
||||
* The method takes categoryName as input and returns a List of parent categories
|
||||
* It uses the generator query API to get the parent categories of a category, 500 at a time.
|
||||
*
|
||||
* @param categoryName Category name as defined on commons
|
||||
* @return
|
||||
*/
|
||||
fun getParentCategoryList(categoryName: String): Single<List<CategoryItem>> =
|
||||
continuationRequest(PARENT_CATEGORY_CONTINUATION_PREFIX, categoryName) {
|
||||
categoryInterface.getParentCategoryList(categoryName, it)
|
||||
}
|
||||
|
||||
fun resetSubCategoryContinuation(category: String) {
|
||||
resetContinuation(SUB_CATEGORY_CONTINUATION_PREFIX, category)
|
||||
}
|
||||
|
||||
fun resetParentCategoryContinuation(category: String) {
|
||||
resetContinuation(PARENT_CATEGORY_CONTINUATION_PREFIX, category)
|
||||
}
|
||||
|
||||
override fun responseMapper(
|
||||
networkResult: Single<MwQueryResponse>,
|
||||
key: String?,
|
||||
): Single<List<CategoryItem>> =
|
||||
networkResult
|
||||
.map {
|
||||
handleContinuationResponse(it.continuation(), key)
|
||||
it.query()?.pages() ?: emptyList()
|
||||
}.map {
|
||||
CategoryItem(it.title().replace(CATEGORY_PREFIX, ""),
|
||||
it.description().toString(), it.thumbUrl().toString(), false)
|
||||
it
|
||||
.filter { page ->
|
||||
page.categoryInfo() == null || !page.categoryInfo().isHidden
|
||||
}.map {
|
||||
CategoryItem(
|
||||
it.title().replace(CATEGORY_PREFIX, ""),
|
||||
it.description().toString(),
|
||||
it.thumbUrl().toString(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,13 @@ interface CategoryInterface {
|
|||
* @param itemLimit How many results are returned
|
||||
* @return
|
||||
*/
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2&generator=search&prop=description|pageimages&piprop=thumbnail&pithumbsize=70&gsrnamespace=14")
|
||||
@GET(
|
||||
"w/api.php?action=query&format=json&formatversion=2&generator=search&prop=description|pageimages&piprop=thumbnail&pithumbsize=70&gsrnamespace=14",
|
||||
)
|
||||
fun searchCategories(
|
||||
@Query("gsrsearch") filter: String?,
|
||||
@Query("gsrlimit") itemLimit: Int,
|
||||
@Query("gsroffset") offset: Int
|
||||
@Query("gsroffset") offset: Int,
|
||||
): Single<MwQueryResponse>
|
||||
|
||||
/**
|
||||
|
|
@ -31,11 +33,13 @@ interface CategoryInterface {
|
|||
* @param itemLimit How many results are returned
|
||||
* @return
|
||||
*/
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70")
|
||||
@GET(
|
||||
"w/api.php?action=query&format=json&formatversion=2&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70",
|
||||
)
|
||||
fun searchCategoriesForPrefix(
|
||||
@Query("gacprefix") prefix: String?,
|
||||
@Query("gaclimit") itemLimit: Int,
|
||||
@Query("gacoffset") offset: Int
|
||||
@Query("gacoffset") offset: Int,
|
||||
): Single<MwQueryResponse>
|
||||
|
||||
/**
|
||||
|
|
@ -47,23 +51,25 @@ interface CategoryInterface {
|
|||
* @param offset offset
|
||||
* @return MwQueryResponse
|
||||
*/
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70")
|
||||
@GET(
|
||||
"w/api.php?action=query&format=json&formatversion=2&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70",
|
||||
)
|
||||
fun getCategoriesByName(
|
||||
@Query("gacfrom") startingCategory: String?,
|
||||
@Query("gacto") endingCategory: String?,
|
||||
@Query("gaclimit") itemLimit: Int,
|
||||
@Query("gacoffset") offset: Int
|
||||
@Query("gacoffset") offset: Int,
|
||||
): Single<MwQueryResponse>
|
||||
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50")
|
||||
fun getSubCategoryList(
|
||||
@Query("gcmtitle") categoryName: String,
|
||||
@QueryMap(encoded = true) continuation: Map<String, String>
|
||||
@QueryMap(encoded = true) continuation: Map<String, String>,
|
||||
): Single<MwQueryResponse>
|
||||
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categories&prop=info&gcllimit=50")
|
||||
fun getParentCategoryList(
|
||||
@Query("titles") categoryName: String?,
|
||||
@QueryMap(encoded = true) continuation: Map<String, String>
|
||||
@QueryMap(encoded = true) continuation: Map<String, String>,
|
||||
): Single<MwQueryResponse>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import android.os.Parcelable
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class CategoryItem(val name: String, val description: String?,
|
||||
val thumbnail: String?, var isSelected: Boolean) : Parcelable {
|
||||
|
||||
override fun toString(): String {
|
||||
return "CategoryItem: '$name'"
|
||||
}
|
||||
data class CategoryItem(
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val thumbnail: String?,
|
||||
var isSelected: Boolean,
|
||||
) : Parcelable {
|
||||
override fun toString(): String = "CategoryItem: '$name'"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
|
@ -22,7 +23,5 @@ data class CategoryItem(val name: String, val description: String?,
|
|||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return name.hashCode()
|
||||
}
|
||||
override fun hashCode(): Int = name.hashCode()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@ package fr.free.nrw.commons.category
|
|||
|
||||
import io.reactivex.Single
|
||||
|
||||
|
||||
abstract class ContinuationClient<Network, Domain> {
|
||||
private val continuationStore: MutableMap<String, Map<String, String>?> = mutableMapOf()
|
||||
private val continuationExists: MutableMap<String, Boolean> = mutableMapOf()
|
||||
|
||||
private fun hasMorePagesFor(key: String) = continuationExists[key] ?: true
|
||||
|
||||
fun continuationRequest(
|
||||
prefix: String,
|
||||
name: String,
|
||||
requestFunction: (Map<String, String>) -> Single<Network>
|
||||
requestFunction: (Map<String, String>) -> Single<Network>,
|
||||
): Single<List<Domain>> {
|
||||
val key = "$prefix$name"
|
||||
return if (hasMorePagesFor(key)) {
|
||||
|
|
@ -21,9 +21,15 @@ abstract class ContinuationClient<Network, Domain> {
|
|||
}
|
||||
}
|
||||
|
||||
abstract fun responseMapper(networkResult: Single<Network>, key: String?=null): Single<List<Domain>>
|
||||
abstract fun responseMapper(
|
||||
networkResult: Single<Network>,
|
||||
key: String? = null,
|
||||
): Single<List<Domain>>
|
||||
|
||||
fun handleContinuationResponse(continuation:Map<String,String>?, key:String?){
|
||||
fun handleContinuationResponse(
|
||||
continuation: Map<String, String>?,
|
||||
key: String?,
|
||||
) {
|
||||
if (key != null) {
|
||||
continuationExists[key] =
|
||||
continuation?.let { continuation ->
|
||||
|
|
@ -33,7 +39,10 @@ abstract class ContinuationClient<Network, Domain> {
|
|||
}
|
||||
}
|
||||
|
||||
protected fun resetContinuation(prefix: String, category: String) {
|
||||
protected fun resetContinuation(
|
||||
prefix: String,
|
||||
category: String,
|
||||
) {
|
||||
continuationExists.remove("$prefix$category")
|
||||
continuationStore.remove("$prefix$category")
|
||||
}
|
||||
|
|
@ -44,9 +53,11 @@ abstract class ContinuationClient<Network, Domain> {
|
|||
* @param prefix
|
||||
* @param userName the username
|
||||
*/
|
||||
protected fun resetUserContinuation(prefix: String, userName: String) {
|
||||
protected fun resetUserContinuation(
|
||||
prefix: String,
|
||||
userName: String,
|
||||
) {
|
||||
continuationExists.remove("$prefix$userName")
|
||||
continuationStore.remove("$prefix$userName")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,32 +7,29 @@ import fr.free.nrw.commons.upload.UploadResult
|
|||
data class ChunkInfo(
|
||||
val uploadResult: UploadResult?,
|
||||
val indexOfNextChunkToUpload: Int,
|
||||
val totalChunks: Int
|
||||
val totalChunks: Int,
|
||||
) : Parcelable {
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readParcelable(UploadResult::class.java.classLoader),
|
||||
parcel.readInt(),
|
||||
parcel.readInt()
|
||||
parcel.readInt(),
|
||||
) {
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
override fun writeToParcel(
|
||||
parcel: Parcel,
|
||||
flags: Int,
|
||||
) {
|
||||
parcel.writeParcelable(uploadResult, flags)
|
||||
parcel.writeInt(indexOfNextChunkToUpload)
|
||||
parcel.writeInt(totalChunks)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
override fun describeContents(): Int = 0
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<ChunkInfo> {
|
||||
override fun createFromParcel(parcel: Parcel): ChunkInfo {
|
||||
return ChunkInfo(parcel)
|
||||
}
|
||||
override fun createFromParcel(parcel: Parcel): ChunkInfo = ChunkInfo(parcel)
|
||||
|
||||
override fun newArray(size: Int): Array<ChunkInfo?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
override fun newArray(size: Int): Array<ChunkInfo?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import android.os.Parcelable
|
|||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.upload.UploadItem
|
||||
|
|
@ -31,8 +30,7 @@ data class Contribution constructor(
|
|||
var errorInfo: String? = null,
|
||||
/**
|
||||
* @return array list of entityids for the depictions
|
||||
*/
|
||||
/**
|
||||
*
|
||||
* Each depiction loaded in depictions activity is associated with a wikidata entity id, this Id
|
||||
* is in turn used to upload depictions to wikibase
|
||||
*/
|
||||
|
|
@ -44,26 +42,23 @@ data class Contribution constructor(
|
|||
var dateCreatedString: String? = null,
|
||||
var dateModified: Date? = null,
|
||||
var dateUploadStarted: Date? = null,
|
||||
var hasInvalidLocation : Int = 0,
|
||||
var hasInvalidLocation: Int = 0,
|
||||
var contentUri: Uri? = null,
|
||||
var countryCode : String? = null,
|
||||
var imageSHA1 : String? = null,
|
||||
var countryCode: String? = null,
|
||||
var imageSHA1: String? = null,
|
||||
/**
|
||||
* Number of times a contribution has been retried after a failure
|
||||
*/
|
||||
var retries: Int = 0
|
||||
var retries: Int = 0,
|
||||
) : Parcelable {
|
||||
|
||||
fun completeWith(media: Media): Contribution {
|
||||
return copy(pageId = media.pageId, media = media, state = STATE_COMPLETED)
|
||||
}
|
||||
fun completeWith(media: Media): Contribution = copy(pageId = media.pageId, media = media, state = STATE_COMPLETED)
|
||||
|
||||
constructor(
|
||||
item: UploadItem,
|
||||
sessionManager: SessionManager,
|
||||
depictedItems: List<DepictedItem>,
|
||||
categories: List<String>,
|
||||
imageSHA1: String
|
||||
imageSHA1: String,
|
||||
) : this(
|
||||
Media(
|
||||
formatCaptions(item.uploadMediaDetails),
|
||||
|
|
@ -71,7 +66,7 @@ data class Contribution constructor(
|
|||
item.fileName,
|
||||
formatDescriptions(item.uploadMediaDetails),
|
||||
sessionManager.userName,
|
||||
sessionManager.userName
|
||||
sessionManager.userName,
|
||||
),
|
||||
localUri = item.mediaUri,
|
||||
decimalCoords = item.gpsCoords.decimalCoords,
|
||||
|
|
@ -80,7 +75,7 @@ data class Contribution constructor(
|
|||
wikidataPlace = from(item.place),
|
||||
contentUri = item.contentUri,
|
||||
dateCreatedString = item.fileCreatedDateString,
|
||||
imageSHA1 = imageSHA1
|
||||
imageSHA1 = imageSHA1,
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
@ -91,9 +86,7 @@ data class Contribution constructor(
|
|||
this.hasInvalidLocation = if (hasInvalidLocation) 1 else 0
|
||||
}
|
||||
|
||||
fun hasInvalidLocation(): Boolean {
|
||||
return hasInvalidLocation == 1
|
||||
}
|
||||
fun hasInvalidLocation(): Boolean = hasInvalidLocation == 1
|
||||
|
||||
companion object {
|
||||
const val STATE_COMPLETED = -1
|
||||
|
|
@ -107,7 +100,8 @@ data class Contribution constructor(
|
|||
* @param uploadMediaDetails list of media Details
|
||||
*/
|
||||
fun formatCaptions(uploadMediaDetails: List<UploadMediaDetail>) =
|
||||
uploadMediaDetails.associate { it.languageCode!! to it.captionText }
|
||||
uploadMediaDetails
|
||||
.associate { it.languageCode!! to it.captionText }
|
||||
.filter { it.value.isNotBlank() }
|
||||
|
||||
/**
|
||||
|
|
@ -117,19 +111,15 @@ data class Contribution constructor(
|
|||
* @return a string with the pattern of {{en|1=descriptionText}}
|
||||
*/
|
||||
fun formatDescriptions(descriptions: List<UploadMediaDetail>) =
|
||||
descriptions.filter { it.descriptionText.isNotEmpty() }
|
||||
descriptions
|
||||
.filter { it.descriptionText.isNotEmpty() }
|
||||
.joinToString(separator = "") { "{{${it.languageCode}|1=${it.descriptionText}}}" }
|
||||
}
|
||||
|
||||
val fileKey : String? get() = chunkInfo?.uploadResult?.filekey
|
||||
val fileKey: String? get() = chunkInfo?.uploadResult?.filekey
|
||||
val localUriPath: File? get() = localUri?.path?.let { File(it) }
|
||||
|
||||
fun isCompleted(): Boolean {
|
||||
return chunkInfo != null && chunkInfo!!.totalChunks == chunkInfo!!.indexOfNextChunkToUpload
|
||||
}
|
||||
|
||||
fun dateUploadStartedInMillis(): Long {
|
||||
return dateUploadStarted!!.time
|
||||
}
|
||||
fun isCompleted(): Boolean = chunkInfo != null && chunkInfo!!.totalChunks == chunkInfo!!.indexOfNextChunkToUpload
|
||||
|
||||
fun dateUploadStartedInMillis(): Long = dateUploadStarted!!.time
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,88 +14,90 @@ import javax.inject.Named
|
|||
* Class that extends PagedList.BoundaryCallback for contributions list It defines the action that
|
||||
* is triggered for various boundary conditions in the list
|
||||
*/
|
||||
class ContributionBoundaryCallback @Inject constructor(
|
||||
private val repository: ContributionsRepository,
|
||||
private val sessionManager: SessionManager,
|
||||
private val mediaClient: MediaClient,
|
||||
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler
|
||||
) : BoundaryCallback<Contribution>() {
|
||||
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
|
||||
var userName: String? = null
|
||||
class ContributionBoundaryCallback
|
||||
@Inject
|
||||
constructor(
|
||||
private val repository: ContributionsRepository,
|
||||
private val sessionManager: SessionManager,
|
||||
private val mediaClient: MediaClient,
|
||||
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler,
|
||||
) : BoundaryCallback<Contribution>() {
|
||||
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
|
||||
var userName: String? = null
|
||||
|
||||
|
||||
/**
|
||||
* It is triggered when the list has no items User's Contributions are then fetched from the
|
||||
* network
|
||||
*/
|
||||
override fun onZeroItemsLoaded() {
|
||||
if (sessionManager.userName != null) {
|
||||
mediaClient.resetUserNameContinuation(sessionManager.userName!!)
|
||||
/**
|
||||
* It is triggered when the list has no items User's Contributions are then fetched from the
|
||||
* network
|
||||
*/
|
||||
override fun onZeroItemsLoaded() {
|
||||
if (sessionManager.userName != null) {
|
||||
mediaClient.resetUserNameContinuation(sessionManager.userName!!)
|
||||
}
|
||||
fetchContributions()
|
||||
}
|
||||
fetchContributions()
|
||||
}
|
||||
|
||||
/**
|
||||
* It is triggered when the user scrolls to the top of the list
|
||||
* */
|
||||
override fun onItemAtFrontLoaded(itemAtFront: Contribution) {
|
||||
/**
|
||||
* It is triggered when the user scrolls to the top of the list
|
||||
* */
|
||||
override fun onItemAtFrontLoaded(itemAtFront: Contribution) {
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* It is triggered when the user scrolls to the end of the list. User's Contributions are then
|
||||
* fetched from the network
|
||||
*/
|
||||
override fun onItemAtEndLoaded(itemAtEnd: Contribution) {
|
||||
fetchContributions()
|
||||
}
|
||||
|
||||
/**
|
||||
* It is triggered when the user scrolls to the end of the list. User's Contributions are then
|
||||
* fetched from the network
|
||||
*/
|
||||
override fun onItemAtEndLoaded(itemAtEnd: Contribution) {
|
||||
fetchContributions()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches contributions using the MediaWiki API
|
||||
*/
|
||||
private fun fetchContributions() {
|
||||
if (sessionManager.userName != null) {
|
||||
userName?.let { userName ->
|
||||
mediaClient.getMediaListForUser(userName)
|
||||
.map { mediaList ->
|
||||
mediaList.map { media ->
|
||||
Contribution(media = media, state = Contribution.STATE_COMPLETED)
|
||||
}
|
||||
}
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.subscribe(::saveContributionsToDB) { error: Throwable ->
|
||||
Timber.e(
|
||||
"Failed to fetch contributions: %s",
|
||||
error.message
|
||||
/**
|
||||
* Fetches contributions using the MediaWiki API
|
||||
*/
|
||||
private fun fetchContributions() {
|
||||
if (sessionManager.userName != null) {
|
||||
userName
|
||||
?.let { userName ->
|
||||
mediaClient
|
||||
.getMediaListForUser(userName)
|
||||
.map { mediaList ->
|
||||
mediaList.map { media ->
|
||||
Contribution(media = media, state = Contribution.STATE_COMPLETED)
|
||||
}
|
||||
}.subscribeOn(ioThreadScheduler)
|
||||
.subscribe(::saveContributionsToDB) { error: Throwable ->
|
||||
Timber.e(
|
||||
"Failed to fetch contributions: %s",
|
||||
error.message,
|
||||
)
|
||||
}
|
||||
}?.let {
|
||||
compositeDisposable.add(
|
||||
it,
|
||||
)
|
||||
}
|
||||
}?.let {
|
||||
compositeDisposable.add(
|
||||
it
|
||||
)
|
||||
} else {
|
||||
compositeDisposable.clear()
|
||||
}
|
||||
}else {
|
||||
compositeDisposable.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the contributions the the local DB
|
||||
*/
|
||||
private fun saveContributionsToDB(contributions: List<Contribution>) {
|
||||
compositeDisposable.add(
|
||||
repository
|
||||
.save(contributions)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.subscribe { longs: List<Long?>? ->
|
||||
repository["last_fetch_timestamp"] = System.currentTimeMillis()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up
|
||||
*/
|
||||
fun dispose() {
|
||||
compositeDisposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the contributions the the local DB
|
||||
*/
|
||||
private fun saveContributionsToDB(contributions: List<Contribution>) {
|
||||
compositeDisposable.add(
|
||||
repository.save(contributions)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.subscribe { longs: List<Long?>? ->
|
||||
repository["last_fetch_timestamp"] = System.currentTimeMillis()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up
|
||||
*/
|
||||
fun dispose() {
|
||||
compositeDisposable.dispose()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,62 +12,61 @@ import javax.inject.Named
|
|||
/**
|
||||
* Data-Source which acts as mediator for contributions-data from the API
|
||||
*/
|
||||
class ContributionsRemoteDataSource @Inject constructor(
|
||||
private val mediaClient: MediaClient,
|
||||
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler
|
||||
) : ItemKeyedDataSource<Int, Contribution>() {
|
||||
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
|
||||
var userName: String? = null
|
||||
class ContributionsRemoteDataSource
|
||||
@Inject
|
||||
constructor(
|
||||
private val mediaClient: MediaClient,
|
||||
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler,
|
||||
) : ItemKeyedDataSource<Int, Contribution>() {
|
||||
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
|
||||
var userName: String? = null
|
||||
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<Int>,
|
||||
callback: LoadInitialCallback<Contribution>
|
||||
) {
|
||||
fetchContributions(callback)
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<Int>,
|
||||
callback: LoadInitialCallback<Contribution>,
|
||||
) {
|
||||
fetchContributions(callback)
|
||||
}
|
||||
|
||||
override fun loadAfter(
|
||||
params: LoadParams<Int>,
|
||||
callback: LoadCallback<Contribution>,
|
||||
) {
|
||||
fetchContributions(callback)
|
||||
}
|
||||
|
||||
override fun loadBefore(
|
||||
params: LoadParams<Int>,
|
||||
callback: LoadCallback<Contribution>,
|
||||
) {
|
||||
}
|
||||
|
||||
override fun getKey(item: Contribution): Int = item.pageId.hashCode()
|
||||
|
||||
/**
|
||||
* Fetches contributions using the MediaWiki API
|
||||
*/
|
||||
private fun fetchContributions(callback: LoadCallback<Contribution>) {
|
||||
compositeDisposable.add(
|
||||
mediaClient
|
||||
.getMediaListForUser(userName!!)
|
||||
.map { mediaList ->
|
||||
mediaList.map {
|
||||
Contribution(media = it, state = Contribution.STATE_COMPLETED)
|
||||
}
|
||||
}.subscribeOn(ioThreadScheduler)
|
||||
.subscribe({
|
||||
callback.onResult(it)
|
||||
}) { error: Throwable ->
|
||||
Timber.e(
|
||||
"Failed to fetch contributions: %s",
|
||||
error.message,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
compositeDisposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadAfter(
|
||||
params: LoadParams<Int>,
|
||||
callback: LoadCallback<Contribution>
|
||||
) {
|
||||
fetchContributions(callback)
|
||||
}
|
||||
|
||||
override fun loadBefore(
|
||||
params: LoadParams<Int>,
|
||||
callback: LoadCallback<Contribution>
|
||||
) {
|
||||
}
|
||||
|
||||
override fun getKey(item: Contribution): Int {
|
||||
return item.pageId.hashCode()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetches contributions using the MediaWiki API
|
||||
*/
|
||||
private fun fetchContributions(callback: LoadCallback<Contribution>) {
|
||||
compositeDisposable.add(
|
||||
mediaClient.getMediaListForUser(userName!!)
|
||||
.map { mediaList ->
|
||||
mediaList.map {
|
||||
Contribution(media = it, state = Contribution.STATE_COMPLETED)
|
||||
}
|
||||
}
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.subscribe({
|
||||
callback.onResult(it)
|
||||
}) { error: Throwable ->
|
||||
Timber.e(
|
||||
"Failed to fetch contributions: %s",
|
||||
error.message
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
compositeDisposable.dispose()
|
||||
}
|
||||
}
|
||||
|
|
@ -13,26 +13,30 @@ import fr.free.nrw.commons.databinding.DialogAddToWikipediaInstructionsBinding
|
|||
* Dialog fragment for displaying instructions for editing wikipedia
|
||||
*/
|
||||
class WikipediaInstructionsDialogFragment : DialogFragment() {
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
) = DialogAddToWikipediaInstructionsBinding.inflate(inflater, container, false).apply {
|
||||
val contribution: Contribution? = arguments!!.getParcelable(ARG_CONTRIBUTION)
|
||||
tvWikicode.setText(contribution?.media?.wikiCode)
|
||||
instructionsCancel.setOnClickListener { dismiss() }
|
||||
instructionsConfirm.setOnClickListener {
|
||||
callback?.onConfirmClicked(contribution, checkboxCopyWikicode.isChecked)
|
||||
}
|
||||
}.root
|
||||
savedInstanceState: Bundle?,
|
||||
) = DialogAddToWikipediaInstructionsBinding
|
||||
.inflate(inflater, container, false)
|
||||
.apply {
|
||||
val contribution: Contribution? = arguments!!.getParcelable(ARG_CONTRIBUTION)
|
||||
tvWikicode.setText(contribution?.media?.wikiCode)
|
||||
instructionsCancel.setOnClickListener { dismiss() }
|
||||
instructionsConfirm.setOnClickListener {
|
||||
callback?.onConfirmClicked(contribution, checkboxCopyWikicode.isChecked)
|
||||
}
|
||||
}.root
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
override fun onViewCreated(
|
||||
view: View,
|
||||
savedInstanceState: Bundle?,
|
||||
) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
dialog!!.window?.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
|
||||
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -40,15 +44,19 @@ class WikipediaInstructionsDialogFragment : DialogFragment() {
|
|||
* Callback for handling confirm button clicked
|
||||
*/
|
||||
interface Callback {
|
||||
fun onConfirmClicked(contribution: Contribution?, copyWikicode: Boolean)
|
||||
fun onConfirmClicked(
|
||||
contribution: Contribution?,
|
||||
copyWikicode: Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ARG_CONTRIBUTION = "contribution"
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(contribution: Contribution) = WikipediaInstructionsDialogFragment().apply {
|
||||
arguments = bundleOf(ARG_CONTRIBUTION to contribution)
|
||||
}
|
||||
fun newInstance(contribution: Contribution) =
|
||||
WikipediaInstructionsDialogFragment().apply {
|
||||
arguments = bundleOf(ARG_CONTRIBUTION to contribution)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
package fr.free.nrw.commons.customselector.database
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
/**
|
||||
* Entity class for Not For Upload status.
|
||||
*/
|
||||
@Entity(tableName = "images_not_for_upload_table")
|
||||
data class NotForUploadStatus(
|
||||
|
||||
/**
|
||||
* Original image sha1.
|
||||
*/
|
||||
@PrimaryKey
|
||||
val imageSHA1 : String
|
||||
val imageSHA1: String,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
package fr.free.nrw.commons.customselector.database
|
||||
|
||||
import androidx.room.*
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
|
||||
/**
|
||||
* Dao class for Not For Upload
|
||||
*/
|
||||
@Dao
|
||||
abstract class NotForUploadStatusDao {
|
||||
|
||||
/**
|
||||
* Insert into Not For Upload status.
|
||||
*/
|
||||
@Insert( onConflict = OnConflictStrategy.REPLACE )
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun insert(notForUploadStatus: NotForUploadStatus)
|
||||
|
||||
/**
|
||||
|
|
@ -25,33 +27,27 @@ abstract class NotForUploadStatusDao {
|
|||
* Query Not For Upload status with image sha1.
|
||||
*/
|
||||
@Query("SELECT * FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ")
|
||||
abstract suspend fun getFromImageSHA1(imageSHA1 : String) : NotForUploadStatus?
|
||||
abstract suspend fun getFromImageSHA1(imageSHA1: String): NotForUploadStatus?
|
||||
|
||||
/**
|
||||
* Asynchronous image sha1 query.
|
||||
*/
|
||||
suspend fun getNotForUploadFromImageSHA1(imageSHA1: String):NotForUploadStatus? {
|
||||
return getFromImageSHA1(imageSHA1)
|
||||
}
|
||||
suspend fun getNotForUploadFromImageSHA1(imageSHA1: String): NotForUploadStatus? = getFromImageSHA1(imageSHA1)
|
||||
|
||||
/**
|
||||
* Deletion Not For Upload status with image sha1.
|
||||
*/
|
||||
@Query("DELETE FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ")
|
||||
abstract suspend fun deleteWithImageSHA1(imageSHA1 : String)
|
||||
abstract suspend fun deleteWithImageSHA1(imageSHA1: String)
|
||||
|
||||
/**
|
||||
* Asynchronous image sha1 deletion.
|
||||
*/
|
||||
suspend fun deleteNotForUploadWithImageSHA1(imageSHA1: String) {
|
||||
return deleteWithImageSHA1(imageSHA1)
|
||||
}
|
||||
suspend fun deleteNotForUploadWithImageSHA1(imageSHA1: String) = deleteWithImageSHA1(imageSHA1)
|
||||
|
||||
/**
|
||||
* Check whether the imageSHA1 is present in database
|
||||
*/
|
||||
@Query("SELECT COUNT() FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ")
|
||||
abstract suspend fun find(imageSHA1 : String): Int
|
||||
abstract suspend fun find(imageSHA1: String): Int
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -3,37 +3,32 @@ package fr.free.nrw.commons.customselector.database
|
|||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Entity class for Uploaded Status.
|
||||
*/
|
||||
@Entity(tableName = "uploaded_table", indices = [Index(value = ["modifiedImageSHA1"], unique = true)])
|
||||
data class UploadedStatus(
|
||||
|
||||
/**
|
||||
* Original image sha1.
|
||||
*/
|
||||
@PrimaryKey
|
||||
val imageSHA1 : String,
|
||||
|
||||
val imageSHA1: String,
|
||||
/**
|
||||
* Modified image sha1 (after exif changes).
|
||||
*/
|
||||
val modifiedImageSHA1 : String,
|
||||
|
||||
val modifiedImageSHA1: String,
|
||||
/**
|
||||
* imageSHA1 query result from API.
|
||||
*/
|
||||
var imageResult : Boolean,
|
||||
|
||||
var imageResult: Boolean,
|
||||
/**
|
||||
* modifiedImageSHA1 query result from API.
|
||||
*/
|
||||
var modifiedImageResult : Boolean,
|
||||
|
||||
var modifiedImageResult: Boolean,
|
||||
/**
|
||||
* lastUpdated for data validation.
|
||||
*/
|
||||
var lastUpdated : Date? = null
|
||||
var lastUpdated: Date? = null,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
package fr.free.nrw.commons.customselector.database
|
||||
|
||||
import androidx.room.*
|
||||
import java.util.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import java.util.Calendar
|
||||
|
||||
/**
|
||||
* UploadedStatusDao for Custom Selector.
|
||||
*/
|
||||
@Dao
|
||||
abstract class UploadedStatusDao {
|
||||
|
||||
/**
|
||||
* Insert into uploaded status.
|
||||
*/
|
||||
@Insert( onConflict = OnConflictStrategy.REPLACE )
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun insert(uploadedStatus: UploadedStatus)
|
||||
|
||||
/**
|
||||
|
|
@ -31,13 +35,13 @@ abstract class UploadedStatusDao {
|
|||
* Query uploaded status with image sha1.
|
||||
*/
|
||||
@Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ")
|
||||
abstract suspend fun getFromImageSHA1(imageSHA1 : String) : UploadedStatus?
|
||||
abstract suspend fun getFromImageSHA1(imageSHA1: String): UploadedStatus?
|
||||
|
||||
/**
|
||||
* Query uploaded status with modified image sha1.
|
||||
*/
|
||||
@Query("SELECT * FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) ")
|
||||
abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1 : String) : UploadedStatus?
|
||||
abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1: String): UploadedStatus?
|
||||
|
||||
/**
|
||||
* Asynchronous insert into uploaded status table.
|
||||
|
|
@ -51,20 +55,24 @@ abstract class UploadedStatusDao {
|
|||
* Check whether the imageSHA1 is present in database
|
||||
*/
|
||||
@Query("SELECT COUNT() FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) AND imageResult = (:imageResult) ")
|
||||
abstract suspend fun findByImageSHA1(imageSHA1 : String, imageResult: Boolean): Int
|
||||
abstract suspend fun findByImageSHA1(
|
||||
imageSHA1: String,
|
||||
imageResult: Boolean,
|
||||
): Int
|
||||
|
||||
/**
|
||||
* Check whether the modifiedImageSHA1 is present in database
|
||||
*/
|
||||
@Query("SELECT COUNT() FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) AND modifiedImageResult = (:modifiedImageResult) ")
|
||||
abstract suspend fun findByModifiedImageSHA1(modifiedImageSHA1 : String,
|
||||
modifiedImageResult: Boolean): Int
|
||||
@Query(
|
||||
"SELECT COUNT() FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) AND modifiedImageResult = (:modifiedImageResult) ",
|
||||
)
|
||||
abstract suspend fun findByModifiedImageSHA1(
|
||||
modifiedImageSHA1: String,
|
||||
modifiedImageResult: Boolean,
|
||||
): Int
|
||||
|
||||
/**
|
||||
* Asynchronous image sha1 query.
|
||||
*/
|
||||
suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus? {
|
||||
return getFromImageSHA1(imageSHA1)
|
||||
}
|
||||
|
||||
}
|
||||
suspend fun getUploadedFromImageSHA1(imageSHA1: String): UploadedStatus? = getFromImageSHA1(imageSHA1)
|
||||
}
|
||||
|
|
@ -4,12 +4,10 @@ package fr.free.nrw.commons.customselector.helper
|
|||
* Stores constants related to custom image selector
|
||||
*/
|
||||
object CustomSelectorConstants {
|
||||
|
||||
const val BUCKET_ID = "bucket_id"
|
||||
const val TOTAL_SELECTED_IMAGES = "total_selected_images"
|
||||
const val PRESENT_POSITION = "present_position"
|
||||
const val NEW_SELECTED_IMAGES = "new_selected_images"
|
||||
const val SHOULD_REFRESH = "should_refresh"
|
||||
const val FULL_SCREEN_MODE_FIRST_LUNCH = "full_screen_mode_first_launch"
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import fr.free.nrw.commons.customselector.model.Image
|
|||
* Image Helper object, includes all the static functions and variables required by custom selector.
|
||||
*/
|
||||
object ImageHelper {
|
||||
|
||||
/**
|
||||
* Custom selector preference key
|
||||
*/
|
||||
|
|
@ -39,7 +38,10 @@ object ImageHelper {
|
|||
/**
|
||||
* Filters the images based on the given bucketId (folder)
|
||||
*/
|
||||
fun filterImages(images: ArrayList<Image>, bukketId: Long?): ArrayList<Image> {
|
||||
fun filterImages(
|
||||
images: ArrayList<Image>,
|
||||
bukketId: Long?,
|
||||
): ArrayList<Image> {
|
||||
if (bukketId == null) return images
|
||||
|
||||
val filteredImages = arrayListOf<Image>()
|
||||
|
|
@ -54,30 +56,37 @@ object ImageHelper {
|
|||
/**
|
||||
* getIndex: Returns the index of image in given list.
|
||||
*/
|
||||
fun getIndex(list: ArrayList<Image>, image: Image): Int {
|
||||
return list.indexOf(image)
|
||||
}
|
||||
fun getIndex(
|
||||
list: ArrayList<Image>,
|
||||
image: Image,
|
||||
): Int = list.indexOf(image)
|
||||
|
||||
/**
|
||||
* getIndex: Returns the index of image in given list.
|
||||
*/
|
||||
fun getIndexFromId(list: ArrayList<Image>, imageId: Long): Int {
|
||||
for(i in list){
|
||||
if(i.id == imageId)
|
||||
fun getIndexFromId(
|
||||
list: ArrayList<Image>,
|
||||
imageId: Long,
|
||||
): Int {
|
||||
for (i in list) {
|
||||
if (i.id == imageId) {
|
||||
return list.indexOf(i)
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of indices from the master list.
|
||||
*/
|
||||
fun getIndexList(list: ArrayList<Image>, masterList: ArrayList<Image>): ArrayList<Int> {
|
||||
|
||||
// Can be optimised as masterList is sorted by time.
|
||||
fun getIndexList(
|
||||
list: ArrayList<Image>,
|
||||
masterList: ArrayList<Image>,
|
||||
): ArrayList<Int> {
|
||||
// Can be optimised as masterList is sorted by time.
|
||||
|
||||
val indexes = arrayListOf<Int>()
|
||||
for(image in list) {
|
||||
for (image in list) {
|
||||
val index = getIndex(masterList, image)
|
||||
if (index == -1) {
|
||||
continue
|
||||
|
|
@ -86,4 +95,4 @@ object ImageHelper {
|
|||
}
|
||||
return indexes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,23 +2,29 @@ package fr.free.nrw.commons.customselector.helper
|
|||
|
||||
import android.content.Context
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.*
|
||||
import android.view.Display
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Class for detecting swipe gestures
|
||||
*/
|
||||
open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
|
||||
|
||||
open class OnSwipeTouchListener(
|
||||
context: Context?,
|
||||
) : View.OnTouchListener {
|
||||
private val gestureDetector: GestureDetector
|
||||
|
||||
private val SWIPE_THRESHOLD_HEIGHT = (getScreenResolution(context!!)).second / 3
|
||||
private val SWIPE_THRESHOLD_WIDTH = (getScreenResolution(context!!)).first / 3
|
||||
private val SWIPE_VELOCITY_THRESHOLD = 1000
|
||||
private val swipeThresholdHeight = (getScreenResolution(context!!)).second / 3
|
||||
private val swipeThresholdWidth = (getScreenResolution(context!!)).first / 3
|
||||
private val swipeVelocityThreshold = 1000
|
||||
|
||||
override fun onTouch(view: View?, motionEvent: MotionEvent): Boolean {
|
||||
return gestureDetector.onTouchEvent(motionEvent)
|
||||
}
|
||||
override fun onTouch(
|
||||
view: View?,
|
||||
motionEvent: MotionEvent,
|
||||
): Boolean = gestureDetector.onTouchEvent(motionEvent)
|
||||
|
||||
fun getScreenResolution(context: Context): Pair<Int, Int> {
|
||||
val wm: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
|
|
@ -31,10 +37,7 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
|
|||
}
|
||||
|
||||
inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
return true
|
||||
}
|
||||
override fun onDown(e: MotionEvent): Boolean = true
|
||||
|
||||
/**
|
||||
* Detects the gestures
|
||||
|
|
@ -43,14 +46,16 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
|
|||
event1: MotionEvent?,
|
||||
event2: MotionEvent,
|
||||
velocityX: Float,
|
||||
velocityY: Float
|
||||
velocityY: Float,
|
||||
): Boolean {
|
||||
try {
|
||||
val diffY: Float = event2.y - (event1?.y ?: event2.y)
|
||||
val diffX: Float = event2.x - (event1?.x ?: event2.x)
|
||||
if (abs(diffX) > abs(diffY)) {
|
||||
if (abs(diffX) > SWIPE_THRESHOLD_WIDTH && abs(velocityX) >
|
||||
SWIPE_VELOCITY_THRESHOLD) {
|
||||
if (abs(diffX) > swipeThresholdWidth &&
|
||||
abs(velocityX) >
|
||||
swipeVelocityThreshold
|
||||
) {
|
||||
if (diffX > 0) {
|
||||
onSwipeRight()
|
||||
} else {
|
||||
|
|
@ -58,8 +63,10 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
if (abs(diffY) > SWIPE_THRESHOLD_HEIGHT && abs(velocityY) >
|
||||
SWIPE_VELOCITY_THRESHOLD) {
|
||||
if (abs(diffY) > swipeThresholdHeight &&
|
||||
abs(velocityY) >
|
||||
swipeVelocityThreshold
|
||||
) {
|
||||
if (diffY > 0) {
|
||||
onSwipeDown()
|
||||
} else {
|
||||
|
|
@ -100,4 +107,4 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
|
|||
init {
|
||||
gestureDetector = GestureDetector(context, GestureListener())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@ package fr.free.nrw.commons.customselector.listeners
|
|||
* Custom Selector Folder Click Listener
|
||||
*/
|
||||
interface FolderClickListener {
|
||||
|
||||
/**
|
||||
* onFolderClick
|
||||
* @param folderId : folder id of the folder.
|
||||
* @param folderName : folder name of the folder.
|
||||
* @param lastItemId : last scroll position in the folder.
|
||||
*/
|
||||
fun onFolderClick(folderId: Long, folderName: String, lastItemId: Long)
|
||||
}
|
||||
fun onFolderClick(
|
||||
folderId: Long,
|
||||
folderName: String,
|
||||
lastItemId: Long,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import fr.free.nrw.commons.customselector.model.Image
|
|||
* responds to the device image query.
|
||||
*/
|
||||
interface ImageLoaderListener {
|
||||
|
||||
/**
|
||||
* On image loaded
|
||||
* @param images : queried device images.
|
||||
|
|
@ -19,4 +18,4 @@ interface ImageLoaderListener {
|
|||
* @param throwable : throwable exception on failure.
|
||||
*/
|
||||
fun onFailed(throwable: Throwable)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
package fr.free.nrw.commons.customselector.listeners
|
||||
|
||||
import android.net.Uri
|
||||
import fr.free.nrw.commons.customselector.model.Image
|
||||
|
||||
/**
|
||||
* Custom selector Image select listener
|
||||
*/
|
||||
interface ImageSelectListener {
|
||||
|
||||
/**
|
||||
* onSelectedImagesChanged
|
||||
* @param selectedImages : new selected images.
|
||||
* @param selectedNotForUploadImages : number of selected not for upload images
|
||||
*/
|
||||
fun onSelectedImagesChanged(selectedImages: ArrayList<Image>, selectedNotForUploadImages: Int)
|
||||
fun onSelectedImagesChanged(
|
||||
selectedImages: ArrayList<Image>,
|
||||
selectedNotForUploadImages: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* onLongPress
|
||||
|
|
@ -22,6 +23,6 @@ interface ImageSelectListener {
|
|||
fun onLongPress(
|
||||
position: Int,
|
||||
images: ArrayList<Image>,
|
||||
selectedImages: ArrayList<Image>
|
||||
selectedImages: ArrayList<Image>,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,5 +6,8 @@ import fr.free.nrw.commons.customselector.model.Image
|
|||
* Interface to pass data between fragment and activity
|
||||
*/
|
||||
interface PassDataListener {
|
||||
fun passSelectedImages(selectedImages: ArrayList<Image>, shouldRefresh: Boolean)
|
||||
}
|
||||
fun passSelectedImages(
|
||||
selectedImages: ArrayList<Image>,
|
||||
shouldRefresh: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@ interface RefreshUIListener {
|
|||
* Refreshes the data in adapter
|
||||
*/
|
||||
fun refresh()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,17 +6,17 @@ package fr.free.nrw.commons.customselector.model
|
|||
*/
|
||||
sealed class CallbackStatus {
|
||||
/**
|
||||
IDLE : The callback is idle , doing nothing.
|
||||
IDLE : The callback is idle , doing nothing.
|
||||
*/
|
||||
object IDLE : CallbackStatus()
|
||||
|
||||
/**
|
||||
FETCHING : Fetching images.
|
||||
FETCHING : Fetching images.
|
||||
*/
|
||||
object FETCHING : CallbackStatus()
|
||||
|
||||
/**
|
||||
SUCCESS : Success fetching images.
|
||||
SUCCESS : Success fetching images.
|
||||
*/
|
||||
object SUCCESS : CallbackStatus()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,27 +5,22 @@ package fr.free.nrw.commons.customselector.model
|
|||
*/
|
||||
data class Folder(
|
||||
/**
|
||||
bucketId : Unique directory id, eg 540528482
|
||||
bucketId : Unique directory id, eg 540528482
|
||||
*/
|
||||
var bucketId: Long,
|
||||
|
||||
/**
|
||||
name : bucket/folder name, eg Camera
|
||||
name : bucket/folder name, eg Camera
|
||||
*/
|
||||
var name: String,
|
||||
|
||||
/**
|
||||
images : folder images, list of all images under this folder.
|
||||
images : folder images, list of all images under this folder.
|
||||
*/
|
||||
var images: ArrayList<Image> = arrayListOf<Image>()
|
||||
|
||||
|
||||
var images: ArrayList<Image> = arrayListOf<Image>(),
|
||||
) {
|
||||
/**
|
||||
* Indicates whether some other object is "equal to" this one.
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
if (javaClass != other?.javaClass) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -44,4 +39,4 @@ data class Folder(
|
|||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,65 +9,60 @@ import android.os.Parcelable
|
|||
*/
|
||||
data class Image(
|
||||
/**
|
||||
id : Unique image id, primary key of image in device, eg 104950
|
||||
id : Unique image id, primary key of image in device, eg 104950
|
||||
*/
|
||||
var id: Long,
|
||||
|
||||
/**
|
||||
name : Name of the image with extension, eg CommonsLogo.jpeg
|
||||
name : Name of the image with extension, eg CommonsLogo.jpeg
|
||||
*/
|
||||
var name: String,
|
||||
|
||||
/**
|
||||
uri : Uri of the image, points to image location or name, eg content://media/external/images/camera/10495 (Android 10)
|
||||
uri : Uri of the image, points to image location or name, eg content://media/external/images/camera/10495 (Android 10)
|
||||
*/
|
||||
var uri: Uri,
|
||||
|
||||
/**
|
||||
path : System path of the image, eg storage/emulated/0/camera/CommonsLogo.jpeg
|
||||
path : System path of the image, eg storage/emulated/0/camera/CommonsLogo.jpeg
|
||||
*/
|
||||
var path: String,
|
||||
|
||||
/**
|
||||
bucketId : bucketId of folder, eg 540528482
|
||||
bucketId : bucketId of folder, eg 540528482
|
||||
*/
|
||||
var bucketId: Long = 0,
|
||||
|
||||
/**
|
||||
bucketName : name of folder, eg Camera
|
||||
bucketName : name of folder, eg Camera
|
||||
*/
|
||||
var bucketName: String = "",
|
||||
|
||||
/**
|
||||
sha1 : sha1 of original image.
|
||||
sha1 : sha1 of original image.
|
||||
*/
|
||||
var sha1: String = "",
|
||||
|
||||
/**
|
||||
* date: Creation date of the image to show it inside the bubble during bubble scroll.
|
||||
*/
|
||||
var date: String = ""
|
||||
|
||||
var date: String = "",
|
||||
) : Parcelable {
|
||||
/**
|
||||
default parcelable constructor.
|
||||
*/
|
||||
constructor(parcel: Parcel) :
|
||||
this(
|
||||
parcel.readLong(),
|
||||
parcel.readString()!!,
|
||||
parcel.readParcelable(Uri::class.java.classLoader)!!,
|
||||
parcel.readString()!!,
|
||||
parcel.readLong(),
|
||||
parcel.readString()!!,
|
||||
parcel.readString()!!,
|
||||
parcel.readString()!!,
|
||||
)
|
||||
|
||||
/**
|
||||
default parcelable constructor.
|
||||
Write to parcel method.
|
||||
*/
|
||||
constructor(parcel: Parcel):
|
||||
this(parcel.readLong(),
|
||||
parcel.readString()!!,
|
||||
parcel.readParcelable(Uri::class.java.classLoader)!!,
|
||||
parcel.readString()!!,
|
||||
parcel.readLong(),
|
||||
parcel.readString()!!,
|
||||
parcel.readString()!!,
|
||||
parcel.readString()!!
|
||||
)
|
||||
|
||||
/**
|
||||
Write to parcel method.
|
||||
*/
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
override fun writeToParcel(
|
||||
parcel: Parcel,
|
||||
flags: Int,
|
||||
) {
|
||||
parcel.writeLong(id)
|
||||
parcel.writeString(name)
|
||||
parcel.writeParcelable(uri, flags)
|
||||
|
|
@ -81,41 +76,38 @@ data class Image(
|
|||
/**
|
||||
* Describe the kinds of special objects contained in this Parcelable
|
||||
*/
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
override fun describeContents(): Int = 0
|
||||
|
||||
/**
|
||||
* Indicates whether some other object is "equal to" this one.
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
if(javaClass != other?.javaClass) {
|
||||
if (javaClass != other?.javaClass) {
|
||||
return false
|
||||
}
|
||||
|
||||
other as Image
|
||||
|
||||
if(id != other.id) {
|
||||
return false;
|
||||
if (id != other.id) {
|
||||
return false
|
||||
}
|
||||
if(name != other.name) {
|
||||
return false;
|
||||
if (name != other.name) {
|
||||
return false
|
||||
}
|
||||
if(uri != other.uri) {
|
||||
return false;
|
||||
if (uri != other.uri) {
|
||||
return false
|
||||
}
|
||||
if(path != other.path) {
|
||||
return false;
|
||||
if (path != other.path) {
|
||||
return false
|
||||
}
|
||||
if(bucketId != other.bucketId) {
|
||||
return false;
|
||||
if (bucketId != other.bucketId) {
|
||||
return false
|
||||
}
|
||||
if(bucketName != other.bucketName) {
|
||||
return false;
|
||||
if (bucketName != other.bucketName) {
|
||||
return false
|
||||
}
|
||||
if(sha1 != other.sha1) {
|
||||
return false;
|
||||
if (sha1 != other.sha1) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
|
@ -125,12 +117,8 @@ data class Image(
|
|||
* Parcelable companion object
|
||||
*/
|
||||
companion object CREATOR : Parcelable.Creator<Image> {
|
||||
override fun createFromParcel(parcel: Parcel): Image {
|
||||
return Image(parcel)
|
||||
}
|
||||
override fun createFromParcel(parcel: Parcel): Image = Image(parcel)
|
||||
|
||||
override fun newArray(size: Int): Array<Image?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
override fun newArray(size: Int): Array<Image?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@ data class Result(
|
|||
/**
|
||||
* CallbackStatus : stores the result status
|
||||
*/
|
||||
val status:CallbackStatus,
|
||||
|
||||
val status: CallbackStatus,
|
||||
/**
|
||||
* Images : images retrieved
|
||||
*/
|
||||
val images: ArrayList<Image>) {
|
||||
}
|
||||
val images: ArrayList<Image>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,14 +21,11 @@ class FolderAdapter(
|
|||
* Application context.
|
||||
*/
|
||||
context: Context,
|
||||
|
||||
/**
|
||||
* Folder Click listener for click events.
|
||||
*/
|
||||
private val itemClickListener: FolderClickListener
|
||||
|
||||
private val itemClickListener: FolderClickListener,
|
||||
) : RecyclerViewAdapter<FolderAdapter.FolderViewHolder?>(context) {
|
||||
|
||||
/**
|
||||
* List of folders.
|
||||
*/
|
||||
|
|
@ -37,7 +34,10 @@ class FolderAdapter(
|
|||
/**
|
||||
* Create view holder, returns View holder item.
|
||||
*/
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): FolderViewHolder {
|
||||
val itemView = inflater.inflate(R.layout.item_custom_selector_folder, parent, false)
|
||||
return FolderViewHolder(itemView)
|
||||
}
|
||||
|
|
@ -45,28 +45,31 @@ class FolderAdapter(
|
|||
/**
|
||||
* Bind view holder, setup the item view, title, count and click listener
|
||||
*/
|
||||
override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
|
||||
override fun onBindViewHolder(
|
||||
holder: FolderViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
val folder = folders[position]
|
||||
val toBeRemoved = ArrayList<Image>()
|
||||
|
||||
for(image in folder.images) {
|
||||
for (image in folder.images) {
|
||||
// Remove all the top images that do not exist anymore
|
||||
if(context.contentResolver.getType(image.uri) == null){
|
||||
if (context.contentResolver.getType(image.uri) == null) {
|
||||
// File not found
|
||||
toBeRemoved.add(image)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
holder.image.setImageDrawable (null)
|
||||
holder.image.setImageDrawable(null)
|
||||
folder.images.removeAll(toBeRemoved)
|
||||
val count = folder.images.size
|
||||
|
||||
if(count == 0 && folders.size > 0) {
|
||||
if (count == 0 && folders.size > 0) {
|
||||
// Folder is empty, remove folder from the adapter.
|
||||
holder.itemView.post{
|
||||
holder.itemView.post {
|
||||
val updatePosition = folders.indexOf(folder)
|
||||
if(updatePosition != -1) {
|
||||
if (updatePosition != -1) {
|
||||
folders.removeAt(updatePosition)
|
||||
notifyItemRemoved(updatePosition)
|
||||
notifyItemRangeChanged(updatePosition, folders.size)
|
||||
|
|
@ -89,9 +92,10 @@ class FolderAdapter(
|
|||
fun init(newFolders: List<Folder>) {
|
||||
val oldFolderList: MutableList<Folder> = folders
|
||||
val newFolderList = newFolders.toMutableList()
|
||||
val diffResult = DiffUtil.calculateDiff(
|
||||
FoldersDiffCallback(oldFolderList, newFolderList)
|
||||
)
|
||||
val diffResult =
|
||||
DiffUtil.calculateDiff(
|
||||
FoldersDiffCallback(oldFolderList, newFolderList),
|
||||
)
|
||||
folders = newFolderList
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
|
@ -99,15 +103,14 @@ class FolderAdapter(
|
|||
/**
|
||||
* returns item count.
|
||||
*/
|
||||
override fun getItemCount(): Int {
|
||||
return folders.size
|
||||
}
|
||||
override fun getItemCount(): Int = folders.size
|
||||
|
||||
/**
|
||||
* Folder view holder.
|
||||
*/
|
||||
class FolderViewHolder(itemView:View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
class FolderViewHolder(
|
||||
itemView: View,
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
/**
|
||||
* Folder thumbnail image view.
|
||||
*/
|
||||
|
|
@ -129,37 +132,33 @@ class FolderAdapter(
|
|||
*/
|
||||
class FoldersDiffCallback(
|
||||
var oldFolders: MutableList<Folder>,
|
||||
var newFolders: MutableList<Folder>
|
||||
var newFolders: MutableList<Folder>,
|
||||
) : DiffUtil.Callback() {
|
||||
/**
|
||||
* Returns the size of the old list.
|
||||
*/
|
||||
override fun getOldListSize(): Int {
|
||||
return oldFolders.size
|
||||
}
|
||||
override fun getOldListSize(): Int = oldFolders.size
|
||||
|
||||
/**
|
||||
* Returns the size of the new list.
|
||||
*/
|
||||
override fun getNewListSize(): Int {
|
||||
return newFolders.size
|
||||
}
|
||||
override fun getNewListSize(): Int = newFolders.size
|
||||
|
||||
/**
|
||||
* Called by the DiffUtil to decide whether two object represent the same Item.
|
||||
*/
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldFolders.get(oldItemPosition).bucketId == newFolders.get(newItemPosition).bucketId
|
||||
}
|
||||
override fun areItemsTheSame(
|
||||
oldItemPosition: Int,
|
||||
newItemPosition: Int,
|
||||
): Boolean = oldFolders.get(oldItemPosition).bucketId == newFolders.get(newItemPosition).bucketId
|
||||
|
||||
/**
|
||||
* Called by the DiffUtil when it wants to check whether two items have the same data.
|
||||
* DiffUtil uses this information to detect if the contents of an item has changed.
|
||||
*/
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldFolders.get(oldItemPosition).equals(newFolders.get(newItemPosition))
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItemPosition: Int,
|
||||
newItemPosition: Int,
|
||||
): Boolean = oldFolders.get(oldItemPosition).equals(newFolders.get(newItemPosition))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import android.content.SharedPreferences
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.constraintlayout.widget.Group
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
|
|
@ -20,8 +19,13 @@ import fr.free.nrw.commons.customselector.helper.ImageHelper.SHOW_ALREADY_ACTION
|
|||
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
||||
import fr.free.nrw.commons.customselector.model.Image
|
||||
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.TreeMap
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
/**
|
||||
|
|
@ -32,20 +36,16 @@ class ImageAdapter(
|
|||
* Application Context.
|
||||
*/
|
||||
context: Context,
|
||||
|
||||
/**
|
||||
* Image select listener for click events on image.
|
||||
*/
|
||||
private var imageSelectListener: ImageSelectListener,
|
||||
|
||||
/**
|
||||
* ImageLoader queries images.
|
||||
*/
|
||||
private var imageLoader: ImageLoader
|
||||
):
|
||||
|
||||
RecyclerViewAdapter<ImageAdapter.ImageViewHolder>(context), FastScrollRecyclerView.SectionedAdapter {
|
||||
|
||||
private var imageLoader: ImageLoader,
|
||||
) : RecyclerViewAdapter<ImageAdapter.ImageViewHolder>(context),
|
||||
FastScrollRecyclerView.SectionedAdapter {
|
||||
/**
|
||||
* ImageSelectedOrUpdated payload class.
|
||||
*/
|
||||
|
|
@ -106,14 +106,17 @@ class ImageAdapter(
|
|||
/**
|
||||
* Coroutine Dispatchers and Scope.
|
||||
*/
|
||||
private var defaultDispatcher : CoroutineDispatcher = Dispatchers.Default
|
||||
private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO
|
||||
private val scope : CoroutineScope = MainScope()
|
||||
private var defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||
private var ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
private val scope: CoroutineScope = MainScope()
|
||||
|
||||
/**
|
||||
* Create View holder.
|
||||
*/
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): ImageViewHolder {
|
||||
val itemView = inflater.inflate(R.layout.item_custom_selector_image, parent, false)
|
||||
return ImageViewHolder(itemView)
|
||||
}
|
||||
|
|
@ -121,10 +124,15 @@ class ImageAdapter(
|
|||
/**
|
||||
* Bind View holder, load image, selected view, click listeners.
|
||||
*/
|
||||
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
||||
if(images.size == 0) { return }
|
||||
var image=images[position]
|
||||
holder.image.setImageDrawable (null)
|
||||
override fun onBindViewHolder(
|
||||
holder: ImageViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
if (images.size == 0) {
|
||||
return
|
||||
}
|
||||
var image = images[position]
|
||||
holder.image.setImageDrawable(null)
|
||||
if (context.contentResolver.getType(image.uri) == null) {
|
||||
// Image does not exist anymore, update adapter.
|
||||
holder.itemView.post {
|
||||
|
|
@ -140,18 +148,19 @@ class ImageAdapter(
|
|||
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
|
||||
|
||||
// Getting selected index when switch is on
|
||||
val selectedIndex: Int = if (showAlreadyActionedImages) {
|
||||
ImageHelper.getIndex(selectedImages, image)
|
||||
val selectedIndex: Int =
|
||||
if (showAlreadyActionedImages) {
|
||||
ImageHelper.getIndex(selectedImages, image)
|
||||
|
||||
// Getting selected index when switch is off
|
||||
} else if (actionableImagesMap.size > position) {
|
||||
ImageHelper
|
||||
.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
|
||||
// Getting selected index when switch is off
|
||||
} else if (actionableImagesMap.size > position) {
|
||||
ImageHelper
|
||||
.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
|
||||
|
||||
// For any other case return -1
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
// For any other case return -1
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
|
||||
val isSelected = selectedIndex != -1
|
||||
if (isSelected) {
|
||||
|
|
@ -160,7 +169,11 @@ class ImageAdapter(
|
|||
holder.itemUnselected()
|
||||
}
|
||||
imageLoader.queryAndSetView(
|
||||
holder, image, ioDispatcher, defaultDispatcher ,uploadingContributionList
|
||||
holder,
|
||||
image,
|
||||
ioDispatcher,
|
||||
defaultDispatcher,
|
||||
uploadingContributionList,
|
||||
)
|
||||
scope.launch {
|
||||
val sharedPreferences: SharedPreferences =
|
||||
|
|
@ -173,22 +186,28 @@ class ImageAdapter(
|
|||
if (!alreadyAddedPositions.contains(position)) {
|
||||
processThumbnailForActionedImage(holder, position, uploadingContributionList)
|
||||
|
||||
// If the position is already visited, that means the image is already present
|
||||
// inside map, so it will fetch the image from the map and load in the holder
|
||||
// If the position is already visited, that means the image is already present
|
||||
// inside map, so it will fetch the image from the map and load in the holder
|
||||
} else {
|
||||
val actionableImages: List<Image> = ArrayList(actionableImagesMap.values)
|
||||
if(actionableImages.size > position) {
|
||||
if (actionableImages.size > position) {
|
||||
image = actionableImages[position]
|
||||
Glide.with(holder.image).load(image.uri)
|
||||
.thumbnail(0.3f).into(holder.image)
|
||||
Glide
|
||||
.with(holder.image)
|
||||
.load(image.uri)
|
||||
.thumbnail(0.3f)
|
||||
.into(holder.image)
|
||||
}
|
||||
}
|
||||
|
||||
// If switch is turned off, it just fetches the image from all images without any
|
||||
// further operations
|
||||
// If switch is turned off, it just fetches the image from all images without any
|
||||
// further operations
|
||||
} else {
|
||||
Glide.with(holder.image).load(image.uri)
|
||||
.thumbnail(0.3f).into(holder.image)
|
||||
Glide
|
||||
.with(holder.image)
|
||||
.load(image.uri)
|
||||
.thumbnail(0.3f)
|
||||
.into(holder.image)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -210,12 +229,16 @@ class ImageAdapter(
|
|||
suspend fun processThumbnailForActionedImage(
|
||||
holder: ImageViewHolder,
|
||||
position: Int,
|
||||
uploadingContributionList: List<Contribution>
|
||||
uploadingContributionList: List<Contribution>,
|
||||
) {
|
||||
val next = imageLoader.nextActionableImage(
|
||||
allImages, ioDispatcher, defaultDispatcher,
|
||||
nextImagePosition, uploadingContributionList
|
||||
)
|
||||
val next =
|
||||
imageLoader.nextActionableImage(
|
||||
allImages,
|
||||
ioDispatcher,
|
||||
defaultDispatcher,
|
||||
nextImagePosition,
|
||||
uploadingContributionList,
|
||||
)
|
||||
|
||||
// If next actionable image is found, saves it, as the the search for
|
||||
// finding next actionable image will start from this position
|
||||
|
|
@ -229,8 +252,11 @@ class ImageAdapter(
|
|||
actionableImagesMap[next] = allImages[next]
|
||||
alreadyAddedPositions.add(imagePositionAsPerIncreasingOrder)
|
||||
imagePositionAsPerIncreasingOrder++
|
||||
Glide.with(holder.image).load(allImages[next].uri)
|
||||
.thumbnail(0.3f).into(holder.image)
|
||||
Glide
|
||||
.with(holder.image)
|
||||
.load(allImages[next].uri)
|
||||
.thumbnail(0.3f)
|
||||
.into(holder.image)
|
||||
notifyItemInserted(position)
|
||||
notifyItemRangeChanged(position, itemCount + 1)
|
||||
}
|
||||
|
|
@ -248,7 +274,7 @@ class ImageAdapter(
|
|||
*/
|
||||
private fun onThumbnailClicked(
|
||||
position: Int,
|
||||
holder: ImageViewHolder
|
||||
holder: ImageViewHolder,
|
||||
) {
|
||||
val sharedPreferences: SharedPreferences =
|
||||
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
|
||||
|
|
@ -269,7 +295,10 @@ class ImageAdapter(
|
|||
/**
|
||||
* Handle click event on an image, update counter on images.
|
||||
*/
|
||||
private fun selectOrRemoveImage(holder: ImageViewHolder, position: Int){
|
||||
private fun selectOrRemoveImage(
|
||||
holder: ImageViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
val sharedPreferences: SharedPreferences =
|
||||
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
|
||||
val showAlreadyActionedImages =
|
||||
|
|
@ -277,14 +306,15 @@ class ImageAdapter(
|
|||
|
||||
// Getting clicked index from all images index when show_already_actioned_images
|
||||
// switch is on
|
||||
val clickedIndex: Int = if(showAlreadyActionedImages) {
|
||||
ImageHelper.getIndex(selectedImages, images[position])
|
||||
val clickedIndex: Int =
|
||||
if (showAlreadyActionedImages) {
|
||||
ImageHelper.getIndex(selectedImages, images[position])
|
||||
|
||||
// Getting clicked index from actionable images when show_already_actioned_images
|
||||
// switch is off
|
||||
} else {
|
||||
ImageHelper.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
|
||||
}
|
||||
// Getting clicked index from actionable images when show_already_actioned_images
|
||||
// switch is off
|
||||
} else {
|
||||
ImageHelper.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
|
||||
}
|
||||
|
||||
if (clickedIndex != -1) {
|
||||
selectedImages.removeAt(clickedIndex)
|
||||
|
|
@ -294,13 +324,14 @@ class ImageAdapter(
|
|||
notifyItemChanged(position, ImageUnselected())
|
||||
|
||||
// Getting index from all images index when switch is on
|
||||
val indexes = if (showAlreadyActionedImages) {
|
||||
ImageHelper.getIndexList(selectedImages, images)
|
||||
val indexes =
|
||||
if (showAlreadyActionedImages) {
|
||||
ImageHelper.getIndexList(selectedImages, images)
|
||||
|
||||
// Getting index from actionable images when switch is off
|
||||
} else {
|
||||
ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
|
||||
}
|
||||
// Getting index from actionable images when switch is off
|
||||
} else {
|
||||
ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
|
||||
}
|
||||
for (index in indexes) {
|
||||
notifyItemChanged(index, ImageSelectedOrUpdated())
|
||||
}
|
||||
|
|
@ -313,15 +344,16 @@ class ImageAdapter(
|
|||
}
|
||||
|
||||
// Getting index from all images index when switch is on
|
||||
val indexes: ArrayList<Int> = if (showAlreadyActionedImages) {
|
||||
selectedImages.add(images[position])
|
||||
ImageHelper.getIndexList(selectedImages, images)
|
||||
val indexes: ArrayList<Int> =
|
||||
if (showAlreadyActionedImages) {
|
||||
selectedImages.add(images[position])
|
||||
ImageHelper.getIndexList(selectedImages, images)
|
||||
|
||||
// Getting index from actionable images when switch is off
|
||||
} else {
|
||||
selectedImages.add(ArrayList(actionableImagesMap.values)[position])
|
||||
ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
|
||||
}
|
||||
// Getting index from actionable images when switch is off
|
||||
} else {
|
||||
selectedImages.add(ArrayList(actionableImagesMap.values)[position])
|
||||
ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
|
||||
}
|
||||
|
||||
for (index in indexes) {
|
||||
notifyItemChanged(index, ImageSelectedOrUpdated())
|
||||
|
|
@ -334,10 +366,15 @@ class ImageAdapter(
|
|||
/**
|
||||
* Initialize the data set.
|
||||
*/
|
||||
fun init(newImages: List<Image>, fixedImages: List<Image>, emptyMap: TreeMap<Int, Image>, uploadedImages: List<Contribution> = ArrayList()) {
|
||||
fun init(
|
||||
newImages: List<Image>,
|
||||
fixedImages: List<Image>,
|
||||
emptyMap: TreeMap<Int, Image>,
|
||||
uploadedImages: List<Contribution> = ArrayList(),
|
||||
) {
|
||||
allImages = fixedImages
|
||||
val oldImageList:ArrayList<Image> = images
|
||||
val newImageList:ArrayList<Image> = ArrayList(newImages)
|
||||
val oldImageList: ArrayList<Image> = images
|
||||
val newImageList: ArrayList<Image> = ArrayList(newImages)
|
||||
actionableImagesMap = emptyMap
|
||||
alreadyAddedPositions = ArrayList()
|
||||
uploadingContributionList = uploadedImages
|
||||
|
|
@ -345,9 +382,10 @@ class ImageAdapter(
|
|||
reachedEndOfFolder = false
|
||||
selectedImages = ArrayList()
|
||||
imagePositionAsPerIncreasingOrder = 0
|
||||
val diffResult = DiffUtil.calculateDiff(
|
||||
ImagesDiffCallback(oldImageList, newImageList)
|
||||
)
|
||||
val diffResult =
|
||||
DiffUtil.calculateDiff(
|
||||
ImagesDiffCallback(oldImageList, newImageList),
|
||||
)
|
||||
images = newImageList
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
|
@ -355,31 +393,35 @@ class ImageAdapter(
|
|||
/**
|
||||
* Set new selected images
|
||||
*/
|
||||
fun setSelectedImages(newSelectedImages: ArrayList<Image>){
|
||||
fun setSelectedImages(newSelectedImages: ArrayList<Image>) {
|
||||
selectedImages = ArrayList(newSelectedImages)
|
||||
imageSelectListener.onSelectedImagesChanged(selectedImages, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data in the adapter
|
||||
*/
|
||||
fun refresh(newImages: List<Image>, fixedImages: List<Image>, uploadingImages: List<Contribution> = ArrayList()) {
|
||||
fun refresh(
|
||||
newImages: List<Image>,
|
||||
fixedImages: List<Image>,
|
||||
uploadingImages: List<Contribution> = ArrayList(),
|
||||
) {
|
||||
numberOfSelectedImagesMarkedAsNotForUpload = 0
|
||||
images.clear()
|
||||
selectedImages = arrayListOf()
|
||||
init(newImages, fixedImages, TreeMap(),uploadingImages)
|
||||
init(newImages, fixedImages, TreeMap(), uploadingImages)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear selected images and empty the list.
|
||||
*/
|
||||
fun clearSelectedImages(){
|
||||
fun clearSelectedImages() {
|
||||
numberOfSelectedImagesMarkedAsNotForUpload = 0
|
||||
selectedImages.clear()
|
||||
selectedImages = arrayListOf()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove image from actionable images map.
|
||||
*/
|
||||
|
|
@ -389,7 +431,7 @@ class ImageAdapter(
|
|||
val showAlreadyActionedImages =
|
||||
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
|
||||
|
||||
if(showAlreadyActionedImages) {
|
||||
if (showAlreadyActionedImages) {
|
||||
refresh(allImages, allImages, uploadingContributionList)
|
||||
} else {
|
||||
val iterator = actionableImagesMap.entries.iterator()
|
||||
|
|
@ -402,16 +444,14 @@ class ImageAdapter(
|
|||
iterator.remove()
|
||||
alreadyAddedPositions.removeAt(alreadyAddedPositions.size - 1)
|
||||
notifyItemRemoved(index)
|
||||
notifyItemRangeChanged(index, itemCount )
|
||||
notifyItemRangeChanged(index, itemCount)
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the total number of items in the data set held by the adapter.
|
||||
*
|
||||
|
|
@ -424,24 +464,22 @@ class ImageAdapter(
|
|||
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
|
||||
|
||||
// While switch is on initializes the holder with all images size
|
||||
return if(showAlreadyActionedImages) {
|
||||
return if (showAlreadyActionedImages) {
|
||||
allImages.size
|
||||
|
||||
// While switch is off and searching for next actionable has ended, initializes the holder
|
||||
// with size of all actionable images
|
||||
// While switch is off and searching for next actionable has ended, initializes the holder
|
||||
// with size of all actionable images
|
||||
} else if (actionableImagesMap.size == allImages.size || reachedEndOfFolder) {
|
||||
actionableImagesMap.size
|
||||
|
||||
// While switch is off, initializes the holder with and extra view holder so that finding
|
||||
// and addition of the next actionable image in the adapter can be continued
|
||||
// While switch is off, initializes the holder with and extra view holder so that finding
|
||||
// and addition of the next actionable image in the adapter can be continued
|
||||
} else {
|
||||
actionableImagesMap.size + 1
|
||||
}
|
||||
}
|
||||
|
||||
fun getImageIdAt(position: Int): Long {
|
||||
return images.get(position).id
|
||||
}
|
||||
fun getImageIdAt(position: Int): Long = images.get(position).id
|
||||
|
||||
/**
|
||||
* CleanUp function.
|
||||
|
|
@ -453,7 +491,9 @@ class ImageAdapter(
|
|||
/**
|
||||
* Image view holder.
|
||||
*/
|
||||
class ImageViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
|
||||
class ImageViewHolder(
|
||||
itemView: View,
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
val image: ImageView = itemView.findViewById(R.id.image_thumbnail)
|
||||
private val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group)
|
||||
private val uploadingGroup: Group = itemView.findViewById(R.id.uploading_group)
|
||||
|
|
@ -495,16 +535,12 @@ class ImageAdapter(
|
|||
notForUploadGroup.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
fun isItemUploaded():Boolean {
|
||||
return uploadedGroup.visibility == View.VISIBLE
|
||||
}
|
||||
fun isItemUploaded(): Boolean = uploadedGroup.visibility == View.VISIBLE
|
||||
|
||||
/**
|
||||
* Item is not for upload
|
||||
*/
|
||||
fun isItemNotForUpload():Boolean {
|
||||
return notForUploadGroup.visibility == View.VISIBLE
|
||||
}
|
||||
fun isItemNotForUpload(): Boolean = notForUploadGroup.visibility == View.VISIBLE
|
||||
|
||||
/**
|
||||
* Item is not uploading
|
||||
|
|
@ -533,45 +569,38 @@ class ImageAdapter(
|
|||
*/
|
||||
class ImagesDiffCallback(
|
||||
var oldImageList: ArrayList<Image>,
|
||||
var newImageList: ArrayList<Image>
|
||||
) : DiffUtil.Callback(){
|
||||
|
||||
var newImageList: ArrayList<Image>,
|
||||
) : DiffUtil.Callback() {
|
||||
/**
|
||||
* Returns the size of the old list.
|
||||
*/
|
||||
override fun getOldListSize(): Int {
|
||||
return oldImageList.size
|
||||
}
|
||||
override fun getOldListSize(): Int = oldImageList.size
|
||||
|
||||
/**
|
||||
* Returns the size of the new list.
|
||||
*/
|
||||
override fun getNewListSize(): Int {
|
||||
return newImageList.size
|
||||
}
|
||||
override fun getNewListSize(): Int = newImageList.size
|
||||
|
||||
/**
|
||||
* Called by the DiffUtil to decide whether two object represent the same Item.
|
||||
*/
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return newImageList[newItemPosition].id == oldImageList[oldItemPosition].id
|
||||
}
|
||||
override fun areItemsTheSame(
|
||||
oldItemPosition: Int,
|
||||
newItemPosition: Int,
|
||||
): Boolean = newImageList[newItemPosition].id == oldImageList[oldItemPosition].id
|
||||
|
||||
/**
|
||||
* Called by the DiffUtil when it wants to check whether two items have the same data.
|
||||
* DiffUtil uses this information to detect if the contents of an item has changed.
|
||||
*/
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldImageList[oldItemPosition].equals(newImageList[newItemPosition])
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItemPosition: Int,
|
||||
newItemPosition: Int,
|
||||
): Boolean = oldImageList[oldItemPosition].equals(newImageList[newItemPosition])
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text for showing inside the bubble during bubble scroll.
|
||||
*/
|
||||
override fun getSectionName(position: Int): String {
|
||||
return images[position].date
|
||||
}
|
||||
|
||||
override fun getSectionName(position: Int): String = images[position].date
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
/**
|
||||
* Generic Recycler view adapter.
|
||||
*/
|
||||
abstract class RecyclerViewAdapter<T : RecyclerView.ViewHolder?>(val context: Context): RecyclerView.Adapter<T>() {
|
||||
abstract class RecyclerViewAdapter<T : RecyclerView.ViewHolder?>(
|
||||
val context: Context,
|
||||
) : RecyclerView.Adapter<T>() {
|
||||
val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,23 +8,16 @@ import android.content.SharedPreferences
|
|||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -38,7 +31,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -57,23 +49,27 @@ import fr.free.nrw.commons.databinding.ActivityCustomSelectorBinding
|
|||
import fr.free.nrw.commons.databinding.CustomSelectorBottomLayoutBinding
|
||||
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
|
||||
import fr.free.nrw.commons.filepicker.Constants
|
||||
import fr.free.nrw.commons.filepicker.FilePicker
|
||||
import fr.free.nrw.commons.media.ZoomableActivity
|
||||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||
import fr.free.nrw.commons.utils.CustomSelectorUtils
|
||||
import fr.free.nrw.commons.utils.PermissionUtils
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.lang.Integer.max
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
/**
|
||||
* Custom Selector Activity.
|
||||
*/
|
||||
class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectListener {
|
||||
|
||||
class CustomSelectorActivity :
|
||||
BaseActivity(),
|
||||
FolderClickListener,
|
||||
ImageSelectListener {
|
||||
/**
|
||||
* ViewBindings
|
||||
*/
|
||||
|
|
@ -147,7 +143,7 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
*/
|
||||
var imageFragment: ImageFragment? = null
|
||||
|
||||
private var progressDialogText:String=""
|
||||
private var progressDialogText: String = ""
|
||||
|
||||
private var showPartialAccessIndicator by mutableStateOf(false)
|
||||
|
||||
|
|
@ -158,7 +154,8 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
super.onCreate(savedInstanceState)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
|
||||
ContextCompat.checkSelfPermission(
|
||||
this, Manifest.permission.READ_MEDIA_IMAGES
|
||||
this,
|
||||
Manifest.permission.READ_MEDIA_IMAGES,
|
||||
) == PackageManager.PERMISSION_DENIED
|
||||
) {
|
||||
showPartialAccessIndicator = true
|
||||
|
|
@ -168,25 +165,27 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
toolbarBinding = CustomSelectorToolbarBinding.bind(binding.root)
|
||||
bottomSheetBinding = CustomSelectorBottomLayoutBinding.bind(binding.root)
|
||||
binding.partialAccessIndicator.setContent {
|
||||
PartialStorageAccessIndicator(
|
||||
partialStorageAccessIndicator(
|
||||
isVisible = showPartialAccessIndicator,
|
||||
onManage = {
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
requestPermissions(arrayOf(Manifest.permission.READ_MEDIA_IMAGES), 1)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp)
|
||||
.fillMaxWidth()
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
|
||||
prefs = applicationContext.getSharedPreferences("CustomSelector", MODE_PRIVATE)
|
||||
viewModel = ViewModelProvider(this, customSelectorViewModelFactory).get(
|
||||
CustomSelectorViewModel::class.java
|
||||
)
|
||||
viewModel =
|
||||
ViewModelProvider(this, customSelectorViewModelFactory).get(
|
||||
CustomSelectorViewModel::class.java,
|
||||
)
|
||||
|
||||
setupViews()
|
||||
|
||||
|
|
@ -208,11 +207,11 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
grantResults: IntArray,
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if(requestCode == 1 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
if(grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
if (requestCode == 1 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
showPartialAccessIndicator = false
|
||||
}
|
||||
}
|
||||
|
|
@ -226,7 +225,11 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
/**
|
||||
* When data will be send from full screen mode, it will be passed to fragment
|
||||
*/
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?,
|
||||
) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE &&
|
||||
resultCode == Activity.RESULT_OK
|
||||
|
|
@ -254,7 +257,8 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
* Set up view, default folder view.
|
||||
*/
|
||||
private fun setupViews() {
|
||||
supportFragmentManager.beginTransaction()
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.fragment_container, FolderFragment.newInstance())
|
||||
.commit()
|
||||
setUpToolbar()
|
||||
|
|
@ -322,12 +326,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
|
||||
var allImagesAlreadyNotForUpload = true
|
||||
images.forEach { image ->
|
||||
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
|
||||
image.uri,
|
||||
ioDispatcher,
|
||||
fileUtilsWrapper,
|
||||
contentResolver
|
||||
)
|
||||
val imageSHA1 =
|
||||
CustomSelectorUtils.getImageSHA1(
|
||||
image.uri,
|
||||
ioDispatcher,
|
||||
fileUtilsWrapper,
|
||||
contentResolver,
|
||||
)
|
||||
val exists = notForUploadStatusDao.find(imageSHA1)
|
||||
if (exists < 1) {
|
||||
allImagesAlreadyNotForUpload = false
|
||||
|
|
@ -337,12 +342,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
if (!allImagesAlreadyNotForUpload) {
|
||||
// Insert or delete images as necessary, but the UI updates should be posted back to the main thread
|
||||
images.forEach { image ->
|
||||
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
|
||||
image.uri,
|
||||
ioDispatcher,
|
||||
fileUtilsWrapper,
|
||||
contentResolver
|
||||
)
|
||||
val imageSHA1 =
|
||||
CustomSelectorUtils.getImageSHA1(
|
||||
image.uri,
|
||||
ioDispatcher,
|
||||
fileUtilsWrapper,
|
||||
contentResolver,
|
||||
)
|
||||
notForUploadStatusDao.insert(NotForUploadStatus(imageSHA1))
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
|
|
@ -353,12 +359,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
}
|
||||
} else {
|
||||
images.forEach { image ->
|
||||
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
|
||||
image.uri,
|
||||
ioDispatcher,
|
||||
fileUtilsWrapper,
|
||||
contentResolver
|
||||
)
|
||||
val imageSHA1 =
|
||||
CustomSelectorUtils.getImageSHA1(
|
||||
image.uri,
|
||||
ioDispatcher,
|
||||
fileUtilsWrapper,
|
||||
contentResolver,
|
||||
)
|
||||
notForUploadStatusDao.deleteNotForUploadWithImageSHA1(imageSHA1)
|
||||
}
|
||||
|
||||
|
|
@ -386,13 +393,19 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
/**
|
||||
* Change the title of the toolbar.
|
||||
*/
|
||||
private fun changeTitle(title: String, selectedImageCount:Int) {
|
||||
if (title.isNotEmpty()){
|
||||
private fun changeTitle(
|
||||
title: String,
|
||||
selectedImageCount: Int,
|
||||
) {
|
||||
if (title.isNotEmpty()) {
|
||||
val titleText = findViewById<TextView>(R.id.title)
|
||||
var titleWithAppendedImageCount = title
|
||||
if (selectedImageCount > 0) {
|
||||
titleWithAppendedImageCount += " (${resources.getQuantityString(R.plurals.custom_picker_images_selected_title_appendix,
|
||||
selectedImageCount, selectedImageCount)})"
|
||||
titleWithAppendedImageCount += " (${resources.getQuantityString(
|
||||
R.plurals.custom_picker_images_selected_title_appendix,
|
||||
selectedImageCount,
|
||||
selectedImageCount,
|
||||
)})"
|
||||
}
|
||||
if (titleText != null) {
|
||||
titleText.text = titleWithAppendedImageCount
|
||||
|
|
@ -415,8 +428,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
/**
|
||||
* override on folder click, change the toolbar title on folder click.
|
||||
*/
|
||||
override fun onFolderClick(folderId: Long, folderName: String, lastItemId: Long) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
override fun onFolderClick(
|
||||
folderId: Long,
|
||||
folderName: String,
|
||||
lastItemId: Long,
|
||||
) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.add(R.id.fragment_container, ImageFragment.newInstance(folderId, lastItemId))
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
|
|
@ -433,18 +451,21 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
*/
|
||||
override fun onSelectedImagesChanged(
|
||||
selectedImages: ArrayList<Image>,
|
||||
selectedNotForUploadImages: Int
|
||||
selectedNotForUploadImages: Int,
|
||||
) {
|
||||
viewModel.selectedImages.value = selectedImages
|
||||
changeTitle(bucketName, selectedImages.size)
|
||||
|
||||
uploadLimitExceeded = selectedImages.size > uploadLimit
|
||||
uploadLimitExceededBy = max(selectedImages.size - uploadLimit,0)
|
||||
uploadLimitExceededBy = max(selectedImages.size - uploadLimit, 0)
|
||||
|
||||
if (uploadLimitExceeded && selectedNotForUploadImages == 0) {
|
||||
toolbarBinding.imageLimitError.visibility = View.VISIBLE
|
||||
bottomSheetBinding.upload.text = resources.getString(
|
||||
R.string.custom_selector_button_limit_text, uploadLimit)
|
||||
bottomSheetBinding.upload.text =
|
||||
resources.getString(
|
||||
R.string.custom_selector_button_limit_text,
|
||||
uploadLimit,
|
||||
)
|
||||
} else {
|
||||
toolbarBinding.imageLimitError.visibility = View.INVISIBLE
|
||||
bottomSheetBinding.upload.text = resources.getString(R.string.upload)
|
||||
|
|
@ -461,11 +482,11 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
bottomSheetBinding.notForUpload.text =
|
||||
when (selectedImages.size == selectedNotForUploadImages) {
|
||||
true -> {
|
||||
progressDialogText=getString(R.string.unmarking_as_not_for_upload)
|
||||
progressDialogText = getString(R.string.unmarking_as_not_for_upload)
|
||||
getString(R.string.unmark_as_not_for_upload)
|
||||
}
|
||||
else -> {
|
||||
progressDialogText=getString(R.string.marking_as_not_for_upload)
|
||||
progressDialogText = getString(R.string.marking_as_not_for_upload)
|
||||
getString(R.string.mark_as_not_for_upload)
|
||||
}
|
||||
}
|
||||
|
|
@ -481,13 +502,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
override fun onLongPress(
|
||||
position: Int,
|
||||
images: ArrayList<Image>,
|
||||
selectedImages: ArrayList<Image>
|
||||
selectedImages: ArrayList<Image>,
|
||||
) {
|
||||
val intent = Intent(this, ZoomableActivity::class.java)
|
||||
intent.putExtra(CustomSelectorConstants.PRESENT_POSITION, position)
|
||||
intent.putParcelableArrayListExtra(
|
||||
CustomSelectorConstants.TOTAL_SELECTED_IMAGES,
|
||||
selectedImages
|
||||
selectedImages,
|
||||
)
|
||||
intent.putExtra(CustomSelectorConstants.BUCKET_ID, bucketId)
|
||||
startActivityForResult(intent, Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE)
|
||||
|
|
@ -498,22 +519,22 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
* Get the selected images. Remove any non existent file, forward the data to finish selector.
|
||||
*/
|
||||
fun onDone() {
|
||||
val selectedImages = viewModel.selectedImages.value
|
||||
if (selectedImages.isNullOrEmpty()) {
|
||||
finishPickImages(arrayListOf())
|
||||
return
|
||||
val selectedImages = viewModel.selectedImages.value
|
||||
if (selectedImages.isNullOrEmpty()) {
|
||||
finishPickImages(arrayListOf())
|
||||
return
|
||||
}
|
||||
var i = 0
|
||||
while (i < selectedImages.size) {
|
||||
val path = selectedImages[i].path
|
||||
val file = File(path)
|
||||
if (!file.exists()) {
|
||||
selectedImages.removeAt(i)
|
||||
i--
|
||||
}
|
||||
var i = 0
|
||||
while (i < selectedImages.size) {
|
||||
val path = selectedImages[i].path
|
||||
val file = File(path)
|
||||
if (!file.exists()) {
|
||||
selectedImages.removeAt(i)
|
||||
i--
|
||||
}
|
||||
i++
|
||||
}
|
||||
finishPickImages(selectedImages)
|
||||
i++
|
||||
}
|
||||
finishPickImages(selectedImages)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -547,10 +568,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
val dialog = Dialog(this)
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
dialog.setContentView(R.layout.custom_selector_limit_dialog)
|
||||
(dialog.findViewById(R.id.btn_dismiss_limit_warning) as Button).setOnClickListener()
|
||||
{ dialog.dismiss() }
|
||||
(dialog.findViewById(R.id.upload_limit_warning) as TextView).text = resources.getString(
|
||||
R.string.custom_selector_over_limit_warning, uploadLimit, uploadLimitExceededBy)
|
||||
(dialog.findViewById(R.id.btn_dismiss_limit_warning) as Button).setOnClickListener { dialog.dismiss() }
|
||||
(dialog.findViewById(R.id.upload_limit_warning) as TextView).text =
|
||||
resources.getString(
|
||||
R.string.custom_selector_over_limit_warning,
|
||||
uploadLimit,
|
||||
uploadLimitExceededBy,
|
||||
)
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
|
|
@ -560,9 +584,17 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
*/
|
||||
override fun onDestroy() {
|
||||
if (isImageFragmentOpen) {
|
||||
prefs.edit().putLong(FOLDER_ID, bucketId).putString(FOLDER_NAME, bucketName).apply()
|
||||
prefs
|
||||
.edit()
|
||||
.putLong(FOLDER_ID, bucketId)
|
||||
.putString(FOLDER_NAME, bucketName)
|
||||
.apply()
|
||||
} else {
|
||||
prefs.edit().remove(FOLDER_ID).remove(FOLDER_NAME).apply()
|
||||
prefs
|
||||
.edit()
|
||||
.remove(FOLDER_ID)
|
||||
.remove(FOLDER_NAME)
|
||||
.apply()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
|
@ -573,38 +605,41 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
|||
const val ITEM_ID: String = "ItemId"
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PartialStorageAccessIndicator(
|
||||
fun partialStorageAccessIndicator(
|
||||
isVisible: Boolean,
|
||||
onManage: ()-> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
onManage: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if(isVisible) {
|
||||
if (isVisible) {
|
||||
OutlinedCard(
|
||||
modifier = modifier,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = colorResource(R.color.primarySuperLightColor)
|
||||
),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = colorResource(R.color.primarySuperLightColor),
|
||||
),
|
||||
border = BorderStroke(0.5.dp, color = colorResource(R.color.primaryColor)),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
) {
|
||||
Row(modifier = Modifier.padding(16.dp).fillMaxWidth()) {
|
||||
Text(
|
||||
text = "You've given access to a select number of photos",
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
TextButton(
|
||||
onClick = onManage,
|
||||
modifier = Modifier.align(Alignment.Bottom),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = colorResource(R.color.primaryColor)
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = colorResource(R.color.primaryColor),
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Manage",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = colorResource(R.color.primaryTextColor)
|
||||
color = colorResource(R.color.primaryTextColor),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -614,11 +649,15 @@ fun PartialStorageAccessIndicator(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PartialStorageAccessIndicatorPreview() {
|
||||
fun partialStorageAccessIndicatorPreview() {
|
||||
Surface {
|
||||
PartialStorageAccessIndicator(isVisible = true, onManage = {}, modifier = Modifier
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp)
|
||||
.fillMaxWidth()
|
||||
partialStorageAccessIndicator(
|
||||
isVisible = true,
|
||||
onManage = {},
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ import kotlinx.coroutines.cancel
|
|||
/**
|
||||
* Custom Selector view model.
|
||||
*/
|
||||
class CustomSelectorViewModel(var context: Context,var imageFileLoader: ImageFileLoader) : ViewModel() {
|
||||
|
||||
class CustomSelectorViewModel(
|
||||
var context: Context,
|
||||
var imageFileLoader: ImageFileLoader,
|
||||
) : ViewModel() {
|
||||
/**
|
||||
* Scope for coroutine task (image fetch).
|
||||
*/
|
||||
|
|
@ -37,15 +39,17 @@ class CustomSelectorViewModel(var context: Context,var imageFileLoader: ImageFil
|
|||
fun fetchImages() {
|
||||
result.postValue(Result(CallbackStatus.FETCHING, arrayListOf()))
|
||||
scope.cancel()
|
||||
imageFileLoader.loadDeviceImages(object: ImageLoaderListener {
|
||||
override fun onImageLoaded(images: ArrayList<Image>) {
|
||||
result.postValue(Result(CallbackStatus.SUCCESS, images))
|
||||
}
|
||||
imageFileLoader.loadDeviceImages(
|
||||
object : ImageLoaderListener {
|
||||
override fun onImageLoaded(images: ArrayList<Image>) {
|
||||
result.postValue(Result(CallbackStatus.SUCCESS, images))
|
||||
}
|
||||
|
||||
override fun onFailed(throwable: Throwable) {
|
||||
result.postValue(Result(CallbackStatus.SUCCESS, arrayListOf()))
|
||||
}
|
||||
})
|
||||
override fun onFailed(throwable: Throwable) {
|
||||
result.postValue(Result(CallbackStatus.SUCCESS, arrayListOf()))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -55,4 +59,4 @@ class CustomSelectorViewModel(var context: Context,var imageFileLoader: ImageFil
|
|||
scope.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ import javax.inject.Inject
|
|||
/**
|
||||
* View Model Factory.
|
||||
*/
|
||||
class CustomSelectorViewModelFactory @Inject constructor(val context: Context,val imageFileLoader: ImageFileLoader) : ViewModelProvider.Factory {
|
||||
|
||||
override fun<CustomSelectorViewModel: ViewModel> create(modelClass: Class<CustomSelectorViewModel>) : CustomSelectorViewModel {
|
||||
return CustomSelectorViewModel(context,imageFileLoader) as CustomSelectorViewModel
|
||||
class CustomSelectorViewModelFactory
|
||||
@Inject
|
||||
constructor(
|
||||
val context: Context,
|
||||
val imageFileLoader: ImageFileLoader,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <CustomSelectorViewModel : ViewModel> create(modelClass: Class<CustomSelectorViewModel>): CustomSelectorViewModel =
|
||||
CustomSelectorViewModel(context, imageFileLoader) as CustomSelectorViewModel
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -9,10 +9,10 @@ import androidx.lifecycle.ViewModelProvider
|
|||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
||||
import fr.free.nrw.commons.customselector.model.Result
|
||||
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
|
||||
import fr.free.nrw.commons.customselector.model.CallbackStatus
|
||||
import fr.free.nrw.commons.customselector.model.Folder
|
||||
import fr.free.nrw.commons.customselector.model.Result
|
||||
import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter
|
||||
import fr.free.nrw.commons.databinding.FragmentCustomSelectorBinding
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||
|
|
@ -24,12 +24,11 @@ import javax.inject.Inject
|
|||
* Custom selector folder fragment.
|
||||
*/
|
||||
class FolderFragment : CommonsDaggerSupportFragment() {
|
||||
|
||||
/**
|
||||
* ViewBinding
|
||||
*/
|
||||
private var _binding: FragmentCustomSelectorBinding? = null
|
||||
private val binding get() = _binding
|
||||
val binding get() = _binding
|
||||
|
||||
/**
|
||||
* View Model for images.
|
||||
|
|
@ -53,6 +52,7 @@ class FolderFragment : CommonsDaggerSupportFragment() {
|
|||
|
||||
var mediaClient: MediaClient? = null
|
||||
@Inject set
|
||||
|
||||
/**
|
||||
* Folder Adapter.
|
||||
*/
|
||||
|
|
@ -66,15 +66,13 @@ class FolderFragment : CommonsDaggerSupportFragment() {
|
|||
/**
|
||||
* Folder List.
|
||||
*/
|
||||
private lateinit var folders : ArrayList<Folder>
|
||||
private lateinit var folders: ArrayList<Folder>
|
||||
|
||||
/**
|
||||
* Companion newInstance.
|
||||
*/
|
||||
companion object{
|
||||
fun newInstance(): FolderFragment {
|
||||
return FolderFragment()
|
||||
}
|
||||
companion object {
|
||||
fun newInstance(): FolderFragment = FolderFragment()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -83,21 +81,24 @@ class FolderFragment : CommonsDaggerSupportFragment() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory!!).get(CustomSelectorViewModel::class.java)
|
||||
|
||||
viewModel = ViewModelProvider(requireActivity(), customSelectorViewModelFactory!!).get(CustomSelectorViewModel::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* OnCreateView.
|
||||
* Inflate Layout, init adapter, init gridLayoutManager, setUp recycler view, observe the view model for result.
|
||||
*/
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
_binding = FragmentCustomSelectorBinding.inflate(inflater, container, false)
|
||||
folderAdapter = FolderAdapter(requireActivity(), activity as FolderClickListener)
|
||||
gridLayoutManager = GridLayoutManager(context, columnCount())
|
||||
selectorRV = binding?.selectorRv
|
||||
loader = binding?.loader
|
||||
with(binding?.selectorRv){
|
||||
with(binding?.selectorRv) {
|
||||
this?.layoutManager = gridLayoutManager
|
||||
this?.setHasFixedSize(true)
|
||||
this?.adapter = folderAdapter
|
||||
|
|
@ -114,9 +115,9 @@ class FolderFragment : CommonsDaggerSupportFragment() {
|
|||
* Load adapter.
|
||||
*/
|
||||
private fun handleResult(result: Result) {
|
||||
if(result.status is CallbackStatus.SUCCESS){
|
||||
if (result.status is CallbackStatus.SUCCESS) {
|
||||
val images = result.images
|
||||
if(images.isEmpty()){
|
||||
if (images.isEmpty()) {
|
||||
binding?.emptyText?.let {
|
||||
it.visibility = View.VISIBLE
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ import kotlin.coroutines.CoroutineContext
|
|||
* Custom Selector Image File Loader.
|
||||
* Loads device images.
|
||||
*/
|
||||
class ImageFileLoader(val context: Context) : CoroutineScope{
|
||||
|
||||
class ImageFileLoader(
|
||||
val context: Context,
|
||||
) : CoroutineScope {
|
||||
/**
|
||||
* Coroutine context for fetching images.
|
||||
*/
|
||||
|
|
@ -30,14 +31,15 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
|
|||
/**
|
||||
* Media paramerters required.
|
||||
*/
|
||||
private val projection = arrayOf(
|
||||
MediaStore.Images.Media._ID,
|
||||
MediaStore.Images.Media.DISPLAY_NAME,
|
||||
MediaStore.Images.Media.DATA,
|
||||
MediaStore.Images.Media.BUCKET_ID,
|
||||
MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
|
||||
MediaStore.Images.Media.DATE_ADDED
|
||||
)
|
||||
private val projection =
|
||||
arrayOf(
|
||||
MediaStore.Images.Media._ID,
|
||||
MediaStore.Images.Media.DISPLAY_NAME,
|
||||
MediaStore.Images.Media.DATA,
|
||||
MediaStore.Images.Media.BUCKET_ID,
|
||||
MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
|
||||
MediaStore.Images.Media.DATE_ADDED,
|
||||
)
|
||||
|
||||
/**
|
||||
* Load Device Images under coroutine.
|
||||
|
|
@ -50,12 +52,18 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load Device images using cursor
|
||||
*/
|
||||
private fun getImages(listener:ImageLoaderListener) {
|
||||
val cursor = context.contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, MediaStore.Images.Media.DATE_ADDED + " DESC")
|
||||
private fun getImages(listener: ImageLoaderListener) {
|
||||
val cursor =
|
||||
context.contentResolver.query(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
projection,
|
||||
null,
|
||||
null,
|
||||
MediaStore.Images.Media.DATE_ADDED + " DESC",
|
||||
)
|
||||
if (cursor == null) {
|
||||
listener.onFailed(NullPointerException())
|
||||
return
|
||||
|
|
@ -85,10 +93,12 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
|
|||
val file =
|
||||
if (path == null || path.isEmpty()) {
|
||||
null
|
||||
} else try {
|
||||
File(path)
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
} else {
|
||||
try {
|
||||
File(path)
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (file != null && file.exists() && name != null && path != null && bucketName != null) {
|
||||
|
|
@ -106,30 +116,29 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
|
|||
val dateFormat = DateFormat.getMediumDateFormat(context)
|
||||
val formattedDate = dateFormat.format(date)
|
||||
|
||||
val image = Image(
|
||||
id,
|
||||
name,
|
||||
uri,
|
||||
path,
|
||||
bucketId,
|
||||
bucketName,
|
||||
date = (formattedDate)
|
||||
)
|
||||
val image =
|
||||
Image(
|
||||
id,
|
||||
name,
|
||||
uri,
|
||||
path,
|
||||
bucketId,
|
||||
bucketName,
|
||||
date = (formattedDate),
|
||||
)
|
||||
images.add(image)
|
||||
}
|
||||
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
cursor.close()
|
||||
listener.onImageLoaded(images)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Abort loading images.
|
||||
*/
|
||||
fun abortLoadImage(){
|
||||
//todo Abort loading images.
|
||||
fun abortLoadImage() {
|
||||
// todo Abort loading images.
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -37,17 +37,19 @@ import fr.free.nrw.commons.theme.BaseActivity
|
|||
import fr.free.nrw.commons.upload.FileProcessor
|
||||
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.util.*
|
||||
import java.util.TreeMap
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
/**
|
||||
* Custom Selector Image Fragment.
|
||||
*/
|
||||
class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDataListener {
|
||||
|
||||
class ImageFragment :
|
||||
CommonsDaggerSupportFragment(),
|
||||
RefreshUIListener,
|
||||
PassDataListener {
|
||||
private var _binding: FragmentCustomSelectorBinding? = null
|
||||
private val binding get() = _binding
|
||||
val binding get() = _binding
|
||||
|
||||
/**
|
||||
* Current bucketId.
|
||||
|
|
@ -107,7 +109,6 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
|||
private lateinit var progressDialog: AlertDialog
|
||||
private lateinit var progressDialogLayout: ProgressDialogBinding
|
||||
|
||||
|
||||
/**
|
||||
* NotForUploadStatus Dao class for database operations
|
||||
*/
|
||||
|
|
@ -142,7 +143,6 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
|||
lateinit var contributionDao: ContributionDao
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Switch state
|
||||
*/
|
||||
|
|
@ -157,7 +157,10 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
|||
/**
|
||||
* newInstance from bucketId.
|
||||
*/
|
||||
fun newInstance(bucketId: Long, lastItemId: Long): ImageFragment {
|
||||
fun newInstance(
|
||||
bucketId: Long,
|
||||
lastItemId: Long,
|
||||
): ImageFragment {
|
||||
val fragment = ImageFragment()
|
||||
val args = Bundle()
|
||||
args.putLong(BUCKET_ID, bucketId)
|
||||
|
|
@ -175,9 +178,10 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
|||
super.onCreate(savedInstanceState)
|
||||
bucketId = arguments?.getLong(BUCKET_ID)
|
||||
lastItemId = arguments?.getLong(LAST_ITEM_ID, 0)
|
||||
viewModel = ViewModelProvider(requireActivity(), customSelectorViewModelFactory).get(
|
||||
CustomSelectorViewModel::class.java
|
||||
)
|
||||
viewModel =
|
||||
ViewModelProvider(requireActivity(), customSelectorViewModelFactory).get(
|
||||
CustomSelectorViewModel::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -188,7 +192,7 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
|||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
_binding = FragmentCustomSelectorBinding.inflate(inflater, container, false)
|
||||
imageAdapter =
|
||||
|
|
@ -200,9 +204,12 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
|||
this?.adapter = imageAdapter
|
||||
}
|
||||
|
||||
viewModel?.result?.observe(viewLifecycleOwner, Observer {
|
||||
handleResult(it)
|
||||
})
|
||||
viewModel?.result?.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer {
|
||||
handleResult(it)
|
||||
},
|
||||
)
|
||||
|
||||
switch = binding?.switchWidget
|
||||
switch?.visibility = View.VISIBLE
|
||||
|
|
@ -323,20 +330,22 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
|||
override fun onDestroy() {
|
||||
imageAdapter.cleanUp()
|
||||
|
||||
val position = (selectorRV?.layoutManager as GridLayoutManager)
|
||||
.findFirstVisibleItemPosition()
|
||||
val position =
|
||||
(selectorRV?.layoutManager as GridLayoutManager)
|
||||
.findFirstVisibleItemPosition()
|
||||
|
||||
// Check for empty RecyclerView.
|
||||
if (position != -1 && filteredImages.size > 0) {
|
||||
context?.let { context ->
|
||||
context.getSharedPreferences(
|
||||
"CustomSelector",
|
||||
BaseActivity.MODE_PRIVATE
|
||||
)?.let { prefs ->
|
||||
prefs.edit()?.let { editor ->
|
||||
editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply()
|
||||
context
|
||||
.getSharedPreferences(
|
||||
"CustomSelector",
|
||||
BaseActivity.MODE_PRIVATE,
|
||||
)?.let { prefs ->
|
||||
prefs.edit()?.let { editor ->
|
||||
editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onDestroy()
|
||||
|
|
@ -354,7 +363,7 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
|||
/**
|
||||
* Removes the image from the actionable image map
|
||||
*/
|
||||
fun removeImage(image : Image){
|
||||
fun removeImage(image: Image) {
|
||||
imageAdapter.removeImageFromActionableImageMap(image)
|
||||
}
|
||||
|
||||
|
|
@ -364,11 +373,15 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
|||
fun clearSelectedImages() {
|
||||
imageAdapter.clearSelectedImages()
|
||||
}
|
||||
|
||||
/**
|
||||
* Passes selected images and other information from Activity to Fragment and connects it with
|
||||
* the adapter
|
||||
*/
|
||||
override fun passSelectedImages(selectedImages: ArrayList<Image>, shouldRefresh: Boolean) {
|
||||
override fun passSelectedImages(
|
||||
selectedImages: ArrayList<Image>,
|
||||
shouldRefresh: Boolean,
|
||||
) {
|
||||
imageAdapter.setSelectedImages(selectedImages)
|
||||
|
||||
val uploadingContributions = getUploadingContributions()
|
||||
|
|
@ -398,11 +411,10 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
|||
}
|
||||
}
|
||||
|
||||
private fun getUploadingContributions(): List<Contribution> {
|
||||
|
||||
return contributionDao.getContribution(
|
||||
listOf(Contribution.STATE_IN_PROGRESS, Contribution.STATE_FAILED, Contribution.STATE_QUEUED, Contribution.STATE_PAUSED)
|
||||
)?.subscribeOn(Schedulers.io())?.blockingGet() ?: emptyList()
|
||||
}
|
||||
|
||||
private fun getUploadingContributions(): List<Contribution> =
|
||||
contributionDao
|
||||
.getContribution(
|
||||
listOf(Contribution.STATE_IN_PROGRESS, Contribution.STATE_FAILED, Contribution.STATE_QUEUED, Contribution.STATE_PAUSED),
|
||||
)?.subscribeOn(Schedulers.io())
|
||||
?.blockingGet() ?: emptyList()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,368 +15,389 @@ import fr.free.nrw.commons.upload.FileProcessor
|
|||
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||
import fr.free.nrw.commons.utils.CustomSelectorUtils
|
||||
import fr.free.nrw.commons.utils.CustomSelectorUtils.Companion.checkWhetherFileExistsOnCommonsUsingSHA1
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Image Loader class, loads images, depending on API results.
|
||||
*/
|
||||
class ImageLoader @Inject constructor(
|
||||
|
||||
/**
|
||||
* MediaClient for SHA1 query.
|
||||
*/
|
||||
var mediaClient: MediaClient,
|
||||
|
||||
/**
|
||||
* FileProcessor to pre-process the file.
|
||||
*/
|
||||
var fileProcessor: FileProcessor,
|
||||
|
||||
/**
|
||||
* File Utils Wrapper for SHA1
|
||||
*/
|
||||
var fileUtilsWrapper: FileUtilsWrapper,
|
||||
|
||||
/**
|
||||
* UploadedStatusDao for cache query.
|
||||
*/
|
||||
var uploadedStatusDao: UploadedStatusDao,
|
||||
|
||||
/**
|
||||
* NotForUploadDao for database operations
|
||||
*/
|
||||
var notForUploadStatusDao: NotForUploadStatusDao,
|
||||
|
||||
/**
|
||||
* Context for coroutine.
|
||||
*/
|
||||
val context: Context
|
||||
) {
|
||||
|
||||
/**
|
||||
* Maps to facilitate image query.
|
||||
*/
|
||||
private var mapModifiedImageSHA1: HashMap<Image, String> = HashMap()
|
||||
private var mapHolderImage : HashMap<ImageViewHolder, Image> = HashMap()
|
||||
private var mapResult: HashMap<String, Result> = HashMap()
|
||||
private var mapImageSHA1: HashMap<Uri, String> = HashMap()
|
||||
|
||||
/**
|
||||
* Coroutine Scope.
|
||||
*/
|
||||
private val scope : CoroutineScope = MainScope()
|
||||
|
||||
/**
|
||||
* Query image and setUp the view.
|
||||
*/
|
||||
fun queryAndSetView(
|
||||
holder: ImageViewHolder,
|
||||
image: Image,
|
||||
ioDispatcher: CoroutineDispatcher,
|
||||
defaultDispatcher: CoroutineDispatcher,
|
||||
uploadedContributionsList : List<Contribution>
|
||||
class ImageLoader
|
||||
@Inject
|
||||
constructor(
|
||||
/**
|
||||
* MediaClient for SHA1 query.
|
||||
*/
|
||||
var mediaClient: MediaClient,
|
||||
/**
|
||||
* FileProcessor to pre-process the file.
|
||||
*/
|
||||
var fileProcessor: FileProcessor,
|
||||
/**
|
||||
* File Utils Wrapper for SHA1
|
||||
*/
|
||||
var fileUtilsWrapper: FileUtilsWrapper,
|
||||
/**
|
||||
* UploadedStatusDao for cache query.
|
||||
*/
|
||||
var uploadedStatusDao: UploadedStatusDao,
|
||||
/**
|
||||
* NotForUploadDao for database operations
|
||||
*/
|
||||
var notForUploadStatusDao: NotForUploadStatusDao,
|
||||
/**
|
||||
* Context for coroutine.
|
||||
*/
|
||||
val context: Context,
|
||||
) {
|
||||
/**
|
||||
* Maps to facilitate image query.
|
||||
*/
|
||||
private var mapModifiedImageSHA1: HashMap<Image, String> = HashMap()
|
||||
private var mapHolderImage: HashMap<ImageViewHolder, Image> = HashMap()
|
||||
private var mapResult: HashMap<String, Result> = HashMap()
|
||||
private var mapImageSHA1: HashMap<Uri, String> = HashMap()
|
||||
|
||||
/**
|
||||
* Recycler view uses same view holder, so we can identify the latest query image from holder.
|
||||
* Coroutine Scope.
|
||||
*/
|
||||
mapHolderImage[holder] = image
|
||||
holder.itemNotUploaded()
|
||||
holder.itemForUpload()
|
||||
holder.itemNotUploading()
|
||||
private val scope: CoroutineScope = MainScope()
|
||||
|
||||
scope.launch {
|
||||
var result: Result = Result.NOTFOUND
|
||||
/**
|
||||
* Query image and setUp the view.
|
||||
*/
|
||||
fun queryAndSetView(
|
||||
holder: ImageViewHolder,
|
||||
image: Image,
|
||||
ioDispatcher: CoroutineDispatcher,
|
||||
defaultDispatcher: CoroutineDispatcher,
|
||||
uploadedContributionsList: List<Contribution>,
|
||||
) {
|
||||
/**
|
||||
* Recycler view uses same view holder, so we can identify the latest query image from holder.
|
||||
*/
|
||||
mapHolderImage[holder] = image
|
||||
holder.itemNotUploaded()
|
||||
holder.itemForUpload()
|
||||
holder.itemNotUploading()
|
||||
|
||||
if (mapHolderImage[holder] != image) {
|
||||
return@launch
|
||||
}
|
||||
scope.launch {
|
||||
var result: Result = Result.NOTFOUND
|
||||
|
||||
val imageSHA1: String = when (mapImageSHA1[image.uri] != null) {
|
||||
true -> mapImageSHA1[image.uri]!!
|
||||
else -> CustomSelectorUtils.getImageSHA1(
|
||||
image.uri,
|
||||
ioDispatcher,
|
||||
fileUtilsWrapper,
|
||||
context.contentResolver
|
||||
)
|
||||
}
|
||||
mapImageSHA1[image.uri] = imageSHA1
|
||||
|
||||
if (imageSHA1.isEmpty()) {
|
||||
return@launch
|
||||
}
|
||||
val uploadedStatus = getFromUploaded(imageSHA1)
|
||||
|
||||
val sha1 = uploadedStatus?.let {
|
||||
result = getResultFromUploadedStatus(uploadedStatus)
|
||||
uploadedStatus.modifiedImageSHA1
|
||||
} ?: run {
|
||||
if (mapHolderImage[holder] == image) {
|
||||
getSHA1(image, defaultDispatcher)
|
||||
} else {
|
||||
""
|
||||
if (mapHolderImage[holder] != image) {
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
if (mapHolderImage[holder] != image) {
|
||||
return@launch
|
||||
}
|
||||
val imageSHA1: String =
|
||||
when (mapImageSHA1[image.uri] != null) {
|
||||
true -> mapImageSHA1[image.uri]!!
|
||||
else ->
|
||||
CustomSelectorUtils.getImageSHA1(
|
||||
image.uri,
|
||||
ioDispatcher,
|
||||
fileUtilsWrapper,
|
||||
context.contentResolver,
|
||||
)
|
||||
}
|
||||
mapImageSHA1[image.uri] = imageSHA1
|
||||
|
||||
val existsInNotForUploadTable = notForUploadStatusDao.find(imageSHA1)
|
||||
if (imageSHA1.isEmpty()) {
|
||||
return@launch
|
||||
}
|
||||
val uploadedStatus = getFromUploaded(imageSHA1)
|
||||
|
||||
if (result in arrayOf(Result.NOTFOUND, Result.INVALID) && sha1.isNotEmpty()) {
|
||||
when {
|
||||
mapResult[imageSHA1] == null -> {
|
||||
// Query original image.
|
||||
result = checkWhetherFileExistsOnCommonsUsingSHA1(
|
||||
imageSHA1,
|
||||
ioDispatcher,
|
||||
mediaClient
|
||||
)
|
||||
when (result) {
|
||||
is Result.TRUE -> {
|
||||
mapResult[imageSHA1] = Result.TRUE
|
||||
}
|
||||
is Result.ERROR -> {
|
||||
mapResult[imageSHA1] = Result.ERROR
|
||||
}
|
||||
is Result.FALSE -> {
|
||||
mapResult[imageSHA1] = Result.FALSE
|
||||
}
|
||||
is Result.INVALID -> {
|
||||
mapResult[imageSHA1] = Result.INVALID
|
||||
}
|
||||
is Result.NOTFOUND -> {
|
||||
mapResult[imageSHA1] = Result.NOTFOUND
|
||||
}
|
||||
val sha1 =
|
||||
uploadedStatus?.let {
|
||||
result = getResultFromUploadedStatus(uploadedStatus)
|
||||
uploadedStatus.modifiedImageSHA1
|
||||
} ?: run {
|
||||
if (mapHolderImage[holder] == image) {
|
||||
getSHA1(image, defaultDispatcher)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
result = mapResult[imageSHA1]!!
|
||||
}
|
||||
|
||||
if (mapHolderImage[holder] != image) {
|
||||
return@launch
|
||||
}
|
||||
if (result is Result.TRUE) {
|
||||
// Original image found.
|
||||
insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false)
|
||||
} else {
|
||||
|
||||
val existsInNotForUploadTable = notForUploadStatusDao.find(imageSHA1)
|
||||
|
||||
if (result in arrayOf(Result.NOTFOUND, Result.INVALID) && sha1.isNotEmpty()) {
|
||||
when {
|
||||
mapResult[sha1] == null -> {
|
||||
// Original image not found, query modified image.
|
||||
result = checkWhetherFileExistsOnCommonsUsingSHA1(
|
||||
sha1,
|
||||
ioDispatcher,
|
||||
mediaClient
|
||||
)
|
||||
mapResult[imageSHA1] == null -> {
|
||||
// Query original image.
|
||||
result =
|
||||
checkWhetherFileExistsOnCommonsUsingSHA1(
|
||||
imageSHA1,
|
||||
ioDispatcher,
|
||||
mediaClient,
|
||||
)
|
||||
when (result) {
|
||||
is Result.TRUE -> {
|
||||
mapResult[sha1] = Result.TRUE
|
||||
mapResult[imageSHA1] = Result.TRUE
|
||||
}
|
||||
is Result.ERROR -> {
|
||||
mapResult[sha1] = Result.ERROR
|
||||
mapResult[imageSHA1] = Result.ERROR
|
||||
}
|
||||
is Result.FALSE -> {
|
||||
mapResult[sha1] = Result.FALSE
|
||||
mapResult[imageSHA1] = Result.FALSE
|
||||
}
|
||||
is Result.INVALID -> {
|
||||
mapResult[sha1] = Result.INVALID
|
||||
mapResult[imageSHA1] = Result.INVALID
|
||||
}
|
||||
is Result.NOTFOUND -> {
|
||||
mapResult[sha1] = Result.NOTFOUND
|
||||
mapResult[imageSHA1] = Result.NOTFOUND
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
result = mapResult[sha1]!!
|
||||
result = mapResult[imageSHA1]!!
|
||||
}
|
||||
}
|
||||
if (result != Result.ERROR) {
|
||||
insertIntoUploaded(imageSHA1, sha1, false, result is Result.TRUE)
|
||||
if (result is Result.TRUE) {
|
||||
// Original image found.
|
||||
insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false)
|
||||
} else {
|
||||
when {
|
||||
mapResult[sha1] == null -> {
|
||||
// Original image not found, query modified image.
|
||||
result =
|
||||
checkWhetherFileExistsOnCommonsUsingSHA1(
|
||||
sha1,
|
||||
ioDispatcher,
|
||||
mediaClient,
|
||||
)
|
||||
when (result) {
|
||||
is Result.TRUE -> {
|
||||
mapResult[sha1] = Result.TRUE
|
||||
}
|
||||
is Result.ERROR -> {
|
||||
mapResult[sha1] = Result.ERROR
|
||||
}
|
||||
is Result.FALSE -> {
|
||||
mapResult[sha1] = Result.FALSE
|
||||
}
|
||||
is Result.INVALID -> {
|
||||
mapResult[sha1] = Result.INVALID
|
||||
}
|
||||
is Result.NOTFOUND -> {
|
||||
mapResult[sha1] = Result.NOTFOUND
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
result = mapResult[sha1]!!
|
||||
}
|
||||
}
|
||||
if (result != Result.ERROR) {
|
||||
insertIntoUploaded(imageSHA1, sha1, false, result is Result.TRUE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val sharedPreferences: SharedPreferences =
|
||||
context
|
||||
.getSharedPreferences(ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
|
||||
val showAlreadyActionedImages =
|
||||
sharedPreferences.getBoolean(
|
||||
ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY,
|
||||
true
|
||||
)
|
||||
val sharedPreferences: SharedPreferences =
|
||||
context
|
||||
.getSharedPreferences(ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
|
||||
val showAlreadyActionedImages =
|
||||
sharedPreferences.getBoolean(
|
||||
ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY,
|
||||
true,
|
||||
)
|
||||
|
||||
if (mapHolderImage[holder] == image) {
|
||||
if ((result is Result.TRUE) && showAlreadyActionedImages) {
|
||||
holder.itemUploaded()
|
||||
} else holder.itemNotUploaded()
|
||||
|
||||
if ((existsInNotForUploadTable > 0) && showAlreadyActionedImages) {
|
||||
holder.itemNotForUpload()
|
||||
} else holder.itemForUpload()
|
||||
}
|
||||
|
||||
if (uploadedContributionsList.isNotEmpty()) {
|
||||
for (contribution in uploadedContributionsList ) {
|
||||
if (contribution.contentUri == image.uri && showAlreadyActionedImages) {
|
||||
holder.itemUploading()
|
||||
break
|
||||
if (mapHolderImage[holder] == image) {
|
||||
if ((result is Result.TRUE) && showAlreadyActionedImages) {
|
||||
holder.itemUploaded()
|
||||
} else {
|
||||
holder.itemNotUploading()
|
||||
holder.itemNotUploaded()
|
||||
}
|
||||
|
||||
if ((existsInNotForUploadTable > 0) && showAlreadyActionedImages) {
|
||||
holder.itemNotForUpload()
|
||||
} else {
|
||||
holder.itemForUpload()
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadedContributionsList.isNotEmpty()) {
|
||||
for (contribution in uploadedContributionsList) {
|
||||
if (contribution.contentUri == image.uri && showAlreadyActionedImages) {
|
||||
holder.itemUploading()
|
||||
break
|
||||
} else {
|
||||
holder.itemNotUploading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds out the next actionable image position
|
||||
*/
|
||||
suspend fun nextActionableImage(
|
||||
allImages: List<Image>, ioDispatcher: CoroutineDispatcher,
|
||||
defaultDispatcher: CoroutineDispatcher,
|
||||
nextImagePosition: Int,
|
||||
currentlyUploadingImages: List<Contribution>
|
||||
): Int {
|
||||
var next: Int
|
||||
// Traversing from given position to the end
|
||||
for (i in nextImagePosition until allImages.size){
|
||||
val currentImage = allImages[i]
|
||||
/**
|
||||
* Finds out the next actionable image position
|
||||
*/
|
||||
suspend fun nextActionableImage(
|
||||
allImages: List<Image>,
|
||||
ioDispatcher: CoroutineDispatcher,
|
||||
defaultDispatcher: CoroutineDispatcher,
|
||||
nextImagePosition: Int,
|
||||
currentlyUploadingImages: List<Contribution>,
|
||||
): Int {
|
||||
var next: Int
|
||||
// Traversing from given position to the end
|
||||
for (i in nextImagePosition until allImages.size) {
|
||||
val currentImage = allImages[i]
|
||||
|
||||
if (currentlyUploadingImages.any { it.contentUri == currentImage.uri }) {
|
||||
continue // Skip this image as it's currently being uploaded
|
||||
}
|
||||
if (currentlyUploadingImages.any { it.contentUri == currentImage.uri }) {
|
||||
continue // Skip this image as it's currently being uploaded
|
||||
}
|
||||
|
||||
val imageSHA1: String = when (mapImageSHA1[currentImage.uri] != null) {
|
||||
true -> mapImageSHA1[currentImage.uri]!!
|
||||
else -> CustomSelectorUtils.getImageSHA1(
|
||||
currentImage.uri,
|
||||
ioDispatcher,
|
||||
fileUtilsWrapper,
|
||||
context.contentResolver
|
||||
)
|
||||
}
|
||||
next = notForUploadStatusDao.find(imageSHA1)
|
||||
val imageSHA1: String =
|
||||
when (mapImageSHA1[currentImage.uri] != null) {
|
||||
true -> mapImageSHA1[currentImage.uri]!!
|
||||
else ->
|
||||
CustomSelectorUtils.getImageSHA1(
|
||||
currentImage.uri,
|
||||
ioDispatcher,
|
||||
fileUtilsWrapper,
|
||||
context.contentResolver,
|
||||
)
|
||||
}
|
||||
next = notForUploadStatusDao.find(imageSHA1)
|
||||
|
||||
// After checking the image in the not for upload table, if the image is present then
|
||||
// skips the image and moves to next image for checking
|
||||
if(next > 0){
|
||||
continue
|
||||
// After checking the image in the not for upload table, if the image is present then
|
||||
// skips the image and moves to next image for checking
|
||||
if (next > 0) {
|
||||
continue
|
||||
|
||||
// Otherwise checks in already uploaded table
|
||||
} else {
|
||||
next = uploadedStatusDao.findByImageSHA1(imageSHA1, true)
|
||||
// Otherwise checks in already uploaded table
|
||||
} else {
|
||||
next = uploadedStatusDao.findByImageSHA1(imageSHA1, true)
|
||||
|
||||
// If the image is not present in the already uploaded table, checks for its
|
||||
// modified SHA1 in already uploaded table
|
||||
if (next <= 0) {
|
||||
val modifiedImageSha1 = getSHA1(currentImage, defaultDispatcher)
|
||||
next = uploadedStatusDao.findByModifiedImageSHA1(
|
||||
modifiedImageSha1,
|
||||
true
|
||||
)
|
||||
|
||||
// If the modified image SHA1 is not present in the already uploaded table,
|
||||
// returns the position as next actionable image position
|
||||
// If the image is not present in the already uploaded table, checks for its
|
||||
// modified SHA1 in already uploaded table
|
||||
if (next <= 0) {
|
||||
return i
|
||||
val modifiedImageSha1 = getSHA1(currentImage, defaultDispatcher)
|
||||
next =
|
||||
uploadedStatusDao.findByModifiedImageSHA1(
|
||||
modifiedImageSha1,
|
||||
true,
|
||||
)
|
||||
|
||||
// If present in the db then skips iteration for the image and moves to the next
|
||||
// for checking
|
||||
// If the modified image SHA1 is not present in the already uploaded table,
|
||||
// returns the position as next actionable image position
|
||||
if (next <= 0) {
|
||||
return i
|
||||
|
||||
// If present in the db then skips iteration for the image and moves to the next
|
||||
// for checking
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
// If present in the db then skips iteration for the image and moves to the next
|
||||
// for checking
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
// If present in the db then skips iteration for the image and moves to the next
|
||||
// for checking
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SHA1, return SHA1 if available, otherwise generate and store the SHA1.
|
||||
*
|
||||
* @return sha1 of the image
|
||||
*/
|
||||
suspend fun getSHA1(image: Image, defaultDispatcher: CoroutineDispatcher): String {
|
||||
mapModifiedImageSHA1[image]?.let{
|
||||
return it
|
||||
}
|
||||
val sha1 = CustomSelectorUtils
|
||||
.generateModifiedSHA1(image,
|
||||
defaultDispatcher,
|
||||
context,
|
||||
fileProcessor,
|
||||
fileUtilsWrapper
|
||||
)
|
||||
mapModifiedImageSHA1[image] = sha1;
|
||||
return sha1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the uploaded status entry from the database.
|
||||
*/
|
||||
suspend fun getFromUploaded(imageSha1:String): UploadedStatus? {
|
||||
return uploadedStatusDao.getUploadedFromImageSHA1(imageSha1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert into uploaded status table.
|
||||
*/
|
||||
suspend fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){
|
||||
uploadedStatusDao.insertUploaded(
|
||||
UploadedStatus(
|
||||
imageSha1,
|
||||
modifiedImageSha1,
|
||||
imageResult,
|
||||
modifiedImageResult
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get result data from database.
|
||||
*/
|
||||
fun getResultFromUploadedStatus(uploadedStatus: UploadedStatus): Result {
|
||||
if (uploadedStatus.imageResult || uploadedStatus.modifiedImageResult) {
|
||||
return Result.TRUE
|
||||
} else {
|
||||
uploadedStatus.lastUpdated?.let {
|
||||
val duration = Calendar.getInstance().time.time - it.time
|
||||
if (TimeUnit.MILLISECONDS.toDays(duration) < INVALIDATE_DAY_COUNT) {
|
||||
return Result.FALSE
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result.INVALID
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealed Result class.
|
||||
*/
|
||||
sealed class Result {
|
||||
object TRUE : Result()
|
||||
object FALSE : Result()
|
||||
object INVALID : Result()
|
||||
object NOTFOUND : Result()
|
||||
object ERROR : Result()
|
||||
}
|
||||
|
||||
/**
|
||||
* Companion Object
|
||||
*/
|
||||
companion object {
|
||||
/**
|
||||
* Invalidate Day count.
|
||||
* False Database Entries are invalid after INVALIDATE_DAY_COUNT and need to be re-queried.
|
||||
* Get SHA1, return SHA1 if available, otherwise generate and store the SHA1.
|
||||
*
|
||||
* @return sha1 of the image
|
||||
*/
|
||||
const val INVALIDATE_DAY_COUNT: Long = 7
|
||||
}
|
||||
suspend fun getSHA1(
|
||||
image: Image,
|
||||
defaultDispatcher: CoroutineDispatcher,
|
||||
): String {
|
||||
mapModifiedImageSHA1[image]?.let {
|
||||
return it
|
||||
}
|
||||
val sha1 =
|
||||
CustomSelectorUtils
|
||||
.generateModifiedSHA1(
|
||||
image,
|
||||
defaultDispatcher,
|
||||
context,
|
||||
fileProcessor,
|
||||
fileUtilsWrapper,
|
||||
)
|
||||
mapModifiedImageSHA1[image] = sha1
|
||||
return sha1
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Get the uploaded status entry from the database.
|
||||
*/
|
||||
suspend fun getFromUploaded(imageSha1: String): UploadedStatus? = uploadedStatusDao.getUploadedFromImageSHA1(imageSha1)
|
||||
|
||||
/**
|
||||
* Insert into uploaded status table.
|
||||
*/
|
||||
suspend fun insertIntoUploaded(
|
||||
imageSha1: String,
|
||||
modifiedImageSha1: String,
|
||||
imageResult: Boolean,
|
||||
modifiedImageResult: Boolean,
|
||||
) {
|
||||
uploadedStatusDao.insertUploaded(
|
||||
UploadedStatus(
|
||||
imageSha1,
|
||||
modifiedImageSha1,
|
||||
imageResult,
|
||||
modifiedImageResult,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get result data from database.
|
||||
*/
|
||||
fun getResultFromUploadedStatus(uploadedStatus: UploadedStatus): Result {
|
||||
if (uploadedStatus.imageResult || uploadedStatus.modifiedImageResult) {
|
||||
return Result.TRUE
|
||||
} else {
|
||||
uploadedStatus.lastUpdated?.let {
|
||||
val duration = Calendar.getInstance().time.time - it.time
|
||||
if (TimeUnit.MILLISECONDS.toDays(duration) < INVALIDATE_DAY_COUNT) {
|
||||
return Result.FALSE
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result.INVALID
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealed Result class.
|
||||
*/
|
||||
sealed class Result {
|
||||
object TRUE : Result()
|
||||
|
||||
object FALSE : Result()
|
||||
|
||||
object INVALID : Result()
|
||||
|
||||
object NOTFOUND : Result()
|
||||
|
||||
object ERROR : Result()
|
||||
}
|
||||
|
||||
/**
|
||||
* Companion Object
|
||||
*/
|
||||
companion object {
|
||||
/**
|
||||
* Invalidate Day count.
|
||||
* False Database Entries are invalid after INVALIDATE_DAY_COUNT and need to be re-queried.
|
||||
*/
|
||||
const val INVALIDATE_DAY_COUNT: Long = 7
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import androidx.room.RoomDatabase
|
|||
import androidx.room.TypeConverters
|
||||
import fr.free.nrw.commons.contributions.Contribution
|
||||
import fr.free.nrw.commons.contributions.ContributionDao
|
||||
import fr.free.nrw.commons.customselector.database.*
|
||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||
import fr.free.nrw.commons.customselector.database.UploadedStatus
|
||||
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
||||
import fr.free.nrw.commons.nearby.Place
|
||||
import fr.free.nrw.commons.nearby.PlaceDao
|
||||
import fr.free.nrw.commons.review.ReviewDao
|
||||
|
|
@ -17,13 +20,22 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
|
|||
* The database for accessing the respective DAOs
|
||||
*
|
||||
*/
|
||||
@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class], version = 18, exportSchema = false)
|
||||
@Database(
|
||||
entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class],
|
||||
version = 18,
|
||||
exportSchema = false,
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun contributionDao(): ContributionDao
|
||||
|
||||
abstract fun PlaceDao(): PlaceDao
|
||||
abstract fun DepictsDao(): DepictsDao;
|
||||
abstract fun UploadedStatusDao(): UploadedStatusDao;
|
||||
|
||||
abstract fun DepictsDao(): DepictsDao
|
||||
|
||||
abstract fun UploadedStatusDao(): UploadedStatusDao
|
||||
|
||||
abstract fun NotForUploadStatusDao(): NotForUploadStatusDao
|
||||
|
||||
abstract fun ReviewDao(): ReviewDao
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package fr.free.nrw.commons.description
|
||||
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
|
|
@ -29,11 +28,12 @@ import io.reactivex.schedulers.Schedulers
|
|||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
/**
|
||||
* Activity for populating and editing existing description and caption
|
||||
*/
|
||||
class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventListener {
|
||||
class DescriptionEditActivity :
|
||||
BaseActivity(),
|
||||
UploadMediaDetailAdapter.EventListener {
|
||||
/**
|
||||
* Adapter for showing UploadMediaDetail in the activity
|
||||
*/
|
||||
|
|
@ -70,7 +70,7 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
|
||||
private lateinit var binding: ActivityDescriptionEditBinding
|
||||
|
||||
private val REQUEST_CODE_FOR_VOICE_INPUT = 1213
|
||||
private val requestCodeForVoiceInput = 1213
|
||||
|
||||
private var descriptionAndCaptions: ArrayList<UploadMediaDetail>? = null
|
||||
|
||||
|
|
@ -78,7 +78,6 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
|
||||
@Inject lateinit var sessionManager: SessionManager
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
|
@ -110,12 +109,17 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
* @param descriptionAndCaptions list of description and caption
|
||||
*/
|
||||
private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) {
|
||||
uploadMediaDetailAdapter = UploadMediaDetailAdapter(this,
|
||||
savedLanguageValue, descriptionAndCaptions, recentLanguagesDao)
|
||||
uploadMediaDetailAdapter =
|
||||
UploadMediaDetailAdapter(
|
||||
this,
|
||||
savedLanguageValue,
|
||||
descriptionAndCaptions,
|
||||
recentLanguagesDao,
|
||||
)
|
||||
uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int ->
|
||||
showInfoAlert(
|
||||
titleStringID,
|
||||
messageStringId
|
||||
messageStringId,
|
||||
)
|
||||
}
|
||||
uploadMediaDetailAdapter.setEventListener(this)
|
||||
|
|
@ -129,11 +133,17 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
* @param titleStringID Title ID
|
||||
* @param messageStringId Message ID
|
||||
*/
|
||||
private fun showInfoAlert(titleStringID: Int, messageStringId: Int) {
|
||||
private fun showInfoAlert(
|
||||
titleStringID: Int,
|
||||
messageStringId: Int,
|
||||
) {
|
||||
showAlertDialog(
|
||||
this, getString(titleStringID),
|
||||
getString(messageStringId), getString(android.R.string.ok),
|
||||
null, true
|
||||
this,
|
||||
getString(titleStringID),
|
||||
getString(messageStringId),
|
||||
getString(android.R.string.ok),
|
||||
null,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -144,13 +154,13 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
*/
|
||||
override fun addLanguage() {
|
||||
val uploadMediaDetail = UploadMediaDetail()
|
||||
uploadMediaDetail.isManuallyAdded = true //This was manually added by the user
|
||||
uploadMediaDetail.isManuallyAdded = true // This was manually added by the user
|
||||
uploadMediaDetailAdapter.addDescription(uploadMediaDetail)
|
||||
rvDescriptions!!.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1)
|
||||
}
|
||||
|
||||
private fun onBackButtonClicked(view: View) {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
private fun onSubmitButtonClicked(view: View) {
|
||||
|
|
@ -174,10 +184,11 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
val descriptionStart = wikiText!!.substring(0, descriptionIndex + 12)
|
||||
val descriptionToEnd = wikiText!!.substring(descriptionIndex + 12)
|
||||
val descriptionEndIndex = descriptionToEnd.indexOf("\n")
|
||||
val descriptionEnd = wikiText!!.substring(
|
||||
descriptionStart.length
|
||||
+ descriptionEndIndex
|
||||
)
|
||||
val descriptionEnd =
|
||||
wikiText!!.substring(
|
||||
descriptionStart.length +
|
||||
descriptionEndIndex,
|
||||
)
|
||||
buffer.append(descriptionStart)
|
||||
for (i in uploadMediaDetails.indices) {
|
||||
val uploadDetails = uploadMediaDetails[i]
|
||||
|
|
@ -203,65 +214,72 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
* @param updatedWikiText updated wiki text
|
||||
* @param uploadMediaDetails descriptions and captions
|
||||
*/
|
||||
private fun editDescription(media : Media, updatedWikiText : String, uploadMediaDetails : ArrayList<UploadMediaDetail>){
|
||||
|
||||
private fun editDescription(
|
||||
media: Media,
|
||||
updatedWikiText: String,
|
||||
uploadMediaDetails: ArrayList<UploadMediaDetail>,
|
||||
) {
|
||||
try {
|
||||
descriptionEditHelper?.addDescription(
|
||||
applicationContext, media,
|
||||
updatedWikiText
|
||||
)
|
||||
?.subscribeOn(Schedulers.io())
|
||||
descriptionEditHelper
|
||||
?.addDescription(
|
||||
applicationContext,
|
||||
media,
|
||||
updatedWikiText,
|
||||
)?.subscribeOn(Schedulers.io())
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribe(Consumer<Boolean> { s: Boolean? -> Timber.d("Descriptions are added.") })?.let {
|
||||
?.subscribe(Consumer<Boolean> { s: Boolean? -> Timber.d("Descriptions are added.") })
|
||||
?.let {
|
||||
compositeDisposable.add(
|
||||
it
|
||||
it,
|
||||
)
|
||||
}
|
||||
} catch (e : InvalidLoginTokenException) {
|
||||
} catch (e: InvalidLoginTokenException) {
|
||||
val username: String? = sessionManager?.userName
|
||||
val logoutListener = CommonsApplication.BaseLogoutListener(
|
||||
this,
|
||||
getString(R.string.invalid_login_message),
|
||||
username
|
||||
)
|
||||
val logoutListener =
|
||||
CommonsApplication.BaseLogoutListener(
|
||||
this,
|
||||
getString(R.string.invalid_login_message),
|
||||
username,
|
||||
)
|
||||
|
||||
val commonsApplication = CommonsApplication.getInstance()
|
||||
if (commonsApplication != null ){
|
||||
commonsApplication.clearApplicationData(this,logoutListener)
|
||||
if (commonsApplication != null) {
|
||||
commonsApplication.clearApplicationData(this, logoutListener)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val updatedCaptions = LinkedHashMap<String, String>()
|
||||
for (mediaDetail in uploadMediaDetails) {
|
||||
try {
|
||||
compositeDisposable.add(
|
||||
descriptionEditHelper!!.addCaption(
|
||||
applicationContext, media,
|
||||
mediaDetail.languageCode, mediaDetail.captionText
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
descriptionEditHelper!!
|
||||
.addCaption(
|
||||
applicationContext,
|
||||
media,
|
||||
mediaDetail.languageCode,
|
||||
mediaDetail.captionText,
|
||||
).subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { s: Boolean? ->
|
||||
updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText
|
||||
media.captions = updatedCaptions
|
||||
Timber.d("Caption is added.")
|
||||
})
|
||||
}
|
||||
catch (e : InvalidLoginTokenException) {
|
||||
val username = sessionManager.userName
|
||||
val logoutListener = CommonsApplication.BaseLogoutListener(
|
||||
this,
|
||||
getString(R.string.invalid_login_message),
|
||||
username
|
||||
},
|
||||
)
|
||||
} catch (e: InvalidLoginTokenException) {
|
||||
val username = sessionManager.userName
|
||||
val logoutListener =
|
||||
CommonsApplication.BaseLogoutListener(
|
||||
this,
|
||||
getString(R.string.invalid_login_message),
|
||||
username,
|
||||
)
|
||||
|
||||
val commonsApplication = CommonsApplication.getInstance()
|
||||
if (commonsApplication != null ){
|
||||
commonsApplication.clearApplicationData(this,logoutListener)
|
||||
if (commonsApplication != null) {
|
||||
commonsApplication.clearApplicationData(this, logoutListener)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -274,23 +292,29 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
|||
progressDialog!!.show()
|
||||
}
|
||||
|
||||
override
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_CODE_FOR_VOICE_INPUT) {
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?,
|
||||
) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == requestCodeForVoiceInput) {
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
val result = data.getStringArrayListExtra( RecognizerIntent.EXTRA_RESULTS )
|
||||
uploadMediaDetailAdapter.handleSpeechResult(result!![0]) }
|
||||
else { Timber.e("Error %s", resultCode) }
|
||||
val result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
|
||||
uploadMediaDetailAdapter.handleSpeechResult(result!![0])
|
||||
} else {
|
||||
Timber.e("Error %s", resultCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION, uploadMediaDetailAdapter.items as ArrayList<out Parcelable?>)
|
||||
outState.putString(WIKITEXT, wikiText)
|
||||
outState.putString(Prefs.DESCRIPTION_LANGUAGE, savedLanguageValue)
|
||||
//save Media
|
||||
// save Media
|
||||
outState.putParcelable("media", media)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,5 +6,5 @@ package fr.free.nrw.commons.description
|
|||
object EditDescriptionConstants {
|
||||
const val LIST_OF_DESCRIPTION_AND_CAPTION = "description.descriptionAndCaption"
|
||||
const val WIKITEXT = "description.wikiText"
|
||||
const val UPDATED_WIKITEXT = "description.updatedWikiText";
|
||||
}
|
||||
const val UPDATED_WIKITEXT = "description.updatedWikiText"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import dagger.Provides
|
|||
import fr.free.nrw.commons.explore.map.ExploreMapFragment
|
||||
|
||||
@Module
|
||||
class ExploreMapFragmentModule{
|
||||
|
||||
class ExploreMapFragmentModule {
|
||||
@Provides
|
||||
fun ExploreMapFragment.providesActivity(): Activity = activity!!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import dagger.Provides
|
|||
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment
|
||||
|
||||
@Module
|
||||
class NearbyParentFragmentModule{
|
||||
|
||||
class NearbyParentFragmentModule {
|
||||
@Provides
|
||||
fun NearbyParentFragment.providesActivity(): Activity = activity!!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,31 +44,32 @@ class EditActivity : AppCompatActivity() {
|
|||
imageUri = intent.getStringExtra("image") ?: ""
|
||||
vm = ViewModelProvider(this).get(EditViewModel::class.java)
|
||||
val sourceExif = imageUri.toUri().path?.let { ExifInterface(it) }
|
||||
val exifTags = arrayOf(
|
||||
ExifInterface.TAG_APERTURE,
|
||||
ExifInterface.TAG_DATETIME,
|
||||
ExifInterface.TAG_EXPOSURE_TIME,
|
||||
ExifInterface.TAG_FLASH,
|
||||
ExifInterface.TAG_FOCAL_LENGTH,
|
||||
ExifInterface.TAG_GPS_ALTITUDE,
|
||||
ExifInterface.TAG_GPS_ALTITUDE_REF,
|
||||
ExifInterface.TAG_GPS_DATESTAMP,
|
||||
ExifInterface.TAG_GPS_LATITUDE,
|
||||
ExifInterface.TAG_GPS_LATITUDE_REF,
|
||||
ExifInterface.TAG_GPS_LONGITUDE,
|
||||
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
||||
ExifInterface.TAG_GPS_PROCESSING_METHOD,
|
||||
ExifInterface.TAG_GPS_TIMESTAMP,
|
||||
ExifInterface.TAG_IMAGE_LENGTH,
|
||||
ExifInterface.TAG_IMAGE_WIDTH,
|
||||
ExifInterface.TAG_ISO,
|
||||
ExifInterface.TAG_MAKE,
|
||||
ExifInterface.TAG_MODEL,
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.TAG_WHITE_BALANCE,
|
||||
ExifInterface.WHITEBALANCE_AUTO,
|
||||
ExifInterface.WHITEBALANCE_MANUAL
|
||||
)
|
||||
val exifTags =
|
||||
arrayOf(
|
||||
ExifInterface.TAG_APERTURE,
|
||||
ExifInterface.TAG_DATETIME,
|
||||
ExifInterface.TAG_EXPOSURE_TIME,
|
||||
ExifInterface.TAG_FLASH,
|
||||
ExifInterface.TAG_FOCAL_LENGTH,
|
||||
ExifInterface.TAG_GPS_ALTITUDE,
|
||||
ExifInterface.TAG_GPS_ALTITUDE_REF,
|
||||
ExifInterface.TAG_GPS_DATESTAMP,
|
||||
ExifInterface.TAG_GPS_LATITUDE,
|
||||
ExifInterface.TAG_GPS_LATITUDE_REF,
|
||||
ExifInterface.TAG_GPS_LONGITUDE,
|
||||
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
||||
ExifInterface.TAG_GPS_PROCESSING_METHOD,
|
||||
ExifInterface.TAG_GPS_TIMESTAMP,
|
||||
ExifInterface.TAG_IMAGE_LENGTH,
|
||||
ExifInterface.TAG_IMAGE_WIDTH,
|
||||
ExifInterface.TAG_ISO,
|
||||
ExifInterface.TAG_MAKE,
|
||||
ExifInterface.TAG_MODEL,
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.TAG_WHITE_BALANCE,
|
||||
ExifInterface.WHITEBALANCE_AUTO,
|
||||
ExifInterface.WHITEBALANCE_MANUAL,
|
||||
)
|
||||
for (tag in exifTags) {
|
||||
val attribute = sourceExif?.getAttribute(tag.toString())
|
||||
sourceExifAttributeList.add(Pair(tag.toString(), attribute))
|
||||
|
|
@ -87,37 +88,38 @@ class EditActivity : AppCompatActivity() {
|
|||
private fun init() {
|
||||
binding.iv.adjustViewBounds = true
|
||||
binding.iv.scaleType = ImageView.ScaleType.MATRIX
|
||||
binding.iv.post(Runnable {
|
||||
val options = BitmapFactory.Options()
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeFile(imageUri, options)
|
||||
binding.iv.post(
|
||||
Runnable {
|
||||
val options = BitmapFactory.Options()
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeFile(imageUri, options)
|
||||
|
||||
val bitmapWidth = options.outWidth
|
||||
val bitmapHeight = options.outHeight
|
||||
val bitmapWidth = options.outWidth
|
||||
val bitmapHeight = options.outHeight
|
||||
|
||||
// Check if the bitmap dimensions exceed a certain threshold
|
||||
val maxBitmapSize = 2000 // Set your maximum size here
|
||||
if (bitmapWidth > maxBitmapSize || bitmapHeight > maxBitmapSize) {
|
||||
val scaleFactor = calculateScaleFactor(bitmapWidth, bitmapHeight, maxBitmapSize)
|
||||
options.inSampleSize = scaleFactor
|
||||
options.inJustDecodeBounds = false
|
||||
val scaledBitmap = BitmapFactory.decodeFile(imageUri, options)
|
||||
binding.iv.setImageBitmap(scaledBitmap)
|
||||
// Update the ImageView with the scaled bitmap
|
||||
val scale = binding.iv.measuredWidth.toFloat() / scaledBitmap.width.toFloat()
|
||||
binding.iv.layoutParams.height = (scale * scaledBitmap.height).toInt()
|
||||
binding.iv.imageMatrix = scaleMatrix(scale, scale)
|
||||
} else {
|
||||
// Check if the bitmap dimensions exceed a certain threshold
|
||||
val maxBitmapSize = 2000 // Set your maximum size here
|
||||
if (bitmapWidth > maxBitmapSize || bitmapHeight > maxBitmapSize) {
|
||||
val scaleFactor = calculateScaleFactor(bitmapWidth, bitmapHeight, maxBitmapSize)
|
||||
options.inSampleSize = scaleFactor
|
||||
options.inJustDecodeBounds = false
|
||||
val scaledBitmap = BitmapFactory.decodeFile(imageUri, options)
|
||||
binding.iv.setImageBitmap(scaledBitmap)
|
||||
// Update the ImageView with the scaled bitmap
|
||||
val scale = binding.iv.measuredWidth.toFloat() / scaledBitmap.width.toFloat()
|
||||
binding.iv.layoutParams.height = (scale * scaledBitmap.height).toInt()
|
||||
binding.iv.imageMatrix = scaleMatrix(scale, scale)
|
||||
} else {
|
||||
options.inJustDecodeBounds = false
|
||||
val bitmap = BitmapFactory.decodeFile(imageUri, options)
|
||||
binding.iv.setImageBitmap(bitmap)
|
||||
|
||||
options.inJustDecodeBounds = false
|
||||
val bitmap = BitmapFactory.decodeFile(imageUri, options)
|
||||
binding.iv.setImageBitmap(bitmap)
|
||||
|
||||
val scale = binding.iv.measuredWidth.toFloat() / bitmapWidth.toFloat()
|
||||
binding.iv.layoutParams.height = (scale * bitmapHeight).toInt()
|
||||
binding.iv.imageMatrix = scaleMatrix(scale, scale)
|
||||
}
|
||||
})
|
||||
val scale = binding.iv.measuredWidth.toFloat() / bitmapWidth.toFloat()
|
||||
binding.iv.layoutParams.height = (scale * bitmapHeight).toInt()
|
||||
binding.iv.imageMatrix = scaleMatrix(scale, scale)
|
||||
}
|
||||
},
|
||||
)
|
||||
binding.rotateBtn.setOnClickListener {
|
||||
animateImageHeight()
|
||||
}
|
||||
|
|
@ -138,8 +140,16 @@ class EditActivity : AppCompatActivity() {
|
|||
* further rotation actions.
|
||||
*/
|
||||
private fun animateImageHeight() {
|
||||
val drawableWidth: Float = binding.iv.getDrawable().getIntrinsicWidth().toFloat()
|
||||
val drawableHeight: Float = binding.iv.getDrawable().getIntrinsicHeight().toFloat()
|
||||
val drawableWidth: Float =
|
||||
binding.iv
|
||||
.getDrawable()
|
||||
.getIntrinsicWidth()
|
||||
.toFloat()
|
||||
val drawableHeight: Float =
|
||||
binding.iv
|
||||
.getDrawable()
|
||||
.getIntrinsicHeight()
|
||||
.toFloat()
|
||||
val viewWidth: Float = binding.iv.getMeasuredWidth().toFloat()
|
||||
val viewHeight: Float = binding.iv.getMeasuredHeight().toFloat()
|
||||
val rotation = imageRotation % 360
|
||||
|
|
@ -152,7 +162,6 @@ class EditActivity : AppCompatActivity() {
|
|||
Timber.d("Rotation $rotation")
|
||||
Timber.d("new Rotation $newRotation")
|
||||
|
||||
|
||||
if (rotation == 0 || rotation == 180) {
|
||||
imageScale = viewWidth / drawableWidth
|
||||
newImageScale = viewWidth / drawableHeight
|
||||
|
|
@ -169,23 +178,24 @@ class EditActivity : AppCompatActivity() {
|
|||
|
||||
animator.interpolator = AccelerateDecelerateInterpolator()
|
||||
|
||||
animator.addListener(object : AnimatorListener {
|
||||
override fun onAnimationStart(animation: Animator) {
|
||||
binding.rotateBtn.setEnabled(false)
|
||||
}
|
||||
animator.addListener(
|
||||
object : AnimatorListener {
|
||||
override fun onAnimationStart(animation: Animator) {
|
||||
binding.rotateBtn.setEnabled(false)
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
imageRotation = newRotation % 360
|
||||
binding.rotateBtn.setEnabled(true)
|
||||
}
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
imageRotation = newRotation % 360
|
||||
binding.rotateBtn.setEnabled(true)
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator) {
|
||||
}
|
||||
override fun onAnimationCancel(animation: Animator) {
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animator) {
|
||||
}
|
||||
|
||||
})
|
||||
override fun onAnimationRepeat(animation: Animator) {
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
animator.addUpdateListener { animation ->
|
||||
val animVal = animation.animatedValue as Float
|
||||
|
|
@ -195,20 +205,21 @@ class EditActivity : AppCompatActivity() {
|
|||
val animatedScale = complementaryAnimVal * imageScale + animVal * newImageScale
|
||||
val animatedRotation = complementaryAnimVal * rotation + animVal * newRotation
|
||||
binding.iv.getLayoutParams().height = animatedHeight
|
||||
val matrix: Matrix = rotationMatrix(
|
||||
animatedRotation,
|
||||
drawableWidth / 2,
|
||||
drawableHeight / 2
|
||||
)
|
||||
val matrix: Matrix =
|
||||
rotationMatrix(
|
||||
animatedRotation,
|
||||
drawableWidth / 2,
|
||||
drawableHeight / 2,
|
||||
)
|
||||
matrix.postScale(
|
||||
animatedScale,
|
||||
animatedScale,
|
||||
drawableWidth / 2,
|
||||
drawableHeight / 2
|
||||
drawableHeight / 2,
|
||||
)
|
||||
matrix.postTranslate(
|
||||
-(drawableWidth - binding.iv.getMeasuredWidth()) / 2,
|
||||
-(drawableHeight - binding.iv.getMeasuredHeight()) / 2
|
||||
-(drawableHeight - binding.iv.getMeasuredHeight()) / 2,
|
||||
)
|
||||
binding.iv.setImageMatrix(matrix)
|
||||
binding.iv.requestLayout()
|
||||
|
|
@ -228,11 +239,9 @@ class EditActivity : AppCompatActivity() {
|
|||
* as a result, and finishes the current activity.
|
||||
*/
|
||||
fun getRotatedImage() {
|
||||
|
||||
val filePath = imageUri.toUri().path
|
||||
val file = filePath?.let { File(it) }
|
||||
|
||||
|
||||
val rotatedImage = file?.let { vm.rotateImage(imageRotation, it) }
|
||||
if (rotatedImage == null) {
|
||||
Toast.makeText(this, "Failed to rotate to image", Toast.LENGTH_LONG).show()
|
||||
|
|
@ -243,9 +252,9 @@ class EditActivity : AppCompatActivity() {
|
|||
copyExifData(editedImageExif)
|
||||
}
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra("editedImageFilePath", rotatedImage?.toUri()?.path ?: "Error");
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
finish();
|
||||
resultIntent.putExtra("editedImageFilePath", rotatedImage?.toUri()?.path ?: "Error")
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
finish()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -257,7 +266,6 @@ class EditActivity : AppCompatActivity() {
|
|||
* @param editedImageExif The ExifInterface object for the edited image.
|
||||
*/
|
||||
private fun copyExifData(editedImageExif: ExifInterface?) {
|
||||
|
||||
for (attr in sourceExifAttributeList) {
|
||||
Log.d("Tag is ${attr.first}", "Value is ${attr.second}")
|
||||
editedImageExif!!.setAttribute(attr.first, attr.second)
|
||||
|
|
@ -282,7 +290,11 @@ class EditActivity : AppCompatActivity() {
|
|||
* The scale factor ensures that the scaled bitmap will fit within the maximum size
|
||||
* while maintaining aspect ratio.
|
||||
*/
|
||||
private fun calculateScaleFactor(originalWidth: Int, originalHeight: Int, maxSize: Int): Int {
|
||||
private fun calculateScaleFactor(
|
||||
originalWidth: Int,
|
||||
originalHeight: Int,
|
||||
maxSize: Int,
|
||||
): Int {
|
||||
var scaleFactor = 1
|
||||
|
||||
if (originalWidth > maxSize || originalHeight > maxSize) {
|
||||
|
|
@ -295,7 +307,4 @@ class EditActivity : AppCompatActivity() {
|
|||
|
||||
return scaleFactor
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ import java.io.File
|
|||
* This ViewModel class is responsible for managing image editing operations, such as
|
||||
* rotating images. It utilizes a TransformImage implementation to perform image transformations.
|
||||
*/
|
||||
class EditViewModel() : ViewModel() {
|
||||
|
||||
class EditViewModel : ViewModel() {
|
||||
// Ideally should be injected using DI
|
||||
private val transformImage: TransformImage = TransformImageImpl()
|
||||
|
||||
|
|
@ -21,7 +20,8 @@ class EditViewModel() : ViewModel() {
|
|||
* @param imageFile The File representing the image to be rotated.
|
||||
* @return The rotated image File, or null if the rotation operation fails.
|
||||
*/
|
||||
fun rotateImage(degree: Int, imageFile: File): File? {
|
||||
return transformImage.rotateImage(imageFile, degree)
|
||||
}
|
||||
}
|
||||
fun rotateImage(
|
||||
degree: Int,
|
||||
imageFile: File,
|
||||
): File? = transformImage.rotateImage(imageFile, degree)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import java.io.File
|
|||
* implementations to provide specific functionality for tasks like rotating images.
|
||||
*/
|
||||
interface TransformImage {
|
||||
|
||||
/**
|
||||
* Rotates the specified image file by the given degree.
|
||||
*
|
||||
|
|
@ -17,5 +16,8 @@ interface TransformImage {
|
|||
* @param degree The degree by which to rotate the image.
|
||||
* @return The rotated image File, or null if the rotation operation fails.
|
||||
*/
|
||||
fun rotateImage(imageFile: File, degree : Int ):File?
|
||||
}
|
||||
fun rotateImage(
|
||||
imageFile: File,
|
||||
degree: Int,
|
||||
): File?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ import java.io.FileOutputStream
|
|||
* function for rotating images by a specified degree using the LLJTran library. Right now it reads
|
||||
* the input image file, performs the rotation, and saves the rotated image to a new file.
|
||||
*/
|
||||
class TransformImageImpl() : TransformImage {
|
||||
|
||||
class TransformImageImpl : TransformImage {
|
||||
/**
|
||||
* Rotates the specified image file by the given degree.
|
||||
*
|
||||
|
|
@ -24,46 +23,50 @@ class TransformImageImpl() : TransformImage {
|
|||
* @param degree The degree by which to rotate the image.
|
||||
* @return The rotated image File, or null if the rotation operation fails.
|
||||
*/
|
||||
override fun rotateImage(imageFile: File, degree : Int): File? {
|
||||
|
||||
override fun rotateImage(
|
||||
imageFile: File,
|
||||
degree: Int,
|
||||
): File? {
|
||||
Timber.tag("Trying to rotate image").d("Starting")
|
||||
|
||||
val path = Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS
|
||||
)
|
||||
val path =
|
||||
Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
)
|
||||
|
||||
val imagePath = System.currentTimeMillis()
|
||||
val file: File = File(path, "$imagePath.jpg")
|
||||
|
||||
val output = file
|
||||
|
||||
val rotated = try {
|
||||
val lljTran = LLJTran(imageFile)
|
||||
lljTran.read(
|
||||
LLJTran.READ_ALL,
|
||||
false,
|
||||
) // This could throw an LLJTranException. I am not catching it for now... Let's see.
|
||||
lljTran.transform(
|
||||
when(degree){
|
||||
90 -> LLJTran.ROT_90
|
||||
180 -> LLJTran.ROT_180
|
||||
270 -> LLJTran.ROT_270
|
||||
else -> {
|
||||
LLJTran.ROT_90
|
||||
}
|
||||
},
|
||||
LLJTran.OPT_DEFAULTS or LLJTran.OPT_XFORM_ORIENTATION
|
||||
)
|
||||
BufferedOutputStream(FileOutputStream(output)).use { writer ->
|
||||
lljTran.save(writer, LLJTran.OPT_WRITE_ALL )
|
||||
val rotated =
|
||||
try {
|
||||
val lljTran = LLJTran(imageFile)
|
||||
lljTran.read(
|
||||
LLJTran.READ_ALL,
|
||||
false,
|
||||
) // This could throw an LLJTranException. I am not catching it for now... Let's see.
|
||||
lljTran.transform(
|
||||
when (degree) {
|
||||
90 -> LLJTran.ROT_90
|
||||
180 -> LLJTran.ROT_180
|
||||
270 -> LLJTran.ROT_270
|
||||
else -> {
|
||||
LLJTran.ROT_90
|
||||
}
|
||||
},
|
||||
LLJTran.OPT_DEFAULTS or LLJTran.OPT_XFORM_ORIENTATION,
|
||||
)
|
||||
BufferedOutputStream(FileOutputStream(output)).use { writer ->
|
||||
lljTran.save(writer, LLJTran.OPT_WRITE_ALL)
|
||||
}
|
||||
lljTran.freeMemory()
|
||||
true
|
||||
} catch (e: LLJTranException) {
|
||||
Timber.tag("Error").d(e)
|
||||
return null
|
||||
false
|
||||
}
|
||||
lljTran.freeMemory()
|
||||
true
|
||||
} catch (e: LLJTranException) {
|
||||
Timber.tag("Error").d(e)
|
||||
return null
|
||||
false
|
||||
}
|
||||
|
||||
if (rotated) {
|
||||
Timber.tag("Done rotating image").d("Done")
|
||||
|
|
|
|||
|
|
@ -15,14 +15,11 @@ import fr.free.nrw.commons.explore.media.SearchMediaFragmentPresenterImpl
|
|||
@Module
|
||||
abstract class SearchModule {
|
||||
@Binds
|
||||
abstract fun SearchDepictionsFragmentPresenterImpl.bindsSearchDepictionsFragmentPresenter()
|
||||
: SearchDepictionsFragmentPresenter
|
||||
abstract fun SearchDepictionsFragmentPresenterImpl.bindsSearchDepictionsFragmentPresenter(): SearchDepictionsFragmentPresenter
|
||||
|
||||
@Binds
|
||||
abstract fun SearchCategoriesFragmentPresenterImpl.bindsSearchCategoriesFragmentPresenter()
|
||||
: SearchCategoriesFragmentPresenter
|
||||
abstract fun SearchCategoriesFragmentPresenterImpl.bindsSearchCategoriesFragmentPresenter(): SearchCategoriesFragmentPresenter
|
||||
|
||||
@Binds
|
||||
abstract fun SearchMediaFragmentPresenterImpl.bindsSearchMediaFragmentPresenter()
|
||||
: SearchMediaFragmentPresenter
|
||||
abstract fun SearchMediaFragmentPresenterImpl.bindsSearchMediaFragmentPresenter(): SearchMediaFragmentPresenter
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,19 +9,14 @@ import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesPresenterIm
|
|||
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesPresenter
|
||||
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesPresenterImpl
|
||||
|
||||
|
||||
@Module
|
||||
abstract class CategoriesModule {
|
||||
@Binds
|
||||
abstract fun CategoryMediaPresenterImpl.bindsCategoryMediaPresenter(): CategoryMediaPresenter
|
||||
|
||||
@Binds
|
||||
abstract fun CategoryMediaPresenterImpl.bindsCategoryMediaPresenter()
|
||||
: CategoryMediaPresenter
|
||||
abstract fun SubCategoriesPresenterImpl.bindsSubCategoriesPresenter(): SubCategoriesPresenter
|
||||
|
||||
@Binds
|
||||
abstract fun SubCategoriesPresenterImpl.bindsSubCategoriesPresenter()
|
||||
: SubCategoriesPresenter
|
||||
|
||||
@Binds
|
||||
abstract fun ParentCategoriesPresenterImpl.bindsParentCategoriesPresenter()
|
||||
: ParentCategoriesPresenter
|
||||
abstract fun ParentCategoriesPresenterImpl.bindsParentCategoriesPresenter(): ParentCategoriesPresenter
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import fr.free.nrw.commons.R
|
|||
import fr.free.nrw.commons.category.CategoryDetailsActivity
|
||||
import fr.free.nrw.commons.explore.paging.BasePagingFragment
|
||||
|
||||
|
||||
abstract class PageableCategoryFragment : BasePagingFragment<String>() {
|
||||
override val errorTextId: Int = R.string.error_loading_categories
|
||||
override val pagedListAdapter by lazy {
|
||||
|
|
|
|||
|
|
@ -8,31 +8,44 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import fr.free.nrw.commons.category.CATEGORY_PREFIX
|
||||
import fr.free.nrw.commons.databinding.ItemRecentSearchesBinding
|
||||
|
||||
class PagedSearchCategoriesAdapter(private val onCategoryClicked: (String) -> Unit) :
|
||||
PagedListAdapter<String, CategoryItemViewHolder>(PagedSearchCategoriesDiffUtilCallback) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = CategoryItemViewHolder(
|
||||
ItemRecentSearchesBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
class PagedSearchCategoriesAdapter(
|
||||
private val onCategoryClicked: (String) -> Unit,
|
||||
) : PagedListAdapter<String, CategoryItemViewHolder>(PagedSearchCategoriesDiffUtilCallback) {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
) = CategoryItemViewHolder(
|
||||
ItemRecentSearchesBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: CategoryItemViewHolder, position: Int) {
|
||||
override fun onBindViewHolder(
|
||||
holder: CategoryItemViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
holder.bind(getItem(position)!!, onCategoryClicked)
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryItemViewHolder(
|
||||
private val binding: ItemRecentSearchesBinding
|
||||
private val binding: ItemRecentSearchesBinding,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: String, onCategoryClicked: (String) -> Unit) = with(binding) {
|
||||
fun bind(
|
||||
item: String,
|
||||
onCategoryClicked: (String) -> Unit,
|
||||
) = with(binding) {
|
||||
root.setOnClickListener { onCategoryClicked(item) }
|
||||
textView1.text = item.substringAfter(CATEGORY_PREFIX)
|
||||
}
|
||||
}
|
||||
|
||||
private object PagedSearchCategoriesDiffUtilCallback : DiffUtil.ItemCallback<String>() {
|
||||
override fun areItemsTheSame(oldItem: String, newItem: String) =
|
||||
oldItem == newItem
|
||||
override fun areItemsTheSame(
|
||||
oldItem: String,
|
||||
newItem: String,
|
||||
) = oldItem == newItem
|
||||
|
||||
override fun areContentsTheSame(oldItem: String, newItem: String) =
|
||||
oldItem == newItem
|
||||
override fun areContentsTheSame(
|
||||
oldItem: String,
|
||||
newItem: String,
|
||||
) = oldItem == newItem
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,16 +6,17 @@ import fr.free.nrw.commons.category.CATEGORY_PREFIX
|
|||
import fr.free.nrw.commons.explore.media.PageableMediaFragment
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class CategoriesMediaFragment : PageableMediaFragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var presenter: CategoryMediaPresenter
|
||||
|
||||
override val injectedPresenter
|
||||
get() = presenter
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
override fun onViewCreated(
|
||||
view: View,
|
||||
savedInstanceState: Bundle?,
|
||||
) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ interface CategoryMediaPresenter : PagingContract.Presenter<Media>
|
|||
/**
|
||||
* Presenter for DepictedImagesFragment
|
||||
*/
|
||||
class CategoryMediaPresenterImpl @Inject constructor(
|
||||
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
||||
dataSourceFactory: PageableCategoriesMediaDataSource
|
||||
) : BasePagingPresenter<Media>(mainThreadScheduler, dataSourceFactory),
|
||||
CategoryMediaPresenter
|
||||
class CategoryMediaPresenterImpl
|
||||
@Inject
|
||||
constructor(
|
||||
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
||||
dataSourceFactory: PageableCategoriesMediaDataSource,
|
||||
) : BasePagingPresenter<Media>(mainThreadScheduler, dataSourceFactory),
|
||||
CategoryMediaPresenter
|
||||
|
|
|
|||
|
|
@ -7,14 +7,16 @@ import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
|
|||
import fr.free.nrw.commons.media.MediaClient
|
||||
import javax.inject.Inject
|
||||
|
||||
class PageableCategoriesMediaDataSource @Inject constructor(
|
||||
liveDataConverter: LiveDataConverter,
|
||||
private val mediaClient: MediaClient
|
||||
) : PageableBaseDataSource<Media>(liveDataConverter) {
|
||||
override val loadFunction: LoadFunction<Media> = { loadSize: Int, startPosition: Int ->
|
||||
if(startPosition == 0){
|
||||
mediaClient.resetCategoryContinuation(query)
|
||||
class PageableCategoriesMediaDataSource
|
||||
@Inject
|
||||
constructor(
|
||||
liveDataConverter: LiveDataConverter,
|
||||
private val mediaClient: MediaClient,
|
||||
) : PageableBaseDataSource<Media>(liveDataConverter) {
|
||||
override val loadFunction: LoadFunction<Media> = { loadSize: Int, startPosition: Int ->
|
||||
if (startPosition == 0) {
|
||||
mediaClient.resetCategoryContinuation(query)
|
||||
}
|
||||
mediaClient.getMediaListFromCategory(query).blockingGet()
|
||||
}
|
||||
mediaClient.getMediaListFromCategory(query).blockingGet()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,15 +5,16 @@ import fr.free.nrw.commons.explore.paging.LiveDataConverter
|
|||
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
|
||||
import javax.inject.Inject
|
||||
|
||||
class PageableParentCategoriesDataSource @Inject constructor(
|
||||
liveDataConverter: LiveDataConverter,
|
||||
val categoryClient: CategoryClient
|
||||
) : PageableBaseDataSource<String>(liveDataConverter) {
|
||||
|
||||
override val loadFunction = { loadSize: Int, startPosition: Int ->
|
||||
if (startPosition == 0) {
|
||||
categoryClient.resetParentCategoryContinuation(query)
|
||||
class PageableParentCategoriesDataSource
|
||||
@Inject
|
||||
constructor(
|
||||
liveDataConverter: LiveDataConverter,
|
||||
val categoryClient: CategoryClient,
|
||||
) : PageableBaseDataSource<String>(liveDataConverter) {
|
||||
override val loadFunction = { loadSize: Int, startPosition: Int ->
|
||||
if (startPosition == 0) {
|
||||
categoryClient.resetParentCategoryContinuation(query)
|
||||
}
|
||||
categoryClient.getParentCategoryList(query).blockingGet().map { it.name }
|
||||
}
|
||||
categoryClient.getParentCategoryList(query).blockingGet().map { it.name }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ import fr.free.nrw.commons.category.CATEGORY_PREFIX
|
|||
import fr.free.nrw.commons.explore.categories.PageableCategoryFragment
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class ParentCategoriesFragment : PageableCategoryFragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var presenter: ParentCategoriesPresenter
|
||||
|
||||
|
|
@ -18,9 +16,11 @@ class ParentCategoriesFragment : PageableCategoryFragment() {
|
|||
|
||||
override fun getEmptyText(query: String) = getString(R.string.no_parentcategory_found)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
override fun onViewCreated(
|
||||
view: View,
|
||||
savedInstanceState: Bundle?,
|
||||
) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@ import io.reactivex.Scheduler
|
|||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
|
||||
interface ParentCategoriesPresenter : PagingContract.Presenter<String>
|
||||
|
||||
class ParentCategoriesPresenterImpl @Inject constructor(
|
||||
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
||||
dataSourceFactory: PageableParentCategoriesDataSource
|
||||
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
|
||||
ParentCategoriesPresenter
|
||||
class ParentCategoriesPresenterImpl
|
||||
@Inject
|
||||
constructor(
|
||||
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
||||
dataSourceFactory: PageableParentCategoriesDataSource,
|
||||
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
|
||||
ParentCategoriesPresenter
|
||||
|
|
|
|||
|
|
@ -5,13 +5,16 @@ import fr.free.nrw.commons.explore.paging.LiveDataConverter
|
|||
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
|
||||
import javax.inject.Inject
|
||||
|
||||
class PageableSearchCategoriesDataSource @Inject constructor(
|
||||
liveDataConverter: LiveDataConverter,
|
||||
val categoryClient: CategoryClient
|
||||
) : PageableBaseDataSource<String>(liveDataConverter) {
|
||||
|
||||
override val loadFunction = { loadSize: Int, startPosition: Int ->
|
||||
categoryClient.searchCategories(query, loadSize, startPosition).blockingGet()
|
||||
.map { it.name }
|
||||
class PageableSearchCategoriesDataSource
|
||||
@Inject
|
||||
constructor(
|
||||
liveDataConverter: LiveDataConverter,
|
||||
val categoryClient: CategoryClient,
|
||||
) : PageableBaseDataSource<String>(liveDataConverter) {
|
||||
override val loadFunction = { loadSize: Int, startPosition: Int ->
|
||||
categoryClient
|
||||
.searchCategories(query, loadSize, startPosition)
|
||||
.blockingGet()
|
||||
.map { it.name }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ import javax.inject.Named
|
|||
|
||||
interface SearchCategoriesFragmentPresenter : PagingContract.Presenter<String>
|
||||
|
||||
class SearchCategoriesFragmentPresenterImpl @Inject constructor(
|
||||
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
||||
dataSourceFactory: PageableSearchCategoriesDataSource
|
||||
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
|
||||
SearchCategoriesFragmentPresenter
|
||||
class SearchCategoriesFragmentPresenterImpl
|
||||
@Inject
|
||||
constructor(
|
||||
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
||||
dataSourceFactory: PageableSearchCategoriesDataSource,
|
||||
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
|
||||
SearchCategoriesFragmentPresenter
|
||||
|
|
|
|||
|
|
@ -5,15 +5,16 @@ import fr.free.nrw.commons.explore.paging.LiveDataConverter
|
|||
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
|
||||
import javax.inject.Inject
|
||||
|
||||
class PageableSubCategoriesDataSource @Inject constructor(
|
||||
liveDataConverter: LiveDataConverter,
|
||||
val categoryClient: CategoryClient
|
||||
) : PageableBaseDataSource<String>(liveDataConverter) {
|
||||
|
||||
override val loadFunction = { loadSize: Int, startPosition: Int ->
|
||||
if (startPosition == 0) {
|
||||
categoryClient.resetSubCategoryContinuation(query)
|
||||
class PageableSubCategoriesDataSource
|
||||
@Inject
|
||||
constructor(
|
||||
liveDataConverter: LiveDataConverter,
|
||||
val categoryClient: CategoryClient,
|
||||
) : PageableBaseDataSource<String>(liveDataConverter) {
|
||||
override val loadFunction = { loadSize: Int, startPosition: Int ->
|
||||
if (startPosition == 0) {
|
||||
categoryClient.resetSubCategoryContinuation(query)
|
||||
}
|
||||
categoryClient.getSubCategoryList(query).blockingGet().map { it.name }
|
||||
}
|
||||
categoryClient.getSubCategoryList(query).blockingGet().map { it.name }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ import fr.free.nrw.commons.category.CATEGORY_PREFIX
|
|||
import fr.free.nrw.commons.explore.categories.PageableCategoryFragment
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class SubCategoriesFragment : PageableCategoryFragment() {
|
||||
|
||||
@Inject lateinit var presenter: SubCategoriesPresenter
|
||||
|
||||
override val injectedPresenter
|
||||
|
|
@ -17,7 +15,10 @@ class SubCategoriesFragment : PageableCategoryFragment() {
|
|||
|
||||
override fun getEmptyText(query: String) = getString(R.string.no_subcategory_found)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
override fun onViewCreated(
|
||||
view: View,
|
||||
savedInstanceState: Bundle?,
|
||||
) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ import javax.inject.Named
|
|||
|
||||
interface SubCategoriesPresenter : PagingContract.Presenter<String>
|
||||
|
||||
class SubCategoriesPresenterImpl @Inject constructor(
|
||||
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
||||
dataSourceFactory: PageableSubCategoriesDataSource
|
||||
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
|
||||
SubCategoriesPresenter
|
||||
class SubCategoriesPresenterImpl
|
||||
@Inject
|
||||
constructor(
|
||||
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
||||
dataSourceFactory: PageableSubCategoriesDataSource,
|
||||
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
|
||||
SubCategoriesPresenter
|
||||
|
|
|
|||
|
|
@ -9,22 +9,31 @@ import fr.free.nrw.commons.R
|
|||
import fr.free.nrw.commons.databinding.ItemDepictionsBinding
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||
|
||||
class DepictionAdapter(private val onDepictionClicked: (DepictedItem) -> Unit) :
|
||||
PagedListAdapter<DepictedItem, DepictedItemViewHolder>(DepictionDiffUtilCallback) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = DepictedItemViewHolder(
|
||||
ItemDepictionsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
class DepictionAdapter(
|
||||
private val onDepictionClicked: (DepictedItem) -> Unit,
|
||||
) : PagedListAdapter<DepictedItem, DepictedItemViewHolder>(DepictionDiffUtilCallback) {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
) = DepictedItemViewHolder(
|
||||
ItemDepictionsBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: DepictedItemViewHolder, position: Int) {
|
||||
override fun onBindViewHolder(
|
||||
holder: DepictedItemViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
holder.bind(getItem(position)!!, onDepictionClicked)
|
||||
}
|
||||
}
|
||||
|
||||
class DepictedItemViewHolder(
|
||||
private val binding: ItemDepictionsBinding
|
||||
private val binding: ItemDepictionsBinding,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: DepictedItem, onDepictionClicked: (DepictedItem) -> Unit) = with(binding) {
|
||||
fun bind(
|
||||
item: DepictedItem,
|
||||
onDepictionClicked: (DepictedItem) -> Unit,
|
||||
) = with(binding) {
|
||||
root.setOnClickListener { onDepictionClicked(item) }
|
||||
depictsLabel.text = item.name
|
||||
description.text = item.description
|
||||
|
|
@ -37,9 +46,13 @@ class DepictedItemViewHolder(
|
|||
}
|
||||
|
||||
private object DepictionDiffUtilCallback : DiffUtil.ItemCallback<DepictedItem>() {
|
||||
override fun areItemsTheSame(oldItem: DepictedItem, newItem: DepictedItem) =
|
||||
oldItem.id == newItem.id
|
||||
override fun areItemsTheSame(
|
||||
oldItem: DepictedItem,
|
||||
newItem: DepictedItem,
|
||||
) = oldItem.id == newItem.id
|
||||
|
||||
override fun areContentsTheSame(oldItem: DepictedItem, newItem: DepictedItem) =
|
||||
oldItem == newItem
|
||||
override fun areContentsTheSame(
|
||||
oldItem: DepictedItem,
|
||||
newItem: DepictedItem,
|
||||
) = oldItem == newItem
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,16 +14,12 @@ import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsPresenterIm
|
|||
*/
|
||||
@Module
|
||||
abstract class DepictionModule {
|
||||
@Binds
|
||||
abstract fun ParentDepictionsPresenterImpl.bindsParentDepictionPresenter(): ParentDepictionsPresenter
|
||||
|
||||
@Binds
|
||||
abstract fun ParentDepictionsPresenterImpl.bindsParentDepictionPresenter()
|
||||
: ParentDepictionsPresenter
|
||||
abstract fun ChildDepictionsPresenterImpl.bindsChildDepictionPresenter(): ChildDepictionsPresenter
|
||||
|
||||
@Binds
|
||||
abstract fun ChildDepictionsPresenterImpl.bindsChildDepictionPresenter()
|
||||
: ChildDepictionsPresenter
|
||||
|
||||
@Binds
|
||||
abstract fun DepictedImagesPresenterImpl.bindsDepictedImagesContractPresenter()
|
||||
: DepictedImagesPresenter
|
||||
abstract fun DepictedImagesPresenterImpl.bindsDepictedImagesContractPresenter(): DepictedImagesPresenter
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import fr.free.nrw.commons.wikidata.WikidataProperties
|
|||
import fr.free.nrw.commons.wikidata.model.DataValue
|
||||
import fr.free.nrw.commons.wikidata.model.DepictSearchItem
|
||||
import fr.free.nrw.commons.wikidata.model.Entities
|
||||
import fr.free.nrw.commons.wikidata.model.Statement_partial
|
||||
import fr.free.nrw.commons.wikidata.model.StatementPartial
|
||||
import io.reactivex.Single
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -20,89 +20,101 @@ import javax.inject.Singleton
|
|||
* Depicts Client to handle custom calls to Commons Wikibase APIs
|
||||
*/
|
||||
@Singleton
|
||||
class DepictsClient @Inject constructor(private val depictsInterface: DepictsInterface) {
|
||||
|
||||
/**
|
||||
* Search for depictions using the search item
|
||||
* @return list of depicted items
|
||||
*/
|
||||
fun searchForDepictions(query: String?, limit: Int, offset: Int): Single<List<DepictedItem>> {
|
||||
val language = Locale.getDefault().language
|
||||
return depictsInterface.searchForDepicts(query, "$limit", language, language, "$offset")
|
||||
.map { it.search.joinToString("|", transform = DepictSearchItem::id) }
|
||||
.mapToDepictions()
|
||||
}
|
||||
|
||||
fun getEntities(ids: String): Single<Entities> {
|
||||
return depictsInterface.getEntities(ids)
|
||||
}
|
||||
|
||||
fun toDepictions(sparqlResponse: Single<SparqlResponse>): Single<List<DepictedItem>> {
|
||||
return sparqlResponse.map {
|
||||
it.results.bindings.joinToString("|", transform = Binding::id)
|
||||
}.mapToDepictions()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches Entities from ids ex. "Q1233|Q546" and converts them into DepictedItem
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
private fun Single<String>.mapToDepictions() =
|
||||
flatMap(::getEntities)
|
||||
.map { entities ->
|
||||
entities.entities().values.map { entity ->
|
||||
mapToDepictItem(entity)
|
||||
}
|
||||
class DepictsClient
|
||||
@Inject
|
||||
constructor(
|
||||
private val depictsInterface: DepictsInterface,
|
||||
) {
|
||||
/**
|
||||
* Search for depictions using the search item
|
||||
* @return list of depicted items
|
||||
*/
|
||||
fun searchForDepictions(
|
||||
query: String?,
|
||||
limit: Int,
|
||||
offset: Int,
|
||||
): Single<List<DepictedItem>> {
|
||||
val language = Locale.getDefault().language
|
||||
return depictsInterface
|
||||
.searchForDepicts(query, "$limit", language, language, "$offset")
|
||||
.map { it.search.joinToString("|", transform = DepictSearchItem::id) }
|
||||
.mapToDepictions()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert different entities into DepictedItem
|
||||
*/
|
||||
private fun mapToDepictItem(entity: Entities.Entity): DepictedItem {
|
||||
return if (entity.descriptions().byLanguageOrFirstOrEmpty() == "") {
|
||||
val instanceOfIDs = entity[WikidataProperties.INSTANCE_OF]
|
||||
.toIds()
|
||||
if (instanceOfIDs.isNotEmpty()) {
|
||||
val entities: Entities = getEntities(instanceOfIDs[0]).blockingGet()
|
||||
val nameAsDescription = entities.entities().values.first().labels()
|
||||
.byLanguageOrFirstOrEmpty()
|
||||
DepictedItem(
|
||||
entity,
|
||||
entity.labels().byLanguageOrFirstOrEmpty(),
|
||||
nameAsDescription
|
||||
)
|
||||
fun getEntities(ids: String): Single<Entities> = depictsInterface.getEntities(ids)
|
||||
|
||||
fun toDepictions(sparqlResponse: Single<SparqlResponse>): Single<List<DepictedItem>> =
|
||||
sparqlResponse
|
||||
.map {
|
||||
it.results.bindings.joinToString("|", transform = Binding::id)
|
||||
}.mapToDepictions()
|
||||
|
||||
/**
|
||||
* Fetches Entities from ids ex. "Q1233|Q546" and converts them into DepictedItem
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
private fun Single<String>.mapToDepictions() =
|
||||
flatMap(::getEntities)
|
||||
.map { entities ->
|
||||
entities.entities().values.map { entity ->
|
||||
mapToDepictItem(entity)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert different entities into DepictedItem
|
||||
*/
|
||||
private fun mapToDepictItem(entity: Entities.Entity): DepictedItem =
|
||||
if (entity.descriptions().byLanguageOrFirstOrEmpty() == "") {
|
||||
val instanceOfIDs =
|
||||
entity[WikidataProperties.INSTANCE_OF]
|
||||
.toIds()
|
||||
if (instanceOfIDs.isNotEmpty()) {
|
||||
val entities: Entities = getEntities(instanceOfIDs[0]).blockingGet()
|
||||
val nameAsDescription =
|
||||
entities
|
||||
.entities()
|
||||
.values
|
||||
.first()
|
||||
.labels()
|
||||
.byLanguageOrFirstOrEmpty()
|
||||
DepictedItem(
|
||||
entity,
|
||||
entity.labels().byLanguageOrFirstOrEmpty(),
|
||||
nameAsDescription,
|
||||
)
|
||||
} else {
|
||||
DepictedItem(
|
||||
entity,
|
||||
entity.labels().byLanguageOrFirstOrEmpty(),
|
||||
"",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
DepictedItem(
|
||||
entity,
|
||||
entity.labels().byLanguageOrFirstOrEmpty(),
|
||||
""
|
||||
entity.descriptions().byLanguageOrFirstOrEmpty(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
DepictedItem(
|
||||
entity,
|
||||
entity.labels().byLanguageOrFirstOrEmpty(),
|
||||
entity.descriptions().byLanguageOrFirstOrEmpty()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to get Entities.Label by default language from the map.
|
||||
* If that returns null, Tries to retrieve first element from the map.
|
||||
* If that still returns null, function returns "".
|
||||
*/
|
||||
private fun Map<String, Entities.Label>.byLanguageOrFirstOrEmpty() =
|
||||
let {
|
||||
it[Locale.getDefault().language] ?: it.values.firstOrNull() }?.value() ?: ""
|
||||
/**
|
||||
* Tries to get Entities.Label by default language from the map.
|
||||
* If that returns null, Tries to retrieve first element from the map.
|
||||
* If that still returns null, function returns "".
|
||||
*/
|
||||
private fun Map<String, Entities.Label>.byLanguageOrFirstOrEmpty() =
|
||||
let {
|
||||
it[Locale.getDefault().language] ?: it.values.firstOrNull()
|
||||
}?.value() ?: ""
|
||||
|
||||
/**
|
||||
* returns list of id ex. "Q2323" from Statement_partial
|
||||
*/
|
||||
private fun List<Statement_partial>?.toIds(): List<String> {
|
||||
return this?.map { it.mainSnak.dataValue }
|
||||
?.filterIsInstance<DataValue.EntityId>()
|
||||
?.map { it.value.id }
|
||||
?: emptyList()
|
||||
/**
|
||||
* returns list of id ex. "Q2323" from Statement_partial
|
||||
*/
|
||||
private fun List<StatementPartial>?.toIds(): List<String> =
|
||||
this
|
||||
?.map { it.mainSnak.dataValue }
|
||||
?.filterIsInstance<DataValue.EntityId>()
|
||||
?.map { it.value.id }
|
||||
?: emptyList()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue