mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 14:53:59 +01:00 
			
		
		
		
	*.kt: bulk correction of formatting using ktlint --format
This commit is contained in:
		
							parent
							
								
									f751ab4a75
								
							
						
					
					
						commit
						4aca2a26ae
					
				
					 401 changed files with 10409 additions and 8827 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), | ||||||
|             ) |             ), | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -23,7 +23,6 @@ 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 +48,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 +63,4 @@ class LoginActivityTest { | ||||||
|     fun orientationChange() { |     fun orientationChange() { | ||||||
|         UITestHelper.changeOrientation(activityRule) |         UITestHelper.changeOrientation(activityRule) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -27,14 +27,14 @@ 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 +48,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 +63,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 | ||||||
|  |  | ||||||
|  | @ -13,14 +13,13 @@ import org.apache.commons.lang3.StringUtils | ||||||
| import org.hamcrest.* | import org.hamcrest.* | ||||||
| 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 +28,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 +60,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 +79,6 @@ class UITestHelper { | ||||||
|                 sleep(10000) |                 sleep(10000) | ||||||
|             } catch (ignored: NoMatchingViewException) { |             } catch (ignored: NoMatchingViewException) { | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         fun logoutUser() { |         fun logoutUser() { | ||||||
|  | @ -87,36 +89,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 +128,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 +139,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 +159,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 +183,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 +198,4 @@ class UITestHelper { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -28,7 +28,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 +36,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 +47,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 +63,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()) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -42,8 +42,11 @@ import java.util.* | ||||||
| @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 +64,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 +96,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 +110,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 +141,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 +169,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 +180,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 +230,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 +260,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 +316,6 @@ class UploadTest { | ||||||
|             } catch (e: IOException) { |             } catch (e: IOException) { | ||||||
|                 e.printStackTrace() |                 e.printStackTrace() | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -328,8 +337,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 +346,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,8 +2,8 @@ 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 kotlinx.parcelize.Parcelize | ||||||
| import java.util.* | import java.util.* | ||||||
| 
 | 
 | ||||||
| @Parcelize | @Parcelize | ||||||
|  | @ -14,7 +14,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 | ||||||
|  | @ -35,7 +34,6 @@ class Media constructor( | ||||||
|      * @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. | ||||||
|  | @ -62,9 +60,7 @@ class Media constructor( | ||||||
|      * @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 +79,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 +104,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 +124,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 +136,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); |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -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,9 +3,9 @@ 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.* | import retrofit2.http.* | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -33,7 +33,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 +60,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 +79,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 +98,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 +109,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 +120,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 +130,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,12 +2,15 @@ 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 |     /** | ||||||
|                 */ |      * Modifies the content URI - marking this bookmark as already saved in the database | ||||||
|                var contentUri: Uri?) { |      * @param contentUri the content URI | ||||||
|  |      */ | ||||||
|  |     var contentUri: Uri?, | ||||||
|  | ) { | ||||||
|     /** |     /** | ||||||
|      * Gets the content URI for this bookmark |      * Gets the content URI for this bookmark | ||||||
|      * @return content URI |      * @return content URI | ||||||
|  | @ -17,10 +20,10 @@ class Bookmark(mediaName: String?, mediaCreator: String?, | ||||||
|      * @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, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | @ -14,269 +14,280 @@ 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 | ||||||
|  | @ -44,26 +43,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 +67,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 +76,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 +87,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 +101,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 +112,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) | ||||||
|  |             } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,17 +2,15 @@ package fr.free.nrw.commons.customselector.database | ||||||
| 
 | 
 | ||||||
| import androidx.room.* | import androidx.room.* | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|  * 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 +23,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 | ||||||
| } | } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -7,10 +7,9 @@ import androidx.room.* | ||||||
|  */ |  */ | ||||||
| @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, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -8,11 +8,10 @@ import java.util.* | ||||||
|  */ |  */ | ||||||
| @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 +30,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 +50,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) | } | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -10,30 +10,25 @@ import java.util.* | ||||||
|  */ |  */ | ||||||
| @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, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -8,17 +8,19 @@ 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 SWIPE_THRESHOLD_HEIGHT = (getScreenResolution(context!!)).second / 3 | ||||||
|     private val SWIPE_THRESHOLD_WIDTH = (getScreenResolution(context!!)).first / 3 |     private val SWIPE_THRESHOLD_WIDTH = (getScreenResolution(context!!)).first / 3 | ||||||
|     private val SWIPE_VELOCITY_THRESHOLD = 1000 |     private val SWIPE_VELOCITY_THRESHOLD = 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 +33,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 +42,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) > SWIPE_THRESHOLD_WIDTH && | ||||||
|                         SWIPE_VELOCITY_THRESHOLD) { |                         abs(velocityX) > | ||||||
|  |                         SWIPE_VELOCITY_THRESHOLD | ||||||
|  |                     ) { | ||||||
|                         if (diffX > 0) { |                         if (diffX > 0) { | ||||||
|                             onSwipeRight() |                             onSwipeRight() | ||||||
|                         } else { |                         } else { | ||||||
|  | @ -58,8 +59,10 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener { | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } else { |                 } else { | ||||||
|                     if (abs(diffY) > SWIPE_THRESHOLD_HEIGHT && abs(velocityY) > |                     if (abs(diffY) > SWIPE_THRESHOLD_HEIGHT && | ||||||
|                         SWIPE_VELOCITY_THRESHOLD) { |                         abs(velocityY) > | ||||||
|  |                         SWIPE_VELOCITY_THRESHOLD | ||||||
|  |                     ) { | ||||||
|                         if (diffY > 0) { |                         if (diffY > 0) { | ||||||
|                             onSwipeDown() |                             onSwipeDown() | ||||||
|                         } else { |                         } else { | ||||||
|  | @ -100,4 +103,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 | ||||||
|  | @ -32,20 +31,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 +101,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 +119,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 +143,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 +164,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 +181,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 +224,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 +247,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 +269,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 +290,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 +301,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 +319,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 +339,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 +361,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 +377,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 +388,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 +426,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 +439,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 +459,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 +486,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 +530,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 +564,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,20 +8,14 @@ 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.Button | ||||||
|  | @ -38,7 +32,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 +50,22 @@ 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.* | import kotlinx.coroutines.* | ||||||
| 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 +139,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 +150,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 | ||||||
|  | @ -171,22 +164,24 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL | ||||||
|             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 +203,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 +221,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 +253,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 +322,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 +338,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 +355,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 +389,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 +424,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 +447,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 +478,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 +498,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 +515,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 +564,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 +580,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 +601,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), | ||||||
|                     ) |                     ) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  | @ -616,9 +647,13 @@ fun PartialStorageAccessIndicator( | ||||||
| @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,7 +24,6 @@ import javax.inject.Inject | ||||||
|  * Custom selector folder fragment. |  * Custom selector folder fragment. | ||||||
|  */ |  */ | ||||||
| class FolderFragment : CommonsDaggerSupportFragment() { | class FolderFragment : CommonsDaggerSupportFragment() { | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * ViewBinding |      * ViewBinding | ||||||
|      */ |      */ | ||||||
|  | @ -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. | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /* |     /* | ||||||
|  |  | ||||||
|  | @ -44,8 +44,10 @@ 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 |     private val binding get() = _binding | ||||||
| 
 | 
 | ||||||
|  | @ -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() | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -23,360 +23,378 @@ 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 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | @ -17,13 +17,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 | ||||||
|      */ |      */ | ||||||
|  | @ -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, | ||||||
|  |         data: Intent?, | ||||||
|  |     ) { | ||||||
|  |         super.onActivityResult(requestCode, resultCode, data) | ||||||
|         if (requestCode == REQUEST_CODE_FOR_VOICE_INPUT) { |         if (requestCode == REQUEST_CODE_FOR_VOICE_INPUT) { | ||||||
|             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 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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<Statement_partial>?.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() | ||||||
|     } |     } | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -4,7 +4,6 @@ import fr.free.nrw.commons.R | ||||||
| import fr.free.nrw.commons.explore.paging.BasePagingFragment | import fr.free.nrw.commons.explore.paging.BasePagingFragment | ||||||
| import fr.free.nrw.commons.upload.structure.depictions.DepictedItem | import fr.free.nrw.commons.upload.structure.depictions.DepictedItem | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| abstract class PageableDepictionsFragment : BasePagingFragment<DepictedItem>() { | abstract class PageableDepictionsFragment : BasePagingFragment<DepictedItem>() { | ||||||
|     override val errorTextId: Int = R.string.error_loading_depictions |     override val errorTextId: Int = R.string.error_loading_depictions | ||||||
|     override val pagedListAdapter by lazy { |     override val pagedListAdapter by lazy { | ||||||
|  |  | ||||||
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
	
	 tristan81
						tristan81