mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +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)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class AboutActivityTest {
|
class AboutActivityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(AboutActivity::class.java)
|
var activityRule: ActivityTestRule<*> = ActivityTestRule(AboutActivity::class.java)
|
||||||
|
|
||||||
|
|
@ -36,7 +35,8 @@ class AboutActivityTest {
|
||||||
device.setOrientationNatural()
|
device.setOrientationNatural()
|
||||||
device.freezeRotation()
|
device.freezeRotation()
|
||||||
Intents.init()
|
Intents.init()
|
||||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
Intents
|
||||||
|
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,11 +47,12 @@ class AboutActivityTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testBuildNumber() {
|
fun testBuildNumber() {
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.about_version))
|
Espresso
|
||||||
|
.onView(ViewMatchers.withId(R.id.about_version))
|
||||||
.check(
|
.check(
|
||||||
ViewAssertions.matches(
|
ViewAssertions.matches(
|
||||||
withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha())
|
withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha()),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,8 +62,8 @@ class AboutActivityTest {
|
||||||
Intents.intended(
|
Intents.intended(
|
||||||
CoreMatchers.allOf(
|
CoreMatchers.allOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||||
IntentMatchers.hasData(Urls.WEBSITE_URL)
|
IntentMatchers.hasData(Urls.WEBSITE_URL),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,8 +74,8 @@ class AboutActivityTest {
|
||||||
CoreMatchers.anyOf(
|
CoreMatchers.anyOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||||
IntentMatchers.hasData(Urls.FACEBOOK_WEB_URL),
|
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(
|
Intents.intended(
|
||||||
CoreMatchers.allOf(
|
CoreMatchers.allOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
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(
|
Intents.intended(
|
||||||
CoreMatchers.allOf(
|
CoreMatchers.allOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
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(
|
Intents.intended(
|
||||||
CoreMatchers.allOf(
|
CoreMatchers.allOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
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(
|
Intents.intended(
|
||||||
CoreMatchers.allOf(
|
CoreMatchers.allOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||||
IntentMatchers.hasData(Urls.CREDITS_URL)
|
IntentMatchers.hasData(Urls.CREDITS_URL),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testLaunchUserGuide() {
|
fun testLaunchUserGuide() {
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.about_user_guide)).perform(ViewActions.click())
|
Espresso.onView(ViewMatchers.withId(R.id.about_user_guide)).perform(ViewActions.click())
|
||||||
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
Intents.intended(
|
||||||
IntentMatchers.hasData(Urls.USER_GUIDE_URL)))
|
CoreMatchers.allOf(
|
||||||
|
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||||
|
IntentMatchers.hasData(Urls.USER_GUIDE_URL),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testLaunchAboutFaq() {
|
fun testLaunchAboutFaq() {
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.about_faq)).perform(ViewActions.click())
|
Espresso.onView(ViewMatchers.withId(R.id.about_faq)).perform(ViewActions.click())
|
||||||
Intents.intended(
|
Intents.intended(
|
||||||
CoreMatchers.allOf(
|
CoreMatchers.allOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
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 fr.free.nrw.commons.auth.SignupActivity
|
||||||
import org.hamcrest.CoreMatchers
|
import org.hamcrest.CoreMatchers
|
||||||
import org.hamcrest.CoreMatchers.not
|
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
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class LoginActivityTest {
|
class LoginActivityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
||||||
|
|
||||||
|
|
@ -49,8 +51,8 @@ class LoginActivityTest {
|
||||||
Intents.intended(
|
Intents.intended(
|
||||||
CoreMatchers.allOf(
|
CoreMatchers.allOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
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() {
|
fun orientationChange() {
|
||||||
UITestHelper.changeOrientation(activityRule)
|
UITestHelper.changeOrientation(activityRule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,20 +21,23 @@ import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
import fr.free.nrw.commons.notification.NotificationActivity
|
import fr.free.nrw.commons.notification.NotificationActivity
|
||||||
import org.hamcrest.CoreMatchers
|
import org.hamcrest.CoreMatchers
|
||||||
import org.hamcrest.Matchers
|
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
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class MainActivityTest {
|
class MainActivityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
|
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var mGrantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
|
var mGrantPermissionRule: GrantPermissionRule =
|
||||||
"android.permission.ACCESS_FINE_LOCATION"
|
GrantPermissionRule.grant(
|
||||||
)
|
"android.permission.ACCESS_FINE_LOCATION",
|
||||||
|
)
|
||||||
|
|
||||||
private val device: UiDevice =
|
private val device: UiDevice =
|
||||||
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||||
|
|
@ -48,7 +51,8 @@ class MainActivityTest {
|
||||||
UITestHelper.loginUser()
|
UITestHelper.loginUser()
|
||||||
UITestHelper.skipWelcome()
|
UITestHelper.skipWelcome()
|
||||||
Intents.init()
|
Intents.init()
|
||||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
Intents
|
||||||
|
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
val storeName = context.packageName + "_preferences"
|
val storeName = context.packageName + "_preferences"
|
||||||
|
|
@ -62,137 +66,149 @@ class MainActivityTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testNearby() {
|
fun testNearby() {
|
||||||
Espresso.onView(
|
Espresso
|
||||||
Matchers.allOf(
|
.onView(
|
||||||
childAtPosition(
|
Matchers.allOf(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
childAtPosition(
|
||||||
0
|
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
1
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
).perform(ViewActions.click())
|
||||||
)
|
Espresso
|
||||||
).perform(ViewActions.click())
|
.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
|
||||||
.check(matches(ViewMatchers.isDisplayed()))
|
.check(matches(ViewMatchers.isDisplayed()))
|
||||||
UITestHelper.sleep(10000)
|
UITestHelper.sleep(10000)
|
||||||
val actionMenuItemView2 = Espresso.onView(
|
val actionMenuItemView2 =
|
||||||
Matchers.allOf(
|
Espresso.onView(
|
||||||
ViewMatchers.withId(R.id.list_sheet), ViewMatchers.withContentDescription("List"),
|
Matchers.allOf(
|
||||||
childAtPosition(
|
ViewMatchers.withId(R.id.list_sheet),
|
||||||
|
ViewMatchers.withContentDescription("List"),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.toolbar),
|
childAtPosition(
|
||||||
1
|
ViewMatchers.withId(R.id.toolbar),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
actionMenuItemView2.perform(ViewActions.click())
|
actionMenuItemView2.perform(ViewActions.click())
|
||||||
UITestHelper.sleep(1000)
|
UITestHelper.sleep(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testExplore() {
|
fun testExplore() {
|
||||||
Espresso.onView(
|
Espresso
|
||||||
Matchers.allOf(
|
.onView(
|
||||||
childAtPosition(
|
Matchers.allOf(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
childAtPosition(
|
||||||
0
|
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
2,
|
||||||
),
|
),
|
||||||
2
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
).perform(ViewActions.click())
|
||||||
)
|
Espresso
|
||||||
).perform(ViewActions.click())
|
.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
|
||||||
.check(matches(ViewMatchers.isDisplayed()))
|
.check(matches(ViewMatchers.isDisplayed()))
|
||||||
UITestHelper.sleep(1000)
|
UITestHelper.sleep(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testContributions() {
|
fun testContributions() {
|
||||||
Espresso.onView(
|
Espresso
|
||||||
Matchers.allOf(
|
.onView(
|
||||||
childAtPosition(
|
Matchers.allOf(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
childAtPosition(
|
||||||
0
|
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
).perform(ViewActions.click())
|
||||||
)
|
Espresso
|
||||||
).perform(ViewActions.click())
|
.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
|
||||||
.check(matches(ViewMatchers.isDisplayed()))
|
.check(matches(ViewMatchers.isDisplayed()))
|
||||||
Espresso.onView(
|
Espresso
|
||||||
Matchers.allOf(
|
.onView(
|
||||||
ViewMatchers.withId(R.id.contributionImage),
|
Matchers.allOf(
|
||||||
childAtPosition(
|
ViewMatchers.withId(R.id.contributionImage),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.contributionsList),
|
childAtPosition(
|
||||||
0
|
ViewMatchers.withId(R.id.contributionsList),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
1
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
).perform(ViewActions.click())
|
||||||
)
|
val actionMenuItemView =
|
||||||
).perform(ViewActions.click())
|
Espresso.onView(
|
||||||
val actionMenuItemView = Espresso.onView(
|
Matchers.allOf(
|
||||||
Matchers.allOf(
|
ViewMatchers.withId(R.id.menu_bookmark_current_image),
|
||||||
ViewMatchers.withId(R.id.menu_bookmark_current_image),
|
|
||||||
childAtPosition(
|
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.toolbar),
|
childAtPosition(
|
||||||
1
|
ViewMatchers.withId(R.id.toolbar),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
actionMenuItemView.perform(ViewActions.click())
|
actionMenuItemView.perform(ViewActions.click())
|
||||||
UITestHelper.sleep(3000)
|
UITestHelper.sleep(3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testBookmarks() {
|
fun testBookmarks() {
|
||||||
Espresso.onView(
|
Espresso
|
||||||
Matchers.allOf(
|
.onView(
|
||||||
childAtPosition(
|
Matchers.allOf(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
childAtPosition(
|
||||||
0
|
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)
|
UITestHelper.sleep(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testNotifications() {
|
fun testNotifications() {
|
||||||
Espresso.onView(
|
Espresso
|
||||||
Matchers.allOf(
|
.onView(
|
||||||
ViewMatchers.withId(R.id.notifications),
|
Matchers.allOf(
|
||||||
childAtPosition(
|
ViewMatchers.withId(R.id.notifications),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.toolbar),
|
childAtPosition(
|
||||||
1
|
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))
|
Intents.intended(IntentMatchers.hasComponent(NotificationActivity::class.java.name))
|
||||||
Espresso.pressBack()
|
Espresso.pressBack()
|
||||||
UITestHelper.sleep(1000)
|
UITestHelper.sleep(1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import android.app.Activity
|
||||||
import android.app.Instrumentation
|
import android.app.Instrumentation
|
||||||
import androidx.test.espresso.Espresso.onView
|
import androidx.test.espresso.Espresso.onView
|
||||||
import androidx.test.espresso.action.ViewActions
|
import androidx.test.espresso.action.ViewActions
|
||||||
import androidx.test.espresso.action.ViewActions.swipeRight
|
|
||||||
import androidx.test.espresso.intent.Intents
|
import androidx.test.espresso.intent.Intents
|
||||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||||
|
|
@ -26,7 +25,6 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ProfileActivityTest {
|
class ProfileActivityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule = IntentsTestRule(LoginActivity::class.java)
|
var activityRule = IntentsTestRule(LoginActivity::class.java)
|
||||||
|
|
||||||
|
|
@ -38,7 +36,8 @@ class ProfileActivityTest {
|
||||||
device.freezeRotation()
|
device.freezeRotation()
|
||||||
UITestHelper.loginUser()
|
UITestHelper.loginUser()
|
||||||
UITestHelper.skipWelcome()
|
UITestHelper.skipWelcome()
|
||||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
Intents
|
||||||
|
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,20 +49,19 @@ class ProfileActivityTest {
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
withId(R.id.fragment_main_nav_tab_layout),
|
withId(R.id.fragment_main_nav_tab_layout),
|
||||||
0
|
0,
|
||||||
),
|
),
|
||||||
4
|
4,
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
ViewMatchers.isDisplayed(),
|
||||||
)
|
),
|
||||||
).perform(ViewActions.click())
|
).perform(ViewActions.click())
|
||||||
onView(Matchers.allOf(withId(R.id.more_profile))).perform(
|
onView(Matchers.allOf(withId(R.id.more_profile))).perform(
|
||||||
ViewActions.scrollTo(),
|
ViewActions.scrollTo(),
|
||||||
ViewActions.click()
|
ViewActions.click(),
|
||||||
)
|
)
|
||||||
device.swipe(1033,1346,531,1346,20)
|
device.swipe(1033, 1346, 531, 1346, 20)
|
||||||
UITestHelper.sleep(5000)
|
UITestHelper.sleep(5000)
|
||||||
Intents.intended(hasComponent(ProfileActivity::class.java.name))
|
Intents.intended(hasComponent(ProfileActivity::class.java.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ReviewActivityTest {
|
class ReviewActivityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(ReviewActivity::class.java)
|
var activityRule: ActivityTestRule<*> = ActivityTestRule(ReviewActivity::class.java)
|
||||||
|
|
||||||
|
|
@ -17,5 +16,4 @@ class ReviewActivityTest {
|
||||||
fun orientationChange() {
|
fun orientationChange() {
|
||||||
UITestHelper.changeOrientation(activityRule)
|
UITestHelper.changeOrientation(activityRule)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class SearchActivityTest {
|
class SearchActivityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule = ActivityTestRule(SearchActivity::class.java)
|
var activityRule = ActivityTestRule(SearchActivity::class.java)
|
||||||
|
|
||||||
|
|
@ -31,21 +30,22 @@ class SearchActivityTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun exploreActivityTest() {
|
fun exploreActivityTest() {
|
||||||
val searchAutoComplete = Espresso.onView(
|
val searchAutoComplete =
|
||||||
Matchers.allOf(
|
Espresso.onView(
|
||||||
UITestHelper.childAtPosition(
|
Matchers.allOf(
|
||||||
Matchers.allOf(
|
UITestHelper.childAtPosition(
|
||||||
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
|
Matchers.allOf(
|
||||||
UITestHelper.childAtPosition(
|
|
||||||
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
|
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())
|
searchAutoComplete.perform(ViewActions.replaceText("cat"), ViewActions.closeSoftKeyboard())
|
||||||
UITestHelper.sleep(5000)
|
UITestHelper.sleep(5000)
|
||||||
device.swipe(1000, 1400, 500, 1400, 20)
|
device.swipe(1000, 1400, 500, 1400, 20)
|
||||||
|
|
@ -56,4 +56,4 @@ class SearchActivityTest {
|
||||||
device.swipe(800, 1400, 600, 1400, 20)
|
device.swipe(800, 1400, 600, 1400, 20)
|
||||||
UITestHelper.sleep(1000)
|
UITestHelper.sleep(1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class SettingsActivityLoggedInTest {
|
class SettingsActivityLoggedInTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
|
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
|
||||||
|
|
||||||
|
|
@ -35,31 +34,32 @@ class SettingsActivityLoggedInTest {
|
||||||
device.freezeRotation()
|
device.freezeRotation()
|
||||||
UITestHelper.loginUser()
|
UITestHelper.loginUser()
|
||||||
UITestHelper.skipWelcome()
|
UITestHelper.skipWelcome()
|
||||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
Intents
|
||||||
|
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSettings() {
|
fun testSettings() {
|
||||||
Espresso.onView(
|
Espresso
|
||||||
Matchers.allOf(
|
.onView(
|
||||||
ViewMatchers.withContentDescription("More"),
|
Matchers.allOf(
|
||||||
UITestHelper.childAtPosition(
|
ViewMatchers.withContentDescription("More"),
|
||||||
UITestHelper.childAtPosition(
|
UITestHelper.childAtPosition(
|
||||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
UITestHelper.childAtPosition(
|
||||||
0
|
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(
|
Espresso.onView(Matchers.allOf(ViewMatchers.withId(R.id.more_settings))).perform(
|
||||||
ViewActions.scrollTo(),
|
ViewActions.scrollTo(),
|
||||||
ViewActions.click()
|
ViewActions.click(),
|
||||||
)
|
)
|
||||||
Intents.intended(IntentMatchers.hasComponent(SettingsActivity::class.java.name))
|
Intents.intended(IntentMatchers.hasComponent(SettingsActivity::class.java.name))
|
||||||
UITestHelper.sleep(1000)
|
UITestHelper.sleep(1000)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class SettingsActivityTest {
|
class SettingsActivityTest {
|
||||||
|
|
||||||
private lateinit var defaultKvStore: JsonKvStore
|
private lateinit var defaultKvStore: JsonKvStore
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
|
|
@ -44,22 +43,24 @@ class SettingsActivityTest {
|
||||||
fun useAuthorNameTogglesOn() {
|
fun useAuthorNameTogglesOn() {
|
||||||
// Turn on "Use author name" preference if currently off
|
// Turn on "Use author name" preference if currently off
|
||||||
if (!defaultKvStore.getBoolean("useAuthorName", false)) {
|
if (!defaultKvStore.getBoolean("useAuthorName", false)) {
|
||||||
Espresso.onView(
|
Espresso
|
||||||
allOf(
|
.onView(
|
||||||
withId(R.id.recycler_view),
|
allOf(
|
||||||
childAtPosition(withId(android.R.id.list_container), 0)
|
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
|
// Check authorName preference is enabled
|
||||||
Espresso.onView(
|
Espresso
|
||||||
allOf(
|
.onView(
|
||||||
withId(R.id.recycler_view),
|
allOf(
|
||||||
childAtPosition(withId(android.R.id.list_container), 0)
|
withId(R.id.recycler_view),
|
||||||
)
|
childAtPosition(withId(android.R.id.list_container), 0),
|
||||||
).check(matches(isEnabled()))
|
),
|
||||||
|
).check(matches(isEnabled()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,20 @@ import androidx.test.espresso.action.ViewActions
|
||||||
import androidx.test.espresso.matcher.ViewMatchers
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
import androidx.test.rule.ActivityTestRule
|
import androidx.test.rule.ActivityTestRule
|
||||||
import org.apache.commons.lang3.StringUtils
|
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
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
class UITestHelper {
|
class UITestHelper {
|
||||||
companion object {
|
companion object {
|
||||||
fun skipWelcome() {
|
fun skipWelcome() {
|
||||||
try {
|
try {
|
||||||
onView(ViewMatchers.withId(R.id.button_ok))
|
onView(ViewMatchers.withId(R.id.button_ok))
|
||||||
.perform(ViewActions.click())
|
.perform(ViewActions.click())
|
||||||
//Skip tutorial
|
// Skip tutorial
|
||||||
onView(ViewMatchers.withId(R.id.finishTutorialButton))
|
onView(ViewMatchers.withId(R.id.finishTutorialButton))
|
||||||
.perform(ViewActions.click())
|
.perform(ViewActions.click())
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
|
|
@ -29,27 +32,31 @@ class UITestHelper {
|
||||||
|
|
||||||
fun skipLogin() {
|
fun skipLogin() {
|
||||||
try {
|
try {
|
||||||
//Skip Login
|
// Skip Login
|
||||||
val htmlTextView = onView(
|
val htmlTextView =
|
||||||
Matchers.allOf(
|
onView(
|
||||||
ViewMatchers.withId(R.id.skip_login), ViewMatchers.withText("Skip"),
|
Matchers.allOf(
|
||||||
ViewMatchers.isDisplayed()
|
ViewMatchers.withId(R.id.skip_login),
|
||||||
|
ViewMatchers.withText("Skip"),
|
||||||
|
ViewMatchers.isDisplayed(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
htmlTextView.perform(ViewActions.click())
|
htmlTextView.perform(ViewActions.click())
|
||||||
|
|
||||||
val appCompatButton = onView(
|
val appCompatButton =
|
||||||
Matchers.allOf(
|
onView(
|
||||||
ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText("Yes"),
|
Matchers.allOf(
|
||||||
childAtPosition(
|
ViewMatchers.withId(android.R.id.button1),
|
||||||
|
ViewMatchers.withText("Yes"),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.buttonPanel),
|
childAtPosition(
|
||||||
0
|
ViewMatchers.withId(R.id.buttonPanel),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
3,
|
||||||
),
|
),
|
||||||
3
|
),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
appCompatButton.perform(ViewActions.scrollTo(), ViewActions.click())
|
appCompatButton.perform(ViewActions.scrollTo(), ViewActions.click())
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
}
|
}
|
||||||
|
|
@ -57,18 +64,18 @@ class UITestHelper {
|
||||||
|
|
||||||
fun loginUser() {
|
fun loginUser() {
|
||||||
try {
|
try {
|
||||||
//Perform Login
|
// Perform Login
|
||||||
sleep(3000)
|
sleep(3000)
|
||||||
onView(ViewMatchers.withId(R.id.login_username))
|
onView(ViewMatchers.withId(R.id.login_username))
|
||||||
.perform(
|
.perform(
|
||||||
ViewActions.replaceText(getTestUsername()),
|
ViewActions.replaceText(getTestUsername()),
|
||||||
ViewActions.closeSoftKeyboard()
|
ViewActions.closeSoftKeyboard(),
|
||||||
)
|
)
|
||||||
sleep(2000)
|
sleep(2000)
|
||||||
onView(ViewMatchers.withId(R.id.login_password))
|
onView(ViewMatchers.withId(R.id.login_password))
|
||||||
.perform(
|
.perform(
|
||||||
ViewActions.replaceText(getTestUserPassword()),
|
ViewActions.replaceText(getTestUserPassword()),
|
||||||
ViewActions.closeSoftKeyboard()
|
ViewActions.closeSoftKeyboard(),
|
||||||
)
|
)
|
||||||
sleep(2000)
|
sleep(2000)
|
||||||
onView(ViewMatchers.withId(R.id.login_button))
|
onView(ViewMatchers.withId(R.id.login_button))
|
||||||
|
|
@ -76,7 +83,6 @@ class UITestHelper {
|
||||||
sleep(10000)
|
sleep(10000)
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun logoutUser() {
|
fun logoutUser() {
|
||||||
|
|
@ -87,36 +93,38 @@ class UITestHelper {
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||||
0
|
0,
|
||||||
),
|
),
|
||||||
4
|
4,
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
ViewMatchers.isDisplayed(),
|
||||||
)
|
),
|
||||||
).perform(ViewActions.click())
|
).perform(ViewActions.click())
|
||||||
onView(
|
onView(
|
||||||
Matchers.allOf(
|
Matchers.allOf(
|
||||||
ViewMatchers.withId(R.id.more_logout), ViewMatchers.withText("Logout"),
|
ViewMatchers.withId(R.id.more_logout),
|
||||||
|
ViewMatchers.withText("Logout"),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.scroll_view_more_bottom_sheet),
|
ViewMatchers.withId(R.id.scroll_view_more_bottom_sheet),
|
||||||
0
|
0,
|
||||||
),
|
),
|
||||||
6
|
6,
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
).perform(ViewActions.scrollTo(), ViewActions.click())
|
).perform(ViewActions.scrollTo(), ViewActions.click())
|
||||||
onView(
|
onView(
|
||||||
Matchers.allOf(
|
Matchers.allOf(
|
||||||
ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText("Yes"),
|
ViewMatchers.withId(android.R.id.button1),
|
||||||
|
ViewMatchers.withText("Yes"),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.buttonPanel),
|
ViewMatchers.withId(R.id.buttonPanel),
|
||||||
0
|
0,
|
||||||
),
|
),
|
||||||
3
|
3,
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
).perform(ViewActions.scrollTo(), ViewActions.click())
|
).perform(ViewActions.scrollTo(), ViewActions.click())
|
||||||
sleep(5000)
|
sleep(5000)
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
|
|
@ -124,9 +132,9 @@ class UITestHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun childAtPosition(
|
fun childAtPosition(
|
||||||
parentMatcher: Matcher<View>, position: Int
|
parentMatcher: Matcher<View>,
|
||||||
|
position: Int,
|
||||||
): Matcher<View> {
|
): Matcher<View> {
|
||||||
|
|
||||||
return object : TypeSafeMatcher<View>() {
|
return object : TypeSafeMatcher<View>() {
|
||||||
override fun describeTo(description: Description) {
|
override fun describeTo(description: Description) {
|
||||||
description.appendText("Child at position $position in parent ")
|
description.appendText("Child at position $position in parent ")
|
||||||
|
|
@ -135,8 +143,9 @@ class UITestHelper {
|
||||||
|
|
||||||
public override fun matchesSafely(view: View): Boolean {
|
public override fun matchesSafely(view: View): Boolean {
|
||||||
val parent = view.parent
|
val parent = view.parent
|
||||||
return parent is ViewGroup && parentMatcher.matches(parent)
|
return parent is ViewGroup &&
|
||||||
&& view == parent.getChildAt(position)
|
parentMatcher.matches(parent) &&
|
||||||
|
view == parent.getChildAt(position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,14 +163,18 @@ class UITestHelper {
|
||||||
val username = BuildConfig.TEST_USERNAME
|
val username = BuildConfig.TEST_USERNAME
|
||||||
if (StringUtils.isEmpty(username) || username == "null") {
|
if (StringUtils.isEmpty(username) || username == "null") {
|
||||||
throw NotImplementedError("Configure your beta account's username")
|
throw NotImplementedError("Configure your beta account's username")
|
||||||
} else return username
|
} else {
|
||||||
|
return username
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTestUserPassword(): String {
|
private fun getTestUserPassword(): String {
|
||||||
val password = BuildConfig.TEST_PASSWORD
|
val password = BuildConfig.TEST_PASSWORD
|
||||||
if (StringUtils.isEmpty(password) || password == "null") {
|
if (StringUtils.isEmpty(password) || password == "null") {
|
||||||
throw NotImplementedError("Configure your beta account's password")
|
throw NotImplementedError("Configure your beta account's password")
|
||||||
} else return password
|
} else {
|
||||||
|
return password
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Activity> changeOrientation(activityRule: ActivityTestRule<T>) {
|
fun <T : Activity> changeOrientation(activityRule: ActivityTestRule<T>) {
|
||||||
|
|
@ -174,6 +187,7 @@ class UITestHelper {
|
||||||
fun <T> first(matcher: Matcher<T>): Matcher<T>? {
|
fun <T> first(matcher: Matcher<T>): Matcher<T>? {
|
||||||
return object : BaseMatcher<T>() {
|
return object : BaseMatcher<T>() {
|
||||||
var isFirst = true
|
var isFirst = true
|
||||||
|
|
||||||
override fun matches(item: Any): Boolean {
|
override fun matches(item: Any): Boolean {
|
||||||
if (isFirst && matcher.matches(item)) {
|
if (isFirst && matcher.matches(item)) {
|
||||||
isFirst = false
|
isFirst = false
|
||||||
|
|
@ -188,4 +202,4 @@ class UITestHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ import android.app.Activity
|
||||||
import android.app.Instrumentation
|
import android.app.Instrumentation
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.test.espresso.Espresso.onView
|
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.contrib.RecyclerViewActions
|
||||||
import androidx.test.espresso.intent.Intents
|
import androidx.test.espresso.intent.Intents
|
||||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||||
|
|
@ -28,7 +31,6 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class UploadCancelledTest {
|
class UploadCancelledTest {
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
var mActivityTestRule = ActivityTestRule(LoginActivity::class.java)
|
var mActivityTestRule = ActivityTestRule(LoginActivity::class.java)
|
||||||
|
|
@ -37,7 +39,7 @@ class UploadCancelledTest {
|
||||||
@JvmField
|
@JvmField
|
||||||
var mGrantPermissionRule: GrantPermissionRule =
|
var mGrantPermissionRule: GrantPermissionRule =
|
||||||
GrantPermissionRule.grant(
|
GrantPermissionRule.grant(
|
||||||
"android.permission.WRITE_EXTERNAL_STORAGE"
|
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||||
)
|
)
|
||||||
|
|
||||||
private val device: UiDevice =
|
private val device: UiDevice =
|
||||||
|
|
@ -48,14 +50,14 @@ class UploadCancelledTest {
|
||||||
try {
|
try {
|
||||||
Intents.init()
|
Intents.init()
|
||||||
} catch (ex: IllegalStateException) {
|
} catch (ex: IllegalStateException) {
|
||||||
|
|
||||||
}
|
}
|
||||||
device.unfreezeRotation()
|
device.unfreezeRotation()
|
||||||
device.setOrientationNatural()
|
device.setOrientationNatural()
|
||||||
device.freezeRotation()
|
device.freezeRotation()
|
||||||
UITestHelper.loginUser()
|
UITestHelper.loginUser()
|
||||||
UITestHelper.skipWelcome()
|
UITestHelper.skipWelcome()
|
||||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
Intents
|
||||||
|
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,130 +66,137 @@ class UploadCancelledTest {
|
||||||
try {
|
try {
|
||||||
Intents.release()
|
Intents.release()
|
||||||
} catch (ex: IllegalStateException) {
|
} catch (ex: IllegalStateException) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun uploadCancelledAfterLocationPickedTest() {
|
fun uploadCancelledAfterLocationPickedTest() {
|
||||||
|
val bottomNavigationItemView =
|
||||||
val bottomNavigationItemView = onView(
|
onView(
|
||||||
allOf(
|
allOf(
|
||||||
childAtPosition(
|
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
withId(R.id.fragment_main_nav_tab_layout),
|
childAtPosition(
|
||||||
0
|
withId(R.id.fragment_main_nav_tab_layout),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
1
|
isDisplayed(),
|
||||||
),
|
),
|
||||||
isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
bottomNavigationItemView.perform(click())
|
bottomNavigationItemView.perform(click())
|
||||||
|
|
||||||
UITestHelper.sleep(12000)
|
UITestHelper.sleep(12000)
|
||||||
|
|
||||||
val actionMenuItemView = onView(
|
val actionMenuItemView =
|
||||||
allOf(
|
onView(
|
||||||
withId(R.id.list_sheet),
|
allOf(
|
||||||
childAtPosition(
|
withId(R.id.list_sheet),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
withId(R.id.toolbar),
|
childAtPosition(
|
||||||
1
|
withId(R.id.toolbar),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
isDisplayed(),
|
||||||
),
|
),
|
||||||
isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
actionMenuItemView.perform(click())
|
actionMenuItemView.perform(click())
|
||||||
|
|
||||||
val recyclerView = onView(
|
val recyclerView =
|
||||||
allOf(
|
onView(
|
||||||
withId(R.id.rv_nearby_list),
|
allOf(
|
||||||
|
withId(R.id.rv_nearby_list),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
recyclerView.perform(
|
recyclerView.perform(
|
||||||
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
|
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
|
||||||
0,
|
0,
|
||||||
click()
|
click(),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val linearLayout3 = onView(
|
val linearLayout3 =
|
||||||
allOf(
|
onView(
|
||||||
withId(R.id.cameraButton),
|
allOf(
|
||||||
childAtPosition(
|
withId(R.id.cameraButton),
|
||||||
allOf(
|
childAtPosition(
|
||||||
withId(R.id.nearby_button_layout),
|
allOf(
|
||||||
|
withId(R.id.nearby_button_layout),
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
isDisplayed(),
|
||||||
),
|
),
|
||||||
isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
linearLayout3.perform(click())
|
linearLayout3.perform(click())
|
||||||
|
|
||||||
val pasteSensitiveTextInputEditText = onView(
|
val pasteSensitiveTextInputEditText =
|
||||||
allOf(
|
onView(
|
||||||
withId(R.id.caption_item_edit_text),
|
allOf(
|
||||||
childAtPosition(
|
withId(R.id.caption_item_edit_text),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
withId(R.id.caption_item_edit_text_input_layout),
|
childAtPosition(
|
||||||
0
|
withId(R.id.caption_item_edit_text_input_layout),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
isDisplayed(),
|
||||||
),
|
),
|
||||||
isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
pasteSensitiveTextInputEditText.perform(replaceText("test"), closeSoftKeyboard())
|
pasteSensitiveTextInputEditText.perform(replaceText("test"), closeSoftKeyboard())
|
||||||
|
|
||||||
val pasteSensitiveTextInputEditText2 = onView(
|
val pasteSensitiveTextInputEditText2 =
|
||||||
allOf(
|
onView(
|
||||||
withId(R.id.description_item_edit_text),
|
allOf(
|
||||||
childAtPosition(
|
withId(R.id.description_item_edit_text),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
withId(R.id.description_item_edit_text_input_layout),
|
childAtPosition(
|
||||||
0
|
withId(R.id.description_item_edit_text_input_layout),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
isDisplayed(),
|
||||||
),
|
),
|
||||||
isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
pasteSensitiveTextInputEditText2.perform(replaceText("test"), closeSoftKeyboard())
|
pasteSensitiveTextInputEditText2.perform(replaceText("test"), closeSoftKeyboard())
|
||||||
|
|
||||||
val appCompatButton2 = onView(
|
val appCompatButton2 =
|
||||||
allOf(
|
onView(
|
||||||
withId(R.id.btn_next),
|
allOf(
|
||||||
childAtPosition(
|
withId(R.id.btn_next),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
withId(R.id.ll_container_media_detail),
|
childAtPosition(
|
||||||
2
|
withId(R.id.ll_container_media_detail),
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
1
|
isDisplayed(),
|
||||||
),
|
),
|
||||||
isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
appCompatButton2.perform(click())
|
appCompatButton2.perform(click())
|
||||||
|
|
||||||
val appCompatButton3 = onView(
|
val appCompatButton3 =
|
||||||
allOf(
|
onView(
|
||||||
withId(android.R.id.button1),
|
allOf(
|
||||||
|
withId(android.R.id.button1),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
appCompatButton3.perform(scrollTo(), click())
|
appCompatButton3.perform(scrollTo(), click())
|
||||||
|
|
||||||
Intents.intended(IntentMatchers.hasComponent(LocationPickerActivity::class.java.name))
|
Intents.intended(IntentMatchers.hasComponent(LocationPickerActivity::class.java.name))
|
||||||
|
|
||||||
val floatingActionButton3 = onView(
|
val floatingActionButton3 =
|
||||||
allOf(
|
onView(
|
||||||
withId(R.id.location_chosen_button),
|
allOf(
|
||||||
isDisplayed()
|
withId(R.id.location_chosen_button),
|
||||||
|
isDisplayed(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
UITestHelper.sleep(2000)
|
UITestHelper.sleep(2000)
|
||||||
floatingActionButton3.perform(click())
|
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.Intents.intending
|
||||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasType
|
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.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import androidx.test.rule.ActivityTestRule
|
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.util.MyViewAction
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils
|
import fr.free.nrw.commons.utils.ConfigUtils
|
||||||
import org.hamcrest.core.AllOf.allOf
|
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 org.junit.runner.RunWith
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
import java.util.Random
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class UploadTest {
|
class UploadTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
var permissionRule =
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION)!!
|
GrantPermissionRule.grant(
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
)!!
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
||||||
|
|
@ -61,7 +72,6 @@ class UploadTest {
|
||||||
try {
|
try {
|
||||||
Intents.init()
|
Intents.init()
|
||||||
} catch (ex: IllegalStateException) {
|
} catch (ex: IllegalStateException) {
|
||||||
|
|
||||||
}
|
}
|
||||||
UITestHelper.loginUser()
|
UITestHelper.loginUser()
|
||||||
UITestHelper.skipWelcome()
|
UITestHelper.skipWelcome()
|
||||||
|
|
@ -94,14 +104,13 @@ class UploadTest {
|
||||||
dismissWarning("Yes")
|
dismissWarning("Yes")
|
||||||
|
|
||||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
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)))
|
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)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
UITestHelper.sleep(5000)
|
UITestHelper.sleep(5000)
|
||||||
dismissWarning("Yes")
|
dismissWarning("Yes")
|
||||||
|
|
@ -109,29 +118,30 @@ class UploadTest {
|
||||||
UITestHelper.sleep(3000)
|
UITestHelper.sleep(3000)
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
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)
|
UITestHelper.sleep(3000)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
dismissWarning("Yes, Submit")
|
dismissWarning("Yes, Submit")
|
||||||
|
|
||||||
UITestHelper.sleep(500)
|
UITestHelper.sleep(500)
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
UITestHelper.sleep(10000)
|
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"
|
commonsFileName.replace(' ', '_') + ".jpg"
|
||||||
Timber.i("File should be uploaded to $fileUrl")
|
Timber.i("File should be uploaded to $fileUrl")
|
||||||
}
|
}
|
||||||
|
|
@ -139,8 +149,8 @@ class UploadTest {
|
||||||
private fun dismissWarning(warningText: String) {
|
private fun dismissWarning(warningText: String) {
|
||||||
try {
|
try {
|
||||||
onView(withText(warningText))
|
onView(withText(warningText))
|
||||||
.check(matches(isDisplayed()))
|
.check(matches(isDisplayed()))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -167,10 +177,10 @@ class UploadTest {
|
||||||
dismissWarning("Yes")
|
dismissWarning("Yes")
|
||||||
|
|
||||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||||
.perform(replaceText(commonsFileName))
|
.perform(replaceText(commonsFileName))
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
UITestHelper.sleep(10000)
|
UITestHelper.sleep(10000)
|
||||||
dismissWarning("Yes")
|
dismissWarning("Yes")
|
||||||
|
|
@ -178,29 +188,30 @@ class UploadTest {
|
||||||
UITestHelper.sleep(3000)
|
UITestHelper.sleep(3000)
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
||||||
.perform(replaceText("Test"))
|
.perform(replaceText("Test"))
|
||||||
|
|
||||||
UITestHelper.sleep(3000)
|
UITestHelper.sleep(3000)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
dismissWarning("Yes, Submit")
|
dismissWarning("Yes, Submit")
|
||||||
|
|
||||||
UITestHelper.sleep(500)
|
UITestHelper.sleep(500)
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
UITestHelper.sleep(10000)
|
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"
|
commonsFileName.replace(' ', '_') + ".jpg"
|
||||||
Timber.i("File should be uploaded to $fileUrl")
|
Timber.i("File should be uploaded to $fileUrl")
|
||||||
}
|
}
|
||||||
|
|
@ -227,23 +238,29 @@ class UploadTest {
|
||||||
dismissWarningDialog()
|
dismissWarningDialog()
|
||||||
|
|
||||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||||
.perform(replaceText(commonsFileName))
|
.perform(replaceText(commonsFileName))
|
||||||
|
|
||||||
onView(withId(R.id.rv_descriptions)).perform(
|
onView(withId(R.id.rv_descriptions)).perform(
|
||||||
RecyclerViewActions
|
RecyclerViewActions
|
||||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(0,
|
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(
|
||||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))
|
0,
|
||||||
|
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
onView(withId(R.id.btn_add))
|
onView(withId(R.id.btn_add))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
onView(withId(R.id.rv_descriptions)).perform(
|
onView(withId(R.id.rv_descriptions)).perform(
|
||||||
RecyclerViewActions
|
RecyclerViewActions
|
||||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(1,
|
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(
|
||||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description")))
|
1,
|
||||||
|
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
UITestHelper.sleep(5000)
|
UITestHelper.sleep(5000)
|
||||||
dismissWarning("Yes")
|
dismissWarning("Yes")
|
||||||
|
|
@ -251,29 +268,30 @@ class UploadTest {
|
||||||
UITestHelper.sleep(3000)
|
UITestHelper.sleep(3000)
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
||||||
.perform(replaceText("Test"))
|
.perform(replaceText("Test"))
|
||||||
|
|
||||||
UITestHelper.sleep(3000)
|
UITestHelper.sleep(3000)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
dismissWarning("Yes, Submit")
|
dismissWarning("Yes, Submit")
|
||||||
|
|
||||||
UITestHelper.sleep(500)
|
UITestHelper.sleep(500)
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
UITestHelper.sleep(10000)
|
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"
|
commonsFileName.replace(' ', '_') + ".jpg"
|
||||||
Timber.i("File should be uploaded to $fileUrl")
|
Timber.i("File should be uploaded to $fileUrl")
|
||||||
}
|
}
|
||||||
|
|
@ -306,7 +324,6 @@ class UploadTest {
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -328,8 +345,8 @@ class UploadTest {
|
||||||
private fun dismissWarningDialog() {
|
private fun dismissWarningDialog() {
|
||||||
try {
|
try {
|
||||||
onView(withText("Yes"))
|
onView(withText("Yes"))
|
||||||
.check(matches(isDisplayed()))
|
.check(matches(isDisplayed()))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -337,10 +354,10 @@ class UploadTest {
|
||||||
private fun openGallery() {
|
private fun openGallery() {
|
||||||
// Open FAB
|
// Open FAB
|
||||||
onView(allOf<View>(withId(R.id.fab_plus), isDisplayed()))
|
onView(allOf<View>(withId(R.id.fab_plus), isDisplayed()))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
// Click gallery
|
// Click gallery
|
||||||
onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed()))
|
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.Espresso.onView
|
||||||
import androidx.test.espresso.action.ViewActions
|
import androidx.test.espresso.action.ViewActions
|
||||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
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.isDisplayed
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
@ -22,7 +21,6 @@ import org.junit.runner.RunWith
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class WelcomeActivityTest {
|
class WelcomeActivityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(WelcomeActivity::class.java)
|
var activityRule: ActivityTestRule<*> = ActivityTestRule(WelcomeActivity::class.java)
|
||||||
|
|
||||||
|
|
@ -130,4 +128,4 @@ class WelcomeActivityTest {
|
||||||
fun orientationChange() {
|
fun orientationChange() {
|
||||||
UITestHelper.changeOrientation(activityRule)
|
UITestHelper.changeOrientation(activityRule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class PasteSensitiveTextInputEditTextTest {
|
class PasteSensitiveTextInputEditTextTest {
|
||||||
|
|
||||||
private var context: Context? = null
|
private var context: Context? = null
|
||||||
private var textView: PasteSensitiveTextInputEditText? = null
|
private var textView: PasteSensitiveTextInputEditText? = null
|
||||||
|
|
||||||
|
|
@ -23,9 +22,13 @@ class PasteSensitiveTextInputEditTextTest {
|
||||||
|
|
||||||
// this test has no real value, just % for test code coverage
|
// this test has no real value, just % for test code coverage
|
||||||
@Test
|
@Test
|
||||||
fun extractFormattingAttributeSet(){
|
fun extractFormattingAttributeSet() {
|
||||||
val methodExtractFormattingAttribute = textView!!.javaClass.getDeclaredMethod(
|
val methodExtractFormattingAttribute =
|
||||||
"extractFormattingAttribute", Context::class.java, AttributeSet::class.java)
|
textView!!.javaClass.getDeclaredMethod(
|
||||||
|
"extractFormattingAttribute",
|
||||||
|
Context::class.java,
|
||||||
|
AttributeSet::class.java,
|
||||||
|
)
|
||||||
methodExtractFormattingAttribute.isAccessible = true
|
methodExtractFormattingAttribute.isAccessible = true
|
||||||
methodExtractFormattingAttribute.invoke(textView, context, null)
|
methodExtractFormattingAttribute.invoke(textView, context, null)
|
||||||
}
|
}
|
||||||
|
|
@ -40,4 +43,4 @@ class PasteSensitiveTextInputEditTextTest {
|
||||||
textView!!.setFormattingAllowed(false)
|
textView!!.setFormattingAllowed(false)
|
||||||
Assert.assertFalse(fieldFormattingAllowed.getBoolean(textView))
|
Assert.assertFalse(fieldFormattingAllowed.getBoolean(textView))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,56 +9,58 @@ import org.hamcrest.Matcher
|
||||||
|
|
||||||
class MyViewAction {
|
class MyViewAction {
|
||||||
companion object {
|
companion object {
|
||||||
fun typeTextInChildViewWithId(id: Int, textToBeTyped: String): ViewAction {
|
fun typeTextInChildViewWithId(
|
||||||
return object : ViewAction {
|
id: Int,
|
||||||
override fun getConstraints(): Matcher<View>? {
|
textToBeTyped: String,
|
||||||
return null
|
): ViewAction =
|
||||||
}
|
object : ViewAction {
|
||||||
|
override fun getConstraints(): Matcher<View>? = null
|
||||||
|
|
||||||
override fun getDescription(): String {
|
override fun getDescription(): String = "Click on a child view with specified id."
|
||||||
return "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
|
val v = view.findViewById<View>(id) as EditText
|
||||||
v.setText(textToBeTyped)
|
v.setText(textToBeTyped)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun selectSpinnerItemInChildViewWithId(id: Int, position: Int): ViewAction {
|
fun selectSpinnerItemInChildViewWithId(
|
||||||
return object : ViewAction {
|
id: Int,
|
||||||
override fun getConstraints(): Matcher<View>? {
|
position: Int,
|
||||||
return null
|
): ViewAction =
|
||||||
}
|
object : ViewAction {
|
||||||
|
override fun getConstraints(): Matcher<View>? = null
|
||||||
|
|
||||||
override fun getDescription(): String {
|
override fun getDescription(): String = "Click on a child view with specified id."
|
||||||
return "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
|
val v = view.findViewById<View>(id) as AppCompatSpinner
|
||||||
v.setSelection(position)
|
v.setSelection(position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun clickItemWithId(id: Int, position: Int): ViewAction {
|
fun clickItemWithId(
|
||||||
return object : ViewAction {
|
id: Int,
|
||||||
override fun getConstraints(): Matcher<View>? {
|
position: Int,
|
||||||
return null
|
): ViewAction =
|
||||||
}
|
object : ViewAction {
|
||||||
|
override fun getConstraints(): Matcher<View>? = null
|
||||||
|
|
||||||
override fun getDescription(): String {
|
override fun getDescription(): String = "Click on a child view with specified id."
|
||||||
return "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
|
val v = view.findViewById<View>(id) as View
|
||||||
v.performClick()
|
v.performClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,26 +39,25 @@ class BaseMarker {
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromResource(context: Context, drawableResId: Int) {
|
fun fromResource(
|
||||||
|
context: Context,
|
||||||
|
drawableResId: Int,
|
||||||
|
) {
|
||||||
val drawable: Drawable = context.resources.getDrawable(drawableResId)
|
val drawable: Drawable = context.resources.getDrawable(drawableResId)
|
||||||
icon = if (drawable is BitmapDrawable) {
|
icon =
|
||||||
(drawable as BitmapDrawable).bitmap
|
if (drawable is BitmapDrawable) {
|
||||||
} else {
|
(drawable as BitmapDrawable).bitmap
|
||||||
val bitmap = Bitmap.createBitmap(
|
} else {
|
||||||
drawable.intrinsicWidth,
|
val bitmap =
|
||||||
drawable.intrinsicHeight, Bitmap.Config.ARGB_8888
|
Bitmap.createBitmap(
|
||||||
)
|
drawable.intrinsicWidth,
|
||||||
val canvas = Canvas(bitmap)
|
drawable.intrinsicHeight,
|
||||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
Bitmap.Config.ARGB_8888,
|
||||||
drawable.draw(canvas)
|
)
|
||||||
bitmap
|
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
|
* production server where beta server does not work
|
||||||
*/
|
*/
|
||||||
const val COMMONS_URL = "https://commons.wikimedia.org/"
|
const val COMMONS_URL = "https://commons.wikimedia.org/"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Commons production's depicts property which is used in beta for some specific GET calls on
|
* Commons production's depicts property which is used in beta for some specific GET calls on
|
||||||
* production server where beta server does not work
|
* production server where beta server does not work
|
||||||
*/
|
*/
|
||||||
const val DEPICTS_PROPERTY = "P180"
|
const val DEPICTS_PROPERTY = "P180"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,31 +3,31 @@ package fr.free.nrw.commons
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
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(
|
constructor(parcel: Parcel) : this(
|
||||||
parcel.readDouble(),
|
parcel.readDouble(),
|
||||||
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(latitude)
|
||||||
parcel.writeDouble(longitude)
|
parcel.writeDouble(longitude)
|
||||||
parcel.writeDouble(zoom)
|
parcel.writeDouble(zoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int = 0
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<CameraPosition> {
|
companion object CREATOR : Parcelable.Creator<CameraPosition> {
|
||||||
override fun createFromParcel(parcel: Parcel): CameraPosition {
|
override fun createFromParcel(parcel: Parcel): CameraPosition = CameraPosition(parcel)
|
||||||
return CameraPosition(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<CameraPosition?> {
|
override fun newArray(size: Int): Array<CameraPosition?> = arrayOfNulls(size)
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@ package fr.free.nrw.commons
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import fr.free.nrw.commons.location.LatLng
|
import fr.free.nrw.commons.location.LatLng
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import fr.free.nrw.commons.wikidata.model.page.PageTitle
|
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
|
@Parcelize
|
||||||
class Media constructor(
|
class Media constructor(
|
||||||
|
|
@ -14,7 +16,6 @@ class Media constructor(
|
||||||
*/
|
*/
|
||||||
var pageId: String = UUID.randomUUID().toString(),
|
var pageId: String = UUID.randomUUID().toString(),
|
||||||
var thumbUrl: String? = null,
|
var thumbUrl: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets image URL
|
* Gets image URL
|
||||||
* @return Image URL
|
* @return Image URL
|
||||||
|
|
@ -26,16 +27,11 @@ class Media constructor(
|
||||||
*/
|
*/
|
||||||
var filename: String? = null,
|
var filename: String? = null,
|
||||||
/**
|
/**
|
||||||
* Gets the file description.
|
* Gets or sets the file description.
|
||||||
* @return file description as a string
|
* @return file description as a string
|
||||||
*/
|
|
||||||
// monolingual description on input...
|
|
||||||
/**
|
|
||||||
* Sets the file description.
|
|
||||||
* @param fallbackDescription the new description of the file
|
* @param fallbackDescription the new description of the file
|
||||||
*/
|
*/
|
||||||
var fallbackDescription: String? = null,
|
var fallbackDescription: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the upload date of the file.
|
* Gets the upload date of the file.
|
||||||
* Can be null.
|
* Can be null.
|
||||||
|
|
@ -43,28 +39,19 @@ class Media constructor(
|
||||||
*/
|
*/
|
||||||
var dateUploaded: Date? = null,
|
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
|
* @return license as a String
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Sets the license name of the file.
|
|
||||||
*
|
|
||||||
* @param license license name as a String
|
* @param license license name as a String
|
||||||
*/
|
*/
|
||||||
var license: String? = null,
|
var license: String? = null,
|
||||||
var licenseUrl: 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
|
* @return author name as a String
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Sets the author name of the file.
|
|
||||||
* @param author creator name as a string
|
* @param author creator name as a string
|
||||||
*/
|
*/
|
||||||
var author: String? = null,
|
var author: String? = null,
|
||||||
|
var user: String? = null,
|
||||||
var user:String?=null,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the categories the file falls under.
|
* Gets the categories the file falls under.
|
||||||
* @return file categories as an ArrayList of Strings
|
* @return file categories as an ArrayList of Strings
|
||||||
|
|
@ -83,23 +70,23 @@ class Media constructor(
|
||||||
* Stores the mapping of category title to hidden attribute
|
* Stores the mapping of category title to hidden attribute
|
||||||
* Example: "Mountains" => false, "CC-BY-SA-2.0" => true
|
* Example: "Mountains" => false, "CC-BY-SA-2.0" => true
|
||||||
*/
|
*/
|
||||||
var categoriesHiddenStatus: Map<String, Boolean> = emptyMap()
|
var categoriesHiddenStatus: Map<String, Boolean> = emptyMap(),
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
captions: Map<String, String>,
|
captions: Map<String, String>,
|
||||||
categories: List<String>?,
|
categories: List<String>?,
|
||||||
filename: String?,
|
filename: String?,
|
||||||
fallbackDescription: String?,
|
fallbackDescription: String?,
|
||||||
author: String?, user:String?
|
author: String?,
|
||||||
|
user: String?,
|
||||||
) : this(
|
) : this(
|
||||||
filename = filename,
|
filename = filename,
|
||||||
fallbackDescription = fallbackDescription,
|
fallbackDescription = fallbackDescription,
|
||||||
dateUploaded = Date(),
|
dateUploaded = Date(),
|
||||||
author = author,
|
author = author,
|
||||||
user=user,
|
user = user,
|
||||||
categories = categories,
|
categories = categories,
|
||||||
captions = captions
|
captions = captions,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -108,10 +95,11 @@ class Media constructor(
|
||||||
*/
|
*/
|
||||||
val displayTitle: String
|
val displayTitle: String
|
||||||
get() =
|
get() =
|
||||||
if (filename != null)
|
if (filename != null) {
|
||||||
pageTitle.displayTextWithoutNamespace.replaceFirst("[.][^.]+$".toRegex(), "")
|
pageTitle.displayTextWithoutNamespace.replaceFirst("[.][^.]+$".toRegex(), "")
|
||||||
else
|
} else {
|
||||||
""
|
""
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets file page title
|
* Gets file page title
|
||||||
|
|
@ -127,9 +115,10 @@ class Media constructor(
|
||||||
get() = String.format("[[%s|thumb|%s]]", filename, mostRelevantCaption)
|
get() = String.format("[[%s|thumb|%s]]", filename, mostRelevantCaption)
|
||||||
|
|
||||||
val mostRelevantCaption: String
|
val mostRelevantCaption: String
|
||||||
get() = captions[Locale.getDefault().language]
|
get() =
|
||||||
?: captions.values.firstOrNull()
|
captions[Locale.getDefault().language]
|
||||||
?: displayTitle
|
?: captions.values.firstOrNull()
|
||||||
|
?: displayTitle
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the categories the file falls under.
|
* Gets the categories the file falls under.
|
||||||
|
|
@ -138,6 +127,8 @@ class Media constructor(
|
||||||
var addedCategories: List<String>? = null
|
var addedCategories: List<String>? = null
|
||||||
// TODO added categories should be removed. It is added for a short fix. On category update,
|
// TODO added categories should be removed. It is added for a short fix. On category update,
|
||||||
// categories should be re-fetched instead
|
// categories should be re-fetched instead
|
||||||
get() = field // getter
|
get() = field // getter
|
||||||
set(value) { field = value } // setter
|
set(value) {
|
||||||
|
field = value
|
||||||
|
} // setter
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package fr.free.nrw.commons
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
import androidx.core.text.HtmlCompat
|
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.IdAndCaptions
|
||||||
import fr.free.nrw.commons.media.MediaClient
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
|
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -17,42 +17,46 @@ import javax.inject.Singleton
|
||||||
* to the media and may change due to editing.
|
* to the media and may change due to editing.
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@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) =
|
fun checkDeletionRequestExists(media: Media) = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
|
||||||
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) =
|
fun fetchDiscussion(media: Media) =
|
||||||
mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
|
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) =
|
fun refresh(media: Media): Single<Media> =
|
||||||
mediaClient.getPageHtml(media.filename!!.replace("File", "File talk"))
|
Single.ambArray(
|
||||||
.map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() }
|
mediaClient
|
||||||
.onErrorReturn {
|
.getMediaById(PAGE_ID_PREFIX + media.pageId)
|
||||||
Timber.d("Error occurred while fetching discussion")
|
.onErrorResumeNext { Single.never() },
|
||||||
""
|
mediaClient
|
||||||
}
|
.getMediaSuppressingErrors(media.filename)
|
||||||
|
.onErrorResumeNext { Single.never() },
|
||||||
|
)
|
||||||
|
|
||||||
fun refresh(media: Media): Single<Media> {
|
fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title)
|
||||||
return Single.ambArray(
|
|
||||||
mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId)
|
|
||||||
.onErrorResumeNext { Single.never() },
|
|
||||||
mediaClient.getMediaSuppressingErrors(media.filename)
|
|
||||||
.onErrorResumeNext { Single.never() }
|
|
||||||
)
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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_PREFIX = "market://details?id="
|
||||||
const val PLAY_STORE_URL_PREFIX = "https://play.google.com/store/apps/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_WEB_URL = "https://www.facebook.com/1921335171459985"
|
||||||
const val FACEBOOK_APP_URL = "fb://page/1921335171459985"
|
const val FACEBOOK_APP_URL = "fb://page/1921335171459985"
|
||||||
const val FACEBOOK_PACKAGE_NAME = "com.facebook.katana"
|
const val FACEBOOK_PACKAGE_NAME = "com.facebook.katana"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
package fr.free.nrw.commons.actions
|
package fr.free.nrw.commons.actions
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.Single
|
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
|
* This class acts as a Client to facilitate wiki page editing
|
||||||
|
|
@ -15,9 +14,8 @@ import timber.log.Timber
|
||||||
*/
|
*/
|
||||||
class PageEditClient(
|
class PageEditClient(
|
||||||
private val csrfTokenClient: CsrfTokenClient,
|
private val csrfTokenClient: CsrfTokenClient,
|
||||||
private val pageEditInterface: PageEditInterface
|
private val pageEditInterface: PageEditInterface,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace the content of a wiki page
|
* Replace the content of a wiki page
|
||||||
* @param pageTitle Title of the page to edit
|
* @param pageTitle Title of the page to edit
|
||||||
|
|
@ -25,12 +23,17 @@ class PageEditClient(
|
||||||
* @param summary Edit summary
|
* @param summary Edit summary
|
||||||
* @return whether the edit was successful
|
* @return whether the edit was successful
|
||||||
*/
|
*/
|
||||||
fun edit(pageTitle: String, text: String, summary: String): Observable<Boolean> {
|
fun edit(
|
||||||
return try {
|
pageTitle: String,
|
||||||
pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking())
|
text: String,
|
||||||
|
summary: String,
|
||||||
|
): Observable<Boolean> =
|
||||||
|
try {
|
||||||
|
pageEditInterface
|
||||||
|
.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking())
|
||||||
.map { editResponse ->
|
.map { editResponse ->
|
||||||
editResponse.edit()!!.editSucceeded()
|
editResponse.edit()!!.editSucceeded()
|
||||||
}
|
}
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
if (throwable is InvalidLoginTokenException) {
|
if (throwable is InvalidLoginTokenException) {
|
||||||
throw throwable
|
throw throwable
|
||||||
|
|
@ -38,7 +41,6 @@ class PageEditClient(
|
||||||
Observable.just(false)
|
Observable.just(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new page with the given title, text, and summary.
|
* 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.
|
* @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.
|
* @throws InvalidLoginTokenException If an invalid login token is encountered during the process.
|
||||||
*/
|
*/
|
||||||
fun postCreate(pageTitle: String, text: String, summary: String): Observable<Boolean> {
|
fun postCreate(
|
||||||
return try {
|
pageTitle: String,
|
||||||
pageEditInterface.postCreate(
|
text: String,
|
||||||
pageTitle,
|
summary: String,
|
||||||
summary,
|
): Observable<Boolean> =
|
||||||
text,
|
try {
|
||||||
"text/x-wiki",
|
pageEditInterface
|
||||||
"wikitext",
|
.postCreate(
|
||||||
true,
|
pageTitle,
|
||||||
true,
|
summary,
|
||||||
csrfTokenClient.getTokenBlocking()
|
text,
|
||||||
).map { editResponse ->
|
"text/x-wiki",
|
||||||
editResponse.edit()!!.editSucceeded()
|
"wikitext",
|
||||||
}
|
true,
|
||||||
|
true,
|
||||||
|
csrfTokenClient.getTokenBlocking(),
|
||||||
|
).map { editResponse ->
|
||||||
|
editResponse.edit()!!.editSucceeded()
|
||||||
|
}
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
if (throwable is InvalidLoginTokenException) {
|
if (throwable is InvalidLoginTokenException) {
|
||||||
throw throwable
|
throw throwable
|
||||||
|
|
@ -70,7 +77,6 @@ class PageEditClient(
|
||||||
Observable.just(false)
|
Observable.just(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Append text to the end of a wiki page
|
* Append text to the end of a wiki page
|
||||||
|
|
@ -79,9 +85,14 @@ class PageEditClient(
|
||||||
* @param summary Edit summary
|
* @param summary Edit summary
|
||||||
* @return whether the edit was successful
|
* @return whether the edit was successful
|
||||||
*/
|
*/
|
||||||
fun appendEdit(pageTitle: String, appendText: String, summary: String): Observable<Boolean> {
|
fun appendEdit(
|
||||||
return try {
|
pageTitle: String,
|
||||||
pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking())
|
appendText: String,
|
||||||
|
summary: String,
|
||||||
|
): Observable<Boolean> =
|
||||||
|
try {
|
||||||
|
pageEditInterface
|
||||||
|
.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking())
|
||||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
if (throwable is InvalidLoginTokenException) {
|
if (throwable is InvalidLoginTokenException) {
|
||||||
|
|
@ -90,7 +101,6 @@ class PageEditClient(
|
||||||
Observable.just(false)
|
Observable.just(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepend text to the beginning of a wiki page
|
* Prepend text to the beginning of a wiki page
|
||||||
|
|
@ -99,9 +109,14 @@ class PageEditClient(
|
||||||
* @param summary Edit summary
|
* @param summary Edit summary
|
||||||
* @return whether the edit was successful
|
* @return whether the edit was successful
|
||||||
*/
|
*/
|
||||||
fun prependEdit(pageTitle: String, prependText: String, summary: String): Observable<Boolean> {
|
fun prependEdit(
|
||||||
return try {
|
pageTitle: String,
|
||||||
pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking())
|
prependText: String,
|
||||||
|
summary: String,
|
||||||
|
): Observable<Boolean> =
|
||||||
|
try {
|
||||||
|
pageEditInterface
|
||||||
|
.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking())
|
||||||
.map { editResponse -> editResponse.edit()?.editSucceeded() ?: false }
|
.map { editResponse -> editResponse.edit()?.editSucceeded() ?: false }
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
if (throwable is InvalidLoginTokenException) {
|
if (throwable is InvalidLoginTokenException) {
|
||||||
|
|
@ -110,8 +125,6 @@ class PageEditClient(
|
||||||
Observable.just(false)
|
Observable.just(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends a new section to the wiki page
|
* Appends a new section to the wiki page
|
||||||
|
|
@ -121,9 +134,15 @@ class PageEditClient(
|
||||||
* @param summary Edit summary
|
* @param summary Edit summary
|
||||||
* @return whether the edit was successful
|
* @return whether the edit was successful
|
||||||
*/
|
*/
|
||||||
fun createNewSection(pageTitle: String, sectionTitle: String, sectionText: String, summary: String): Observable<Boolean> {
|
fun createNewSection(
|
||||||
return try {
|
pageTitle: String,
|
||||||
pageEditInterface.postNewSection(pageTitle, summary, sectionTitle, sectionText, csrfTokenClient.getTokenBlocking())
|
sectionTitle: String,
|
||||||
|
sectionText: String,
|
||||||
|
summary: String,
|
||||||
|
): Observable<Boolean> =
|
||||||
|
try {
|
||||||
|
pageEditInterface
|
||||||
|
.postNewSection(pageTitle, summary, sectionTitle, sectionText, csrfTokenClient.getTokenBlocking())
|
||||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
if (throwable is InvalidLoginTokenException) {
|
if (throwable is InvalidLoginTokenException) {
|
||||||
|
|
@ -132,8 +151,6 @@ class PageEditClient(
|
||||||
Observable.just(false)
|
Observable.just(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set new labels to Wikibase server of commons
|
* Set new labels to Wikibase server of commons
|
||||||
|
|
@ -143,12 +160,21 @@ class PageEditClient(
|
||||||
* @param value label
|
* @param value label
|
||||||
* @return 1 when the edit was successful
|
* @return 1 when the edit was successful
|
||||||
*/
|
*/
|
||||||
fun setCaptions(summary: String, title: String,
|
fun setCaptions(
|
||||||
language: String, value: String) : Observable<Int>{
|
summary: String,
|
||||||
return try {
|
title: String,
|
||||||
pageEditInterface.postCaptions(summary, title, language,
|
language: String,
|
||||||
value, csrfTokenClient.getTokenBlocking()
|
value: String,
|
||||||
).map { it.success }
|
): Observable<Int> =
|
||||||
|
try {
|
||||||
|
pageEditInterface
|
||||||
|
.postCaptions(
|
||||||
|
summary,
|
||||||
|
title,
|
||||||
|
language,
|
||||||
|
value,
|
||||||
|
csrfTokenClient.getTokenBlocking(),
|
||||||
|
).map { it.success }
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
if (throwable is InvalidLoginTokenException) {
|
if (throwable is InvalidLoginTokenException) {
|
||||||
throw throwable
|
throw throwable
|
||||||
|
|
@ -156,16 +182,20 @@ class PageEditClient(
|
||||||
Observable.just(0)
|
Observable.just(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get whole WikiText of required file
|
* Get whole WikiText of required file
|
||||||
* @param title : Name of the file
|
* @param title : Name of the file
|
||||||
* @return Observable<MwQueryResult>
|
* @return Observable<MwQueryResult>
|
||||||
*/
|
*/
|
||||||
fun getCurrentWikiText(title: String): Single<String?> {
|
fun getCurrentWikiText(title: String): Single<String?> =
|
||||||
return pageEditInterface.getWikiText(title).map {
|
pageEditInterface.getWikiText(title).map {
|
||||||
it.query()?.pages()?.get(0)?.revisions()?.get(0)?.content()
|
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.WikidataConstants.MW_API_PREFIX
|
||||||
import fr.free.nrw.commons.wikidata.model.Entities
|
import fr.free.nrw.commons.wikidata.model.Entities
|
||||||
import fr.free.nrw.commons.wikidata.model.edit.Edit
|
import fr.free.nrw.commons.wikidata.model.edit.Edit
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
import retrofit2.http.Field
|
||||||
import retrofit2.http.*
|
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
|
* This interface facilitates wiki commons page editing services to the Networking module
|
||||||
|
|
@ -33,7 +38,7 @@ interface PageEditInterface {
|
||||||
@Field("summary") summary: String,
|
@Field("summary") summary: String,
|
||||||
@Field("text") text: String,
|
@Field("text") text: String,
|
||||||
// NOTE: This csrf shold always be sent as the last field of form data
|
// NOTE: This csrf shold always be sent as the last field of form data
|
||||||
@Field("token") token: String
|
@Field("token") token: String,
|
||||||
): Observable<Edit>
|
): Observable<Edit>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -60,7 +65,7 @@ interface PageEditInterface {
|
||||||
@Field("minor") minor: Boolean,
|
@Field("minor") minor: Boolean,
|
||||||
@Field("recreate") recreate: Boolean,
|
@Field("recreate") recreate: Boolean,
|
||||||
// NOTE: This csrf shold always be sent as the last field of form data
|
// NOTE: This csrf shold always be sent as the last field of form data
|
||||||
@Field("token") token: String
|
@Field("token") token: String,
|
||||||
): Observable<Edit>
|
): Observable<Edit>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -79,7 +84,7 @@ interface PageEditInterface {
|
||||||
@Field("title") title: String,
|
@Field("title") title: String,
|
||||||
@Field("summary") summary: String,
|
@Field("summary") summary: String,
|
||||||
@Field("appendtext") appendText: String,
|
@Field("appendtext") appendText: String,
|
||||||
@Field("token") token: String
|
@Field("token") token: String,
|
||||||
): Observable<Edit>
|
): Observable<Edit>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -98,7 +103,7 @@ interface PageEditInterface {
|
||||||
@Field("title") title: String,
|
@Field("title") title: String,
|
||||||
@Field("summary") summary: String,
|
@Field("summary") summary: String,
|
||||||
@Field("prependtext") prependText: String,
|
@Field("prependtext") prependText: String,
|
||||||
@Field("token") token: String
|
@Field("token") token: String,
|
||||||
): Observable<Edit>
|
): Observable<Edit>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
|
|
@ -109,7 +114,7 @@ interface PageEditInterface {
|
||||||
@Field("summary") summary: String,
|
@Field("summary") summary: String,
|
||||||
@Field("sectiontitle") sectionTitle: String,
|
@Field("sectiontitle") sectionTitle: String,
|
||||||
@Field("text") sectionText: String,
|
@Field("text") sectionText: String,
|
||||||
@Field("token") token: String
|
@Field("token") token: String,
|
||||||
): Observable<Edit>
|
): Observable<Edit>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
|
|
@ -120,7 +125,7 @@ interface PageEditInterface {
|
||||||
@Field("title") title: String,
|
@Field("title") title: String,
|
||||||
@Field("language") language: String,
|
@Field("language") language: String,
|
||||||
@Field("value") value: String,
|
@Field("value") value: String,
|
||||||
@Field("token") token: String
|
@Field("token") token: String,
|
||||||
): Observable<Entities>
|
): Observable<Entities>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -130,6 +135,6 @@ interface PageEditInterface {
|
||||||
*/
|
*/
|
||||||
@GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=")
|
@GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=")
|
||||||
fun getWikiText(
|
fun getWikiText(
|
||||||
@Query("titles") title: String
|
@Query("titles") title: String,
|
||||||
): Single<MwQueryResponse?>
|
): Single<MwQueryResponse?>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package fr.free.nrw.commons.actions
|
package fr.free.nrw.commons.actions
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication
|
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.CsrfTokenClient
|
||||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
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.Inject
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
import javax.inject.Singleton
|
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
|
* Thanks are used by a user to show gratitude to another user for their contributions
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class ThanksClient @Inject constructor(
|
class ThanksClient
|
||||||
@param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
|
@Inject
|
||||||
private val service: ThanksInterface
|
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
|
* Thanks a user for a particular revision
|
||||||
*/
|
* @param revisionId The revision ID the user would like to thank someone for
|
||||||
fun thank(revisionId: Long): Observable<Boolean> {
|
* @return if thanks was successfully sent to intended recipient
|
||||||
return try {
|
*/
|
||||||
service.thank(
|
fun thank(revisionId: Long): Observable<Boolean> =
|
||||||
revisionId.toString(), // Rev
|
try {
|
||||||
null, // Log
|
service
|
||||||
csrfTokenClient.getTokenBlocking(), // Token
|
.thank(
|
||||||
CommonsApplication.getInstance().userAgent // Source
|
revisionId.toString(), // Rev
|
||||||
).map {
|
null, // Log
|
||||||
mwThankPostResponse -> mwThankPostResponse.result?.success == 1
|
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("rev") rev: String?,
|
||||||
@Field("log") log: String?,
|
@Field("log") log: String?,
|
||||||
@Field("token") token: String,
|
@Field("token") token: String,
|
||||||
@Field("source") source: String?
|
@Field("source") source: String?,
|
||||||
): Observable<MwThankPostResponse?>
|
): Observable<MwThankPostResponse?>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ package fr.free.nrw.commons.auth.csrf
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import fr.free.nrw.commons.auth.SessionManager
|
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.LoginCallback
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginClient
|
||||||
import fr.free.nrw.commons.auth.login.LoginFailedException
|
import fr.free.nrw.commons.auth.login.LoginFailedException
|
||||||
import fr.free.nrw.commons.auth.login.LoginResult
|
import fr.free.nrw.commons.auth.login.LoginResult
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -17,12 +17,11 @@ class CsrfTokenClient(
|
||||||
private val sessionManager: SessionManager,
|
private val sessionManager: SessionManager,
|
||||||
private val csrfTokenInterface: CsrfTokenInterface,
|
private val csrfTokenInterface: CsrfTokenInterface,
|
||||||
private val loginClient: LoginClient,
|
private val loginClient: LoginClient,
|
||||||
private val logoutClient: LogoutClient
|
private val logoutClient: LogoutClient,
|
||||||
) {
|
) {
|
||||||
private var retries = 0
|
private var retries = 0
|
||||||
private var csrfTokenCall: Call<MwQueryResponse?>? = null
|
private var csrfTokenCall: Call<MwQueryResponse?>? = null
|
||||||
|
|
||||||
|
|
||||||
@Throws(Throwable::class)
|
@Throws(Throwable::class)
|
||||||
fun getTokenBlocking(): String {
|
fun getTokenBlocking(): String {
|
||||||
var token = ""
|
var token = ""
|
||||||
|
|
@ -37,11 +36,20 @@ class CsrfTokenClient(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get CSRFToken response off the main thread.
|
// Get CSRFToken response off the main thread.
|
||||||
val response = newSingleThreadExecutor().submit(Callable {
|
val response =
|
||||||
csrfTokenInterface.getCsrfTokenCall().execute()
|
newSingleThreadExecutor()
|
||||||
}).get()
|
.submit(
|
||||||
|
Callable {
|
||||||
|
csrfTokenInterface.getCsrfTokenCall().execute()
|
||||||
|
},
|
||||||
|
).get()
|
||||||
|
|
||||||
if (response.body()?.query()?.csrfToken().isNullOrEmpty()) {
|
if (response
|
||||||
|
.body()
|
||||||
|
?.query()
|
||||||
|
?.csrfToken()
|
||||||
|
.isNullOrEmpty()
|
||||||
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,9 +59,8 @@ class CsrfTokenClient(
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
} catch (e: LoginFailedException) {
|
} catch (e: LoginFailedException) {
|
||||||
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||||
}
|
} catch (t: Throwable) {
|
||||||
catch (t: Throwable) {
|
|
||||||
Timber.w(t)
|
Timber.w(t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,45 +72,65 @@ class CsrfTokenClient(
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
fun request(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> =
|
fun request(
|
||||||
requestToken(service, object : Callback {
|
service: CsrfTokenInterface,
|
||||||
override fun success(token: String?) {
|
cb: Callback,
|
||||||
if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) {
|
): Call<MwQueryResponse?> =
|
||||||
retryWithLogin(cb) {
|
requestToken(
|
||||||
InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
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
|
@VisibleForTesting
|
||||||
fun requestToken(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> {
|
fun requestToken(
|
||||||
|
service: CsrfTokenInterface,
|
||||||
|
cb: Callback,
|
||||||
|
): Call<MwQueryResponse?> {
|
||||||
val call = service.getCsrfTokenCall()
|
val call = service.getCsrfTokenCall()
|
||||||
call.enqueue(object : retrofit2.Callback<MwQueryResponse?> {
|
call.enqueue(
|
||||||
override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) {
|
object : retrofit2.Callback<MwQueryResponse?> {
|
||||||
if (call.isCanceled) {
|
override fun onResponse(
|
||||||
return
|
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) {
|
override fun onFailure(
|
||||||
if (call.isCanceled) {
|
call: Call<MwQueryResponse?>,
|
||||||
return
|
t: Throwable,
|
||||||
|
) {
|
||||||
|
if (call.isCanceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb.failure(t)
|
||||||
}
|
}
|
||||||
cb.failure(t)
|
},
|
||||||
}
|
)
|
||||||
})
|
|
||||||
return call
|
return call
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun retryWithLogin(callback: Callback, caught: () -> Throwable?) {
|
private fun retryWithLogin(
|
||||||
|
callback: Callback,
|
||||||
|
caught: () -> Throwable?,
|
||||||
|
) {
|
||||||
val userName = sessionManager.userName
|
val userName = sessionManager.userName
|
||||||
val password = sessionManager.password
|
val password = sessionManager.password
|
||||||
if (retries < MAX_RETRIES && !userName.isNullOrEmpty() && !password.isNullOrEmpty()) {
|
if (retries < MAX_RETRIES && !userName.isNullOrEmpty() && !password.isNullOrEmpty()) {
|
||||||
|
|
@ -123,26 +150,31 @@ class CsrfTokenClient(
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
callback: Callback,
|
callback: Callback,
|
||||||
retryCallback: () -> Unit
|
retryCallback: () -> Unit,
|
||||||
) = loginClient.request(username, password, object : LoginCallback {
|
) = loginClient.request(
|
||||||
override fun success(loginResult: LoginResult) {
|
username,
|
||||||
if (loginResult.pass) {
|
password,
|
||||||
sessionManager.updateAccount(loginResult)
|
object : LoginCallback {
|
||||||
retryCallback()
|
override fun success(loginResult: LoginResult) {
|
||||||
} else {
|
if (loginResult.pass) {
|
||||||
callback.failure(LoginFailedException(loginResult.message))
|
sessionManager.updateAccount(loginResult)
|
||||||
|
retryCallback()
|
||||||
|
} else {
|
||||||
|
callback.failure(LoginFailedException(loginResult.message))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun twoFactorPrompt(caught: Throwable, token: String?) =
|
override fun twoFactorPrompt(
|
||||||
callback.twoFactorPrompt()
|
caught: Throwable,
|
||||||
|
token: String?,
|
||||||
|
) = callback.twoFactorPrompt()
|
||||||
|
|
||||||
// Should not happen here, but call the callback just in case.
|
// Should not happen here, but call the callback just in case.
|
||||||
override fun passwordResetPrompt(token: String?) =
|
override fun passwordResetPrompt(token: String?) = callback.failure(LoginFailedException("Logged in with temporary password."))
|
||||||
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() {
|
private fun cancel() {
|
||||||
loginClient.cancel()
|
loginClient.cancel()
|
||||||
|
|
@ -154,7 +186,9 @@ class CsrfTokenClient(
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun success(token: String?)
|
fun success(token: String?)
|
||||||
|
|
||||||
fun failure(caught: Throwable?)
|
fun failure(caught: Throwable?)
|
||||||
|
|
||||||
fun twoFactorPrompt()
|
fun twoFactorPrompt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,5 +200,7 @@ class CsrfTokenClient(
|
||||||
const val ANONYMOUS_TOKEN_MESSAGE = "App believes we're logged in, but got anonymous token."
|
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 fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LogoutClient @Inject constructor(private val store: CommonsCookieStorage) {
|
class LogoutClient
|
||||||
fun logout() = store.clear()
|
@Inject
|
||||||
}
|
constructor(
|
||||||
|
private val store: CommonsCookieStorage,
|
||||||
|
) {
|
||||||
|
fun logout() = store.clear()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,13 @@ package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
interface LoginCallback {
|
interface LoginCallback {
|
||||||
fun success(loginResult: LoginResult)
|
fun success(loginResult: LoginResult)
|
||||||
fun twoFactorPrompt(caught: Throwable, token: String?)
|
|
||||||
|
fun twoFactorPrompt(
|
||||||
|
caught: Throwable,
|
||||||
|
token: String?,
|
||||||
|
)
|
||||||
|
|
||||||
fun passwordResetPrompt(token: String?)
|
fun passwordResetPrompt(token: String?)
|
||||||
|
|
||||||
fun error(caught: Throwable)
|
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.OAuthResult
|
||||||
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
|
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
|
||||||
import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL
|
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.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
@ -16,7 +16,9 @@ import java.io.IOException
|
||||||
/**
|
/**
|
||||||
* Responsible for making login related requests to the server.
|
* 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 tokenCall: Call<MwQueryResponse?>? = null
|
||||||
private var loginCall: Call<LoginResponse?>? = null
|
private var loginCall: Call<LoginResponse?>? = null
|
||||||
|
|
||||||
|
|
@ -30,80 +32,116 @@ class LoginClient(private val loginInterface: LoginInterface) {
|
||||||
|
|
||||||
private fun getLoginToken() = loginInterface.getLoginToken()
|
private fun getLoginToken() = loginInterface.getLoginToken()
|
||||||
|
|
||||||
fun request(userName: String, password: String, cb: LoginCallback) {
|
fun request(
|
||||||
|
userName: String,
|
||||||
|
password: String,
|
||||||
|
cb: LoginCallback,
|
||||||
|
) {
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
tokenCall = getLoginToken()
|
tokenCall = getLoginToken()
|
||||||
tokenCall!!.enqueue(object : Callback<MwQueryResponse?> {
|
tokenCall!!.enqueue(
|
||||||
override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) {
|
object : Callback<MwQueryResponse?> {
|
||||||
login(
|
override fun onResponse(
|
||||||
userName, password, null, null, response.body()!!.query()!!.loginToken(),
|
call: Call<MwQueryResponse?>,
|
||||||
userLanguage, cb
|
response: Response<MwQueryResponse?>,
|
||||||
)
|
) {
|
||||||
}
|
login(
|
||||||
|
userName,
|
||||||
override fun onFailure(call: Call<MwQueryResponse?>, caught: Throwable) {
|
password,
|
||||||
if (call.isCanceled) {
|
null,
|
||||||
return
|
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(
|
fun login(
|
||||||
userName: String, password: String, retypedPassword: String?, twoFactorCode: String?,
|
userName: String,
|
||||||
loginToken: String?, userLanguage: String, cb: LoginCallback
|
password: String,
|
||||||
|
retypedPassword: String?,
|
||||||
|
twoFactorCode: String?,
|
||||||
|
loginToken: String?,
|
||||||
|
userLanguage: String,
|
||||||
|
cb: LoginCallback,
|
||||||
) {
|
) {
|
||||||
this.userLanguage = userLanguage
|
this.userLanguage = userLanguage
|
||||||
|
|
||||||
loginCall = if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
|
loginCall =
|
||||||
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
|
||||||
} else {
|
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
||||||
loginInterface.postLogIn(
|
} else {
|
||||||
userName, password, retypedPassword, twoFactorCode, loginToken, userLanguage, true
|
loginInterface.postLogIn(
|
||||||
)
|
userName,
|
||||||
}
|
password,
|
||||||
|
retypedPassword,
|
||||||
|
twoFactorCode,
|
||||||
|
loginToken,
|
||||||
|
userLanguage,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
loginCall!!.enqueue(object : Callback<LoginResponse?> {
|
loginCall!!.enqueue(
|
||||||
override fun onResponse(
|
object : Callback<LoginResponse?> {
|
||||||
call: Call<LoginResponse?>,
|
override fun onResponse(
|
||||||
response: Response<LoginResponse?>
|
call: Call<LoginResponse?>,
|
||||||
) {
|
response: Response<LoginResponse?>,
|
||||||
val loginResult = response.body()?.toLoginResult(password)
|
) {
|
||||||
if (loginResult != null) {
|
val loginResult = response.body()?.toLoginResult(password)
|
||||||
if (loginResult.pass && !loginResult.userName.isNullOrEmpty()) {
|
if (loginResult != null) {
|
||||||
// The server could do some transformations on user names, e.g. on some
|
if (loginResult.pass && !loginResult.userName.isNullOrEmpty()) {
|
||||||
// wikis is uppercases the first letter.
|
// The server could do some transformations on user names, e.g. on some
|
||||||
getExtendedInfo(loginResult.userName, loginResult, cb)
|
// wikis is uppercases the first letter.
|
||||||
} else if ("UI" == loginResult.status) {
|
getExtendedInfo(loginResult.userName, loginResult, cb)
|
||||||
when (loginResult) {
|
} else if ("UI" == loginResult.status) {
|
||||||
is OAuthResult -> cb.twoFactorPrompt(
|
when (loginResult) {
|
||||||
LoginFailedException(loginResult.message),
|
is OAuthResult ->
|
||||||
loginToken
|
cb.twoFactorPrompt(
|
||||||
)
|
LoginFailedException(loginResult.message),
|
||||||
|
loginToken,
|
||||||
|
)
|
||||||
|
|
||||||
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
|
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
|
||||||
|
|
||||||
is LoginResult.Result -> cb.error(
|
is LoginResult.Result ->
|
||||||
LoginFailedException(loginResult.message)
|
cb.error(
|
||||||
)
|
LoginFailedException(loginResult.message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cb.error(LoginFailedException(loginResult.message))
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
override fun onFailure(
|
||||||
if (call.isCanceled) {
|
call: Call<LoginResponse?>,
|
||||||
return
|
t: Throwable,
|
||||||
|
) {
|
||||||
|
if (call.isCanceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb.error(t)
|
||||||
}
|
}
|
||||||
cb.error(t)
|
},
|
||||||
}
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doLogin(
|
fun doLogin(
|
||||||
|
|
@ -111,43 +149,65 @@ class LoginClient(private val loginInterface: LoginInterface) {
|
||||||
password: String,
|
password: String,
|
||||||
twoFactorCode: String,
|
twoFactorCode: String,
|
||||||
userLanguage: String,
|
userLanguage: String,
|
||||||
loginCallback: LoginCallback
|
loginCallback: LoginCallback,
|
||||||
) {
|
) {
|
||||||
getLoginToken().enqueue(object :Callback<MwQueryResponse?>{
|
getLoginToken().enqueue(
|
||||||
override fun onResponse(
|
object : Callback<MwQueryResponse?> {
|
||||||
call: Call<MwQueryResponse?>,
|
override fun onResponse(
|
||||||
response: Response<MwQueryResponse?>
|
call: Call<MwQueryResponse?>,
|
||||||
) = if (response.isSuccessful){
|
response: Response<MwQueryResponse?>,
|
||||||
val loginToken = response.body()?.query()?.loginToken()
|
) = if (response.isSuccessful) {
|
||||||
loginToken?.let {
|
val loginToken = response.body()?.query()?.loginToken()
|
||||||
login(username, password, null, twoFactorCode, it, userLanguage, loginCallback)
|
loginToken?.let {
|
||||||
} ?: run {
|
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"))
|
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) {
|
override fun onFailure(
|
||||||
loginCallback.error(t)
|
call: Call<MwQueryResponse?>,
|
||||||
}
|
t: Throwable,
|
||||||
})
|
) {
|
||||||
|
loginCallback.error(t)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Throwable::class)
|
@Throws(Throwable::class)
|
||||||
fun loginBlocking(userName: String, password: String, twoFactorCode: String?) {
|
fun loginBlocking(
|
||||||
|
userName: String,
|
||||||
|
password: String,
|
||||||
|
twoFactorCode: String?,
|
||||||
|
) {
|
||||||
val tokenResponse = getLoginToken().execute()
|
val tokenResponse = getLoginToken().execute()
|
||||||
if (tokenResponse.body()?.query()?.loginToken().isNullOrEmpty()) {
|
if (tokenResponse
|
||||||
|
.body()
|
||||||
|
?.query()
|
||||||
|
?.loginToken()
|
||||||
|
.isNullOrEmpty()
|
||||||
|
) {
|
||||||
throw IOException("Unexpected response when getting login token.")
|
throw IOException("Unexpected response when getting login token.")
|
||||||
}
|
}
|
||||||
|
|
||||||
val loginToken = tokenResponse.body()?.query()?.loginToken()
|
val loginToken = tokenResponse.body()?.query()?.loginToken()
|
||||||
val tempLoginCall = if (twoFactorCode.isNullOrEmpty()) {
|
val tempLoginCall =
|
||||||
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
if (twoFactorCode.isNullOrEmpty()) {
|
||||||
} else {
|
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
||||||
loginInterface.postLogIn(
|
} else {
|
||||||
userName, password, null, twoFactorCode, loginToken, userLanguage, true
|
loginInterface.postLogIn(
|
||||||
)
|
userName,
|
||||||
}
|
password,
|
||||||
|
null,
|
||||||
|
twoFactorCode,
|
||||||
|
loginToken,
|
||||||
|
userLanguage,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val response = tempLoginCall.execute()
|
val response = tempLoginCall.execute()
|
||||||
val loginResponse = response.body() ?: throw IOException("Unexpected response when logging in.")
|
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) =
|
private fun getExtendedInfo(
|
||||||
loginInterface.getUserInfo(userName)
|
userName: String,
|
||||||
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
|
loginResult: LoginResult,
|
||||||
.subscribe({ response: MwQueryResponse? ->
|
cb: LoginCallback,
|
||||||
loginResult.userId = response?.query()?.userInfo()?.id() ?: 0
|
) = loginInterface
|
||||||
loginResult.groups =
|
.getUserInfo(userName)
|
||||||
response?.query()?.getUserResponse(userName)?.groups ?: emptySet()
|
.subscribeOn(Schedulers.io())
|
||||||
cb.success(loginResult)
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
}, { caught: Throwable ->
|
.subscribe({ response: MwQueryResponse? ->
|
||||||
Timber.e(caught, "Login succeeded but getting group information failed. ")
|
loginResult.userId = response?.query()?.userInfo()?.id() ?: 0
|
||||||
cb.error(caught)
|
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() {
|
fun cancel() {
|
||||||
tokenCall?.let {
|
tokenCall?.let {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
package fr.free.nrw.commons.auth.login
|
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
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||||
import io.reactivex.Observable
|
|
||||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
|
import io.reactivex.Observable
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.Field
|
import retrofit2.http.Field
|
||||||
import retrofit2.http.FormUrlEncoded
|
import retrofit2.http.FormUrlEncoded
|
||||||
|
|
@ -24,7 +24,7 @@ interface LoginInterface {
|
||||||
@Field("password") pass: String?,
|
@Field("password") pass: String?,
|
||||||
@Field("logintoken") token: String?,
|
@Field("logintoken") token: String?,
|
||||||
@Field("uselang") userLanguage: String?,
|
@Field("uselang") userLanguage: String?,
|
||||||
@Field("loginreturnurl") url: String?
|
@Field("loginreturnurl") url: String?,
|
||||||
): Call<LoginResponse?>
|
): Call<LoginResponse?>
|
||||||
|
|
||||||
@Headers("Cache-Control: no-cache")
|
@Headers("Cache-Control: no-cache")
|
||||||
|
|
@ -37,9 +37,11 @@ interface LoginInterface {
|
||||||
@Field("OATHToken") twoFactorCode: String?,
|
@Field("OATHToken") twoFactorCode: String?,
|
||||||
@Field("logintoken") token: String?,
|
@Field("logintoken") token: String?,
|
||||||
@Field("uselang") userLanguage: String?,
|
@Field("uselang") userLanguage: String?,
|
||||||
@Field("logincontinue") loginContinue: Boolean
|
@Field("logincontinue") loginContinue: Boolean,
|
||||||
): Call<LoginResponse?>
|
): Call<LoginResponse?>
|
||||||
|
|
||||||
@GET(MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate")
|
@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")
|
@SerializedName("clientlogin")
|
||||||
private val clientLogin: ClientLogin? = null
|
private val clientLogin: ClientLogin? = null
|
||||||
|
|
||||||
fun toLoginResult(password: String): LoginResult? {
|
fun toLoginResult(password: String): LoginResult? = clientLogin?.toLoginResult(password)
|
||||||
return clientLogin?.toLoginResult(password)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class ClientLogin {
|
internal class ClientLogin {
|
||||||
|
|
@ -39,7 +37,7 @@ internal class ClientLogin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if ("PASS" != status && "FAIL" != status) {
|
} 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."
|
userMessage = "An unknown error occurred."
|
||||||
}
|
}
|
||||||
return Result(status ?: "", userName, password, userMessage)
|
return Result(status ?: "", userName, password, userMessage)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ sealed class LoginResult(
|
||||||
val status: String,
|
val status: String,
|
||||||
val userName: String?,
|
val userName: String?,
|
||||||
val password: String?,
|
val password: String?,
|
||||||
val message: String?
|
val message: String?,
|
||||||
) {
|
) {
|
||||||
var userId = 0
|
var userId = 0
|
||||||
var groups = emptySet<String>()
|
var groups = emptySet<String>()
|
||||||
|
|
@ -14,20 +14,20 @@ sealed class LoginResult(
|
||||||
status: String,
|
status: String,
|
||||||
userName: String?,
|
userName: String?,
|
||||||
password: String?,
|
password: String?,
|
||||||
message: String?
|
message: String?,
|
||||||
): LoginResult(status, userName, password, message)
|
) : LoginResult(status, userName, password, message)
|
||||||
|
|
||||||
class OAuthResult(
|
class OAuthResult(
|
||||||
status: String,
|
status: String,
|
||||||
userName: String?,
|
userName: String?,
|
||||||
password: String?,
|
password: String?,
|
||||||
message: String?
|
message: String?,
|
||||||
) : LoginResult(status, userName, password, message)
|
) : LoginResult(status, userName, password, message)
|
||||||
|
|
||||||
class ResetPasswordResult(
|
class ResetPasswordResult(
|
||||||
status: String,
|
status: String,
|
||||||
userName: String?,
|
userName: String?,
|
||||||
password: String?,
|
password: String?,
|
||||||
message: String?
|
message: String?,
|
||||||
) : LoginResult(status, userName, password, message)
|
) : 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
|
* Helps to inflate Wikidata Items into Items tab
|
||||||
*/
|
*/
|
||||||
class BookmarkItemsAdapter (val list: List<DepictedItem>, val context: Context) :
|
class BookmarkItemsAdapter(
|
||||||
RecyclerView.Adapter<BookmarkItemsAdapter.BookmarkItemViewHolder>() {
|
val list: List<DepictedItem>,
|
||||||
|
val context: Context,
|
||||||
class BookmarkItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
) : RecyclerView.Adapter<BookmarkItemsAdapter.BookmarkItemViewHolder>() {
|
||||||
|
class BookmarkItemViewHolder(
|
||||||
|
itemView: View,
|
||||||
|
) : RecyclerView.ViewHolder(itemView) {
|
||||||
var depictsLabel: TextView = itemView.findViewById(R.id.depicts_label)
|
var depictsLabel: TextView = itemView.findViewById(R.id.depicts_label)
|
||||||
var description: TextView = itemView.findViewById(R.id.description)
|
var description: TextView = itemView.findViewById(R.id.description)
|
||||||
var depictsImage: SimpleDraweeView = itemView.findViewById(R.id.depicts_image)
|
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 {
|
override fun onCreateViewHolder(
|
||||||
val v: View = LayoutInflater.from(context)
|
parent: ViewGroup,
|
||||||
.inflate(R.layout.item_depictions, parent, false)
|
viewType: Int,
|
||||||
|
): BookmarkItemViewHolder {
|
||||||
|
val v: View =
|
||||||
|
LayoutInflater
|
||||||
|
.from(context)
|
||||||
|
.inflate(R.layout.item_depictions, parent, false)
|
||||||
return BookmarkItemViewHolder(v)
|
return BookmarkItemViewHolder(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BookmarkItemViewHolder, position: Int) {
|
override fun onBindViewHolder(
|
||||||
|
holder: BookmarkItemViewHolder,
|
||||||
|
position: Int,
|
||||||
|
) {
|
||||||
val depictedItem = list[position]
|
val depictedItem = list[position]
|
||||||
holder.depictsLabel.text = depictedItem.name
|
holder.depictsLabel.text = depictedItem.name
|
||||||
holder.description.text = depictedItem.description
|
holder.description.text = depictedItem.description
|
||||||
|
|
@ -48,7 +57,5 @@ class BookmarkItemsAdapter (val list: List<DepictedItem>, val context: Context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int = list.size
|
||||||
return list.size
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,25 @@ package fr.free.nrw.commons.bookmarks.models
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
|
||||||
class Bookmark(mediaName: String?, mediaCreator: String?,
|
class Bookmark(
|
||||||
/**
|
mediaName: String?,
|
||||||
* Modifies the content URI - marking this bookmark as already saved in the database
|
mediaCreator: String?,
|
||||||
* @param contentUri the content URI
|
|
||||||
*/
|
|
||||||
var contentUri: Uri?) {
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* @return content URI
|
||||||
|
* @param contentUri the content URI
|
||||||
*/
|
*/
|
||||||
|
var contentUri: Uri?,
|
||||||
|
) {
|
||||||
/**
|
/**
|
||||||
* Gets the media name
|
* Gets the media name
|
||||||
* @return the media name
|
* @return the media name
|
||||||
*/
|
*/
|
||||||
val mediaName: String = mediaName ?: ""
|
val mediaName: String = mediaName ?: ""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets media creator
|
* Gets media creator
|
||||||
* @return creator name
|
* @return creator name
|
||||||
*/
|
*/
|
||||||
val mediaCreator: String = mediaCreator ?: ""
|
val mediaCreator: String = mediaCreator ?: ""
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import com.google.gson.annotations.SerializedName
|
||||||
class CampaignConfig {
|
class CampaignConfig {
|
||||||
@SerializedName("showOnlyLiveCampaigns")
|
@SerializedName("showOnlyLiveCampaigns")
|
||||||
private val showOnlyLiveCampaigns = false
|
private val showOnlyLiveCampaigns = false
|
||||||
|
|
||||||
@SerializedName("sortBy")
|
@SerializedName("sortBy")
|
||||||
private val sortBy: String? = null
|
private val sortBy: String? = null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import fr.free.nrw.commons.campaigns.models.Campaign
|
||||||
class CampaignResponseDTO {
|
class CampaignResponseDTO {
|
||||||
@SerializedName("config")
|
@SerializedName("config")
|
||||||
val campaignConfig: CampaignConfig? = null
|
val campaignConfig: CampaignConfig? = null
|
||||||
|
|
||||||
@SerializedName("campaigns")
|
@SerializedName("campaigns")
|
||||||
val campaigns: List<Campaign>? = null
|
val campaigns: List<Campaign>? = null
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ package fr.free.nrw.commons.campaigns.models
|
||||||
/**
|
/**
|
||||||
* A data class to hold a campaign
|
* A data class to hold a campaign
|
||||||
*/
|
*/
|
||||||
data class Campaign(var title: String? = null,
|
data class Campaign(
|
||||||
var description: String? = null,
|
var title: String? = null,
|
||||||
var startDate: String? = null,
|
var description: String? = null,
|
||||||
var endDate: String? = null,
|
var startDate: String? = null,
|
||||||
var link: String? = null,
|
var endDate: String? = null,
|
||||||
var isWLMCampaign: Boolean = false)
|
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.Observable
|
||||||
import io.reactivex.functions.Function4
|
import io.reactivex.functions.Function4
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The model class for categories in upload
|
* The model class for categories in upload
|
||||||
*/
|
*/
|
||||||
class CategoriesModel @Inject constructor(
|
class CategoriesModel
|
||||||
private val categoryClient: CategoryClient,
|
@Inject
|
||||||
private val categoryDao: CategoryDao,
|
constructor(
|
||||||
private val gpsCategoryModel: GpsCategoryModel
|
private val categoryClient: CategoryClient,
|
||||||
) {
|
private val categoryDao: CategoryDao,
|
||||||
private val selectedCategories: MutableList<CategoryItem> = mutableListOf()
|
private val gpsCategoryModel: GpsCategoryModel,
|
||||||
|
) {
|
||||||
|
private val selectedCategories: MutableList<CategoryItem> = mutableListOf()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Existing categories which are selected
|
* Existing categories which are selected
|
||||||
*/
|
*/
|
||||||
private var selectedExistingCategories: MutableList<String> = mutableListOf()
|
private var selectedExistingCategories: MutableList<String> = mutableListOf()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if an item is considered to be a spammy category which should be ignored
|
* 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
|
* @param item a category item that needs to be validated to know if it is spammy or not
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
fun isSpammyCategory(item: String): Boolean {
|
fun isSpammyCategory(item: String): Boolean {
|
||||||
//Check for current and previous year to exclude these categories from removal
|
// Check for current and previous year to exclude these categories from removal
|
||||||
val now = Calendar.getInstance()
|
val now = Calendar.getInstance()
|
||||||
val curYear = now[Calendar.YEAR]
|
val curYear = now[Calendar.YEAR]
|
||||||
val curYearInString = curYear.toString()
|
val curYearInString = curYear.toString()
|
||||||
val prevYear = curYear - 1
|
val prevYear = curYear - 1
|
||||||
val prevYearInString = prevYear.toString()
|
val prevYearInString = prevYear.toString()
|
||||||
Timber.d("Previous year: %s", prevYearInString)
|
Timber.d("Previous year: %s", prevYearInString)
|
||||||
|
|
||||||
val mentionsDecade = item.matches(".*0s.*".toRegex())
|
val mentionsDecade = item.matches(".*0s.*".toRegex())
|
||||||
val recentDecade = item.matches(".*20[0-2]0s.*".toRegex())
|
val recentDecade = item.matches(".*20[0-2]0s.*".toRegex())
|
||||||
val spammyCategory = item.matches("(.*)needing(.*)".toRegex())
|
val spammyCategory =
|
||||||
|| item.matches("(.*)taken on(.*)".toRegex())
|
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)
|
// always skip irrelevant categories such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
||||||
if (spammyCategory) {
|
if (spammyCategory) {
|
||||||
return true
|
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
|
* Updates category count in category dao
|
||||||
// Example: "2020s" is OK, but "1920s" is not (and should be skipped)
|
* @param item
|
||||||
return !recentDecade
|
*/
|
||||||
} else {
|
fun updateCategoryCount(item: CategoryItem) {
|
||||||
// If it is not an year in decade form (e.g. 19xxs/20xxs), then check if item contains a 4-digit year
|
var category = categoryDao.find(item.name)
|
||||||
// anywhere within the string (.* is wildcard) (Issue #47)
|
|
||||||
// And that item does not equal the current year or previous year
|
// Newly used category...
|
||||||
return item.matches(".*(19|20)\\d{2}.*".toRegex())
|
if (category == null) {
|
||||||
&& !item.contains(curYearInString)
|
category = Category(null, item.name, item.description, item.thumbnail, Date(), 0)
|
||||||
&& !item.contains(prevYearInString)
|
}
|
||||||
|
category.incTimesUsed()
|
||||||
|
categoryDao.save(category)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates category count in category dao
|
* Regional category search
|
||||||
* @param item
|
* @param term
|
||||||
*/
|
* @param imageTitleList
|
||||||
fun updateCategoryCount(item: CategoryItem) {
|
* @return
|
||||||
var category = categoryDao.find(item.name)
|
*/
|
||||||
|
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...
|
private fun suggestionsOrSearch(
|
||||||
if (category == null) {
|
term: String,
|
||||||
category = Category(null, item.name, item.description, item.thumbnail, Date(), 0)
|
imageTitleList: List<String>,
|
||||||
}
|
selectedDepictions: List<DepictedItem>,
|
||||||
category.incTimesUsed()
|
): Observable<List<CategoryItem>> =
|
||||||
categoryDao.save(category)
|
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
|
* Fetches details of every category associated with selected depictions, converts them into
|
||||||
* @param term
|
* CategoryItem and returns them in a list.
|
||||||
* @param imageTitleList
|
*
|
||||||
* @return
|
* @param selectedDepictions selected DepictItems
|
||||||
*/
|
* @return List of CategoryItem associated with selected depictions
|
||||||
fun searchAll(
|
*/
|
||||||
term: String,
|
private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>): Observable<MutableList<CategoryItem>>? =
|
||||||
imageTitleList: List<String>,
|
Observable
|
||||||
selectedDepictions: List<DepictedItem>
|
.fromIterable(
|
||||||
): Observable<List<CategoryItem>> {
|
selectedDepictions.map { it.commonsCategories }.flatten(),
|
||||||
return suggestionsOrSearch(term, imageTitleList, selectedDepictions)
|
).map { categoryItem ->
|
||||||
.map { it.map { CategoryItem(it.name, it.description, it.thumbnail, false) } }
|
categoryClient
|
||||||
}
|
.getCategoriesByName(
|
||||||
|
categoryItem.name,
|
||||||
private fun suggestionsOrSearch(
|
categoryItem.name,
|
||||||
term: String,
|
SEARCH_CATS_LIMIT,
|
||||||
imageTitleList: List<String>,
|
).map {
|
||||||
selectedDepictions: List<DepictedItem>
|
CategoryItem(
|
||||||
): Observable<List<CategoryItem>> {
|
it[0].name,
|
||||||
return if (TextUtils.isEmpty(term))
|
it[0].description,
|
||||||
Observable.combineLatest(
|
it[0].thumbnail,
|
||||||
categoriesFromDepiction(selectedDepictions),
|
it[0].isSelected,
|
||||||
gpsCategoryModel.categoriesFromLocation,
|
)
|
||||||
titleCategories(imageTitleList),
|
}.blockingGet()
|
||||||
Observable.just(categoryDao.recentCategories(SEARCH_CATS_LIMIT)),
|
}.toList()
|
||||||
Function4(::combine)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
categoryClient.searchCategoriesForPrefix(term, SEARCH_CATS_LIMIT)
|
|
||||||
.map { it.sortedWith(StringSortingUtils.sortBySimilarity(term)) }
|
|
||||||
.toObservable()
|
.toObservable()
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches details of every category associated with selected depictions, converts them into
|
* Fetches details of every category by their name, converts them into
|
||||||
* CategoryItem and returns them in a list.
|
* CategoryItem and returns them in a list.
|
||||||
*
|
*
|
||||||
* @param selectedDepictions selected DepictItems
|
* @param categoryNames selected Categories
|
||||||
* @return List of CategoryItem associated with selected depictions
|
* @return List of CategoryItem
|
||||||
*/
|
*/
|
||||||
private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>):
|
fun getCategoriesByName(categoryNames: List<String>): Observable<MutableList<CategoryItem>>? =
|
||||||
Observable<MutableList<CategoryItem>>? {
|
Observable
|
||||||
return Observable.fromIterable(
|
.fromIterable(categoryNames)
|
||||||
selectedDepictions.map { it.commonsCategories }.flatten())
|
.map { categoryName ->
|
||||||
.map { categoryItem ->
|
buildCategories(categoryName)
|
||||||
categoryClient.getCategoriesByName(categoryItem.name,
|
}.filter { categoryItem ->
|
||||||
categoryItem.name, SEARCH_CATS_LIMIT).map {
|
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()
|
private fun combine(
|
||||||
}.toList().toObservable()
|
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
|
* Returns title based categories
|
||||||
* CategoryItem and returns them in a list.
|
* @param titleList
|
||||||
*
|
* @return
|
||||||
* @param categoryNames selected Categories
|
*/
|
||||||
* @return List of CategoryItem
|
private fun titleCategories(titleList: List<String>) =
|
||||||
*/
|
if (titleList.isNotEmpty()) {
|
||||||
fun getCategoriesByName(categoryNames: List<String>):
|
Observable.combineLatest(titleList.map { getTitleCategories(it) }) { searchResults ->
|
||||||
Observable<MutableList<CategoryItem>>? {
|
searchResults.map { it as List<CategoryItem> }.flatten()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (media.categories?.contains(item.name) == true) {
|
Observable.just(emptyList())
|
||||||
selectedExistingCategories.remove(item.name)
|
}
|
||||||
if (!media.categories?.contains(item.name)!!) {
|
|
||||||
val categoriesList: MutableList<String> = ArrayList()
|
/**
|
||||||
categoriesList.add(item.name)
|
* Return category for single title
|
||||||
categoriesList.addAll(media.categories!!)
|
* @param title
|
||||||
media.categories = categoriesList
|
* @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 {
|
} else {
|
||||||
selectedCategories.remove(item)
|
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
|
* Get Selected Categories
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
fun getSelectedCategories(): List<CategoryItem> {
|
fun getSelectedCategories(): List<CategoryItem> = selectedCategories
|
||||||
return selectedCategories
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup the existing in memory cache's
|
* Cleanup the existing in memory cache's
|
||||||
*/
|
*/
|
||||||
fun cleanUp() {
|
fun cleanUp() {
|
||||||
selectedCategories.clear()
|
selectedCategories.clear()
|
||||||
selectedExistingCategories.clear()
|
selectedExistingCategories.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SEARCH_CATS_LIMIT = 25
|
const val SEARCH_CATS_LIMIT = 25
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides selected existing categories
|
* Provides selected existing categories
|
||||||
*
|
*
|
||||||
* @return selected existing categories
|
* @return selected existing categories
|
||||||
*/
|
*/
|
||||||
fun getSelectedExistingCategories(): List<String> {
|
fun getSelectedExistingCategories(): List<String> = selectedExistingCategories
|
||||||
return selectedExistingCategories
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize existing categories
|
* Initialize existing categories
|
||||||
*
|
*
|
||||||
* @param selectedExistingCategories existing categories
|
* @param selectedExistingCategories existing categories
|
||||||
*/
|
*/
|
||||||
fun setSelectedExistingCategories(selectedExistingCategories: MutableList<String>) {
|
fun setSelectedExistingCategories(selectedExistingCategories: MutableList<String>) {
|
||||||
this.selectedExistingCategories = selectedExistingCategories
|
this.selectedExistingCategories = selectedExistingCategories
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package fr.free.nrw.commons.category
|
package fr.free.nrw.commons.category
|
||||||
|
|
||||||
import io.reactivex.Single
|
|
||||||
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
|
import io.reactivex.Single
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
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
|
* Category Client to handle custom calls to Commons MediaWiki APIs
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class CategoryClient @Inject constructor(private val categoryInterface: CategoryInterface) :
|
class CategoryClient
|
||||||
ContinuationClient<MwQueryResponse, CategoryItem>() {
|
@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.
|
* Searches for categories starting with the specified string.
|
||||||
*
|
*
|
||||||
* @param filter The string to be searched
|
* @param prefix The prefix to be searched
|
||||||
* @param itemLimit How many results are returned
|
* @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
|
* @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
|
* @return
|
||||||
*/
|
*/
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun searchCategories(filter: String?, itemLimit: Int, offset: Int = 0):
|
fun searchCategoriesForPrefix(
|
||||||
Single<List<CategoryItem>> {
|
prefix: String?,
|
||||||
return responseMapper(categoryInterface.searchCategories(filter, itemLimit, offset))
|
itemLimit: Int,
|
||||||
}
|
offset: Int = 0,
|
||||||
|
): Single<List<CategoryItem>> =
|
||||||
/**
|
responseMapper(
|
||||||
* Searches for categories starting with the specified string.
|
categoryInterface.searchCategoriesForPrefix(prefix, itemLimit, offset),
|
||||||
*
|
|
||||||
* @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
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The method takes categoryName as input and returns a List of parent categories
|
* Fetches categories starting and ending with a specified name.
|
||||||
* It uses the generator query API to get the parent categories of a category, 500 at a time.
|
*
|
||||||
*
|
* @param startingCategoryName Name of the category to start
|
||||||
* @param categoryName Category name as defined on commons
|
* @param endingCategoryName Name of the category to end
|
||||||
* @return
|
* @param itemLimit How many categories to return
|
||||||
*/
|
* @param offset offset
|
||||||
fun getParentCategoryList(categoryName: String): Single<List<CategoryItem>> {
|
* @return MwQueryResponse
|
||||||
return continuationRequest(PARENT_CATEGORY_CONTINUATION_PREFIX, categoryName) {
|
*/
|
||||||
categoryInterface.getParentCategoryList(categoryName, it)
|
@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)
|
* 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.
|
||||||
|
*
|
||||||
fun resetParentCategoryContinuation(category: String) {
|
* @param categoryName Category name as defined on commons
|
||||||
resetContinuation(PARENT_CATEGORY_CONTINUATION_PREFIX, category)
|
* @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
|
||||||
}
|
*/
|
||||||
|
fun getSubCategoryList(categoryName: String): Single<List<CategoryItem>> =
|
||||||
override fun responseMapper(
|
continuationRequest(SUB_CATEGORY_CONTINUATION_PREFIX, categoryName) {
|
||||||
networkResult: Single<MwQueryResponse>,
|
categoryInterface.getSubCategoryList(
|
||||||
key: String?
|
categoryName,
|
||||||
): Single<List<CategoryItem>> {
|
it,
|
||||||
return networkResult
|
)
|
||||||
.map {
|
|
||||||
handleContinuationResponse(it.continuation(), key)
|
|
||||||
it.query()?.pages() ?: emptyList()
|
|
||||||
}
|
}
|
||||||
.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 {
|
}.map {
|
||||||
CategoryItem(it.title().replace(CATEGORY_PREFIX, ""),
|
it
|
||||||
it.description().toString(), it.thumbUrl().toString(), false)
|
.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
|
* @param itemLimit How many results are returned
|
||||||
* @return
|
* @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(
|
fun searchCategories(
|
||||||
@Query("gsrsearch") filter: String?,
|
@Query("gsrsearch") filter: String?,
|
||||||
@Query("gsrlimit") itemLimit: Int,
|
@Query("gsrlimit") itemLimit: Int,
|
||||||
@Query("gsroffset") offset: Int
|
@Query("gsroffset") offset: Int,
|
||||||
): Single<MwQueryResponse>
|
): Single<MwQueryResponse>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -31,11 +33,13 @@ interface CategoryInterface {
|
||||||
* @param itemLimit How many results are returned
|
* @param itemLimit How many results are returned
|
||||||
* @return
|
* @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(
|
fun searchCategoriesForPrefix(
|
||||||
@Query("gacprefix") prefix: String?,
|
@Query("gacprefix") prefix: String?,
|
||||||
@Query("gaclimit") itemLimit: Int,
|
@Query("gaclimit") itemLimit: Int,
|
||||||
@Query("gacoffset") offset: Int
|
@Query("gacoffset") offset: Int,
|
||||||
): Single<MwQueryResponse>
|
): Single<MwQueryResponse>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -47,23 +51,25 @@ interface CategoryInterface {
|
||||||
* @param offset offset
|
* @param offset offset
|
||||||
* @return MwQueryResponse
|
* @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(
|
fun getCategoriesByName(
|
||||||
@Query("gacfrom") startingCategory: String?,
|
@Query("gacfrom") startingCategory: String?,
|
||||||
@Query("gacto") endingCategory: String?,
|
@Query("gacto") endingCategory: String?,
|
||||||
@Query("gaclimit") itemLimit: Int,
|
@Query("gaclimit") itemLimit: Int,
|
||||||
@Query("gacoffset") offset: Int
|
@Query("gacoffset") offset: Int,
|
||||||
): Single<MwQueryResponse>
|
): Single<MwQueryResponse>
|
||||||
|
|
||||||
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50")
|
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50")
|
||||||
fun getSubCategoryList(
|
fun getSubCategoryList(
|
||||||
@Query("gcmtitle") categoryName: String,
|
@Query("gcmtitle") categoryName: String,
|
||||||
@QueryMap(encoded = true) continuation: Map<String, String>
|
@QueryMap(encoded = true) continuation: Map<String, String>,
|
||||||
): Single<MwQueryResponse>
|
): Single<MwQueryResponse>
|
||||||
|
|
||||||
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categories&prop=info&gcllimit=50")
|
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categories&prop=info&gcllimit=50")
|
||||||
fun getParentCategoryList(
|
fun getParentCategoryList(
|
||||||
@Query("titles") categoryName: String?,
|
@Query("titles") categoryName: String?,
|
||||||
@QueryMap(encoded = true) continuation: Map<String, String>
|
@QueryMap(encoded = true) continuation: Map<String, String>,
|
||||||
): Single<MwQueryResponse>
|
): Single<MwQueryResponse>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@ import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class CategoryItem(val name: String, val description: String?,
|
data class CategoryItem(
|
||||||
val thumbnail: String?, var isSelected: Boolean) : Parcelable {
|
val name: String,
|
||||||
|
val description: String?,
|
||||||
override fun toString(): String {
|
val thumbnail: String?,
|
||||||
return "CategoryItem: '$name'"
|
var isSelected: Boolean,
|
||||||
}
|
) : Parcelable {
|
||||||
|
override fun toString(): String = "CategoryItem: '$name'"
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
|
|
@ -22,7 +23,5 @@ data class CategoryItem(val name: String, val description: String?,
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int = name.hashCode()
|
||||||
return name.hashCode()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,16 @@ package fr.free.nrw.commons.category
|
||||||
|
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
|
||||||
|
|
||||||
abstract class ContinuationClient<Network, Domain> {
|
abstract class ContinuationClient<Network, Domain> {
|
||||||
private val continuationStore: MutableMap<String, Map<String, String>?> = mutableMapOf()
|
private val continuationStore: MutableMap<String, Map<String, String>?> = mutableMapOf()
|
||||||
private val continuationExists: MutableMap<String, Boolean> = mutableMapOf()
|
private val continuationExists: MutableMap<String, Boolean> = mutableMapOf()
|
||||||
|
|
||||||
private fun hasMorePagesFor(key: String) = continuationExists[key] ?: true
|
private fun hasMorePagesFor(key: String) = continuationExists[key] ?: true
|
||||||
|
|
||||||
fun continuationRequest(
|
fun continuationRequest(
|
||||||
prefix: String,
|
prefix: String,
|
||||||
name: String,
|
name: String,
|
||||||
requestFunction: (Map<String, String>) -> Single<Network>
|
requestFunction: (Map<String, String>) -> Single<Network>,
|
||||||
): Single<List<Domain>> {
|
): Single<List<Domain>> {
|
||||||
val key = "$prefix$name"
|
val key = "$prefix$name"
|
||||||
return if (hasMorePagesFor(key)) {
|
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) {
|
if (key != null) {
|
||||||
continuationExists[key] =
|
continuationExists[key] =
|
||||||
continuation?.let { continuation ->
|
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")
|
continuationExists.remove("$prefix$category")
|
||||||
continuationStore.remove("$prefix$category")
|
continuationStore.remove("$prefix$category")
|
||||||
}
|
}
|
||||||
|
|
@ -44,9 +53,11 @@ abstract class ContinuationClient<Network, Domain> {
|
||||||
* @param prefix
|
* @param prefix
|
||||||
* @param userName the username
|
* @param userName the username
|
||||||
*/
|
*/
|
||||||
protected fun resetUserContinuation(prefix: String, userName: String) {
|
protected fun resetUserContinuation(
|
||||||
|
prefix: String,
|
||||||
|
userName: String,
|
||||||
|
) {
|
||||||
continuationExists.remove("$prefix$userName")
|
continuationExists.remove("$prefix$userName")
|
||||||
continuationStore.remove("$prefix$userName")
|
continuationStore.remove("$prefix$userName")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,32 +7,29 @@ import fr.free.nrw.commons.upload.UploadResult
|
||||||
data class ChunkInfo(
|
data class ChunkInfo(
|
||||||
val uploadResult: UploadResult?,
|
val uploadResult: UploadResult?,
|
||||||
val indexOfNextChunkToUpload: Int,
|
val indexOfNextChunkToUpload: Int,
|
||||||
val totalChunks: Int
|
val totalChunks: Int,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
parcel.readParcelable(UploadResult::class.java.classLoader),
|
parcel.readParcelable(UploadResult::class.java.classLoader),
|
||||||
parcel.readInt(),
|
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.writeParcelable(uploadResult, flags)
|
||||||
parcel.writeInt(indexOfNextChunkToUpload)
|
parcel.writeInt(indexOfNextChunkToUpload)
|
||||||
parcel.writeInt(totalChunks)
|
parcel.writeInt(totalChunks)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int = 0
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<ChunkInfo> {
|
companion object CREATOR : Parcelable.Creator<ChunkInfo> {
|
||||||
override fun createFromParcel(parcel: Parcel): ChunkInfo {
|
override fun createFromParcel(parcel: Parcel): ChunkInfo = ChunkInfo(parcel)
|
||||||
return ChunkInfo(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<ChunkInfo?> {
|
override fun newArray(size: Int): Array<ChunkInfo?> = arrayOfNulls(size)
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import android.os.Parcelable
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import fr.free.nrw.commons.CommonsApplication
|
|
||||||
import fr.free.nrw.commons.Media
|
import fr.free.nrw.commons.Media
|
||||||
import fr.free.nrw.commons.auth.SessionManager
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
import fr.free.nrw.commons.upload.UploadItem
|
import fr.free.nrw.commons.upload.UploadItem
|
||||||
|
|
@ -31,8 +30,7 @@ data class Contribution constructor(
|
||||||
var errorInfo: String? = null,
|
var errorInfo: String? = null,
|
||||||
/**
|
/**
|
||||||
* @return array list of entityids for the depictions
|
* @return array list of entityids for the depictions
|
||||||
*/
|
*
|
||||||
/**
|
|
||||||
* Each depiction loaded in depictions activity is associated with a wikidata entity id, this Id
|
* Each depiction loaded in depictions activity is associated with a wikidata entity id, this Id
|
||||||
* is in turn used to upload depictions to wikibase
|
* is in turn used to upload depictions to wikibase
|
||||||
*/
|
*/
|
||||||
|
|
@ -44,26 +42,23 @@ data class Contribution constructor(
|
||||||
var dateCreatedString: String? = null,
|
var dateCreatedString: String? = null,
|
||||||
var dateModified: Date? = null,
|
var dateModified: Date? = null,
|
||||||
var dateUploadStarted: Date? = null,
|
var dateUploadStarted: Date? = null,
|
||||||
var hasInvalidLocation : Int = 0,
|
var hasInvalidLocation: Int = 0,
|
||||||
var contentUri: Uri? = null,
|
var contentUri: Uri? = null,
|
||||||
var countryCode : String? = null,
|
var countryCode: String? = null,
|
||||||
var imageSHA1 : String? = null,
|
var imageSHA1: String? = null,
|
||||||
/**
|
/**
|
||||||
* Number of times a contribution has been retried after a failure
|
* Number of times a contribution has been retried after a failure
|
||||||
*/
|
*/
|
||||||
var retries: Int = 0
|
var retries: Int = 0,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
fun completeWith(media: Media): Contribution = copy(pageId = media.pageId, media = media, state = STATE_COMPLETED)
|
||||||
fun completeWith(media: Media): Contribution {
|
|
||||||
return copy(pageId = media.pageId, media = media, state = STATE_COMPLETED)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
item: UploadItem,
|
item: UploadItem,
|
||||||
sessionManager: SessionManager,
|
sessionManager: SessionManager,
|
||||||
depictedItems: List<DepictedItem>,
|
depictedItems: List<DepictedItem>,
|
||||||
categories: List<String>,
|
categories: List<String>,
|
||||||
imageSHA1: String
|
imageSHA1: String,
|
||||||
) : this(
|
) : this(
|
||||||
Media(
|
Media(
|
||||||
formatCaptions(item.uploadMediaDetails),
|
formatCaptions(item.uploadMediaDetails),
|
||||||
|
|
@ -71,7 +66,7 @@ data class Contribution constructor(
|
||||||
item.fileName,
|
item.fileName,
|
||||||
formatDescriptions(item.uploadMediaDetails),
|
formatDescriptions(item.uploadMediaDetails),
|
||||||
sessionManager.userName,
|
sessionManager.userName,
|
||||||
sessionManager.userName
|
sessionManager.userName,
|
||||||
),
|
),
|
||||||
localUri = item.mediaUri,
|
localUri = item.mediaUri,
|
||||||
decimalCoords = item.gpsCoords.decimalCoords,
|
decimalCoords = item.gpsCoords.decimalCoords,
|
||||||
|
|
@ -80,7 +75,7 @@ data class Contribution constructor(
|
||||||
wikidataPlace = from(item.place),
|
wikidataPlace = from(item.place),
|
||||||
contentUri = item.contentUri,
|
contentUri = item.contentUri,
|
||||||
dateCreatedString = item.fileCreatedDateString,
|
dateCreatedString = item.fileCreatedDateString,
|
||||||
imageSHA1 = imageSHA1
|
imageSHA1 = imageSHA1,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -91,9 +86,7 @@ data class Contribution constructor(
|
||||||
this.hasInvalidLocation = if (hasInvalidLocation) 1 else 0
|
this.hasInvalidLocation = if (hasInvalidLocation) 1 else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasInvalidLocation(): Boolean {
|
fun hasInvalidLocation(): Boolean = hasInvalidLocation == 1
|
||||||
return hasInvalidLocation == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val STATE_COMPLETED = -1
|
const val STATE_COMPLETED = -1
|
||||||
|
|
@ -107,7 +100,8 @@ data class Contribution constructor(
|
||||||
* @param uploadMediaDetails list of media Details
|
* @param uploadMediaDetails list of media Details
|
||||||
*/
|
*/
|
||||||
fun formatCaptions(uploadMediaDetails: List<UploadMediaDetail>) =
|
fun formatCaptions(uploadMediaDetails: List<UploadMediaDetail>) =
|
||||||
uploadMediaDetails.associate { it.languageCode!! to it.captionText }
|
uploadMediaDetails
|
||||||
|
.associate { it.languageCode!! to it.captionText }
|
||||||
.filter { it.value.isNotBlank() }
|
.filter { it.value.isNotBlank() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -117,19 +111,15 @@ data class Contribution constructor(
|
||||||
* @return a string with the pattern of {{en|1=descriptionText}}
|
* @return a string with the pattern of {{en|1=descriptionText}}
|
||||||
*/
|
*/
|
||||||
fun formatDescriptions(descriptions: List<UploadMediaDetail>) =
|
fun formatDescriptions(descriptions: List<UploadMediaDetail>) =
|
||||||
descriptions.filter { it.descriptionText.isNotEmpty() }
|
descriptions
|
||||||
|
.filter { it.descriptionText.isNotEmpty() }
|
||||||
.joinToString(separator = "") { "{{${it.languageCode}|1=${it.descriptionText}}}" }
|
.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) }
|
val localUriPath: File? get() = localUri?.path?.let { File(it) }
|
||||||
|
|
||||||
fun isCompleted(): Boolean {
|
fun isCompleted(): Boolean = chunkInfo != null && chunkInfo!!.totalChunks == chunkInfo!!.indexOfNextChunkToUpload
|
||||||
return chunkInfo != null && chunkInfo!!.totalChunks == chunkInfo!!.indexOfNextChunkToUpload
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dateUploadStartedInMillis(): Long {
|
|
||||||
return dateUploadStarted!!.time
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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
|
* Class that extends PagedList.BoundaryCallback for contributions list It defines the action that
|
||||||
* is triggered for various boundary conditions in the list
|
* is triggered for various boundary conditions in the list
|
||||||
*/
|
*/
|
||||||
class ContributionBoundaryCallback @Inject constructor(
|
class ContributionBoundaryCallback
|
||||||
private val repository: ContributionsRepository,
|
@Inject
|
||||||
private val sessionManager: SessionManager,
|
constructor(
|
||||||
private val mediaClient: MediaClient,
|
private val repository: ContributionsRepository,
|
||||||
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler
|
private val sessionManager: SessionManager,
|
||||||
) : BoundaryCallback<Contribution>() {
|
private val mediaClient: MediaClient,
|
||||||
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
|
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler,
|
||||||
var userName: String? = null
|
) : 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
|
||||||
* It is triggered when the list has no items User's Contributions are then fetched from the
|
* network
|
||||||
* network
|
*/
|
||||||
*/
|
override fun onZeroItemsLoaded() {
|
||||||
override fun onZeroItemsLoaded() {
|
if (sessionManager.userName != null) {
|
||||||
if (sessionManager.userName != null) {
|
mediaClient.resetUserNameContinuation(sessionManager.userName!!)
|
||||||
mediaClient.resetUserNameContinuation(sessionManager.userName!!)
|
}
|
||||||
|
fetchContributions()
|
||||||
}
|
}
|
||||||
fetchContributions()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It is triggered when the user scrolls to the top of the list
|
* It is triggered when the user scrolls to the top of the list
|
||||||
* */
|
* */
|
||||||
override fun onItemAtFrontLoaded(itemAtFront: Contribution) {
|
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
|
* Fetches contributions using the MediaWiki API
|
||||||
* fetched from the network
|
*/
|
||||||
*/
|
private fun fetchContributions() {
|
||||||
override fun onItemAtEndLoaded(itemAtEnd: Contribution) {
|
if (sessionManager.userName != null) {
|
||||||
fetchContributions()
|
userName
|
||||||
}
|
?.let { userName ->
|
||||||
|
mediaClient
|
||||||
/**
|
.getMediaListForUser(userName)
|
||||||
* Fetches contributions using the MediaWiki API
|
.map { mediaList ->
|
||||||
*/
|
mediaList.map { media ->
|
||||||
private fun fetchContributions() {
|
Contribution(media = media, state = Contribution.STATE_COMPLETED)
|
||||||
if (sessionManager.userName != null) {
|
}
|
||||||
userName?.let { userName ->
|
}.subscribeOn(ioThreadScheduler)
|
||||||
mediaClient.getMediaListForUser(userName)
|
.subscribe(::saveContributionsToDB) { error: Throwable ->
|
||||||
.map { mediaList ->
|
Timber.e(
|
||||||
mediaList.map { media ->
|
"Failed to fetch contributions: %s",
|
||||||
Contribution(media = media, state = Contribution.STATE_COMPLETED)
|
error.message,
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
.subscribeOn(ioThreadScheduler)
|
}?.let {
|
||||||
.subscribe(::saveContributionsToDB) { error: Throwable ->
|
compositeDisposable.add(
|
||||||
Timber.e(
|
it,
|
||||||
"Failed to fetch contributions: %s",
|
|
||||||
error.message
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}?.let {
|
} else {
|
||||||
compositeDisposable.add(
|
compositeDisposable.clear()
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}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
|
* Data-Source which acts as mediator for contributions-data from the API
|
||||||
*/
|
*/
|
||||||
class ContributionsRemoteDataSource @Inject constructor(
|
class ContributionsRemoteDataSource
|
||||||
private val mediaClient: MediaClient,
|
@Inject
|
||||||
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler
|
constructor(
|
||||||
) : ItemKeyedDataSource<Int, Contribution>() {
|
private val mediaClient: MediaClient,
|
||||||
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
|
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler,
|
||||||
var userName: String? = null
|
) : ItemKeyedDataSource<Int, Contribution>() {
|
||||||
|
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
|
||||||
|
var userName: String? = null
|
||||||
|
|
||||||
override fun loadInitial(
|
override fun loadInitial(
|
||||||
params: LoadInitialParams<Int>,
|
params: LoadInitialParams<Int>,
|
||||||
callback: LoadInitialCallback<Contribution>
|
callback: LoadInitialCallback<Contribution>,
|
||||||
) {
|
) {
|
||||||
fetchContributions(callback)
|
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
|
* Dialog fragment for displaying instructions for editing wikipedia
|
||||||
*/
|
*/
|
||||||
class WikipediaInstructionsDialogFragment : DialogFragment() {
|
class WikipediaInstructionsDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
var callback: Callback? = null
|
var callback: Callback? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?,
|
||||||
) = DialogAddToWikipediaInstructionsBinding.inflate(inflater, container, false).apply {
|
) = DialogAddToWikipediaInstructionsBinding
|
||||||
val contribution: Contribution? = arguments!!.getParcelable(ARG_CONTRIBUTION)
|
.inflate(inflater, container, false)
|
||||||
tvWikicode.setText(contribution?.media?.wikiCode)
|
.apply {
|
||||||
instructionsCancel.setOnClickListener { dismiss() }
|
val contribution: Contribution? = arguments!!.getParcelable(ARG_CONTRIBUTION)
|
||||||
instructionsConfirm.setOnClickListener {
|
tvWikicode.setText(contribution?.media?.wikiCode)
|
||||||
callback?.onConfirmClicked(contribution, checkboxCopyWikicode.isChecked)
|
instructionsCancel.setOnClickListener { dismiss() }
|
||||||
}
|
instructionsConfirm.setOnClickListener {
|
||||||
}.root
|
callback?.onConfirmClicked(contribution, checkboxCopyWikicode.isChecked)
|
||||||
|
}
|
||||||
|
}.root
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(
|
||||||
|
view: View,
|
||||||
|
savedInstanceState: Bundle?,
|
||||||
|
) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
dialog!!.window?.setSoftInputMode(
|
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
|
* Callback for handling confirm button clicked
|
||||||
*/
|
*/
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onConfirmClicked(contribution: Contribution?, copyWikicode: Boolean)
|
fun onConfirmClicked(
|
||||||
|
contribution: Contribution?,
|
||||||
|
copyWikicode: Boolean,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ARG_CONTRIBUTION = "contribution"
|
const val ARG_CONTRIBUTION = "contribution"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun newInstance(contribution: Contribution) = WikipediaInstructionsDialogFragment().apply {
|
fun newInstance(contribution: Contribution) =
|
||||||
arguments = bundleOf(ARG_CONTRIBUTION to contribution)
|
WikipediaInstructionsDialogFragment().apply {
|
||||||
}
|
arguments = bundleOf(ARG_CONTRIBUTION to contribution)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
package fr.free.nrw.commons.customselector.database
|
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 class for Not For Upload status.
|
||||||
*/
|
*/
|
||||||
@Entity(tableName = "images_not_for_upload_table")
|
@Entity(tableName = "images_not_for_upload_table")
|
||||||
data class NotForUploadStatus(
|
data class NotForUploadStatus(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Original image sha1.
|
* Original image sha1.
|
||||||
*/
|
*/
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val imageSHA1 : String
|
val imageSHA1: String,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
package fr.free.nrw.commons.customselector.database
|
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 class for Not For Upload
|
||||||
*/
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
abstract class NotForUploadStatusDao {
|
abstract class NotForUploadStatusDao {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert into Not For Upload status.
|
* Insert into Not For Upload status.
|
||||||
*/
|
*/
|
||||||
@Insert( onConflict = OnConflictStrategy.REPLACE )
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
abstract suspend fun insert(notForUploadStatus: NotForUploadStatus)
|
abstract suspend fun insert(notForUploadStatus: NotForUploadStatus)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -25,33 +27,27 @@ abstract class NotForUploadStatusDao {
|
||||||
* Query Not For Upload status with image sha1.
|
* Query Not For Upload status with image sha1.
|
||||||
*/
|
*/
|
||||||
@Query("SELECT * FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ")
|
@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.
|
* Asynchronous image sha1 query.
|
||||||
*/
|
*/
|
||||||
suspend fun getNotForUploadFromImageSHA1(imageSHA1: String):NotForUploadStatus? {
|
suspend fun getNotForUploadFromImageSHA1(imageSHA1: String): NotForUploadStatus? = getFromImageSHA1(imageSHA1)
|
||||||
return getFromImageSHA1(imageSHA1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletion Not For Upload status with image sha1.
|
* Deletion Not For Upload status with image sha1.
|
||||||
*/
|
*/
|
||||||
@Query("DELETE FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ")
|
@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.
|
* Asynchronous image sha1 deletion.
|
||||||
*/
|
*/
|
||||||
suspend fun deleteNotForUploadWithImageSHA1(imageSHA1: String) {
|
suspend fun deleteNotForUploadWithImageSHA1(imageSHA1: String) = deleteWithImageSHA1(imageSHA1)
|
||||||
return deleteWithImageSHA1(imageSHA1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the imageSHA1 is present in database
|
* Check whether the imageSHA1 is present in database
|
||||||
*/
|
*/
|
||||||
@Query("SELECT COUNT() FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ")
|
@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.Entity
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity class for Uploaded Status.
|
* Entity class for Uploaded Status.
|
||||||
*/
|
*/
|
||||||
@Entity(tableName = "uploaded_table", indices = [Index(value = ["modifiedImageSHA1"], unique = true)])
|
@Entity(tableName = "uploaded_table", indices = [Index(value = ["modifiedImageSHA1"], unique = true)])
|
||||||
data class UploadedStatus(
|
data class UploadedStatus(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Original image sha1.
|
* Original image sha1.
|
||||||
*/
|
*/
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val imageSHA1 : String,
|
val imageSHA1: String,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modified image sha1 (after exif changes).
|
* Modified image sha1 (after exif changes).
|
||||||
*/
|
*/
|
||||||
val modifiedImageSHA1 : String,
|
val modifiedImageSHA1: String,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* imageSHA1 query result from API.
|
* imageSHA1 query result from API.
|
||||||
*/
|
*/
|
||||||
var imageResult : Boolean,
|
var imageResult: Boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* modifiedImageSHA1 query result from API.
|
* modifiedImageSHA1 query result from API.
|
||||||
*/
|
*/
|
||||||
var modifiedImageResult : Boolean,
|
var modifiedImageResult: Boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* lastUpdated for data validation.
|
* lastUpdated for data validation.
|
||||||
*/
|
*/
|
||||||
var lastUpdated : Date? = null
|
var lastUpdated: Date? = null,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
package fr.free.nrw.commons.customselector.database
|
package fr.free.nrw.commons.customselector.database
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.Dao
|
||||||
import java.util.*
|
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.
|
* UploadedStatusDao for Custom Selector.
|
||||||
*/
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
abstract class UploadedStatusDao {
|
abstract class UploadedStatusDao {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert into uploaded status.
|
* Insert into uploaded status.
|
||||||
*/
|
*/
|
||||||
@Insert( onConflict = OnConflictStrategy.REPLACE )
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
abstract suspend fun insert(uploadedStatus: UploadedStatus)
|
abstract suspend fun insert(uploadedStatus: UploadedStatus)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -31,13 +35,13 @@ abstract class UploadedStatusDao {
|
||||||
* Query uploaded status with image sha1.
|
* Query uploaded status with image sha1.
|
||||||
*/
|
*/
|
||||||
@Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ")
|
@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 uploaded status with modified image sha1.
|
||||||
*/
|
*/
|
||||||
@Query("SELECT * FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) ")
|
@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.
|
* Asynchronous insert into uploaded status table.
|
||||||
|
|
@ -51,20 +55,24 @@ abstract class UploadedStatusDao {
|
||||||
* Check whether the imageSHA1 is present in database
|
* Check whether the imageSHA1 is present in database
|
||||||
*/
|
*/
|
||||||
@Query("SELECT COUNT() FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) AND imageResult = (:imageResult) ")
|
@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
|
* Check whether the modifiedImageSHA1 is present in database
|
||||||
*/
|
*/
|
||||||
@Query("SELECT COUNT() FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) AND modifiedImageResult = (:modifiedImageResult) ")
|
@Query(
|
||||||
abstract suspend fun findByModifiedImageSHA1(modifiedImageSHA1 : String,
|
"SELECT COUNT() FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) AND modifiedImageResult = (:modifiedImageResult) ",
|
||||||
modifiedImageResult: Boolean): Int
|
)
|
||||||
|
abstract suspend fun findByModifiedImageSHA1(
|
||||||
|
modifiedImageSHA1: String,
|
||||||
|
modifiedImageResult: Boolean,
|
||||||
|
): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronous image sha1 query.
|
* Asynchronous image sha1 query.
|
||||||
*/
|
*/
|
||||||
suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus? {
|
suspend fun getUploadedFromImageSHA1(imageSHA1: String): UploadedStatus? = getFromImageSHA1(imageSHA1)
|
||||||
return getFromImageSHA1(imageSHA1)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -4,12 +4,10 @@ package fr.free.nrw.commons.customselector.helper
|
||||||
* Stores constants related to custom image selector
|
* Stores constants related to custom image selector
|
||||||
*/
|
*/
|
||||||
object CustomSelectorConstants {
|
object CustomSelectorConstants {
|
||||||
|
|
||||||
const val BUCKET_ID = "bucket_id"
|
const val BUCKET_ID = "bucket_id"
|
||||||
const val TOTAL_SELECTED_IMAGES = "total_selected_images"
|
const val TOTAL_SELECTED_IMAGES = "total_selected_images"
|
||||||
const val PRESENT_POSITION = "present_position"
|
const val PRESENT_POSITION = "present_position"
|
||||||
const val NEW_SELECTED_IMAGES = "new_selected_images"
|
const val NEW_SELECTED_IMAGES = "new_selected_images"
|
||||||
const val SHOULD_REFRESH = "should_refresh"
|
const val SHOULD_REFRESH = "should_refresh"
|
||||||
const val FULL_SCREEN_MODE_FIRST_LUNCH = "full_screen_mode_first_launch"
|
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.
|
* Image Helper object, includes all the static functions and variables required by custom selector.
|
||||||
*/
|
*/
|
||||||
object ImageHelper {
|
object ImageHelper {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom selector preference key
|
* Custom selector preference key
|
||||||
*/
|
*/
|
||||||
|
|
@ -39,7 +38,10 @@ object ImageHelper {
|
||||||
/**
|
/**
|
||||||
* Filters the images based on the given bucketId (folder)
|
* 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
|
if (bukketId == null) return images
|
||||||
|
|
||||||
val filteredImages = arrayListOf<Image>()
|
val filteredImages = arrayListOf<Image>()
|
||||||
|
|
@ -54,30 +56,37 @@ object ImageHelper {
|
||||||
/**
|
/**
|
||||||
* getIndex: Returns the index of image in given list.
|
* getIndex: Returns the index of image in given list.
|
||||||
*/
|
*/
|
||||||
fun getIndex(list: ArrayList<Image>, image: Image): Int {
|
fun getIndex(
|
||||||
return list.indexOf(image)
|
list: ArrayList<Image>,
|
||||||
}
|
image: Image,
|
||||||
|
): Int = list.indexOf(image)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getIndex: Returns the index of image in given list.
|
* getIndex: Returns the index of image in given list.
|
||||||
*/
|
*/
|
||||||
fun getIndexFromId(list: ArrayList<Image>, imageId: Long): Int {
|
fun getIndexFromId(
|
||||||
for(i in list){
|
list: ArrayList<Image>,
|
||||||
if(i.id == imageId)
|
imageId: Long,
|
||||||
|
): Int {
|
||||||
|
for (i in list) {
|
||||||
|
if (i.id == imageId) {
|
||||||
return list.indexOf(i)
|
return list.indexOf(i)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the list of indices from the master list.
|
* Gets the list of indices from the master list.
|
||||||
*/
|
*/
|
||||||
fun getIndexList(list: ArrayList<Image>, masterList: ArrayList<Image>): ArrayList<Int> {
|
fun getIndexList(
|
||||||
|
list: ArrayList<Image>,
|
||||||
// Can be optimised as masterList is sorted by time.
|
masterList: ArrayList<Image>,
|
||||||
|
): ArrayList<Int> {
|
||||||
|
// Can be optimised as masterList is sorted by time.
|
||||||
|
|
||||||
val indexes = arrayListOf<Int>()
|
val indexes = arrayListOf<Int>()
|
||||||
for(image in list) {
|
for (image in list) {
|
||||||
val index = getIndex(masterList, image)
|
val index = getIndex(masterList, image)
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
continue
|
continue
|
||||||
|
|
@ -86,4 +95,4 @@ object ImageHelper {
|
||||||
}
|
}
|
||||||
return indexes
|
return indexes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,29 @@ package fr.free.nrw.commons.customselector.helper
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.DisplayMetrics
|
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
|
import kotlin.math.abs
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for detecting swipe gestures
|
* 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 gestureDetector: GestureDetector
|
||||||
|
|
||||||
private val SWIPE_THRESHOLD_HEIGHT = (getScreenResolution(context!!)).second / 3
|
private val swipeThresholdHeight = (getScreenResolution(context!!)).second / 3
|
||||||
private val SWIPE_THRESHOLD_WIDTH = (getScreenResolution(context!!)).first / 3
|
private val swipeThresholdWidth = (getScreenResolution(context!!)).first / 3
|
||||||
private val SWIPE_VELOCITY_THRESHOLD = 1000
|
private val swipeVelocityThreshold = 1000
|
||||||
|
|
||||||
override fun onTouch(view: View?, motionEvent: MotionEvent): Boolean {
|
override fun onTouch(
|
||||||
return gestureDetector.onTouchEvent(motionEvent)
|
view: View?,
|
||||||
}
|
motionEvent: MotionEvent,
|
||||||
|
): Boolean = gestureDetector.onTouchEvent(motionEvent)
|
||||||
|
|
||||||
fun getScreenResolution(context: Context): Pair<Int, Int> {
|
fun getScreenResolution(context: Context): Pair<Int, Int> {
|
||||||
val wm: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
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() {
|
inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
|
||||||
|
override fun onDown(e: MotionEvent): Boolean = true
|
||||||
override fun onDown(e: MotionEvent): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects the gestures
|
* Detects the gestures
|
||||||
|
|
@ -43,14 +46,16 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
|
||||||
event1: MotionEvent?,
|
event1: MotionEvent?,
|
||||||
event2: MotionEvent,
|
event2: MotionEvent,
|
||||||
velocityX: Float,
|
velocityX: Float,
|
||||||
velocityY: Float
|
velocityY: Float,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
try {
|
try {
|
||||||
val diffY: Float = event2.y - (event1?.y ?: event2.y)
|
val diffY: Float = event2.y - (event1?.y ?: event2.y)
|
||||||
val diffX: Float = event2.x - (event1?.x ?: event2.x)
|
val diffX: Float = event2.x - (event1?.x ?: event2.x)
|
||||||
if (abs(diffX) > abs(diffY)) {
|
if (abs(diffX) > abs(diffY)) {
|
||||||
if (abs(diffX) > SWIPE_THRESHOLD_WIDTH && abs(velocityX) >
|
if (abs(diffX) > swipeThresholdWidth &&
|
||||||
SWIPE_VELOCITY_THRESHOLD) {
|
abs(velocityX) >
|
||||||
|
swipeVelocityThreshold
|
||||||
|
) {
|
||||||
if (diffX > 0) {
|
if (diffX > 0) {
|
||||||
onSwipeRight()
|
onSwipeRight()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -58,8 +63,10 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (abs(diffY) > SWIPE_THRESHOLD_HEIGHT && abs(velocityY) >
|
if (abs(diffY) > swipeThresholdHeight &&
|
||||||
SWIPE_VELOCITY_THRESHOLD) {
|
abs(velocityY) >
|
||||||
|
swipeVelocityThreshold
|
||||||
|
) {
|
||||||
if (diffY > 0) {
|
if (diffY > 0) {
|
||||||
onSwipeDown()
|
onSwipeDown()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -100,4 +107,4 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
|
||||||
init {
|
init {
|
||||||
gestureDetector = GestureDetector(context, GestureListener())
|
gestureDetector = GestureDetector(context, GestureListener())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,15 @@ package fr.free.nrw.commons.customselector.listeners
|
||||||
* Custom Selector Folder Click Listener
|
* Custom Selector Folder Click Listener
|
||||||
*/
|
*/
|
||||||
interface FolderClickListener {
|
interface FolderClickListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* onFolderClick
|
* onFolderClick
|
||||||
* @param folderId : folder id of the folder.
|
* @param folderId : folder id of the folder.
|
||||||
* @param folderName : folder name of the folder.
|
* @param folderName : folder name of the folder.
|
||||||
* @param lastItemId : last scroll position in 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.
|
* responds to the device image query.
|
||||||
*/
|
*/
|
||||||
interface ImageLoaderListener {
|
interface ImageLoaderListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On image loaded
|
* On image loaded
|
||||||
* @param images : queried device images.
|
* @param images : queried device images.
|
||||||
|
|
@ -19,4 +18,4 @@ interface ImageLoaderListener {
|
||||||
* @param throwable : throwable exception on failure.
|
* @param throwable : throwable exception on failure.
|
||||||
*/
|
*/
|
||||||
fun onFailed(throwable: Throwable)
|
fun onFailed(throwable: Throwable)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
package fr.free.nrw.commons.customselector.listeners
|
package fr.free.nrw.commons.customselector.listeners
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom selector Image select listener
|
* Custom selector Image select listener
|
||||||
*/
|
*/
|
||||||
interface ImageSelectListener {
|
interface ImageSelectListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* onSelectedImagesChanged
|
* onSelectedImagesChanged
|
||||||
* @param selectedImages : new selected images.
|
* @param selectedImages : new selected images.
|
||||||
* @param selectedNotForUploadImages : number of selected not for upload 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
|
* onLongPress
|
||||||
|
|
@ -22,6 +23,6 @@ interface ImageSelectListener {
|
||||||
fun onLongPress(
|
fun onLongPress(
|
||||||
position: Int,
|
position: Int,
|
||||||
images: ArrayList<Image>,
|
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 to pass data between fragment and activity
|
||||||
*/
|
*/
|
||||||
interface PassDataListener {
|
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
|
* Refreshes the data in adapter
|
||||||
*/
|
*/
|
||||||
fun refresh()
|
fun refresh()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,17 @@ package fr.free.nrw.commons.customselector.model
|
||||||
*/
|
*/
|
||||||
sealed class CallbackStatus {
|
sealed class CallbackStatus {
|
||||||
/**
|
/**
|
||||||
IDLE : The callback is idle , doing nothing.
|
IDLE : The callback is idle , doing nothing.
|
||||||
*/
|
*/
|
||||||
object IDLE : CallbackStatus()
|
object IDLE : CallbackStatus()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
FETCHING : Fetching images.
|
FETCHING : Fetching images.
|
||||||
*/
|
*/
|
||||||
object FETCHING : CallbackStatus()
|
object FETCHING : CallbackStatus()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
SUCCESS : Success fetching images.
|
SUCCESS : Success fetching images.
|
||||||
*/
|
*/
|
||||||
object SUCCESS : CallbackStatus()
|
object SUCCESS : CallbackStatus()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,27 +5,22 @@ package fr.free.nrw.commons.customselector.model
|
||||||
*/
|
*/
|
||||||
data class Folder(
|
data class Folder(
|
||||||
/**
|
/**
|
||||||
bucketId : Unique directory id, eg 540528482
|
bucketId : Unique directory id, eg 540528482
|
||||||
*/
|
*/
|
||||||
var bucketId: Long,
|
var bucketId: Long,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
name : bucket/folder name, eg Camera
|
name : bucket/folder name, eg Camera
|
||||||
*/
|
*/
|
||||||
var name: String,
|
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.
|
* Indicates whether some other object is "equal to" this one.
|
||||||
*/
|
*/
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|
||||||
if (javaClass != other?.javaClass) {
|
if (javaClass != other?.javaClass) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -44,4 +39,4 @@ data class Folder(
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,65 +9,60 @@ import android.os.Parcelable
|
||||||
*/
|
*/
|
||||||
data class Image(
|
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,
|
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,
|
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,
|
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,
|
var path: String,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
bucketId : bucketId of folder, eg 540528482
|
bucketId : bucketId of folder, eg 540528482
|
||||||
*/
|
*/
|
||||||
var bucketId: Long = 0,
|
var bucketId: Long = 0,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
bucketName : name of folder, eg Camera
|
bucketName : name of folder, eg Camera
|
||||||
*/
|
*/
|
||||||
var bucketName: String = "",
|
var bucketName: String = "",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
sha1 : sha1 of original image.
|
sha1 : sha1 of original image.
|
||||||
*/
|
*/
|
||||||
var sha1: String = "",
|
var sha1: String = "",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* date: Creation date of the image to show it inside the bubble during bubble scroll.
|
* date: Creation date of the image to show it inside the bubble during bubble scroll.
|
||||||
*/
|
*/
|
||||||
var date: String = ""
|
var date: String = "",
|
||||||
|
|
||||||
) : Parcelable {
|
) : 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):
|
override fun writeToParcel(
|
||||||
this(parcel.readLong(),
|
parcel: Parcel,
|
||||||
parcel.readString()!!,
|
flags: Int,
|
||||||
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) {
|
|
||||||
parcel.writeLong(id)
|
parcel.writeLong(id)
|
||||||
parcel.writeString(name)
|
parcel.writeString(name)
|
||||||
parcel.writeParcelable(uri, flags)
|
parcel.writeParcelable(uri, flags)
|
||||||
|
|
@ -81,41 +76,38 @@ data class Image(
|
||||||
/**
|
/**
|
||||||
* Describe the kinds of special objects contained in this Parcelable
|
* Describe the kinds of special objects contained in this Parcelable
|
||||||
*/
|
*/
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int = 0
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether some other object is "equal to" this one.
|
* Indicates whether some other object is "equal to" this one.
|
||||||
*/
|
*/
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (javaClass != other?.javaClass) {
|
||||||
if(javaClass != other?.javaClass) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
other as Image
|
other as Image
|
||||||
|
|
||||||
if(id != other.id) {
|
if (id != other.id) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if(name != other.name) {
|
if (name != other.name) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if(uri != other.uri) {
|
if (uri != other.uri) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if(path != other.path) {
|
if (path != other.path) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if(bucketId != other.bucketId) {
|
if (bucketId != other.bucketId) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if(bucketName != other.bucketName) {
|
if (bucketName != other.bucketName) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if(sha1 != other.sha1) {
|
if (sha1 != other.sha1) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
@ -125,12 +117,8 @@ data class Image(
|
||||||
* Parcelable companion object
|
* Parcelable companion object
|
||||||
*/
|
*/
|
||||||
companion object CREATOR : Parcelable.Creator<Image> {
|
companion object CREATOR : Parcelable.Creator<Image> {
|
||||||
override fun createFromParcel(parcel: Parcel): Image {
|
override fun createFromParcel(parcel: Parcel): Image = Image(parcel)
|
||||||
return Image(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<Image?> {
|
override fun newArray(size: Int): Array<Image?> = arrayOfNulls(size)
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,9 @@ data class Result(
|
||||||
/**
|
/**
|
||||||
* CallbackStatus : stores the result status
|
* CallbackStatus : stores the result status
|
||||||
*/
|
*/
|
||||||
val status:CallbackStatus,
|
val status: CallbackStatus,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Images : images retrieved
|
* Images : images retrieved
|
||||||
*/
|
*/
|
||||||
val images: ArrayList<Image>) {
|
val images: ArrayList<Image>,
|
||||||
}
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,11 @@ class FolderAdapter(
|
||||||
* Application context.
|
* Application context.
|
||||||
*/
|
*/
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Folder Click listener for click events.
|
* Folder Click listener for click events.
|
||||||
*/
|
*/
|
||||||
private val itemClickListener: FolderClickListener
|
private val itemClickListener: FolderClickListener,
|
||||||
|
|
||||||
) : RecyclerViewAdapter<FolderAdapter.FolderViewHolder?>(context) {
|
) : RecyclerViewAdapter<FolderAdapter.FolderViewHolder?>(context) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of folders.
|
* List of folders.
|
||||||
*/
|
*/
|
||||||
|
|
@ -37,7 +34,10 @@ class FolderAdapter(
|
||||||
/**
|
/**
|
||||||
* Create view holder, returns View holder item.
|
* 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)
|
val itemView = inflater.inflate(R.layout.item_custom_selector_folder, parent, false)
|
||||||
return FolderViewHolder(itemView)
|
return FolderViewHolder(itemView)
|
||||||
}
|
}
|
||||||
|
|
@ -45,28 +45,31 @@ class FolderAdapter(
|
||||||
/**
|
/**
|
||||||
* Bind view holder, setup the item view, title, count and click listener
|
* 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 folder = folders[position]
|
||||||
val toBeRemoved = ArrayList<Image>()
|
val toBeRemoved = ArrayList<Image>()
|
||||||
|
|
||||||
for(image in folder.images) {
|
for (image in folder.images) {
|
||||||
// Remove all the top images that do not exist anymore
|
// 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
|
// File not found
|
||||||
toBeRemoved.add(image)
|
toBeRemoved.add(image)
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
holder.image.setImageDrawable (null)
|
holder.image.setImageDrawable(null)
|
||||||
folder.images.removeAll(toBeRemoved)
|
folder.images.removeAll(toBeRemoved)
|
||||||
val count = folder.images.size
|
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.
|
// Folder is empty, remove folder from the adapter.
|
||||||
holder.itemView.post{
|
holder.itemView.post {
|
||||||
val updatePosition = folders.indexOf(folder)
|
val updatePosition = folders.indexOf(folder)
|
||||||
if(updatePosition != -1) {
|
if (updatePosition != -1) {
|
||||||
folders.removeAt(updatePosition)
|
folders.removeAt(updatePosition)
|
||||||
notifyItemRemoved(updatePosition)
|
notifyItemRemoved(updatePosition)
|
||||||
notifyItemRangeChanged(updatePosition, folders.size)
|
notifyItemRangeChanged(updatePosition, folders.size)
|
||||||
|
|
@ -89,9 +92,10 @@ class FolderAdapter(
|
||||||
fun init(newFolders: List<Folder>) {
|
fun init(newFolders: List<Folder>) {
|
||||||
val oldFolderList: MutableList<Folder> = folders
|
val oldFolderList: MutableList<Folder> = folders
|
||||||
val newFolderList = newFolders.toMutableList()
|
val newFolderList = newFolders.toMutableList()
|
||||||
val diffResult = DiffUtil.calculateDiff(
|
val diffResult =
|
||||||
FoldersDiffCallback(oldFolderList, newFolderList)
|
DiffUtil.calculateDiff(
|
||||||
)
|
FoldersDiffCallback(oldFolderList, newFolderList),
|
||||||
|
)
|
||||||
folders = newFolderList
|
folders = newFolderList
|
||||||
diffResult.dispatchUpdatesTo(this)
|
diffResult.dispatchUpdatesTo(this)
|
||||||
}
|
}
|
||||||
|
|
@ -99,15 +103,14 @@ class FolderAdapter(
|
||||||
/**
|
/**
|
||||||
* returns item count.
|
* returns item count.
|
||||||
*/
|
*/
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int = folders.size
|
||||||
return folders.size
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Folder view holder.
|
* Folder view holder.
|
||||||
*/
|
*/
|
||||||
class FolderViewHolder(itemView:View) : RecyclerView.ViewHolder(itemView) {
|
class FolderViewHolder(
|
||||||
|
itemView: View,
|
||||||
|
) : RecyclerView.ViewHolder(itemView) {
|
||||||
/**
|
/**
|
||||||
* Folder thumbnail image view.
|
* Folder thumbnail image view.
|
||||||
*/
|
*/
|
||||||
|
|
@ -129,37 +132,33 @@ class FolderAdapter(
|
||||||
*/
|
*/
|
||||||
class FoldersDiffCallback(
|
class FoldersDiffCallback(
|
||||||
var oldFolders: MutableList<Folder>,
|
var oldFolders: MutableList<Folder>,
|
||||||
var newFolders: MutableList<Folder>
|
var newFolders: MutableList<Folder>,
|
||||||
) : DiffUtil.Callback() {
|
) : DiffUtil.Callback() {
|
||||||
/**
|
/**
|
||||||
* Returns the size of the old list.
|
* Returns the size of the old list.
|
||||||
*/
|
*/
|
||||||
override fun getOldListSize(): Int {
|
override fun getOldListSize(): Int = oldFolders.size
|
||||||
return oldFolders.size
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the size of the new list.
|
* Returns the size of the new list.
|
||||||
*/
|
*/
|
||||||
override fun getNewListSize(): Int {
|
override fun getNewListSize(): Int = newFolders.size
|
||||||
return newFolders.size
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called by the DiffUtil to decide whether two object represent the same Item.
|
* Called by the DiffUtil to decide whether two object represent the same Item.
|
||||||
*/
|
*/
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areItemsTheSame(
|
||||||
return oldFolders.get(oldItemPosition).bucketId == newFolders.get(newItemPosition).bucketId
|
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.
|
* 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.
|
* DiffUtil uses this information to detect if the contents of an item has changed.
|
||||||
*/
|
*/
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areContentsTheSame(
|
||||||
return oldFolders.get(oldItemPosition).equals(newFolders.get(newItemPosition))
|
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.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.constraintlayout.widget.Group
|
import androidx.constraintlayout.widget.Group
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
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.listeners.ImageSelectListener
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
|
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import java.util.*
|
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
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -32,20 +36,16 @@ class ImageAdapter(
|
||||||
* Application Context.
|
* Application Context.
|
||||||
*/
|
*/
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image select listener for click events on image.
|
* Image select listener for click events on image.
|
||||||
*/
|
*/
|
||||||
private var imageSelectListener: ImageSelectListener,
|
private var imageSelectListener: ImageSelectListener,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageLoader queries images.
|
* ImageLoader queries images.
|
||||||
*/
|
*/
|
||||||
private var imageLoader: ImageLoader
|
private var imageLoader: ImageLoader,
|
||||||
):
|
) : RecyclerViewAdapter<ImageAdapter.ImageViewHolder>(context),
|
||||||
|
FastScrollRecyclerView.SectionedAdapter {
|
||||||
RecyclerViewAdapter<ImageAdapter.ImageViewHolder>(context), FastScrollRecyclerView.SectionedAdapter {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageSelectedOrUpdated payload class.
|
* ImageSelectedOrUpdated payload class.
|
||||||
*/
|
*/
|
||||||
|
|
@ -106,14 +106,17 @@ class ImageAdapter(
|
||||||
/**
|
/**
|
||||||
* Coroutine Dispatchers and Scope.
|
* Coroutine Dispatchers and Scope.
|
||||||
*/
|
*/
|
||||||
private var defaultDispatcher : CoroutineDispatcher = Dispatchers.Default
|
private var defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||||
private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO
|
private var ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
private val scope : CoroutineScope = MainScope()
|
private val scope: CoroutineScope = MainScope()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create View holder.
|
* 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)
|
val itemView = inflater.inflate(R.layout.item_custom_selector_image, parent, false)
|
||||||
return ImageViewHolder(itemView)
|
return ImageViewHolder(itemView)
|
||||||
}
|
}
|
||||||
|
|
@ -121,10 +124,15 @@ class ImageAdapter(
|
||||||
/**
|
/**
|
||||||
* Bind View holder, load image, selected view, click listeners.
|
* Bind View holder, load image, selected view, click listeners.
|
||||||
*/
|
*/
|
||||||
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
override fun onBindViewHolder(
|
||||||
if(images.size == 0) { return }
|
holder: ImageViewHolder,
|
||||||
var image=images[position]
|
position: Int,
|
||||||
holder.image.setImageDrawable (null)
|
) {
|
||||||
|
if (images.size == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var image = images[position]
|
||||||
|
holder.image.setImageDrawable(null)
|
||||||
if (context.contentResolver.getType(image.uri) == null) {
|
if (context.contentResolver.getType(image.uri) == null) {
|
||||||
// Image does not exist anymore, update adapter.
|
// Image does not exist anymore, update adapter.
|
||||||
holder.itemView.post {
|
holder.itemView.post {
|
||||||
|
|
@ -140,18 +148,19 @@ class ImageAdapter(
|
||||||
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
|
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
|
||||||
|
|
||||||
// Getting selected index when switch is on
|
// Getting selected index when switch is on
|
||||||
val selectedIndex: Int = if (showAlreadyActionedImages) {
|
val selectedIndex: Int =
|
||||||
ImageHelper.getIndex(selectedImages, image)
|
if (showAlreadyActionedImages) {
|
||||||
|
ImageHelper.getIndex(selectedImages, image)
|
||||||
|
|
||||||
// Getting selected index when switch is off
|
// Getting selected index when switch is off
|
||||||
} else if (actionableImagesMap.size > position) {
|
} else if (actionableImagesMap.size > position) {
|
||||||
ImageHelper
|
ImageHelper
|
||||||
.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
|
.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
|
||||||
|
|
||||||
// For any other case return -1
|
// For any other case return -1
|
||||||
} else {
|
} else {
|
||||||
-1
|
-1
|
||||||
}
|
}
|
||||||
|
|
||||||
val isSelected = selectedIndex != -1
|
val isSelected = selectedIndex != -1
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
|
|
@ -160,7 +169,11 @@ class ImageAdapter(
|
||||||
holder.itemUnselected()
|
holder.itemUnselected()
|
||||||
}
|
}
|
||||||
imageLoader.queryAndSetView(
|
imageLoader.queryAndSetView(
|
||||||
holder, image, ioDispatcher, defaultDispatcher ,uploadingContributionList
|
holder,
|
||||||
|
image,
|
||||||
|
ioDispatcher,
|
||||||
|
defaultDispatcher,
|
||||||
|
uploadingContributionList,
|
||||||
)
|
)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val sharedPreferences: SharedPreferences =
|
val sharedPreferences: SharedPreferences =
|
||||||
|
|
@ -173,22 +186,28 @@ class ImageAdapter(
|
||||||
if (!alreadyAddedPositions.contains(position)) {
|
if (!alreadyAddedPositions.contains(position)) {
|
||||||
processThumbnailForActionedImage(holder, position, uploadingContributionList)
|
processThumbnailForActionedImage(holder, position, uploadingContributionList)
|
||||||
|
|
||||||
// If the position is already visited, that means the image is already present
|
// 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
|
// inside map, so it will fetch the image from the map and load in the holder
|
||||||
} else {
|
} else {
|
||||||
val actionableImages: List<Image> = ArrayList(actionableImagesMap.values)
|
val actionableImages: List<Image> = ArrayList(actionableImagesMap.values)
|
||||||
if(actionableImages.size > position) {
|
if (actionableImages.size > position) {
|
||||||
image = actionableImages[position]
|
image = actionableImages[position]
|
||||||
Glide.with(holder.image).load(image.uri)
|
Glide
|
||||||
.thumbnail(0.3f).into(holder.image)
|
.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
|
// If switch is turned off, it just fetches the image from all images without any
|
||||||
// further operations
|
// further operations
|
||||||
} else {
|
} else {
|
||||||
Glide.with(holder.image).load(image.uri)
|
Glide
|
||||||
.thumbnail(0.3f).into(holder.image)
|
.with(holder.image)
|
||||||
|
.load(image.uri)
|
||||||
|
.thumbnail(0.3f)
|
||||||
|
.into(holder.image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,12 +229,16 @@ class ImageAdapter(
|
||||||
suspend fun processThumbnailForActionedImage(
|
suspend fun processThumbnailForActionedImage(
|
||||||
holder: ImageViewHolder,
|
holder: ImageViewHolder,
|
||||||
position: Int,
|
position: Int,
|
||||||
uploadingContributionList: List<Contribution>
|
uploadingContributionList: List<Contribution>,
|
||||||
) {
|
) {
|
||||||
val next = imageLoader.nextActionableImage(
|
val next =
|
||||||
allImages, ioDispatcher, defaultDispatcher,
|
imageLoader.nextActionableImage(
|
||||||
nextImagePosition, uploadingContributionList
|
allImages,
|
||||||
)
|
ioDispatcher,
|
||||||
|
defaultDispatcher,
|
||||||
|
nextImagePosition,
|
||||||
|
uploadingContributionList,
|
||||||
|
)
|
||||||
|
|
||||||
// If next actionable image is found, saves it, as the the search for
|
// If next actionable image is found, saves it, as the the search for
|
||||||
// finding next actionable image will start from this position
|
// finding next actionable image will start from this position
|
||||||
|
|
@ -229,8 +252,11 @@ class ImageAdapter(
|
||||||
actionableImagesMap[next] = allImages[next]
|
actionableImagesMap[next] = allImages[next]
|
||||||
alreadyAddedPositions.add(imagePositionAsPerIncreasingOrder)
|
alreadyAddedPositions.add(imagePositionAsPerIncreasingOrder)
|
||||||
imagePositionAsPerIncreasingOrder++
|
imagePositionAsPerIncreasingOrder++
|
||||||
Glide.with(holder.image).load(allImages[next].uri)
|
Glide
|
||||||
.thumbnail(0.3f).into(holder.image)
|
.with(holder.image)
|
||||||
|
.load(allImages[next].uri)
|
||||||
|
.thumbnail(0.3f)
|
||||||
|
.into(holder.image)
|
||||||
notifyItemInserted(position)
|
notifyItemInserted(position)
|
||||||
notifyItemRangeChanged(position, itemCount + 1)
|
notifyItemRangeChanged(position, itemCount + 1)
|
||||||
}
|
}
|
||||||
|
|
@ -248,7 +274,7 @@ class ImageAdapter(
|
||||||
*/
|
*/
|
||||||
private fun onThumbnailClicked(
|
private fun onThumbnailClicked(
|
||||||
position: Int,
|
position: Int,
|
||||||
holder: ImageViewHolder
|
holder: ImageViewHolder,
|
||||||
) {
|
) {
|
||||||
val sharedPreferences: SharedPreferences =
|
val sharedPreferences: SharedPreferences =
|
||||||
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
|
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
|
||||||
|
|
@ -269,7 +295,10 @@ class ImageAdapter(
|
||||||
/**
|
/**
|
||||||
* Handle click event on an image, update counter on images.
|
* 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 =
|
val sharedPreferences: SharedPreferences =
|
||||||
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
|
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
|
||||||
val showAlreadyActionedImages =
|
val showAlreadyActionedImages =
|
||||||
|
|
@ -277,14 +306,15 @@ class ImageAdapter(
|
||||||
|
|
||||||
// Getting clicked index from all images index when show_already_actioned_images
|
// Getting clicked index from all images index when show_already_actioned_images
|
||||||
// switch is on
|
// switch is on
|
||||||
val clickedIndex: Int = if(showAlreadyActionedImages) {
|
val clickedIndex: Int =
|
||||||
ImageHelper.getIndex(selectedImages, images[position])
|
if (showAlreadyActionedImages) {
|
||||||
|
ImageHelper.getIndex(selectedImages, images[position])
|
||||||
|
|
||||||
// Getting clicked index from actionable images when show_already_actioned_images
|
// Getting clicked index from actionable images when show_already_actioned_images
|
||||||
// switch is off
|
// switch is off
|
||||||
} else {
|
} else {
|
||||||
ImageHelper.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
|
ImageHelper.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clickedIndex != -1) {
|
if (clickedIndex != -1) {
|
||||||
selectedImages.removeAt(clickedIndex)
|
selectedImages.removeAt(clickedIndex)
|
||||||
|
|
@ -294,13 +324,14 @@ class ImageAdapter(
|
||||||
notifyItemChanged(position, ImageUnselected())
|
notifyItemChanged(position, ImageUnselected())
|
||||||
|
|
||||||
// Getting index from all images index when switch is on
|
// Getting index from all images index when switch is on
|
||||||
val indexes = if (showAlreadyActionedImages) {
|
val indexes =
|
||||||
ImageHelper.getIndexList(selectedImages, images)
|
if (showAlreadyActionedImages) {
|
||||||
|
ImageHelper.getIndexList(selectedImages, images)
|
||||||
|
|
||||||
// Getting index from actionable images when switch is off
|
// Getting index from actionable images when switch is off
|
||||||
} else {
|
} else {
|
||||||
ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
|
ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
|
||||||
}
|
}
|
||||||
for (index in indexes) {
|
for (index in indexes) {
|
||||||
notifyItemChanged(index, ImageSelectedOrUpdated())
|
notifyItemChanged(index, ImageSelectedOrUpdated())
|
||||||
}
|
}
|
||||||
|
|
@ -313,15 +344,16 @@ class ImageAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getting index from all images index when switch is on
|
// Getting index from all images index when switch is on
|
||||||
val indexes: ArrayList<Int> = if (showAlreadyActionedImages) {
|
val indexes: ArrayList<Int> =
|
||||||
selectedImages.add(images[position])
|
if (showAlreadyActionedImages) {
|
||||||
ImageHelper.getIndexList(selectedImages, images)
|
selectedImages.add(images[position])
|
||||||
|
ImageHelper.getIndexList(selectedImages, images)
|
||||||
|
|
||||||
// Getting index from actionable images when switch is off
|
// Getting index from actionable images when switch is off
|
||||||
} else {
|
} else {
|
||||||
selectedImages.add(ArrayList(actionableImagesMap.values)[position])
|
selectedImages.add(ArrayList(actionableImagesMap.values)[position])
|
||||||
ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
|
ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
|
||||||
}
|
}
|
||||||
|
|
||||||
for (index in indexes) {
|
for (index in indexes) {
|
||||||
notifyItemChanged(index, ImageSelectedOrUpdated())
|
notifyItemChanged(index, ImageSelectedOrUpdated())
|
||||||
|
|
@ -334,10 +366,15 @@ class ImageAdapter(
|
||||||
/**
|
/**
|
||||||
* Initialize the data set.
|
* 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
|
allImages = fixedImages
|
||||||
val oldImageList:ArrayList<Image> = images
|
val oldImageList: ArrayList<Image> = images
|
||||||
val newImageList:ArrayList<Image> = ArrayList(newImages)
|
val newImageList: ArrayList<Image> = ArrayList(newImages)
|
||||||
actionableImagesMap = emptyMap
|
actionableImagesMap = emptyMap
|
||||||
alreadyAddedPositions = ArrayList()
|
alreadyAddedPositions = ArrayList()
|
||||||
uploadingContributionList = uploadedImages
|
uploadingContributionList = uploadedImages
|
||||||
|
|
@ -345,9 +382,10 @@ class ImageAdapter(
|
||||||
reachedEndOfFolder = false
|
reachedEndOfFolder = false
|
||||||
selectedImages = ArrayList()
|
selectedImages = ArrayList()
|
||||||
imagePositionAsPerIncreasingOrder = 0
|
imagePositionAsPerIncreasingOrder = 0
|
||||||
val diffResult = DiffUtil.calculateDiff(
|
val diffResult =
|
||||||
ImagesDiffCallback(oldImageList, newImageList)
|
DiffUtil.calculateDiff(
|
||||||
)
|
ImagesDiffCallback(oldImageList, newImageList),
|
||||||
|
)
|
||||||
images = newImageList
|
images = newImageList
|
||||||
diffResult.dispatchUpdatesTo(this)
|
diffResult.dispatchUpdatesTo(this)
|
||||||
}
|
}
|
||||||
|
|
@ -355,31 +393,35 @@ class ImageAdapter(
|
||||||
/**
|
/**
|
||||||
* Set new selected images
|
* Set new selected images
|
||||||
*/
|
*/
|
||||||
fun setSelectedImages(newSelectedImages: ArrayList<Image>){
|
fun setSelectedImages(newSelectedImages: ArrayList<Image>) {
|
||||||
selectedImages = ArrayList(newSelectedImages)
|
selectedImages = ArrayList(newSelectedImages)
|
||||||
imageSelectListener.onSelectedImagesChanged(selectedImages, 0)
|
imageSelectListener.onSelectedImagesChanged(selectedImages, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh the data in the adapter
|
* 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
|
numberOfSelectedImagesMarkedAsNotForUpload = 0
|
||||||
images.clear()
|
images.clear()
|
||||||
selectedImages = arrayListOf()
|
selectedImages = arrayListOf()
|
||||||
init(newImages, fixedImages, TreeMap(),uploadingImages)
|
init(newImages, fixedImages, TreeMap(), uploadingImages)
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear selected images and empty the list.
|
* Clear selected images and empty the list.
|
||||||
*/
|
*/
|
||||||
fun clearSelectedImages(){
|
fun clearSelectedImages() {
|
||||||
numberOfSelectedImagesMarkedAsNotForUpload = 0
|
numberOfSelectedImagesMarkedAsNotForUpload = 0
|
||||||
selectedImages.clear()
|
selectedImages.clear()
|
||||||
selectedImages = arrayListOf()
|
selectedImages = arrayListOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove image from actionable images map.
|
* Remove image from actionable images map.
|
||||||
*/
|
*/
|
||||||
|
|
@ -389,7 +431,7 @@ class ImageAdapter(
|
||||||
val showAlreadyActionedImages =
|
val showAlreadyActionedImages =
|
||||||
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
|
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
|
||||||
|
|
||||||
if(showAlreadyActionedImages) {
|
if (showAlreadyActionedImages) {
|
||||||
refresh(allImages, allImages, uploadingContributionList)
|
refresh(allImages, allImages, uploadingContributionList)
|
||||||
} else {
|
} else {
|
||||||
val iterator = actionableImagesMap.entries.iterator()
|
val iterator = actionableImagesMap.entries.iterator()
|
||||||
|
|
@ -402,16 +444,14 @@ class ImageAdapter(
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
alreadyAddedPositions.removeAt(alreadyAddedPositions.size - 1)
|
alreadyAddedPositions.removeAt(alreadyAddedPositions.size - 1)
|
||||||
notifyItemRemoved(index)
|
notifyItemRemoved(index)
|
||||||
notifyItemRangeChanged(index, itemCount )
|
notifyItemRangeChanged(index, itemCount)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the total number of items in the data set held by the adapter.
|
* 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)
|
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
|
||||||
|
|
||||||
// While switch is on initializes the holder with all images size
|
// While switch is on initializes the holder with all images size
|
||||||
return if(showAlreadyActionedImages) {
|
return if (showAlreadyActionedImages) {
|
||||||
allImages.size
|
allImages.size
|
||||||
|
|
||||||
// While switch is off and searching for next actionable has ended, initializes the holder
|
// While switch is off and searching for next actionable has ended, initializes the holder
|
||||||
// with size of all actionable images
|
// with size of all actionable images
|
||||||
} else if (actionableImagesMap.size == allImages.size || reachedEndOfFolder) {
|
} else if (actionableImagesMap.size == allImages.size || reachedEndOfFolder) {
|
||||||
actionableImagesMap.size
|
actionableImagesMap.size
|
||||||
|
|
||||||
// While switch is off, initializes the holder with and extra view holder so that finding
|
// 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
|
// and addition of the next actionable image in the adapter can be continued
|
||||||
} else {
|
} else {
|
||||||
actionableImagesMap.size + 1
|
actionableImagesMap.size + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getImageIdAt(position: Int): Long {
|
fun getImageIdAt(position: Int): Long = images.get(position).id
|
||||||
return images.get(position).id
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CleanUp function.
|
* CleanUp function.
|
||||||
|
|
@ -453,7 +491,9 @@ class ImageAdapter(
|
||||||
/**
|
/**
|
||||||
* Image view holder.
|
* 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)
|
val image: ImageView = itemView.findViewById(R.id.image_thumbnail)
|
||||||
private val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group)
|
private val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group)
|
||||||
private val uploadingGroup: Group = itemView.findViewById(R.id.uploading_group)
|
private val uploadingGroup: Group = itemView.findViewById(R.id.uploading_group)
|
||||||
|
|
@ -495,16 +535,12 @@ class ImageAdapter(
|
||||||
notForUploadGroup.visibility = View.VISIBLE
|
notForUploadGroup.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isItemUploaded():Boolean {
|
fun isItemUploaded(): Boolean = uploadedGroup.visibility == View.VISIBLE
|
||||||
return uploadedGroup.visibility == View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item is not for upload
|
* Item is not for upload
|
||||||
*/
|
*/
|
||||||
fun isItemNotForUpload():Boolean {
|
fun isItemNotForUpload(): Boolean = notForUploadGroup.visibility == View.VISIBLE
|
||||||
return notForUploadGroup.visibility == View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item is not uploading
|
* Item is not uploading
|
||||||
|
|
@ -533,45 +569,38 @@ class ImageAdapter(
|
||||||
*/
|
*/
|
||||||
class ImagesDiffCallback(
|
class ImagesDiffCallback(
|
||||||
var oldImageList: ArrayList<Image>,
|
var oldImageList: ArrayList<Image>,
|
||||||
var newImageList: ArrayList<Image>
|
var newImageList: ArrayList<Image>,
|
||||||
) : DiffUtil.Callback(){
|
) : DiffUtil.Callback() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the size of the old list.
|
* Returns the size of the old list.
|
||||||
*/
|
*/
|
||||||
override fun getOldListSize(): Int {
|
override fun getOldListSize(): Int = oldImageList.size
|
||||||
return oldImageList.size
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the size of the new list.
|
* Returns the size of the new list.
|
||||||
*/
|
*/
|
||||||
override fun getNewListSize(): Int {
|
override fun getNewListSize(): Int = newImageList.size
|
||||||
return newImageList.size
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called by the DiffUtil to decide whether two object represent the same Item.
|
* Called by the DiffUtil to decide whether two object represent the same Item.
|
||||||
*/
|
*/
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areItemsTheSame(
|
||||||
return newImageList[newItemPosition].id == oldImageList[oldItemPosition].id
|
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.
|
* 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.
|
* DiffUtil uses this information to detect if the contents of an item has changed.
|
||||||
*/
|
*/
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areContentsTheSame(
|
||||||
return oldImageList[oldItemPosition].equals(newImageList[newItemPosition])
|
oldItemPosition: Int,
|
||||||
}
|
newItemPosition: Int,
|
||||||
|
): Boolean = oldImageList[oldItemPosition].equals(newImageList[newItemPosition])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the text for showing inside the bubble during bubble scroll.
|
* Returns the text for showing inside the bubble during bubble scroll.
|
||||||
*/
|
*/
|
||||||
override fun getSectionName(position: Int): String {
|
override fun getSectionName(position: Int): String = images[position].date
|
||||||
return images[position].date
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
/**
|
/**
|
||||||
* Generic Recycler view adapter.
|
* 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)
|
val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,23 +8,16 @@ import android.content.SharedPreferences
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.compose.foundation.BorderStroke
|
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.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -38,7 +31,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.res.colorResource
|
import androidx.compose.ui.res.colorResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
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.CustomSelectorBottomLayoutBinding
|
||||||
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
|
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
|
||||||
import fr.free.nrw.commons.filepicker.Constants
|
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.media.ZoomableActivity
|
||||||
import fr.free.nrw.commons.theme.BaseActivity
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||||
import fr.free.nrw.commons.utils.CustomSelectorUtils
|
import fr.free.nrw.commons.utils.CustomSelectorUtils
|
||||||
import fr.free.nrw.commons.utils.PermissionUtils
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.*
|
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.io.File
|
||||||
import java.lang.Integer.max
|
import java.lang.Integer.max
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Selector Activity.
|
* Custom Selector Activity.
|
||||||
*/
|
*/
|
||||||
class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectListener {
|
class CustomSelectorActivity :
|
||||||
|
BaseActivity(),
|
||||||
|
FolderClickListener,
|
||||||
|
ImageSelectListener {
|
||||||
/**
|
/**
|
||||||
* ViewBindings
|
* ViewBindings
|
||||||
*/
|
*/
|
||||||
|
|
@ -147,7 +143,7 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
*/
|
*/
|
||||||
var imageFragment: ImageFragment? = null
|
var imageFragment: ImageFragment? = null
|
||||||
|
|
||||||
private var progressDialogText:String=""
|
private var progressDialogText: String = ""
|
||||||
|
|
||||||
private var showPartialAccessIndicator by mutableStateOf(false)
|
private var showPartialAccessIndicator by mutableStateOf(false)
|
||||||
|
|
||||||
|
|
@ -158,7 +154,8 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
|
||||||
ContextCompat.checkSelfPermission(
|
ContextCompat.checkSelfPermission(
|
||||||
this, Manifest.permission.READ_MEDIA_IMAGES
|
this,
|
||||||
|
Manifest.permission.READ_MEDIA_IMAGES,
|
||||||
) == PackageManager.PERMISSION_DENIED
|
) == PackageManager.PERMISSION_DENIED
|
||||||
) {
|
) {
|
||||||
showPartialAccessIndicator = true
|
showPartialAccessIndicator = true
|
||||||
|
|
@ -168,25 +165,27 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
toolbarBinding = CustomSelectorToolbarBinding.bind(binding.root)
|
toolbarBinding = CustomSelectorToolbarBinding.bind(binding.root)
|
||||||
bottomSheetBinding = CustomSelectorBottomLayoutBinding.bind(binding.root)
|
bottomSheetBinding = CustomSelectorBottomLayoutBinding.bind(binding.root)
|
||||||
binding.partialAccessIndicator.setContent {
|
binding.partialAccessIndicator.setContent {
|
||||||
PartialStorageAccessIndicator(
|
partialStorageAccessIndicator(
|
||||||
isVisible = showPartialAccessIndicator,
|
isVisible = showPartialAccessIndicator,
|
||||||
onManage = {
|
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)
|
requestPermissions(arrayOf(Manifest.permission.READ_MEDIA_IMAGES), 1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.padding(vertical = 8.dp, horizontal = 4.dp)
|
Modifier
|
||||||
.fillMaxWidth()
|
.padding(vertical = 8.dp, horizontal = 4.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val view = binding.root
|
val view = binding.root
|
||||||
setContentView(view)
|
setContentView(view)
|
||||||
|
|
||||||
prefs = applicationContext.getSharedPreferences("CustomSelector", MODE_PRIVATE)
|
prefs = applicationContext.getSharedPreferences("CustomSelector", MODE_PRIVATE)
|
||||||
viewModel = ViewModelProvider(this, customSelectorViewModelFactory).get(
|
viewModel =
|
||||||
CustomSelectorViewModel::class.java
|
ViewModelProvider(this, customSelectorViewModelFactory).get(
|
||||||
)
|
CustomSelectorViewModel::class.java,
|
||||||
|
)
|
||||||
|
|
||||||
setupViews()
|
setupViews()
|
||||||
|
|
||||||
|
|
@ -208,11 +207,11 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
override fun onRequestPermissionsResult(
|
override fun onRequestPermissionsResult(
|
||||||
requestCode: Int,
|
requestCode: Int,
|
||||||
permissions: Array<out String>,
|
permissions: Array<out String>,
|
||||||
grantResults: IntArray
|
grantResults: IntArray,
|
||||||
) {
|
) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
if(requestCode == 1 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
if (requestCode == 1 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
if(grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
showPartialAccessIndicator = false
|
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
|
* 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)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
if (requestCode == Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE &&
|
if (requestCode == Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE &&
|
||||||
resultCode == Activity.RESULT_OK
|
resultCode == Activity.RESULT_OK
|
||||||
|
|
@ -254,7 +257,8 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
* Set up view, default folder view.
|
* Set up view, default folder view.
|
||||||
*/
|
*/
|
||||||
private fun setupViews() {
|
private fun setupViews() {
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager
|
||||||
|
.beginTransaction()
|
||||||
.replace(R.id.fragment_container, FolderFragment.newInstance())
|
.replace(R.id.fragment_container, FolderFragment.newInstance())
|
||||||
.commit()
|
.commit()
|
||||||
setUpToolbar()
|
setUpToolbar()
|
||||||
|
|
@ -322,12 +326,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
|
|
||||||
var allImagesAlreadyNotForUpload = true
|
var allImagesAlreadyNotForUpload = true
|
||||||
images.forEach { image ->
|
images.forEach { image ->
|
||||||
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
|
val imageSHA1 =
|
||||||
image.uri,
|
CustomSelectorUtils.getImageSHA1(
|
||||||
ioDispatcher,
|
image.uri,
|
||||||
fileUtilsWrapper,
|
ioDispatcher,
|
||||||
contentResolver
|
fileUtilsWrapper,
|
||||||
)
|
contentResolver,
|
||||||
|
)
|
||||||
val exists = notForUploadStatusDao.find(imageSHA1)
|
val exists = notForUploadStatusDao.find(imageSHA1)
|
||||||
if (exists < 1) {
|
if (exists < 1) {
|
||||||
allImagesAlreadyNotForUpload = false
|
allImagesAlreadyNotForUpload = false
|
||||||
|
|
@ -337,12 +342,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
if (!allImagesAlreadyNotForUpload) {
|
if (!allImagesAlreadyNotForUpload) {
|
||||||
// Insert or delete images as necessary, but the UI updates should be posted back to the main thread
|
// Insert or delete images as necessary, but the UI updates should be posted back to the main thread
|
||||||
images.forEach { image ->
|
images.forEach { image ->
|
||||||
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
|
val imageSHA1 =
|
||||||
image.uri,
|
CustomSelectorUtils.getImageSHA1(
|
||||||
ioDispatcher,
|
image.uri,
|
||||||
fileUtilsWrapper,
|
ioDispatcher,
|
||||||
contentResolver
|
fileUtilsWrapper,
|
||||||
)
|
contentResolver,
|
||||||
|
)
|
||||||
notForUploadStatusDao.insert(NotForUploadStatus(imageSHA1))
|
notForUploadStatusDao.insert(NotForUploadStatus(imageSHA1))
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|
@ -353,12 +359,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
images.forEach { image ->
|
images.forEach { image ->
|
||||||
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
|
val imageSHA1 =
|
||||||
image.uri,
|
CustomSelectorUtils.getImageSHA1(
|
||||||
ioDispatcher,
|
image.uri,
|
||||||
fileUtilsWrapper,
|
ioDispatcher,
|
||||||
contentResolver
|
fileUtilsWrapper,
|
||||||
)
|
contentResolver,
|
||||||
|
)
|
||||||
notForUploadStatusDao.deleteNotForUploadWithImageSHA1(imageSHA1)
|
notForUploadStatusDao.deleteNotForUploadWithImageSHA1(imageSHA1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,13 +393,19 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
/**
|
/**
|
||||||
* Change the title of the toolbar.
|
* Change the title of the toolbar.
|
||||||
*/
|
*/
|
||||||
private fun changeTitle(title: String, selectedImageCount:Int) {
|
private fun changeTitle(
|
||||||
if (title.isNotEmpty()){
|
title: String,
|
||||||
|
selectedImageCount: Int,
|
||||||
|
) {
|
||||||
|
if (title.isNotEmpty()) {
|
||||||
val titleText = findViewById<TextView>(R.id.title)
|
val titleText = findViewById<TextView>(R.id.title)
|
||||||
var titleWithAppendedImageCount = title
|
var titleWithAppendedImageCount = title
|
||||||
if (selectedImageCount > 0) {
|
if (selectedImageCount > 0) {
|
||||||
titleWithAppendedImageCount += " (${resources.getQuantityString(R.plurals.custom_picker_images_selected_title_appendix,
|
titleWithAppendedImageCount += " (${resources.getQuantityString(
|
||||||
selectedImageCount, selectedImageCount)})"
|
R.plurals.custom_picker_images_selected_title_appendix,
|
||||||
|
selectedImageCount,
|
||||||
|
selectedImageCount,
|
||||||
|
)})"
|
||||||
}
|
}
|
||||||
if (titleText != null) {
|
if (titleText != null) {
|
||||||
titleText.text = titleWithAppendedImageCount
|
titleText.text = titleWithAppendedImageCount
|
||||||
|
|
@ -415,8 +428,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
/**
|
/**
|
||||||
* override on folder click, change the toolbar title on folder click.
|
* override on folder click, change the toolbar title on folder click.
|
||||||
*/
|
*/
|
||||||
override fun onFolderClick(folderId: Long, folderName: String, lastItemId: Long) {
|
override fun onFolderClick(
|
||||||
supportFragmentManager.beginTransaction()
|
folderId: Long,
|
||||||
|
folderName: String,
|
||||||
|
lastItemId: Long,
|
||||||
|
) {
|
||||||
|
supportFragmentManager
|
||||||
|
.beginTransaction()
|
||||||
.add(R.id.fragment_container, ImageFragment.newInstance(folderId, lastItemId))
|
.add(R.id.fragment_container, ImageFragment.newInstance(folderId, lastItemId))
|
||||||
.addToBackStack(null)
|
.addToBackStack(null)
|
||||||
.commit()
|
.commit()
|
||||||
|
|
@ -433,18 +451,21 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
*/
|
*/
|
||||||
override fun onSelectedImagesChanged(
|
override fun onSelectedImagesChanged(
|
||||||
selectedImages: ArrayList<Image>,
|
selectedImages: ArrayList<Image>,
|
||||||
selectedNotForUploadImages: Int
|
selectedNotForUploadImages: Int,
|
||||||
) {
|
) {
|
||||||
viewModel.selectedImages.value = selectedImages
|
viewModel.selectedImages.value = selectedImages
|
||||||
changeTitle(bucketName, selectedImages.size)
|
changeTitle(bucketName, selectedImages.size)
|
||||||
|
|
||||||
uploadLimitExceeded = selectedImages.size > uploadLimit
|
uploadLimitExceeded = selectedImages.size > uploadLimit
|
||||||
uploadLimitExceededBy = max(selectedImages.size - uploadLimit,0)
|
uploadLimitExceededBy = max(selectedImages.size - uploadLimit, 0)
|
||||||
|
|
||||||
if (uploadLimitExceeded && selectedNotForUploadImages == 0) {
|
if (uploadLimitExceeded && selectedNotForUploadImages == 0) {
|
||||||
toolbarBinding.imageLimitError.visibility = View.VISIBLE
|
toolbarBinding.imageLimitError.visibility = View.VISIBLE
|
||||||
bottomSheetBinding.upload.text = resources.getString(
|
bottomSheetBinding.upload.text =
|
||||||
R.string.custom_selector_button_limit_text, uploadLimit)
|
resources.getString(
|
||||||
|
R.string.custom_selector_button_limit_text,
|
||||||
|
uploadLimit,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
toolbarBinding.imageLimitError.visibility = View.INVISIBLE
|
toolbarBinding.imageLimitError.visibility = View.INVISIBLE
|
||||||
bottomSheetBinding.upload.text = resources.getString(R.string.upload)
|
bottomSheetBinding.upload.text = resources.getString(R.string.upload)
|
||||||
|
|
@ -461,11 +482,11 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
bottomSheetBinding.notForUpload.text =
|
bottomSheetBinding.notForUpload.text =
|
||||||
when (selectedImages.size == selectedNotForUploadImages) {
|
when (selectedImages.size == selectedNotForUploadImages) {
|
||||||
true -> {
|
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)
|
getString(R.string.unmark_as_not_for_upload)
|
||||||
}
|
}
|
||||||
else -> {
|
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)
|
getString(R.string.mark_as_not_for_upload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -481,13 +502,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
override fun onLongPress(
|
override fun onLongPress(
|
||||||
position: Int,
|
position: Int,
|
||||||
images: ArrayList<Image>,
|
images: ArrayList<Image>,
|
||||||
selectedImages: ArrayList<Image>
|
selectedImages: ArrayList<Image>,
|
||||||
) {
|
) {
|
||||||
val intent = Intent(this, ZoomableActivity::class.java)
|
val intent = Intent(this, ZoomableActivity::class.java)
|
||||||
intent.putExtra(CustomSelectorConstants.PRESENT_POSITION, position)
|
intent.putExtra(CustomSelectorConstants.PRESENT_POSITION, position)
|
||||||
intent.putParcelableArrayListExtra(
|
intent.putParcelableArrayListExtra(
|
||||||
CustomSelectorConstants.TOTAL_SELECTED_IMAGES,
|
CustomSelectorConstants.TOTAL_SELECTED_IMAGES,
|
||||||
selectedImages
|
selectedImages,
|
||||||
)
|
)
|
||||||
intent.putExtra(CustomSelectorConstants.BUCKET_ID, bucketId)
|
intent.putExtra(CustomSelectorConstants.BUCKET_ID, bucketId)
|
||||||
startActivityForResult(intent, Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE)
|
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.
|
* Get the selected images. Remove any non existent file, forward the data to finish selector.
|
||||||
*/
|
*/
|
||||||
fun onDone() {
|
fun onDone() {
|
||||||
val selectedImages = viewModel.selectedImages.value
|
val selectedImages = viewModel.selectedImages.value
|
||||||
if (selectedImages.isNullOrEmpty()) {
|
if (selectedImages.isNullOrEmpty()) {
|
||||||
finishPickImages(arrayListOf())
|
finishPickImages(arrayListOf())
|
||||||
return
|
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
|
i++
|
||||||
while (i < selectedImages.size) {
|
}
|
||||||
val path = selectedImages[i].path
|
finishPickImages(selectedImages)
|
||||||
val file = File(path)
|
|
||||||
if (!file.exists()) {
|
|
||||||
selectedImages.removeAt(i)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
finishPickImages(selectedImages)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -547,10 +568,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
val dialog = Dialog(this)
|
val dialog = Dialog(this)
|
||||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
dialog.setContentView(R.layout.custom_selector_limit_dialog)
|
dialog.setContentView(R.layout.custom_selector_limit_dialog)
|
||||||
(dialog.findViewById(R.id.btn_dismiss_limit_warning) as Button).setOnClickListener()
|
(dialog.findViewById(R.id.btn_dismiss_limit_warning) as Button).setOnClickListener { dialog.dismiss() }
|
||||||
{ dialog.dismiss() }
|
(dialog.findViewById(R.id.upload_limit_warning) as TextView).text =
|
||||||
(dialog.findViewById(R.id.upload_limit_warning) as TextView).text = resources.getString(
|
resources.getString(
|
||||||
R.string.custom_selector_over_limit_warning, uploadLimit, uploadLimitExceededBy)
|
R.string.custom_selector_over_limit_warning,
|
||||||
|
uploadLimit,
|
||||||
|
uploadLimitExceededBy,
|
||||||
|
)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -560,9 +584,17 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
if (isImageFragmentOpen) {
|
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 {
|
} else {
|
||||||
prefs.edit().remove(FOLDER_ID).remove(FOLDER_NAME).apply()
|
prefs
|
||||||
|
.edit()
|
||||||
|
.remove(FOLDER_ID)
|
||||||
|
.remove(FOLDER_NAME)
|
||||||
|
.apply()
|
||||||
}
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
@ -573,38 +605,41 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
|
||||||
const val ITEM_ID: String = "ItemId"
|
const val ITEM_ID: String = "ItemId"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PartialStorageAccessIndicator(
|
fun partialStorageAccessIndicator(
|
||||||
isVisible: Boolean,
|
isVisible: Boolean,
|
||||||
onManage: ()-> Unit,
|
onManage: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if(isVisible) {
|
if (isVisible) {
|
||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
colors = CardDefaults.cardColors(
|
colors =
|
||||||
containerColor = colorResource(R.color.primarySuperLightColor)
|
CardDefaults.cardColors(
|
||||||
),
|
containerColor = colorResource(R.color.primarySuperLightColor),
|
||||||
|
),
|
||||||
border = BorderStroke(0.5.dp, color = colorResource(R.color.primaryColor)),
|
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()) {
|
Row(modifier = Modifier.padding(16.dp).fillMaxWidth()) {
|
||||||
Text(
|
Text(
|
||||||
text = "You've given access to a select number of photos",
|
text = "You've given access to a select number of photos",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onManage,
|
onClick = onManage,
|
||||||
modifier = Modifier.align(Alignment.Bottom),
|
modifier = Modifier.align(Alignment.Bottom),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors =
|
||||||
containerColor = colorResource(R.color.primaryColor)
|
ButtonDefaults.buttonColors(
|
||||||
),
|
containerColor = colorResource(R.color.primaryColor),
|
||||||
shape = RoundedCornerShape(8.dp)
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Manage",
|
text = "Manage",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = colorResource(R.color.primaryTextColor)
|
color = colorResource(R.color.primaryTextColor),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -614,11 +649,15 @@ fun PartialStorageAccessIndicator(
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun PartialStorageAccessIndicatorPreview() {
|
fun partialStorageAccessIndicatorPreview() {
|
||||||
Surface {
|
Surface {
|
||||||
PartialStorageAccessIndicator(isVisible = true, onManage = {}, modifier = Modifier
|
partialStorageAccessIndicator(
|
||||||
.padding(vertical = 8.dp, horizontal = 4.dp)
|
isVisible = true,
|
||||||
.fillMaxWidth()
|
onManage = {},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(vertical = 8.dp, horizontal = 4.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,10 @@ import kotlinx.coroutines.cancel
|
||||||
/**
|
/**
|
||||||
* Custom Selector view model.
|
* 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).
|
* Scope for coroutine task (image fetch).
|
||||||
*/
|
*/
|
||||||
|
|
@ -37,15 +39,17 @@ class CustomSelectorViewModel(var context: Context,var imageFileLoader: ImageFil
|
||||||
fun fetchImages() {
|
fun fetchImages() {
|
||||||
result.postValue(Result(CallbackStatus.FETCHING, arrayListOf()))
|
result.postValue(Result(CallbackStatus.FETCHING, arrayListOf()))
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
imageFileLoader.loadDeviceImages(object: ImageLoaderListener {
|
imageFileLoader.loadDeviceImages(
|
||||||
override fun onImageLoaded(images: ArrayList<Image>) {
|
object : ImageLoaderListener {
|
||||||
result.postValue(Result(CallbackStatus.SUCCESS, images))
|
override fun onImageLoaded(images: ArrayList<Image>) {
|
||||||
}
|
result.postValue(Result(CallbackStatus.SUCCESS, images))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFailed(throwable: Throwable) {
|
override fun onFailed(throwable: Throwable) {
|
||||||
result.postValue(Result(CallbackStatus.SUCCESS, arrayListOf()))
|
result.postValue(Result(CallbackStatus.SUCCESS, arrayListOf()))
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -55,4 +59,4 @@ class CustomSelectorViewModel(var context: Context,var imageFileLoader: ImageFil
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,12 @@ import javax.inject.Inject
|
||||||
/**
|
/**
|
||||||
* View Model Factory.
|
* View Model Factory.
|
||||||
*/
|
*/
|
||||||
class CustomSelectorViewModelFactory @Inject constructor(val context: Context,val imageFileLoader: ImageFileLoader) : ViewModelProvider.Factory {
|
class CustomSelectorViewModelFactory
|
||||||
|
@Inject
|
||||||
override fun<CustomSelectorViewModel: ViewModel> create(modelClass: Class<CustomSelectorViewModel>) : CustomSelectorViewModel {
|
constructor(
|
||||||
return CustomSelectorViewModel(context,imageFileLoader) as CustomSelectorViewModel
|
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.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
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.listeners.FolderClickListener
|
||||||
import fr.free.nrw.commons.customselector.model.CallbackStatus
|
import fr.free.nrw.commons.customselector.model.CallbackStatus
|
||||||
import fr.free.nrw.commons.customselector.model.Folder
|
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.customselector.ui.adapter.FolderAdapter
|
||||||
import fr.free.nrw.commons.databinding.FragmentCustomSelectorBinding
|
import fr.free.nrw.commons.databinding.FragmentCustomSelectorBinding
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||||
|
|
@ -24,12 +24,11 @@ import javax.inject.Inject
|
||||||
* Custom selector folder fragment.
|
* Custom selector folder fragment.
|
||||||
*/
|
*/
|
||||||
class FolderFragment : CommonsDaggerSupportFragment() {
|
class FolderFragment : CommonsDaggerSupportFragment() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewBinding
|
* ViewBinding
|
||||||
*/
|
*/
|
||||||
private var _binding: FragmentCustomSelectorBinding? = null
|
private var _binding: FragmentCustomSelectorBinding? = null
|
||||||
private val binding get() = _binding
|
val binding get() = _binding
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View Model for images.
|
* View Model for images.
|
||||||
|
|
@ -53,6 +52,7 @@ class FolderFragment : CommonsDaggerSupportFragment() {
|
||||||
|
|
||||||
var mediaClient: MediaClient? = null
|
var mediaClient: MediaClient? = null
|
||||||
@Inject set
|
@Inject set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Folder Adapter.
|
* Folder Adapter.
|
||||||
*/
|
*/
|
||||||
|
|
@ -66,15 +66,13 @@ class FolderFragment : CommonsDaggerSupportFragment() {
|
||||||
/**
|
/**
|
||||||
* Folder List.
|
* Folder List.
|
||||||
*/
|
*/
|
||||||
private lateinit var folders : ArrayList<Folder>
|
private lateinit var folders: ArrayList<Folder>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Companion newInstance.
|
* Companion newInstance.
|
||||||
*/
|
*/
|
||||||
companion object{
|
companion object {
|
||||||
fun newInstance(): FolderFragment {
|
fun newInstance(): FolderFragment = FolderFragment()
|
||||||
return FolderFragment()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -83,21 +81,24 @@ class FolderFragment : CommonsDaggerSupportFragment() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory!!).get(CustomSelectorViewModel::class.java)
|
viewModel = ViewModelProvider(requireActivity(), customSelectorViewModelFactory!!).get(CustomSelectorViewModel::class.java)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OnCreateView.
|
* OnCreateView.
|
||||||
* Inflate Layout, init adapter, init gridLayoutManager, setUp recycler view, observe the view model for result.
|
* 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)
|
_binding = FragmentCustomSelectorBinding.inflate(inflater, container, false)
|
||||||
folderAdapter = FolderAdapter(requireActivity(), activity as FolderClickListener)
|
folderAdapter = FolderAdapter(requireActivity(), activity as FolderClickListener)
|
||||||
gridLayoutManager = GridLayoutManager(context, columnCount())
|
gridLayoutManager = GridLayoutManager(context, columnCount())
|
||||||
selectorRV = binding?.selectorRv
|
selectorRV = binding?.selectorRv
|
||||||
loader = binding?.loader
|
loader = binding?.loader
|
||||||
with(binding?.selectorRv){
|
with(binding?.selectorRv) {
|
||||||
this?.layoutManager = gridLayoutManager
|
this?.layoutManager = gridLayoutManager
|
||||||
this?.setHasFixedSize(true)
|
this?.setHasFixedSize(true)
|
||||||
this?.adapter = folderAdapter
|
this?.adapter = folderAdapter
|
||||||
|
|
@ -114,9 +115,9 @@ class FolderFragment : CommonsDaggerSupportFragment() {
|
||||||
* Load adapter.
|
* Load adapter.
|
||||||
*/
|
*/
|
||||||
private fun handleResult(result: Result) {
|
private fun handleResult(result: Result) {
|
||||||
if(result.status is CallbackStatus.SUCCESS){
|
if (result.status is CallbackStatus.SUCCESS) {
|
||||||
val images = result.images
|
val images = result.images
|
||||||
if(images.isEmpty()){
|
if (images.isEmpty()) {
|
||||||
binding?.emptyText?.let {
|
binding?.emptyText?.let {
|
||||||
it.visibility = View.VISIBLE
|
it.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,9 @@ import kotlin.coroutines.CoroutineContext
|
||||||
* Custom Selector Image File Loader.
|
* Custom Selector Image File Loader.
|
||||||
* Loads device images.
|
* Loads device images.
|
||||||
*/
|
*/
|
||||||
class ImageFileLoader(val context: Context) : CoroutineScope{
|
class ImageFileLoader(
|
||||||
|
val context: Context,
|
||||||
|
) : CoroutineScope {
|
||||||
/**
|
/**
|
||||||
* Coroutine context for fetching images.
|
* Coroutine context for fetching images.
|
||||||
*/
|
*/
|
||||||
|
|
@ -30,14 +31,15 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
|
||||||
/**
|
/**
|
||||||
* Media paramerters required.
|
* Media paramerters required.
|
||||||
*/
|
*/
|
||||||
private val projection = arrayOf(
|
private val projection =
|
||||||
MediaStore.Images.Media._ID,
|
arrayOf(
|
||||||
MediaStore.Images.Media.DISPLAY_NAME,
|
MediaStore.Images.Media._ID,
|
||||||
MediaStore.Images.Media.DATA,
|
MediaStore.Images.Media.DISPLAY_NAME,
|
||||||
MediaStore.Images.Media.BUCKET_ID,
|
MediaStore.Images.Media.DATA,
|
||||||
MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
|
MediaStore.Images.Media.BUCKET_ID,
|
||||||
MediaStore.Images.Media.DATE_ADDED
|
MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
|
||||||
)
|
MediaStore.Images.Media.DATE_ADDED,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load Device Images under coroutine.
|
* Load Device Images under coroutine.
|
||||||
|
|
@ -50,12 +52,18 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load Device images using cursor
|
* Load Device images using cursor
|
||||||
*/
|
*/
|
||||||
private fun getImages(listener:ImageLoaderListener) {
|
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")
|
val cursor =
|
||||||
|
context.contentResolver.query(
|
||||||
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||||
|
projection,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
MediaStore.Images.Media.DATE_ADDED + " DESC",
|
||||||
|
)
|
||||||
if (cursor == null) {
|
if (cursor == null) {
|
||||||
listener.onFailed(NullPointerException())
|
listener.onFailed(NullPointerException())
|
||||||
return
|
return
|
||||||
|
|
@ -85,10 +93,12 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
|
||||||
val file =
|
val file =
|
||||||
if (path == null || path.isEmpty()) {
|
if (path == null || path.isEmpty()) {
|
||||||
null
|
null
|
||||||
} else try {
|
} else {
|
||||||
File(path)
|
try {
|
||||||
} catch (ignored: Exception) {
|
File(path)
|
||||||
null
|
} catch (ignored: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file != null && file.exists() && name != null && path != null && bucketName != 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 dateFormat = DateFormat.getMediumDateFormat(context)
|
||||||
val formattedDate = dateFormat.format(date)
|
val formattedDate = dateFormat.format(date)
|
||||||
|
|
||||||
val image = Image(
|
val image =
|
||||||
id,
|
Image(
|
||||||
name,
|
id,
|
||||||
uri,
|
name,
|
||||||
path,
|
uri,
|
||||||
bucketId,
|
path,
|
||||||
bucketName,
|
bucketId,
|
||||||
date = (formattedDate)
|
bucketName,
|
||||||
)
|
date = (formattedDate),
|
||||||
|
)
|
||||||
images.add(image)
|
images.add(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
} while (cursor.moveToNext())
|
} while (cursor.moveToNext())
|
||||||
}
|
}
|
||||||
cursor.close()
|
cursor.close()
|
||||||
listener.onImageLoaded(images)
|
listener.onImageLoaded(images)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abort loading images.
|
* Abort loading images.
|
||||||
*/
|
*/
|
||||||
fun abortLoadImage(){
|
fun abortLoadImage() {
|
||||||
//todo Abort loading images.
|
// 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.FileProcessor
|
||||||
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import java.util.*
|
import java.util.TreeMap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Selector Image Fragment.
|
* Custom Selector Image Fragment.
|
||||||
*/
|
*/
|
||||||
class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDataListener {
|
class ImageFragment :
|
||||||
|
CommonsDaggerSupportFragment(),
|
||||||
|
RefreshUIListener,
|
||||||
|
PassDataListener {
|
||||||
private var _binding: FragmentCustomSelectorBinding? = null
|
private var _binding: FragmentCustomSelectorBinding? = null
|
||||||
private val binding get() = _binding
|
val binding get() = _binding
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current bucketId.
|
* Current bucketId.
|
||||||
|
|
@ -107,7 +109,6 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
||||||
private lateinit var progressDialog: AlertDialog
|
private lateinit var progressDialog: AlertDialog
|
||||||
private lateinit var progressDialogLayout: ProgressDialogBinding
|
private lateinit var progressDialogLayout: ProgressDialogBinding
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NotForUploadStatus Dao class for database operations
|
* NotForUploadStatus Dao class for database operations
|
||||||
*/
|
*/
|
||||||
|
|
@ -142,7 +143,6 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
||||||
lateinit var contributionDao: ContributionDao
|
lateinit var contributionDao: ContributionDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switch state
|
* Switch state
|
||||||
*/
|
*/
|
||||||
|
|
@ -157,7 +157,10 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
||||||
/**
|
/**
|
||||||
* newInstance from bucketId.
|
* newInstance from bucketId.
|
||||||
*/
|
*/
|
||||||
fun newInstance(bucketId: Long, lastItemId: Long): ImageFragment {
|
fun newInstance(
|
||||||
|
bucketId: Long,
|
||||||
|
lastItemId: Long,
|
||||||
|
): ImageFragment {
|
||||||
val fragment = ImageFragment()
|
val fragment = ImageFragment()
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putLong(BUCKET_ID, bucketId)
|
args.putLong(BUCKET_ID, bucketId)
|
||||||
|
|
@ -175,9 +178,10 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
bucketId = arguments?.getLong(BUCKET_ID)
|
bucketId = arguments?.getLong(BUCKET_ID)
|
||||||
lastItemId = arguments?.getLong(LAST_ITEM_ID, 0)
|
lastItemId = arguments?.getLong(LAST_ITEM_ID, 0)
|
||||||
viewModel = ViewModelProvider(requireActivity(), customSelectorViewModelFactory).get(
|
viewModel =
|
||||||
CustomSelectorViewModel::class.java
|
ViewModelProvider(requireActivity(), customSelectorViewModelFactory).get(
|
||||||
)
|
CustomSelectorViewModel::class.java,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -188,7 +192,7 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?,
|
||||||
): View? {
|
): View? {
|
||||||
_binding = FragmentCustomSelectorBinding.inflate(inflater, container, false)
|
_binding = FragmentCustomSelectorBinding.inflate(inflater, container, false)
|
||||||
imageAdapter =
|
imageAdapter =
|
||||||
|
|
@ -200,9 +204,12 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
||||||
this?.adapter = imageAdapter
|
this?.adapter = imageAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel?.result?.observe(viewLifecycleOwner, Observer {
|
viewModel?.result?.observe(
|
||||||
handleResult(it)
|
viewLifecycleOwner,
|
||||||
})
|
Observer {
|
||||||
|
handleResult(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
switch = binding?.switchWidget
|
switch = binding?.switchWidget
|
||||||
switch?.visibility = View.VISIBLE
|
switch?.visibility = View.VISIBLE
|
||||||
|
|
@ -323,20 +330,22 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
imageAdapter.cleanUp()
|
imageAdapter.cleanUp()
|
||||||
|
|
||||||
val position = (selectorRV?.layoutManager as GridLayoutManager)
|
val position =
|
||||||
.findFirstVisibleItemPosition()
|
(selectorRV?.layoutManager as GridLayoutManager)
|
||||||
|
.findFirstVisibleItemPosition()
|
||||||
|
|
||||||
// Check for empty RecyclerView.
|
// Check for empty RecyclerView.
|
||||||
if (position != -1 && filteredImages.size > 0) {
|
if (position != -1 && filteredImages.size > 0) {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
context.getSharedPreferences(
|
context
|
||||||
"CustomSelector",
|
.getSharedPreferences(
|
||||||
BaseActivity.MODE_PRIVATE
|
"CustomSelector",
|
||||||
)?.let { prefs ->
|
BaseActivity.MODE_PRIVATE,
|
||||||
prefs.edit()?.let { editor ->
|
)?.let { prefs ->
|
||||||
editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply()
|
prefs.edit()?.let { editor ->
|
||||||
|
editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
@ -354,7 +363,7 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
||||||
/**
|
/**
|
||||||
* Removes the image from the actionable image map
|
* Removes the image from the actionable image map
|
||||||
*/
|
*/
|
||||||
fun removeImage(image : Image){
|
fun removeImage(image: Image) {
|
||||||
imageAdapter.removeImageFromActionableImageMap(image)
|
imageAdapter.removeImageFromActionableImageMap(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -364,11 +373,15 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
||||||
fun clearSelectedImages() {
|
fun clearSelectedImages() {
|
||||||
imageAdapter.clearSelectedImages()
|
imageAdapter.clearSelectedImages()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passes selected images and other information from Activity to Fragment and connects it with
|
* Passes selected images and other information from Activity to Fragment and connects it with
|
||||||
* the adapter
|
* the adapter
|
||||||
*/
|
*/
|
||||||
override fun passSelectedImages(selectedImages: ArrayList<Image>, shouldRefresh: Boolean) {
|
override fun passSelectedImages(
|
||||||
|
selectedImages: ArrayList<Image>,
|
||||||
|
shouldRefresh: Boolean,
|
||||||
|
) {
|
||||||
imageAdapter.setSelectedImages(selectedImages)
|
imageAdapter.setSelectedImages(selectedImages)
|
||||||
|
|
||||||
val uploadingContributions = getUploadingContributions()
|
val uploadingContributions = getUploadingContributions()
|
||||||
|
|
@ -398,11 +411,10 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUploadingContributions(): List<Contribution> {
|
private fun getUploadingContributions(): List<Contribution> =
|
||||||
|
contributionDao
|
||||||
return contributionDao.getContribution(
|
.getContribution(
|
||||||
listOf(Contribution.STATE_IN_PROGRESS, Contribution.STATE_FAILED, Contribution.STATE_QUEUED, Contribution.STATE_PAUSED)
|
listOf(Contribution.STATE_IN_PROGRESS, Contribution.STATE_FAILED, Contribution.STATE_QUEUED, Contribution.STATE_PAUSED),
|
||||||
)?.subscribeOn(Schedulers.io())?.blockingGet() ?: emptyList()
|
)?.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.upload.FileUtilsWrapper
|
||||||
import fr.free.nrw.commons.utils.CustomSelectorUtils
|
import fr.free.nrw.commons.utils.CustomSelectorUtils
|
||||||
import fr.free.nrw.commons.utils.CustomSelectorUtils.Companion.checkWhetherFileExistsOnCommonsUsingSHA1
|
import fr.free.nrw.commons.utils.CustomSelectorUtils.Companion.checkWhetherFileExistsOnCommonsUsingSHA1
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import java.util.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.Calendar
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image Loader class, loads images, depending on API results.
|
* Image Loader class, loads images, depending on API results.
|
||||||
*/
|
*/
|
||||||
class ImageLoader @Inject constructor(
|
class ImageLoader
|
||||||
|
@Inject
|
||||||
/**
|
constructor(
|
||||||
* MediaClient for SHA1 query.
|
/**
|
||||||
*/
|
* MediaClient for SHA1 query.
|
||||||
var mediaClient: MediaClient,
|
*/
|
||||||
|
var mediaClient: MediaClient,
|
||||||
/**
|
/**
|
||||||
* FileProcessor to pre-process the file.
|
* FileProcessor to pre-process the file.
|
||||||
*/
|
*/
|
||||||
var fileProcessor: FileProcessor,
|
var fileProcessor: FileProcessor,
|
||||||
|
/**
|
||||||
/**
|
* File Utils Wrapper for SHA1
|
||||||
* File Utils Wrapper for SHA1
|
*/
|
||||||
*/
|
var fileUtilsWrapper: FileUtilsWrapper,
|
||||||
var fileUtilsWrapper: FileUtilsWrapper,
|
/**
|
||||||
|
* UploadedStatusDao for cache query.
|
||||||
/**
|
*/
|
||||||
* UploadedStatusDao for cache query.
|
var uploadedStatusDao: UploadedStatusDao,
|
||||||
*/
|
/**
|
||||||
var uploadedStatusDao: UploadedStatusDao,
|
* NotForUploadDao for database operations
|
||||||
|
*/
|
||||||
/**
|
var notForUploadStatusDao: NotForUploadStatusDao,
|
||||||
* NotForUploadDao for database operations
|
/**
|
||||||
*/
|
* Context for coroutine.
|
||||||
var notForUploadStatusDao: NotForUploadStatusDao,
|
*/
|
||||||
|
val context: Context,
|
||||||
/**
|
|
||||||
* 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>
|
|
||||||
) {
|
) {
|
||||||
|
/**
|
||||||
|
* 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
|
private val scope: CoroutineScope = MainScope()
|
||||||
holder.itemNotUploaded()
|
|
||||||
holder.itemForUpload()
|
|
||||||
holder.itemNotUploading()
|
|
||||||
|
|
||||||
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) {
|
scope.launch {
|
||||||
return@launch
|
var result: Result = Result.NOTFOUND
|
||||||
}
|
|
||||||
|
|
||||||
val imageSHA1: String = when (mapImageSHA1[image.uri] != null) {
|
if (mapHolderImage[holder] != image) {
|
||||||
true -> mapImageSHA1[image.uri]!!
|
return@launch
|
||||||
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) {
|
val imageSHA1: String =
|
||||||
return@launch
|
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()) {
|
val sha1 =
|
||||||
when {
|
uploadedStatus?.let {
|
||||||
mapResult[imageSHA1] == null -> {
|
result = getResultFromUploadedStatus(uploadedStatus)
|
||||||
// Query original image.
|
uploadedStatus.modifiedImageSHA1
|
||||||
result = checkWhetherFileExistsOnCommonsUsingSHA1(
|
} ?: run {
|
||||||
imageSHA1,
|
if (mapHolderImage[holder] == image) {
|
||||||
ioDispatcher,
|
getSHA1(image, defaultDispatcher)
|
||||||
mediaClient
|
} else {
|
||||||
)
|
""
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
|
||||||
result = mapResult[imageSHA1]!!
|
if (mapHolderImage[holder] != image) {
|
||||||
}
|
return@launch
|
||||||
}
|
}
|
||||||
if (result is Result.TRUE) {
|
|
||||||
// Original image found.
|
val existsInNotForUploadTable = notForUploadStatusDao.find(imageSHA1)
|
||||||
insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false)
|
|
||||||
} else {
|
if (result in arrayOf(Result.NOTFOUND, Result.INVALID) && sha1.isNotEmpty()) {
|
||||||
when {
|
when {
|
||||||
mapResult[sha1] == null -> {
|
mapResult[imageSHA1] == null -> {
|
||||||
// Original image not found, query modified image.
|
// Query original image.
|
||||||
result = checkWhetherFileExistsOnCommonsUsingSHA1(
|
result =
|
||||||
sha1,
|
checkWhetherFileExistsOnCommonsUsingSHA1(
|
||||||
ioDispatcher,
|
imageSHA1,
|
||||||
mediaClient
|
ioDispatcher,
|
||||||
)
|
mediaClient,
|
||||||
|
)
|
||||||
when (result) {
|
when (result) {
|
||||||
is Result.TRUE -> {
|
is Result.TRUE -> {
|
||||||
mapResult[sha1] = Result.TRUE
|
mapResult[imageSHA1] = Result.TRUE
|
||||||
}
|
}
|
||||||
is Result.ERROR -> {
|
is Result.ERROR -> {
|
||||||
mapResult[sha1] = Result.ERROR
|
mapResult[imageSHA1] = Result.ERROR
|
||||||
}
|
}
|
||||||
is Result.FALSE -> {
|
is Result.FALSE -> {
|
||||||
mapResult[sha1] = Result.FALSE
|
mapResult[imageSHA1] = Result.FALSE
|
||||||
}
|
}
|
||||||
is Result.INVALID -> {
|
is Result.INVALID -> {
|
||||||
mapResult[sha1] = Result.INVALID
|
mapResult[imageSHA1] = Result.INVALID
|
||||||
}
|
}
|
||||||
is Result.NOTFOUND -> {
|
is Result.NOTFOUND -> {
|
||||||
mapResult[sha1] = Result.NOTFOUND
|
mapResult[imageSHA1] = Result.NOTFOUND
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
result = mapResult[sha1]!!
|
result = mapResult[imageSHA1]!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result != Result.ERROR) {
|
if (result is Result.TRUE) {
|
||||||
insertIntoUploaded(imageSHA1, sha1, false, 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 =
|
val sharedPreferences: SharedPreferences =
|
||||||
context
|
context
|
||||||
.getSharedPreferences(ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
|
.getSharedPreferences(ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
|
||||||
val showAlreadyActionedImages =
|
val showAlreadyActionedImages =
|
||||||
sharedPreferences.getBoolean(
|
sharedPreferences.getBoolean(
|
||||||
ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY,
|
ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY,
|
||||||
true
|
true,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (mapHolderImage[holder] == image) {
|
if (mapHolderImage[holder] == image) {
|
||||||
if ((result is Result.TRUE) && showAlreadyActionedImages) {
|
if ((result is Result.TRUE) && showAlreadyActionedImages) {
|
||||||
holder.itemUploaded()
|
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
|
|
||||||
} else {
|
} 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
|
* Finds out the next actionable image position
|
||||||
*/
|
*/
|
||||||
suspend fun nextActionableImage(
|
suspend fun nextActionableImage(
|
||||||
allImages: List<Image>, ioDispatcher: CoroutineDispatcher,
|
allImages: List<Image>,
|
||||||
defaultDispatcher: CoroutineDispatcher,
|
ioDispatcher: CoroutineDispatcher,
|
||||||
nextImagePosition: Int,
|
defaultDispatcher: CoroutineDispatcher,
|
||||||
currentlyUploadingImages: List<Contribution>
|
nextImagePosition: Int,
|
||||||
): Int {
|
currentlyUploadingImages: List<Contribution>,
|
||||||
var next: Int
|
): Int {
|
||||||
// Traversing from given position to the end
|
var next: Int
|
||||||
for (i in nextImagePosition until allImages.size){
|
// Traversing from given position to the end
|
||||||
val currentImage = allImages[i]
|
for (i in nextImagePosition until allImages.size) {
|
||||||
|
val currentImage = allImages[i]
|
||||||
|
|
||||||
if (currentlyUploadingImages.any { it.contentUri == currentImage.uri }) {
|
if (currentlyUploadingImages.any { it.contentUri == currentImage.uri }) {
|
||||||
continue // Skip this image as it's currently being uploaded
|
continue // Skip this image as it's currently being uploaded
|
||||||
}
|
}
|
||||||
|
|
||||||
val imageSHA1: String = when (mapImageSHA1[currentImage.uri] != null) {
|
val imageSHA1: String =
|
||||||
true -> mapImageSHA1[currentImage.uri]!!
|
when (mapImageSHA1[currentImage.uri] != null) {
|
||||||
else -> CustomSelectorUtils.getImageSHA1(
|
true -> mapImageSHA1[currentImage.uri]!!
|
||||||
currentImage.uri,
|
else ->
|
||||||
ioDispatcher,
|
CustomSelectorUtils.getImageSHA1(
|
||||||
fileUtilsWrapper,
|
currentImage.uri,
|
||||||
context.contentResolver
|
ioDispatcher,
|
||||||
)
|
fileUtilsWrapper,
|
||||||
}
|
context.contentResolver,
|
||||||
next = notForUploadStatusDao.find(imageSHA1)
|
)
|
||||||
|
}
|
||||||
|
next = notForUploadStatusDao.find(imageSHA1)
|
||||||
|
|
||||||
// After checking the image in the not for upload table, if the image is present then
|
// 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
|
// skips the image and moves to next image for checking
|
||||||
if(next > 0){
|
if (next > 0) {
|
||||||
continue
|
continue
|
||||||
|
|
||||||
// Otherwise checks in already uploaded table
|
// Otherwise checks in already uploaded table
|
||||||
} else {
|
} else {
|
||||||
next = uploadedStatusDao.findByImageSHA1(imageSHA1, true)
|
next = uploadedStatusDao.findByImageSHA1(imageSHA1, true)
|
||||||
|
|
||||||
// If the image is not present in the already uploaded table, checks for its
|
// If the image is not present in the already uploaded table, checks for its
|
||||||
// modified SHA1 in already uploaded table
|
// 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 (next <= 0) {
|
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
|
// If the modified image SHA1 is not present in the already uploaded table,
|
||||||
// for checking
|
// 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 {
|
} else {
|
||||||
continue
|
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.
|
* Get SHA1, return SHA1 if available, otherwise generate and store the SHA1.
|
||||||
* False Database Entries are invalid after INVALIDATE_DAY_COUNT and need to be re-queried.
|
*
|
||||||
|
* @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 androidx.room.TypeConverters
|
||||||
import fr.free.nrw.commons.contributions.Contribution
|
import fr.free.nrw.commons.contributions.Contribution
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao
|
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.Place
|
||||||
import fr.free.nrw.commons.nearby.PlaceDao
|
import fr.free.nrw.commons.nearby.PlaceDao
|
||||||
import fr.free.nrw.commons.review.ReviewDao
|
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
|
* 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)
|
@TypeConverters(Converters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun contributionDao(): ContributionDao
|
abstract fun contributionDao(): ContributionDao
|
||||||
|
|
||||||
abstract fun PlaceDao(): PlaceDao
|
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 NotForUploadStatusDao(): NotForUploadStatusDao
|
||||||
|
|
||||||
abstract fun ReviewDao(): ReviewDao
|
abstract fun ReviewDao(): ReviewDao
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package fr.free.nrw.commons.description
|
package fr.free.nrw.commons.description
|
||||||
|
|
||||||
|
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -29,11 +28,12 @@ import io.reactivex.schedulers.Schedulers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity for populating and editing existing description and caption
|
* 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
|
* Adapter for showing UploadMediaDetail in the activity
|
||||||
*/
|
*/
|
||||||
|
|
@ -70,7 +70,7 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
|
|
||||||
private lateinit var binding: ActivityDescriptionEditBinding
|
private lateinit var binding: ActivityDescriptionEditBinding
|
||||||
|
|
||||||
private val REQUEST_CODE_FOR_VOICE_INPUT = 1213
|
private val requestCodeForVoiceInput = 1213
|
||||||
|
|
||||||
private var descriptionAndCaptions: ArrayList<UploadMediaDetail>? = null
|
private var descriptionAndCaptions: ArrayList<UploadMediaDetail>? = null
|
||||||
|
|
||||||
|
|
@ -78,7 +78,6 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
|
|
||||||
@Inject lateinit var sessionManager: SessionManager
|
@Inject lateinit var sessionManager: SessionManager
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
|
@ -110,12 +109,17 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
* @param descriptionAndCaptions list of description and caption
|
* @param descriptionAndCaptions list of description and caption
|
||||||
*/
|
*/
|
||||||
private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) {
|
private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) {
|
||||||
uploadMediaDetailAdapter = UploadMediaDetailAdapter(this,
|
uploadMediaDetailAdapter =
|
||||||
savedLanguageValue, descriptionAndCaptions, recentLanguagesDao)
|
UploadMediaDetailAdapter(
|
||||||
|
this,
|
||||||
|
savedLanguageValue,
|
||||||
|
descriptionAndCaptions,
|
||||||
|
recentLanguagesDao,
|
||||||
|
)
|
||||||
uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int ->
|
uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int ->
|
||||||
showInfoAlert(
|
showInfoAlert(
|
||||||
titleStringID,
|
titleStringID,
|
||||||
messageStringId
|
messageStringId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
uploadMediaDetailAdapter.setEventListener(this)
|
uploadMediaDetailAdapter.setEventListener(this)
|
||||||
|
|
@ -129,11 +133,17 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
* @param titleStringID Title ID
|
* @param titleStringID Title ID
|
||||||
* @param messageStringId Message ID
|
* @param messageStringId Message ID
|
||||||
*/
|
*/
|
||||||
private fun showInfoAlert(titleStringID: Int, messageStringId: Int) {
|
private fun showInfoAlert(
|
||||||
|
titleStringID: Int,
|
||||||
|
messageStringId: Int,
|
||||||
|
) {
|
||||||
showAlertDialog(
|
showAlertDialog(
|
||||||
this, getString(titleStringID),
|
this,
|
||||||
getString(messageStringId), getString(android.R.string.ok),
|
getString(titleStringID),
|
||||||
null, true
|
getString(messageStringId),
|
||||||
|
getString(android.R.string.ok),
|
||||||
|
null,
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,13 +154,13 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
*/
|
*/
|
||||||
override fun addLanguage() {
|
override fun addLanguage() {
|
||||||
val uploadMediaDetail = UploadMediaDetail()
|
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)
|
uploadMediaDetailAdapter.addDescription(uploadMediaDetail)
|
||||||
rvDescriptions!!.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1)
|
rvDescriptions!!.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onBackButtonClicked(view: View) {
|
private fun onBackButtonClicked(view: View) {
|
||||||
onBackPressedDispatcher.onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSubmitButtonClicked(view: View) {
|
private fun onSubmitButtonClicked(view: View) {
|
||||||
|
|
@ -174,10 +184,11 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
val descriptionStart = wikiText!!.substring(0, descriptionIndex + 12)
|
val descriptionStart = wikiText!!.substring(0, descriptionIndex + 12)
|
||||||
val descriptionToEnd = wikiText!!.substring(descriptionIndex + 12)
|
val descriptionToEnd = wikiText!!.substring(descriptionIndex + 12)
|
||||||
val descriptionEndIndex = descriptionToEnd.indexOf("\n")
|
val descriptionEndIndex = descriptionToEnd.indexOf("\n")
|
||||||
val descriptionEnd = wikiText!!.substring(
|
val descriptionEnd =
|
||||||
descriptionStart.length
|
wikiText!!.substring(
|
||||||
+ descriptionEndIndex
|
descriptionStart.length +
|
||||||
)
|
descriptionEndIndex,
|
||||||
|
)
|
||||||
buffer.append(descriptionStart)
|
buffer.append(descriptionStart)
|
||||||
for (i in uploadMediaDetails.indices) {
|
for (i in uploadMediaDetails.indices) {
|
||||||
val uploadDetails = uploadMediaDetails[i]
|
val uploadDetails = uploadMediaDetails[i]
|
||||||
|
|
@ -203,65 +214,72 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
* @param updatedWikiText updated wiki text
|
* @param updatedWikiText updated wiki text
|
||||||
* @param uploadMediaDetails descriptions and captions
|
* @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 {
|
try {
|
||||||
descriptionEditHelper?.addDescription(
|
descriptionEditHelper
|
||||||
applicationContext, media,
|
?.addDescription(
|
||||||
updatedWikiText
|
applicationContext,
|
||||||
)
|
media,
|
||||||
?.subscribeOn(Schedulers.io())
|
updatedWikiText,
|
||||||
|
)?.subscribeOn(Schedulers.io())
|
||||||
?.observeOn(AndroidSchedulers.mainThread())
|
?.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(
|
compositeDisposable.add(
|
||||||
it
|
it,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e : InvalidLoginTokenException) {
|
} catch (e: InvalidLoginTokenException) {
|
||||||
val username: String? = sessionManager?.userName
|
val username: String? = sessionManager?.userName
|
||||||
val logoutListener = CommonsApplication.BaseLogoutListener(
|
val logoutListener =
|
||||||
this,
|
CommonsApplication.BaseLogoutListener(
|
||||||
getString(R.string.invalid_login_message),
|
this,
|
||||||
username
|
getString(R.string.invalid_login_message),
|
||||||
)
|
username,
|
||||||
|
)
|
||||||
|
|
||||||
val commonsApplication = CommonsApplication.getInstance()
|
val commonsApplication = CommonsApplication.getInstance()
|
||||||
if (commonsApplication != null ){
|
if (commonsApplication != null) {
|
||||||
commonsApplication.clearApplicationData(this,logoutListener)
|
commonsApplication.clearApplicationData(this, logoutListener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val updatedCaptions = LinkedHashMap<String, String>()
|
val updatedCaptions = LinkedHashMap<String, String>()
|
||||||
for (mediaDetail in uploadMediaDetails) {
|
for (mediaDetail in uploadMediaDetails) {
|
||||||
try {
|
try {
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
descriptionEditHelper!!.addCaption(
|
descriptionEditHelper!!
|
||||||
applicationContext, media,
|
.addCaption(
|
||||||
mediaDetail.languageCode, mediaDetail.captionText
|
applicationContext,
|
||||||
)
|
media,
|
||||||
.subscribeOn(Schedulers.io())
|
mediaDetail.languageCode,
|
||||||
|
mediaDetail.captionText,
|
||||||
|
).subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { s: Boolean? ->
|
.subscribe { s: Boolean? ->
|
||||||
updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText
|
updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText
|
||||||
media.captions = updatedCaptions
|
media.captions = updatedCaptions
|
||||||
Timber.d("Caption is added.")
|
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()
|
val commonsApplication = CommonsApplication.getInstance()
|
||||||
if (commonsApplication != null ){
|
if (commonsApplication != null) {
|
||||||
commonsApplication.clearApplicationData(this,logoutListener)
|
commonsApplication.clearApplicationData(this, logoutListener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,23 +292,29 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
|
||||||
progressDialog!!.show()
|
progressDialog!!.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override
|
override fun onActivityResult(
|
||||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
requestCode: Int,
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
resultCode: Int,
|
||||||
if (requestCode == REQUEST_CODE_FOR_VOICE_INPUT) {
|
data: Intent?,
|
||||||
|
) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
if (requestCode == requestCodeForVoiceInput) {
|
||||||
if (resultCode == RESULT_OK && data != null) {
|
if (resultCode == RESULT_OK && data != null) {
|
||||||
val result = data.getStringArrayListExtra( RecognizerIntent.EXTRA_RESULTS )
|
val result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
|
||||||
uploadMediaDetailAdapter.handleSpeechResult(result!![0]) }
|
uploadMediaDetailAdapter.handleSpeechResult(result!![0])
|
||||||
else { Timber.e("Error %s", resultCode) }
|
} else {
|
||||||
|
Timber.e("Error %s", resultCode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
outState.putParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION, uploadMediaDetailAdapter.items as ArrayList<out Parcelable?>)
|
outState.putParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION, uploadMediaDetailAdapter.items as ArrayList<out Parcelable?>)
|
||||||
outState.putString(WIKITEXT, wikiText)
|
outState.putString(WIKITEXT, wikiText)
|
||||||
outState.putString(Prefs.DESCRIPTION_LANGUAGE, savedLanguageValue)
|
outState.putString(Prefs.DESCRIPTION_LANGUAGE, savedLanguageValue)
|
||||||
//save Media
|
// save Media
|
||||||
outState.putParcelable("media", media)
|
outState.putParcelable("media", media)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,5 @@ package fr.free.nrw.commons.description
|
||||||
object EditDescriptionConstants {
|
object EditDescriptionConstants {
|
||||||
const val LIST_OF_DESCRIPTION_AND_CAPTION = "description.descriptionAndCaption"
|
const val LIST_OF_DESCRIPTION_AND_CAPTION = "description.descriptionAndCaption"
|
||||||
const val WIKITEXT = "description.wikiText"
|
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
|
import fr.free.nrw.commons.explore.map.ExploreMapFragment
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
class ExploreMapFragmentModule{
|
class ExploreMapFragmentModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun ExploreMapFragment.providesActivity(): Activity = activity!!
|
fun ExploreMapFragment.providesActivity(): Activity = activity!!
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@ import dagger.Provides
|
||||||
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment
|
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
class NearbyParentFragmentModule{
|
class NearbyParentFragmentModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun NearbyParentFragment.providesActivity(): Activity = activity!!
|
fun NearbyParentFragment.providesActivity(): Activity = activity!!
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,31 +44,32 @@ class EditActivity : AppCompatActivity() {
|
||||||
imageUri = intent.getStringExtra("image") ?: ""
|
imageUri = intent.getStringExtra("image") ?: ""
|
||||||
vm = ViewModelProvider(this).get(EditViewModel::class.java)
|
vm = ViewModelProvider(this).get(EditViewModel::class.java)
|
||||||
val sourceExif = imageUri.toUri().path?.let { ExifInterface(it) }
|
val sourceExif = imageUri.toUri().path?.let { ExifInterface(it) }
|
||||||
val exifTags = arrayOf(
|
val exifTags =
|
||||||
ExifInterface.TAG_APERTURE,
|
arrayOf(
|
||||||
ExifInterface.TAG_DATETIME,
|
ExifInterface.TAG_APERTURE,
|
||||||
ExifInterface.TAG_EXPOSURE_TIME,
|
ExifInterface.TAG_DATETIME,
|
||||||
ExifInterface.TAG_FLASH,
|
ExifInterface.TAG_EXPOSURE_TIME,
|
||||||
ExifInterface.TAG_FOCAL_LENGTH,
|
ExifInterface.TAG_FLASH,
|
||||||
ExifInterface.TAG_GPS_ALTITUDE,
|
ExifInterface.TAG_FOCAL_LENGTH,
|
||||||
ExifInterface.TAG_GPS_ALTITUDE_REF,
|
ExifInterface.TAG_GPS_ALTITUDE,
|
||||||
ExifInterface.TAG_GPS_DATESTAMP,
|
ExifInterface.TAG_GPS_ALTITUDE_REF,
|
||||||
ExifInterface.TAG_GPS_LATITUDE,
|
ExifInterface.TAG_GPS_DATESTAMP,
|
||||||
ExifInterface.TAG_GPS_LATITUDE_REF,
|
ExifInterface.TAG_GPS_LATITUDE,
|
||||||
ExifInterface.TAG_GPS_LONGITUDE,
|
ExifInterface.TAG_GPS_LATITUDE_REF,
|
||||||
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
ExifInterface.TAG_GPS_LONGITUDE,
|
||||||
ExifInterface.TAG_GPS_PROCESSING_METHOD,
|
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
||||||
ExifInterface.TAG_GPS_TIMESTAMP,
|
ExifInterface.TAG_GPS_PROCESSING_METHOD,
|
||||||
ExifInterface.TAG_IMAGE_LENGTH,
|
ExifInterface.TAG_GPS_TIMESTAMP,
|
||||||
ExifInterface.TAG_IMAGE_WIDTH,
|
ExifInterface.TAG_IMAGE_LENGTH,
|
||||||
ExifInterface.TAG_ISO,
|
ExifInterface.TAG_IMAGE_WIDTH,
|
||||||
ExifInterface.TAG_MAKE,
|
ExifInterface.TAG_ISO,
|
||||||
ExifInterface.TAG_MODEL,
|
ExifInterface.TAG_MAKE,
|
||||||
ExifInterface.TAG_ORIENTATION,
|
ExifInterface.TAG_MODEL,
|
||||||
ExifInterface.TAG_WHITE_BALANCE,
|
ExifInterface.TAG_ORIENTATION,
|
||||||
ExifInterface.WHITEBALANCE_AUTO,
|
ExifInterface.TAG_WHITE_BALANCE,
|
||||||
ExifInterface.WHITEBALANCE_MANUAL
|
ExifInterface.WHITEBALANCE_AUTO,
|
||||||
)
|
ExifInterface.WHITEBALANCE_MANUAL,
|
||||||
|
)
|
||||||
for (tag in exifTags) {
|
for (tag in exifTags) {
|
||||||
val attribute = sourceExif?.getAttribute(tag.toString())
|
val attribute = sourceExif?.getAttribute(tag.toString())
|
||||||
sourceExifAttributeList.add(Pair(tag.toString(), attribute))
|
sourceExifAttributeList.add(Pair(tag.toString(), attribute))
|
||||||
|
|
@ -87,37 +88,38 @@ class EditActivity : AppCompatActivity() {
|
||||||
private fun init() {
|
private fun init() {
|
||||||
binding.iv.adjustViewBounds = true
|
binding.iv.adjustViewBounds = true
|
||||||
binding.iv.scaleType = ImageView.ScaleType.MATRIX
|
binding.iv.scaleType = ImageView.ScaleType.MATRIX
|
||||||
binding.iv.post(Runnable {
|
binding.iv.post(
|
||||||
val options = BitmapFactory.Options()
|
Runnable {
|
||||||
options.inJustDecodeBounds = true
|
val options = BitmapFactory.Options()
|
||||||
BitmapFactory.decodeFile(imageUri, options)
|
options.inJustDecodeBounds = true
|
||||||
|
BitmapFactory.decodeFile(imageUri, options)
|
||||||
|
|
||||||
val bitmapWidth = options.outWidth
|
val bitmapWidth = options.outWidth
|
||||||
val bitmapHeight = options.outHeight
|
val bitmapHeight = options.outHeight
|
||||||
|
|
||||||
// Check if the bitmap dimensions exceed a certain threshold
|
// Check if the bitmap dimensions exceed a certain threshold
|
||||||
val maxBitmapSize = 2000 // Set your maximum size here
|
val maxBitmapSize = 2000 // Set your maximum size here
|
||||||
if (bitmapWidth > maxBitmapSize || bitmapHeight > maxBitmapSize) {
|
if (bitmapWidth > maxBitmapSize || bitmapHeight > maxBitmapSize) {
|
||||||
val scaleFactor = calculateScaleFactor(bitmapWidth, bitmapHeight, maxBitmapSize)
|
val scaleFactor = calculateScaleFactor(bitmapWidth, bitmapHeight, maxBitmapSize)
|
||||||
options.inSampleSize = scaleFactor
|
options.inSampleSize = scaleFactor
|
||||||
options.inJustDecodeBounds = false
|
options.inJustDecodeBounds = false
|
||||||
val scaledBitmap = BitmapFactory.decodeFile(imageUri, options)
|
val scaledBitmap = BitmapFactory.decodeFile(imageUri, options)
|
||||||
binding.iv.setImageBitmap(scaledBitmap)
|
binding.iv.setImageBitmap(scaledBitmap)
|
||||||
// Update the ImageView with the scaled bitmap
|
// Update the ImageView with the scaled bitmap
|
||||||
val scale = binding.iv.measuredWidth.toFloat() / scaledBitmap.width.toFloat()
|
val scale = binding.iv.measuredWidth.toFloat() / scaledBitmap.width.toFloat()
|
||||||
binding.iv.layoutParams.height = (scale * scaledBitmap.height).toInt()
|
binding.iv.layoutParams.height = (scale * scaledBitmap.height).toInt()
|
||||||
binding.iv.imageMatrix = scaleMatrix(scale, scale)
|
binding.iv.imageMatrix = scaleMatrix(scale, scale)
|
||||||
} else {
|
} else {
|
||||||
|
options.inJustDecodeBounds = false
|
||||||
|
val bitmap = BitmapFactory.decodeFile(imageUri, options)
|
||||||
|
binding.iv.setImageBitmap(bitmap)
|
||||||
|
|
||||||
options.inJustDecodeBounds = false
|
val scale = binding.iv.measuredWidth.toFloat() / bitmapWidth.toFloat()
|
||||||
val bitmap = BitmapFactory.decodeFile(imageUri, options)
|
binding.iv.layoutParams.height = (scale * bitmapHeight).toInt()
|
||||||
binding.iv.setImageBitmap(bitmap)
|
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 {
|
binding.rotateBtn.setOnClickListener {
|
||||||
animateImageHeight()
|
animateImageHeight()
|
||||||
}
|
}
|
||||||
|
|
@ -138,8 +140,16 @@ class EditActivity : AppCompatActivity() {
|
||||||
* further rotation actions.
|
* further rotation actions.
|
||||||
*/
|
*/
|
||||||
private fun animateImageHeight() {
|
private fun animateImageHeight() {
|
||||||
val drawableWidth: Float = binding.iv.getDrawable().getIntrinsicWidth().toFloat()
|
val drawableWidth: Float =
|
||||||
val drawableHeight: Float = binding.iv.getDrawable().getIntrinsicHeight().toFloat()
|
binding.iv
|
||||||
|
.getDrawable()
|
||||||
|
.getIntrinsicWidth()
|
||||||
|
.toFloat()
|
||||||
|
val drawableHeight: Float =
|
||||||
|
binding.iv
|
||||||
|
.getDrawable()
|
||||||
|
.getIntrinsicHeight()
|
||||||
|
.toFloat()
|
||||||
val viewWidth: Float = binding.iv.getMeasuredWidth().toFloat()
|
val viewWidth: Float = binding.iv.getMeasuredWidth().toFloat()
|
||||||
val viewHeight: Float = binding.iv.getMeasuredHeight().toFloat()
|
val viewHeight: Float = binding.iv.getMeasuredHeight().toFloat()
|
||||||
val rotation = imageRotation % 360
|
val rotation = imageRotation % 360
|
||||||
|
|
@ -152,7 +162,6 @@ class EditActivity : AppCompatActivity() {
|
||||||
Timber.d("Rotation $rotation")
|
Timber.d("Rotation $rotation")
|
||||||
Timber.d("new Rotation $newRotation")
|
Timber.d("new Rotation $newRotation")
|
||||||
|
|
||||||
|
|
||||||
if (rotation == 0 || rotation == 180) {
|
if (rotation == 0 || rotation == 180) {
|
||||||
imageScale = viewWidth / drawableWidth
|
imageScale = viewWidth / drawableWidth
|
||||||
newImageScale = viewWidth / drawableHeight
|
newImageScale = viewWidth / drawableHeight
|
||||||
|
|
@ -169,23 +178,24 @@ class EditActivity : AppCompatActivity() {
|
||||||
|
|
||||||
animator.interpolator = AccelerateDecelerateInterpolator()
|
animator.interpolator = AccelerateDecelerateInterpolator()
|
||||||
|
|
||||||
animator.addListener(object : AnimatorListener {
|
animator.addListener(
|
||||||
override fun onAnimationStart(animation: Animator) {
|
object : AnimatorListener {
|
||||||
binding.rotateBtn.setEnabled(false)
|
override fun onAnimationStart(animation: Animator) {
|
||||||
}
|
binding.rotateBtn.setEnabled(false)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
imageRotation = newRotation % 360
|
imageRotation = newRotation % 360
|
||||||
binding.rotateBtn.setEnabled(true)
|
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 ->
|
animator.addUpdateListener { animation ->
|
||||||
val animVal = animation.animatedValue as Float
|
val animVal = animation.animatedValue as Float
|
||||||
|
|
@ -195,20 +205,21 @@ class EditActivity : AppCompatActivity() {
|
||||||
val animatedScale = complementaryAnimVal * imageScale + animVal * newImageScale
|
val animatedScale = complementaryAnimVal * imageScale + animVal * newImageScale
|
||||||
val animatedRotation = complementaryAnimVal * rotation + animVal * newRotation
|
val animatedRotation = complementaryAnimVal * rotation + animVal * newRotation
|
||||||
binding.iv.getLayoutParams().height = animatedHeight
|
binding.iv.getLayoutParams().height = animatedHeight
|
||||||
val matrix: Matrix = rotationMatrix(
|
val matrix: Matrix =
|
||||||
animatedRotation,
|
rotationMatrix(
|
||||||
drawableWidth / 2,
|
animatedRotation,
|
||||||
drawableHeight / 2
|
drawableWidth / 2,
|
||||||
)
|
drawableHeight / 2,
|
||||||
|
)
|
||||||
matrix.postScale(
|
matrix.postScale(
|
||||||
animatedScale,
|
animatedScale,
|
||||||
animatedScale,
|
animatedScale,
|
||||||
drawableWidth / 2,
|
drawableWidth / 2,
|
||||||
drawableHeight / 2
|
drawableHeight / 2,
|
||||||
)
|
)
|
||||||
matrix.postTranslate(
|
matrix.postTranslate(
|
||||||
-(drawableWidth - binding.iv.getMeasuredWidth()) / 2,
|
-(drawableWidth - binding.iv.getMeasuredWidth()) / 2,
|
||||||
-(drawableHeight - binding.iv.getMeasuredHeight()) / 2
|
-(drawableHeight - binding.iv.getMeasuredHeight()) / 2,
|
||||||
)
|
)
|
||||||
binding.iv.setImageMatrix(matrix)
|
binding.iv.setImageMatrix(matrix)
|
||||||
binding.iv.requestLayout()
|
binding.iv.requestLayout()
|
||||||
|
|
@ -228,11 +239,9 @@ class EditActivity : AppCompatActivity() {
|
||||||
* as a result, and finishes the current activity.
|
* as a result, and finishes the current activity.
|
||||||
*/
|
*/
|
||||||
fun getRotatedImage() {
|
fun getRotatedImage() {
|
||||||
|
|
||||||
val filePath = imageUri.toUri().path
|
val filePath = imageUri.toUri().path
|
||||||
val file = filePath?.let { File(it) }
|
val file = filePath?.let { File(it) }
|
||||||
|
|
||||||
|
|
||||||
val rotatedImage = file?.let { vm.rotateImage(imageRotation, it) }
|
val rotatedImage = file?.let { vm.rotateImage(imageRotation, it) }
|
||||||
if (rotatedImage == null) {
|
if (rotatedImage == null) {
|
||||||
Toast.makeText(this, "Failed to rotate to image", Toast.LENGTH_LONG).show()
|
Toast.makeText(this, "Failed to rotate to image", Toast.LENGTH_LONG).show()
|
||||||
|
|
@ -243,9 +252,9 @@ class EditActivity : AppCompatActivity() {
|
||||||
copyExifData(editedImageExif)
|
copyExifData(editedImageExif)
|
||||||
}
|
}
|
||||||
val resultIntent = Intent()
|
val resultIntent = Intent()
|
||||||
resultIntent.putExtra("editedImageFilePath", rotatedImage?.toUri()?.path ?: "Error");
|
resultIntent.putExtra("editedImageFilePath", rotatedImage?.toUri()?.path ?: "Error")
|
||||||
setResult(RESULT_OK, resultIntent);
|
setResult(RESULT_OK, resultIntent)
|
||||||
finish();
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -257,7 +266,6 @@ class EditActivity : AppCompatActivity() {
|
||||||
* @param editedImageExif The ExifInterface object for the edited image.
|
* @param editedImageExif The ExifInterface object for the edited image.
|
||||||
*/
|
*/
|
||||||
private fun copyExifData(editedImageExif: ExifInterface?) {
|
private fun copyExifData(editedImageExif: ExifInterface?) {
|
||||||
|
|
||||||
for (attr in sourceExifAttributeList) {
|
for (attr in sourceExifAttributeList) {
|
||||||
Log.d("Tag is ${attr.first}", "Value is ${attr.second}")
|
Log.d("Tag is ${attr.first}", "Value is ${attr.second}")
|
||||||
editedImageExif!!.setAttribute(attr.first, 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
|
* The scale factor ensures that the scaled bitmap will fit within the maximum size
|
||||||
* while maintaining aspect ratio.
|
* 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
|
var scaleFactor = 1
|
||||||
|
|
||||||
if (originalWidth > maxSize || originalHeight > maxSize) {
|
if (originalWidth > maxSize || originalHeight > maxSize) {
|
||||||
|
|
@ -295,7 +307,4 @@ class EditActivity : AppCompatActivity() {
|
||||||
|
|
||||||
return scaleFactor
|
return scaleFactor
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ import java.io.File
|
||||||
* This ViewModel class is responsible for managing image editing operations, such as
|
* This ViewModel class is responsible for managing image editing operations, such as
|
||||||
* rotating images. It utilizes a TransformImage implementation to perform image transformations.
|
* rotating images. It utilizes a TransformImage implementation to perform image transformations.
|
||||||
*/
|
*/
|
||||||
class EditViewModel() : ViewModel() {
|
class EditViewModel : ViewModel() {
|
||||||
|
|
||||||
// Ideally should be injected using DI
|
// Ideally should be injected using DI
|
||||||
private val transformImage: TransformImage = TransformImageImpl()
|
private val transformImage: TransformImage = TransformImageImpl()
|
||||||
|
|
||||||
|
|
@ -21,7 +20,8 @@ class EditViewModel() : ViewModel() {
|
||||||
* @param imageFile The File representing the image to be rotated.
|
* @param imageFile The File representing the image to be rotated.
|
||||||
* @return The rotated image File, or null if the rotation operation fails.
|
* @return The rotated image File, or null if the rotation operation fails.
|
||||||
*/
|
*/
|
||||||
fun rotateImage(degree: Int, imageFile: File): File? {
|
fun rotateImage(
|
||||||
return transformImage.rotateImage(imageFile, degree)
|
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.
|
* implementations to provide specific functionality for tasks like rotating images.
|
||||||
*/
|
*/
|
||||||
interface TransformImage {
|
interface TransformImage {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rotates the specified image file by the given degree.
|
* 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.
|
* @param degree The degree by which to rotate the image.
|
||||||
* @return The rotated image File, or null if the rotation operation fails.
|
* @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
|
* 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.
|
* 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.
|
* 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.
|
* @param degree The degree by which to rotate the image.
|
||||||
* @return The rotated image File, or null if the rotation operation fails.
|
* @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")
|
Timber.tag("Trying to rotate image").d("Starting")
|
||||||
|
|
||||||
val path = Environment.getExternalStoragePublicDirectory(
|
val path =
|
||||||
Environment.DIRECTORY_DOWNLOADS
|
Environment.getExternalStoragePublicDirectory(
|
||||||
)
|
Environment.DIRECTORY_DOWNLOADS,
|
||||||
|
)
|
||||||
|
|
||||||
val imagePath = System.currentTimeMillis()
|
val imagePath = System.currentTimeMillis()
|
||||||
val file: File = File(path, "$imagePath.jpg")
|
val file: File = File(path, "$imagePath.jpg")
|
||||||
|
|
||||||
val output = file
|
val output = file
|
||||||
|
|
||||||
val rotated = try {
|
val rotated =
|
||||||
val lljTran = LLJTran(imageFile)
|
try {
|
||||||
lljTran.read(
|
val lljTran = LLJTran(imageFile)
|
||||||
LLJTran.READ_ALL,
|
lljTran.read(
|
||||||
false,
|
LLJTran.READ_ALL,
|
||||||
) // This could throw an LLJTranException. I am not catching it for now... Let's see.
|
false,
|
||||||
lljTran.transform(
|
) // This could throw an LLJTranException. I am not catching it for now... Let's see.
|
||||||
when(degree){
|
lljTran.transform(
|
||||||
90 -> LLJTran.ROT_90
|
when (degree) {
|
||||||
180 -> LLJTran.ROT_180
|
90 -> LLJTran.ROT_90
|
||||||
270 -> LLJTran.ROT_270
|
180 -> LLJTran.ROT_180
|
||||||
else -> {
|
270 -> LLJTran.ROT_270
|
||||||
LLJTran.ROT_90
|
else -> {
|
||||||
}
|
LLJTran.ROT_90
|
||||||
},
|
}
|
||||||
LLJTran.OPT_DEFAULTS or LLJTran.OPT_XFORM_ORIENTATION
|
},
|
||||||
)
|
LLJTran.OPT_DEFAULTS or LLJTran.OPT_XFORM_ORIENTATION,
|
||||||
BufferedOutputStream(FileOutputStream(output)).use { writer ->
|
)
|
||||||
lljTran.save(writer, LLJTran.OPT_WRITE_ALL )
|
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) {
|
if (rotated) {
|
||||||
Timber.tag("Done rotating image").d("Done")
|
Timber.tag("Done rotating image").d("Done")
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,11 @@ import fr.free.nrw.commons.explore.media.SearchMediaFragmentPresenterImpl
|
||||||
@Module
|
@Module
|
||||||
abstract class SearchModule {
|
abstract class SearchModule {
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun SearchDepictionsFragmentPresenterImpl.bindsSearchDepictionsFragmentPresenter()
|
abstract fun SearchDepictionsFragmentPresenterImpl.bindsSearchDepictionsFragmentPresenter(): SearchDepictionsFragmentPresenter
|
||||||
: SearchDepictionsFragmentPresenter
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun SearchCategoriesFragmentPresenterImpl.bindsSearchCategoriesFragmentPresenter()
|
abstract fun SearchCategoriesFragmentPresenterImpl.bindsSearchCategoriesFragmentPresenter(): SearchCategoriesFragmentPresenter
|
||||||
: SearchCategoriesFragmentPresenter
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun SearchMediaFragmentPresenterImpl.bindsSearchMediaFragmentPresenter()
|
abstract fun SearchMediaFragmentPresenterImpl.bindsSearchMediaFragmentPresenter(): SearchMediaFragmentPresenter
|
||||||
: 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.SubCategoriesPresenter
|
||||||
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesPresenterImpl
|
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesPresenterImpl
|
||||||
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
abstract class CategoriesModule {
|
abstract class CategoriesModule {
|
||||||
|
@Binds
|
||||||
|
abstract fun CategoryMediaPresenterImpl.bindsCategoryMediaPresenter(): CategoryMediaPresenter
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun CategoryMediaPresenterImpl.bindsCategoryMediaPresenter()
|
abstract fun SubCategoriesPresenterImpl.bindsSubCategoriesPresenter(): SubCategoriesPresenter
|
||||||
: CategoryMediaPresenter
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun SubCategoriesPresenterImpl.bindsSubCategoriesPresenter()
|
abstract fun ParentCategoriesPresenterImpl.bindsParentCategoriesPresenter(): ParentCategoriesPresenter
|
||||||
: SubCategoriesPresenter
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
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.category.CategoryDetailsActivity
|
||||||
import fr.free.nrw.commons.explore.paging.BasePagingFragment
|
import fr.free.nrw.commons.explore.paging.BasePagingFragment
|
||||||
|
|
||||||
|
|
||||||
abstract class PageableCategoryFragment : BasePagingFragment<String>() {
|
abstract class PageableCategoryFragment : BasePagingFragment<String>() {
|
||||||
override val errorTextId: Int = R.string.error_loading_categories
|
override val errorTextId: Int = R.string.error_loading_categories
|
||||||
override val pagedListAdapter by lazy {
|
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.category.CATEGORY_PREFIX
|
||||||
import fr.free.nrw.commons.databinding.ItemRecentSearchesBinding
|
import fr.free.nrw.commons.databinding.ItemRecentSearchesBinding
|
||||||
|
|
||||||
class PagedSearchCategoriesAdapter(private val onCategoryClicked: (String) -> Unit) :
|
class PagedSearchCategoriesAdapter(
|
||||||
PagedListAdapter<String, CategoryItemViewHolder>(PagedSearchCategoriesDiffUtilCallback) {
|
private val onCategoryClicked: (String) -> Unit,
|
||||||
|
) : PagedListAdapter<String, CategoryItemViewHolder>(PagedSearchCategoriesDiffUtilCallback) {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = CategoryItemViewHolder(
|
override fun onCreateViewHolder(
|
||||||
ItemRecentSearchesBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
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)
|
holder.bind(getItem(position)!!, onCategoryClicked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategoryItemViewHolder(
|
class CategoryItemViewHolder(
|
||||||
private val binding: ItemRecentSearchesBinding
|
private val binding: ItemRecentSearchesBinding,
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : 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) }
|
root.setOnClickListener { onCategoryClicked(item) }
|
||||||
textView1.text = item.substringAfter(CATEGORY_PREFIX)
|
textView1.text = item.substringAfter(CATEGORY_PREFIX)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object PagedSearchCategoriesDiffUtilCallback : DiffUtil.ItemCallback<String>() {
|
private object PagedSearchCategoriesDiffUtilCallback : DiffUtil.ItemCallback<String>() {
|
||||||
override fun areItemsTheSame(oldItem: String, newItem: String) =
|
override fun areItemsTheSame(
|
||||||
oldItem == newItem
|
oldItem: String,
|
||||||
|
newItem: String,
|
||||||
|
) = oldItem == newItem
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: String, newItem: String) =
|
override fun areContentsTheSame(
|
||||||
oldItem == newItem
|
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 fr.free.nrw.commons.explore.media.PageableMediaFragment
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
class CategoriesMediaFragment : PageableMediaFragment() {
|
class CategoriesMediaFragment : PageableMediaFragment() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var presenter: CategoryMediaPresenter
|
lateinit var presenter: CategoryMediaPresenter
|
||||||
|
|
||||||
override val injectedPresenter
|
override val injectedPresenter
|
||||||
get() = presenter
|
get() = presenter
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(
|
||||||
|
view: View,
|
||||||
|
savedInstanceState: Bundle?,
|
||||||
|
) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}")
|
onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,10 @@ interface CategoryMediaPresenter : PagingContract.Presenter<Media>
|
||||||
/**
|
/**
|
||||||
* Presenter for DepictedImagesFragment
|
* Presenter for DepictedImagesFragment
|
||||||
*/
|
*/
|
||||||
class CategoryMediaPresenterImpl @Inject constructor(
|
class CategoryMediaPresenterImpl
|
||||||
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
@Inject
|
||||||
dataSourceFactory: PageableCategoriesMediaDataSource
|
constructor(
|
||||||
) : BasePagingPresenter<Media>(mainThreadScheduler, dataSourceFactory),
|
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
||||||
CategoryMediaPresenter
|
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 fr.free.nrw.commons.media.MediaClient
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class PageableCategoriesMediaDataSource @Inject constructor(
|
class PageableCategoriesMediaDataSource
|
||||||
liveDataConverter: LiveDataConverter,
|
@Inject
|
||||||
private val mediaClient: MediaClient
|
constructor(
|
||||||
) : PageableBaseDataSource<Media>(liveDataConverter) {
|
liveDataConverter: LiveDataConverter,
|
||||||
override val loadFunction: LoadFunction<Media> = { loadSize: Int, startPosition: Int ->
|
private val mediaClient: MediaClient,
|
||||||
if(startPosition == 0){
|
) : PageableBaseDataSource<Media>(liveDataConverter) {
|
||||||
mediaClient.resetCategoryContinuation(query)
|
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 fr.free.nrw.commons.explore.paging.PageableBaseDataSource
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class PageableParentCategoriesDataSource @Inject constructor(
|
class PageableParentCategoriesDataSource
|
||||||
liveDataConverter: LiveDataConverter,
|
@Inject
|
||||||
val categoryClient: CategoryClient
|
constructor(
|
||||||
) : PageableBaseDataSource<String>(liveDataConverter) {
|
liveDataConverter: LiveDataConverter,
|
||||||
|
val categoryClient: CategoryClient,
|
||||||
override val loadFunction = { loadSize: Int, startPosition: Int ->
|
) : PageableBaseDataSource<String>(liveDataConverter) {
|
||||||
if (startPosition == 0) {
|
override val loadFunction = { loadSize: Int, startPosition: Int ->
|
||||||
categoryClient.resetParentCategoryContinuation(query)
|
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 fr.free.nrw.commons.explore.categories.PageableCategoryFragment
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
class ParentCategoriesFragment : PageableCategoryFragment() {
|
class ParentCategoriesFragment : PageableCategoryFragment() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var presenter: ParentCategoriesPresenter
|
lateinit var presenter: ParentCategoriesPresenter
|
||||||
|
|
||||||
|
|
@ -18,9 +16,11 @@ class ParentCategoriesFragment : PageableCategoryFragment() {
|
||||||
|
|
||||||
override fun getEmptyText(query: String) = getString(R.string.no_parentcategory_found)
|
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)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}")
|
onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,12 @@ import io.reactivex.Scheduler
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
|
|
||||||
|
|
||||||
interface ParentCategoriesPresenter : PagingContract.Presenter<String>
|
interface ParentCategoriesPresenter : PagingContract.Presenter<String>
|
||||||
|
|
||||||
class ParentCategoriesPresenterImpl @Inject constructor(
|
class ParentCategoriesPresenterImpl
|
||||||
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
@Inject
|
||||||
dataSourceFactory: PageableParentCategoriesDataSource
|
constructor(
|
||||||
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
|
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
||||||
ParentCategoriesPresenter
|
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 fr.free.nrw.commons.explore.paging.PageableBaseDataSource
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class PageableSearchCategoriesDataSource @Inject constructor(
|
class PageableSearchCategoriesDataSource
|
||||||
liveDataConverter: LiveDataConverter,
|
@Inject
|
||||||
val categoryClient: CategoryClient
|
constructor(
|
||||||
) : PageableBaseDataSource<String>(liveDataConverter) {
|
liveDataConverter: LiveDataConverter,
|
||||||
|
val categoryClient: CategoryClient,
|
||||||
override val loadFunction = { loadSize: Int, startPosition: Int ->
|
) : PageableBaseDataSource<String>(liveDataConverter) {
|
||||||
categoryClient.searchCategories(query, loadSize, startPosition).blockingGet()
|
override val loadFunction = { loadSize: Int, startPosition: Int ->
|
||||||
.map { it.name }
|
categoryClient
|
||||||
|
.searchCategories(query, loadSize, startPosition)
|
||||||
|
.blockingGet()
|
||||||
|
.map { it.name }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@ import javax.inject.Named
|
||||||
|
|
||||||
interface SearchCategoriesFragmentPresenter : PagingContract.Presenter<String>
|
interface SearchCategoriesFragmentPresenter : PagingContract.Presenter<String>
|
||||||
|
|
||||||
class SearchCategoriesFragmentPresenterImpl @Inject constructor(
|
class SearchCategoriesFragmentPresenterImpl
|
||||||
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
@Inject
|
||||||
dataSourceFactory: PageableSearchCategoriesDataSource
|
constructor(
|
||||||
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
|
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
||||||
SearchCategoriesFragmentPresenter
|
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 fr.free.nrw.commons.explore.paging.PageableBaseDataSource
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class PageableSubCategoriesDataSource @Inject constructor(
|
class PageableSubCategoriesDataSource
|
||||||
liveDataConverter: LiveDataConverter,
|
@Inject
|
||||||
val categoryClient: CategoryClient
|
constructor(
|
||||||
) : PageableBaseDataSource<String>(liveDataConverter) {
|
liveDataConverter: LiveDataConverter,
|
||||||
|
val categoryClient: CategoryClient,
|
||||||
override val loadFunction = { loadSize: Int, startPosition: Int ->
|
) : PageableBaseDataSource<String>(liveDataConverter) {
|
||||||
if (startPosition == 0) {
|
override val loadFunction = { loadSize: Int, startPosition: Int ->
|
||||||
categoryClient.resetSubCategoryContinuation(query)
|
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 fr.free.nrw.commons.explore.categories.PageableCategoryFragment
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
class SubCategoriesFragment : PageableCategoryFragment() {
|
class SubCategoriesFragment : PageableCategoryFragment() {
|
||||||
|
|
||||||
@Inject lateinit var presenter: SubCategoriesPresenter
|
@Inject lateinit var presenter: SubCategoriesPresenter
|
||||||
|
|
||||||
override val injectedPresenter
|
override val injectedPresenter
|
||||||
|
|
@ -17,7 +15,10 @@ class SubCategoriesFragment : PageableCategoryFragment() {
|
||||||
|
|
||||||
override fun getEmptyText(query: String) = getString(R.string.no_subcategory_found)
|
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)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}")
|
onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@ import javax.inject.Named
|
||||||
|
|
||||||
interface SubCategoriesPresenter : PagingContract.Presenter<String>
|
interface SubCategoriesPresenter : PagingContract.Presenter<String>
|
||||||
|
|
||||||
class SubCategoriesPresenterImpl @Inject constructor(
|
class SubCategoriesPresenterImpl
|
||||||
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
@Inject
|
||||||
dataSourceFactory: PageableSubCategoriesDataSource
|
constructor(
|
||||||
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
|
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
||||||
SubCategoriesPresenter
|
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.databinding.ItemDepictionsBinding
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
|
||||||
class DepictionAdapter(private val onDepictionClicked: (DepictedItem) -> Unit) :
|
class DepictionAdapter(
|
||||||
PagedListAdapter<DepictedItem, DepictedItemViewHolder>(DepictionDiffUtilCallback) {
|
private val onDepictionClicked: (DepictedItem) -> Unit,
|
||||||
|
) : PagedListAdapter<DepictedItem, DepictedItemViewHolder>(DepictionDiffUtilCallback) {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = DepictedItemViewHolder(
|
override fun onCreateViewHolder(
|
||||||
ItemDepictionsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
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)
|
holder.bind(getItem(position)!!, onDepictionClicked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DepictedItemViewHolder(
|
class DepictedItemViewHolder(
|
||||||
private val binding: ItemDepictionsBinding
|
private val binding: ItemDepictionsBinding,
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : 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) }
|
root.setOnClickListener { onDepictionClicked(item) }
|
||||||
depictsLabel.text = item.name
|
depictsLabel.text = item.name
|
||||||
description.text = item.description
|
description.text = item.description
|
||||||
|
|
@ -37,9 +46,13 @@ class DepictedItemViewHolder(
|
||||||
}
|
}
|
||||||
|
|
||||||
private object DepictionDiffUtilCallback : DiffUtil.ItemCallback<DepictedItem>() {
|
private object DepictionDiffUtilCallback : DiffUtil.ItemCallback<DepictedItem>() {
|
||||||
override fun areItemsTheSame(oldItem: DepictedItem, newItem: DepictedItem) =
|
override fun areItemsTheSame(
|
||||||
oldItem.id == newItem.id
|
oldItem: DepictedItem,
|
||||||
|
newItem: DepictedItem,
|
||||||
|
) = oldItem.id == newItem.id
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: DepictedItem, newItem: DepictedItem) =
|
override fun areContentsTheSame(
|
||||||
oldItem == newItem
|
oldItem: DepictedItem,
|
||||||
|
newItem: DepictedItem,
|
||||||
|
) = oldItem == newItem
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,12 @@ import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsPresenterIm
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module
|
||||||
abstract class DepictionModule {
|
abstract class DepictionModule {
|
||||||
|
@Binds
|
||||||
|
abstract fun ParentDepictionsPresenterImpl.bindsParentDepictionPresenter(): ParentDepictionsPresenter
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun ParentDepictionsPresenterImpl.bindsParentDepictionPresenter()
|
abstract fun ChildDepictionsPresenterImpl.bindsChildDepictionPresenter(): ChildDepictionsPresenter
|
||||||
: ParentDepictionsPresenter
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun ChildDepictionsPresenterImpl.bindsChildDepictionPresenter()
|
abstract fun DepictedImagesPresenterImpl.bindsDepictedImagesContractPresenter(): DepictedImagesPresenter
|
||||||
: ChildDepictionsPresenter
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
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.DataValue
|
||||||
import fr.free.nrw.commons.wikidata.model.DepictSearchItem
|
import fr.free.nrw.commons.wikidata.model.DepictSearchItem
|
||||||
import fr.free.nrw.commons.wikidata.model.Entities
|
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 io.reactivex.Single
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
@ -20,89 +20,101 @@ import javax.inject.Singleton
|
||||||
* Depicts Client to handle custom calls to Commons Wikibase APIs
|
* Depicts Client to handle custom calls to Commons Wikibase APIs
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class DepictsClient @Inject constructor(private val depictsInterface: DepictsInterface) {
|
class DepictsClient
|
||||||
|
@Inject
|
||||||
/**
|
constructor(
|
||||||
* Search for depictions using the search item
|
private val depictsInterface: DepictsInterface,
|
||||||
* @return list of depicted items
|
) {
|
||||||
*/
|
/**
|
||||||
fun searchForDepictions(query: String?, limit: Int, offset: Int): Single<List<DepictedItem>> {
|
* Search for depictions using the search item
|
||||||
val language = Locale.getDefault().language
|
* @return list of depicted items
|
||||||
return depictsInterface.searchForDepicts(query, "$limit", language, language, "$offset")
|
*/
|
||||||
.map { it.search.joinToString("|", transform = DepictSearchItem::id) }
|
fun searchForDepictions(
|
||||||
.mapToDepictions()
|
query: String?,
|
||||||
}
|
limit: Int,
|
||||||
|
offset: Int,
|
||||||
fun getEntities(ids: String): Single<Entities> {
|
): Single<List<DepictedItem>> {
|
||||||
return depictsInterface.getEntities(ids)
|
val language = Locale.getDefault().language
|
||||||
}
|
return depictsInterface
|
||||||
|
.searchForDepicts(query, "$limit", language, language, "$offset")
|
||||||
fun toDepictions(sparqlResponse: Single<SparqlResponse>): Single<List<DepictedItem>> {
|
.map { it.search.joinToString("|", transform = DepictSearchItem::id) }
|
||||||
return sparqlResponse.map {
|
.mapToDepictions()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun getEntities(ids: String): Single<Entities> = depictsInterface.getEntities(ids)
|
||||||
* Convert different entities into DepictedItem
|
|
||||||
*/
|
fun toDepictions(sparqlResponse: Single<SparqlResponse>): Single<List<DepictedItem>> =
|
||||||
private fun mapToDepictItem(entity: Entities.Entity): DepictedItem {
|
sparqlResponse
|
||||||
return if (entity.descriptions().byLanguageOrFirstOrEmpty() == "") {
|
.map {
|
||||||
val instanceOfIDs = entity[WikidataProperties.INSTANCE_OF]
|
it.results.bindings.joinToString("|", transform = Binding::id)
|
||||||
.toIds()
|
}.mapToDepictions()
|
||||||
if (instanceOfIDs.isNotEmpty()) {
|
|
||||||
val entities: Entities = getEntities(instanceOfIDs[0]).blockingGet()
|
/**
|
||||||
val nameAsDescription = entities.entities().values.first().labels()
|
* Fetches Entities from ids ex. "Q1233|Q546" and converts them into DepictedItem
|
||||||
.byLanguageOrFirstOrEmpty()
|
*/
|
||||||
DepictedItem(
|
@SuppressLint("CheckResult")
|
||||||
entity,
|
private fun Single<String>.mapToDepictions() =
|
||||||
entity.labels().byLanguageOrFirstOrEmpty(),
|
flatMap(::getEntities)
|
||||||
nameAsDescription
|
.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 {
|
} else {
|
||||||
DepictedItem(
|
DepictedItem(
|
||||||
entity,
|
entity,
|
||||||
entity.labels().byLanguageOrFirstOrEmpty(),
|
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.
|
* 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 returns null, Tries to retrieve first element from the map.
|
||||||
* If that still returns null, function returns "".
|
* If that still returns null, function returns "".
|
||||||
*/
|
*/
|
||||||
private fun Map<String, Entities.Label>.byLanguageOrFirstOrEmpty() =
|
private fun Map<String, Entities.Label>.byLanguageOrFirstOrEmpty() =
|
||||||
let {
|
let {
|
||||||
it[Locale.getDefault().language] ?: it.values.firstOrNull() }?.value() ?: ""
|
it[Locale.getDefault().language] ?: it.values.firstOrNull()
|
||||||
|
}?.value() ?: ""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns list of id ex. "Q2323" from Statement_partial
|
* returns list of id ex. "Q2323" from Statement_partial
|
||||||
*/
|
*/
|
||||||
private fun List<Statement_partial>?.toIds(): List<String> {
|
private fun List<StatementPartial>?.toIds(): List<String> =
|
||||||
return this?.map { it.mainSnak.dataValue }
|
this
|
||||||
?.filterIsInstance<DataValue.EntityId>()
|
?.map { it.mainSnak.dataValue }
|
||||||
?.map { it.value.id }
|
?.filterIsInstance<DataValue.EntityId>()
|
||||||
?: emptyList()
|
?.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