diff --git a/README.md b/README.md index 07beeec8d..3e5774873 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,13 @@ We try to have an extensive documentation at our [documentation repository][4]: Thank you all for your work! -| [
misaochan](https://github.com/misaochan) | [
yuvipanda](https://github.com/yuvipanda) | [
neslihanturan](https://github.com/neslihanturan) | [
maskaravivek](https://github.com/maskaravivek) | [
translatewiki](https://github.com/translatewiki) | +| [
misaochan](https://github.com/misaochan) | [
translatewiki](https://github.com/translatewiki) | [
neslihanturan](https://github.com/neslihanturan) | [
yuvipanda](https://github.com/yuvipanda) | [
nicolas-raoul](https://github.com/nicolas-raoul) | | :---: | :---: | :---: | :---: | :---: | -| [
nicolas-raoul](https://github.com/nicolas-raoul) | [
psh](https://github.com/psh) | [
brion](https://github.com/brion) | [
whym](https://github.com/whym) | [
domdomegg](https://github.com/domdomegg) | -| [
akaita](https://github.com/akaita) | [
veyndan](https://github.com/veyndan) | [
ujjwalagrawal17](https://github.com/ujjwalagrawal17) | [
dbrant](https://github.com/dbrant) | [
sandarumk](https://github.com/sandarumk) | -| [
tobias47n9e](https://github.com/tobias47n9e) | [
tanvidadu](https://github.com/tanvidadu) | [
hismaeel](https://github.com/hismaeel) | [
tshradheya](https://github.com/tshradheya) | [
diddypod](https://github.com/diddypod) | -| [
prxjeen](https://github.com/prxjeen) | [
addshore](https://github.com/addshore) | [
knight-shade](https://github.com/knight-shade) | [
ashishkumar468](https://github.com/ashishkumar468) | [
siebrand](https://github.com/siebrand) | -| [
Bluesir9](https://github.com/Bluesir9) | [
Jatin0312](https://github.com/Jatin0312) | [
mashawan](https://github.com/mashawan) | [
ford-prefect](https://github.com/ford-prefect) | [
seantnemann](https://github.com/seantnemann) | +| [
domdomegg](https://github.com/domdomegg) | [
maskaravivek](https://github.com/maskaravivek) | [
psh](https://github.com/psh) | [
brion](https://github.com/brion) | [
ashishkumar468](https://github.com/ashishkumar468) | +| [
whym](https://github.com/whym) | [
akaita](https://github.com/akaita) | [
madhurgupta10](https://github.com/madhurgupta10) | [
veyndan](https://github.com/veyndan) | [
ujjwalagrawal17](https://github.com/ujjwalagrawal17) | +| [
macgills](https://github.com/macgills) | [
dbrant](https://github.com/dbrant) | [
vanshikaarora](https://github.com/vanshikaarora) | [
sandarumk](https://github.com/sandarumk) | [
tanvidadu](https://github.com/tanvidadu) | +| [
cypherop](https://github.com/cypherop) | [
tobias47n9e](https://github.com/tobias47n9e) | [
hismaeel](https://github.com/hismaeel) | [
tshradheya](https://github.com/tshradheya) | [
addshore](https://github.com/addshore) | +| [
knight-shade](https://github.com/knight-shade) | [
siebrand](https://github.com/siebrand) | [
sivaraam](https://github.com/sivaraam) | [
Bluesir9](https://github.com/Bluesir9) | [
kbhardwaj123](https://github.com/kbhardwaj123) | .. and [many more](https://github.com/commons-app/apps-android-commons/graphs/contributors). diff --git a/app/build.gradle b/app/build.gradle index 3d1ab456f..4c562e7de 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,6 +28,7 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' implementation 'io.reactivex.rxjava2:rxjava:2.2.3' implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' + implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.1.1' implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.1.1' implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1' @@ -66,9 +67,11 @@ dependencies { implementation "com.squareup.okhttp3:logging-interceptor:$OKHTTP_VERSION" // Dependency injector + implementation "com.google.dagger:dagger-android:$DAGGER_VERSION" implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION" kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" + annotationProcessor "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION" @@ -76,28 +79,20 @@ dependencies { //Mocking testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' testImplementation 'org.mockito:mockito-inline:2.13.0' - testImplementation 'org.mockito:mockito-core:2.23.0' - testImplementation "org.powermock:powermock-module-junit4:2.0.0-beta.5" - testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5" + testImplementation 'org.mockito:mockito-core:2.25.1' + testImplementation "org.powermock:powermock-module-junit4:2.0.2" + testImplementation "org.powermock:powermock-api-mockito2:2.0.2" // Unit testing - testImplementation 'junit:junit:4.13.1' - testImplementation 'org.robolectric:robolectric:4.3' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.robolectric:robolectric:4.5.1' testImplementation 'androidx.test:core:1.3.0' testImplementation "com.squareup.okhttp3:mockwebserver:$OKHTTP_VERSION" - testImplementation "org.powermock:powermock-module-junit4:2.0.0-beta.5" - testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5" - testImplementation 'org.mockito:mockito-core:2.23.0' testImplementation "com.jraska.livedata:testing-ktx:1.1.2" testImplementation "androidx.arch.core:core-testing:2.1.0" testImplementation "org.junit.jupiter:junit-jupiter-api:5.3.1" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.3.1" - - // Mockito and PowerMock - androidTestCompile ('org.powermock:powermock-mockito-release-full:1.6.0') { - exclude module: 'hamcrest-core' - exclude module: 'objenesis' - } + testImplementation 'com.facebook.soloader:soloader:0.9.0' // Android testing androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" @@ -117,10 +112,10 @@ dependencies { // Support libraries implementation "com.google.android.material:material:1.1.0-alpha04" - implementation "androidx.browser:browser:1.0.0" + implementation "androidx.browser:browser:1.3.0" implementation "androidx.cardview:cardview:1.0.0" implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation "androidx.exifinterface:exifinterface:1.0.0" + implementation "androidx.exifinterface:exifinterface:1.3.2" implementation "androidx.core:core-ktx:$CORE_KTX_VERSION" implementation "androidx.multidex:multidex:2.0.1" @@ -143,6 +138,10 @@ dependencies { implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION" implementation "androidx.multidex:multidex:$MULTIDEX_VERSION" + + def work_version = "2.4.0" + // Kotlin + coroutines + implementation "androidx.work:work-runtime-ktx:$work_version" } android { @@ -150,6 +149,7 @@ android { defaultConfig { //applicationId 'fr.free.nrw.commons' + versionCode 1016 versionName '3.0.1' setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) diff --git a/app/src/androidTest/java/fr/free/nrw/commons/AchievementsActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/AchievementsActivityTest.kt index 6bded4351..16c27dd77 100644 --- a/app/src/androidTest/java/fr/free/nrw/commons/AchievementsActivityTest.kt +++ b/app/src/androidTest/java/fr/free/nrw/commons/AchievementsActivityTest.kt @@ -29,7 +29,6 @@ class AchievementsActivityTest { @Test fun testAchievements() { onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()) - onView(withId(R.id.user_icon)).perform(click()) Intents.intended(hasComponent(ProfileActivity::class.java.name)) } diff --git a/app/src/androidTest/java/fr/free/nrw/commons/CategoryImagesActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/CategoryImagesActivityTest.kt deleted file mode 100644 index 3b51cd85b..000000000 --- a/app/src/androidTest/java/fr/free/nrw/commons/CategoryImagesActivityTest.kt +++ /dev/null @@ -1,19 +0,0 @@ -package fr.free.nrw.commons - -import androidx.test.rule.ActivityTestRule -import androidx.test.runner.AndroidJUnit4 -import fr.free.nrw.commons.category.CategoryImagesActivity -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class CategoryImagesActivityTest { - @get:Rule - var activityRule = ActivityTestRule(CategoryImagesActivity::class.java) - - @Test - fun orientationChange() { - UITestHelper.changeOrientation(activityRule) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/fr/free/nrw/commons/LeaderboardActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/LeaderboardActivityTest.kt index 7f613fea0..5b6a2c255 100644 --- a/app/src/androidTest/java/fr/free/nrw/commons/LeaderboardActivityTest.kt +++ b/app/src/androidTest/java/fr/free/nrw/commons/LeaderboardActivityTest.kt @@ -45,7 +45,6 @@ class LeaderboardActivityTest { @Test fun testScrollToRankFromAbove() { Espresso.onView(ViewMatchers.withId(R.id.drawer_layout)).perform(DrawerActions.open()) - Espresso.onView(ViewMatchers.withId(R.id.user_icon)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(R.id.tab_layout)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(R.id.tab_layout)).perform(selectTabAtPosition(1)) @@ -58,7 +57,6 @@ class LeaderboardActivityTest { @Test fun testScrollToRankFromBelow() { Espresso.onView(ViewMatchers.withId(R.id.drawer_layout)).perform(DrawerActions.open()) - Espresso.onView(ViewMatchers.withId(R.id.user_icon)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(R.id.tab_layout)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(R.id.tab_layout)).perform(selectTabAtPosition(1)) diff --git a/app/src/androidTest/java/fr/free/nrw/commons/NavigationBaseActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/NavigationBaseActivityTest.kt deleted file mode 100644 index f78142c9f..000000000 --- a/app/src/androidTest/java/fr/free/nrw/commons/NavigationBaseActivityTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -package fr.free.nrw.commons - -import androidx.test.espresso.Espresso -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.contrib.DrawerActions -import androidx.test.espresso.contrib.NavigationViewActions -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.filters.LargeTest -import androidx.test.rule.ActivityTestRule -import androidx.test.runner.AndroidJUnit4 -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@LargeTest -@RunWith(AndroidJUnit4::class) -class NavigationBaseActivityTest { - @get:Rule - var activityRule: ActivityTestRule<*> = ActivityTestRule(AboutActivity::class.java) - - /** - * Goes through all the activities in the app and checks we don't crash - * NB: This is not realistic if we're not logged in; we can access 'home', 'notifications', 'settings' and 'achievements' which we wouldn't otherwise be able to. - */ - @Test - fun goThroughNavigationBaseActivityActivities() { - // Home - openNavigationDrawerAndNavigateTo(R.id.action_home) - - // Explore - openNavigationDrawerAndNavigateTo(R.id.action_explore) - - // Bookmarks - openNavigationDrawerAndNavigateTo(R.id.action_bookmarks) - - // Reviews - openNavigationDrawerAndNavigateTo(R.id.action_review) - - // Settings - openNavigationDrawerAndNavigateTo(R.id.action_settings) - - // About - openNavigationDrawerAndNavigateTo(R.id.action_about) - - // Tutorial - openNavigationDrawerAndNavigateTo(R.id.action_introduction) - Espresso.pressBack() - - // Achievements - openNavigationDrawerAndNavigateTo(R.id.action_login) - - // Feedback - openNavigationDrawerAndNavigateTo(R.id.action_feedback) - } - - private fun openNavigationDrawerAndNavigateTo(menuItemId: Int) { - onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()) - UITestHelper.sleep(500) - onView(withId(R.id.navigation_view)).perform(NavigationViewActions.navigateTo(menuItemId)) - } - - @Test - fun orientationChange() { - UITestHelper.changeOrientation(activityRule) - } -} \ No newline at end of file diff --git a/app/src/beta/res/xml/shortcuts.xml b/app/src/beta/res/xml/shortcuts.xml new file mode 100644 index 000000000..65a51995e --- /dev/null +++ b/app/src/beta/res/xml/shortcuts.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 81b9f7153..47d102fdc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -108,20 +108,16 @@ - - - - diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index f2fdfbd4f..48967e3ff 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -47,7 +47,9 @@ import io.reactivex.internal.functions.Functions; import io.reactivex.plugins.RxJavaPlugins; import io.reactivex.schedulers.Schedulers; import java.io.File; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; import javax.inject.Inject; import javax.inject.Named; @@ -127,7 +129,12 @@ public class CommonsApplication extends MultiDexApplication { @Inject ContributionDao contributionDao; - /** + /** + * In memory list of contributios whose uploads ahve been paused by the user + */ + public static Map pauseUploads = new HashMap<>(); + + /** * Used to declare and initialize various components and dependencies */ @Override diff --git a/app/src/main/java/fr/free/nrw/commons/Utils.java b/app/src/main/java/fr/free/nrw/commons/Utils.java index 5f75c86ba..587a65467 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -13,6 +13,7 @@ import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.browser.customtabs.CustomTabColorSchemeParams; import androidx.browser.customtabs.CustomTabsIntent; import androidx.core.content.ContextCompat; @@ -139,9 +140,13 @@ public class Utils { return; } + final CustomTabColorSchemeParams color = new CustomTabColorSchemeParams.Builder() + .setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor)) + .setSecondaryToolbarColor(ContextCompat.getColor(context, R.color.primaryDarkColor)) + .build(); + CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); - builder.setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor)); - builder.setSecondaryToolbarColor(ContextCompat.getColor(context, R.color.primaryDarkColor)); + builder.setDefaultColorSchemeParams(color); builder.setExitAnimations(context, android.R.anim.slide_in_left, android.R.anim.slide_out_right); CustomTabsIntent customTabsIntent = builder.build(); // Clear previous browser tasks, so that back/exit buttons work as intended. diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ViewPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.java old mode 100755 new mode 100644 similarity index 94% rename from app/src/main/java/fr/free/nrw/commons/explore/ViewPagerAdapter.java rename to app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.java index 51bf77d2b..5ca20372a --- a/app/src/main/java/fr/free/nrw/commons/explore/ViewPagerAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.java @@ -1,57 +1,57 @@ -package fr.free.nrw.commons.explore; - -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentPagerAdapter; - -import java.util.ArrayList; -import java.util.List; - -/** - * This adapter will be used to display fragments in a ViewPager - */ -public class ViewPagerAdapter extends FragmentPagerAdapter { - private List fragmentList = new ArrayList<>(); - private List fragmentTitleList = new ArrayList<>(); - - public ViewPagerAdapter(FragmentManager manager) { - super(manager); - } - - /** - * This method returns the fragment of the viewpager at a particular position - * @param position - */ - @Override - public Fragment getItem(int position) { - return fragmentList.get(position); - } - - /** - * This method returns the total number of fragments in the viewpager. - * @return size - */ - @Override - public int getCount() { - return fragmentList.size(); - } - - /** - * This method sets the fragment and title list in the viewpager - * @param fragmentList List of all fragments to be displayed in the viewpager - * @param fragmentTitleList List of all titles of the fragments - */ - public void setTabData(List fragmentList, List fragmentTitleList) { - this.fragmentList = fragmentList; - this.fragmentTitleList = fragmentTitleList; - } - - /** - * This method returns the title of the page at a particular position - * @param position - */ - @Override - public CharSequence getPageTitle(int position) { - return fragmentTitleList.get(position); - } -} +package fr.free.nrw.commons; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; + +import java.util.ArrayList; +import java.util.List; + +/** + * This adapter will be used to display fragments in a ViewPager + */ +public class ViewPagerAdapter extends FragmentPagerAdapter { + private List fragmentList = new ArrayList<>(); + private List fragmentTitleList = new ArrayList<>(); + + public ViewPagerAdapter(FragmentManager manager) { + super(manager); + } + + /** + * This method returns the fragment of the viewpager at a particular position + * @param position + */ + @Override + public Fragment getItem(int position) { + return fragmentList.get(position); + } + + /** + * This method returns the total number of fragments in the viewpager. + * @return size + */ + @Override + public int getCount() { + return fragmentList.size(); + } + + /** + * This method sets the fragment and title list in the viewpager + * @param fragmentList List of all fragments to be displayed in the viewpager + * @param fragmentTitleList List of all titles of the fragments + */ + public void setTabData(List fragmentList, List fragmentTitleList) { + this.fragmentList = fragmentList; + this.fragmentTitleList = fragmentTitleList; + } + + /** + * This method returns the title of the page at a particular position + * @param position + */ + @Override + public CharSequence getPageTitle(int position) { + return fragmentTitleList.get(position); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.java b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.java deleted file mode 100644 index a2d7bd543..000000000 --- a/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.java +++ /dev/null @@ -1,80 +0,0 @@ -package fr.free.nrw.commons.actions; - -import io.reactivex.Single; -import io.reactivex.SingleOnSubscribe; -import org.wikipedia.csrf.CsrfTokenClient; -import org.wikipedia.dataclient.Service; - -import io.reactivex.Observable; - -/** - * This class acts as a Client to facilitate wiki page editing - * services to various dependency providing modules such as the Network module, the Review Controller ,etc - * - * The methods provided by this class will post to the Media wiki api - * documented at: https://commons.wikimedia.org/w/api.php?action=help&modules=edit - */ -public class PageEditClient { - - private final CsrfTokenClient csrfTokenClient; - private final PageEditInterface pageEditInterface; - private final Service service; - - public PageEditClient(CsrfTokenClient csrfTokenClient, - PageEditInterface pageEditInterface, - Service service) { - this.csrfTokenClient = csrfTokenClient; - this.pageEditInterface = pageEditInterface; - this.service = service; - } - - /** - * This method is used when the content of the page is to be replaced by new content received - * @param pagetitle Title of the page to edit - * @param text Holds the page content - * @param summary Edit summary - */ - public Observable edit(String pageTitle, String text, String summary) { - try { - return pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking()) - .map(editResponse -> editResponse.edit().editSucceeded()); - } catch (Throwable throwable) { - return Observable.just(false); - } - } - - /** - * This method is used when we need to append something to the end of wiki page content - * @param pagetitle Title of the page to edit - * @param appendText The received page content is added to beginning of the page - * @param summary Edit summary - */ - public Observable appendEdit(String pageTitle, String appendText, String summary) { - return Single.create((SingleOnSubscribe) emitter -> { - try { - emitter.onSuccess(csrfTokenClient.getTokenBlocking()); - } catch (Throwable throwable) { - emitter.onError(throwable); - throwable.printStackTrace(); - } - }).flatMapObservable(token -> pageEditInterface.postAppendEdit(pageTitle, summary, appendText, token) - .map(editResponse -> editResponse.edit().editSucceeded())); - - } - - /** - * This method is used when we need to add something to the starting of the page - * @param pagetitle Title of the page to edit - * @param prependText The received page content is added to beginning of the page - * @param summary Edit summary - */ - public Observable prependEdit(String pageTitle, String prependText, String summary) { - try { - return pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking()) - .map(editResponse -> editResponse.edit().editSucceeded()); - } catch (Throwable throwable) { - return Observable.just(false); - } - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt new file mode 100644 index 000000000..0ae7e42de --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt @@ -0,0 +1,65 @@ +package fr.free.nrw.commons.actions + +import io.reactivex.Observable +import org.wikipedia.csrf.CsrfTokenClient + +/** + * This class acts as a Client to facilitate wiki page editing + * services to various dependency providing modules such as the Network module, the Review Controller ,etc + * + * The methods provided by this class will post to the Media wiki api + * documented at: https://commons.wikimedia.org/w/api.php?action=help&modules=edit + */ +class PageEditClient( + private val csrfTokenClient: CsrfTokenClient, + private val pageEditInterface: PageEditInterface +) { + + /** + * Replace the content of a wiki page + * @param pageTitle Title of the page to edit + * @param text Holds the page content + * @param summary Edit summary + * @return whether the edit was successful + */ + fun edit(pageTitle: String, text: String, summary: String): Observable { + return try { + pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.tokenBlocking) + .map { editResponse -> editResponse.edit()!!.editSucceeded() } + } catch (throwable: Throwable) { + Observable.just(false) + } + } + + /** + * Append text to the end of a wiki page + * @param pageTitle Title of the page to edit + * @param appendText The received page content is added to the end of the page + * @param summary Edit summary + * @return whether the edit was successful + */ + fun appendEdit(pageTitle: String, appendText: String, summary: String): Observable { + return try { + pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.tokenBlocking) + .map { editResponse -> editResponse.edit()!!.editSucceeded() } + } catch (throwable: Throwable) { + Observable.just(false) + } + } + + /** + * Prepend text to the beginning of a wiki page + * @param pageTitle Title of the page to edit + * @param prependText The received page content is added to the beginning of the page + * @param summary Edit summary + * @return whether the edit was successful + */ + fun prependEdit(pageTitle: String, prependText: String, summary: String): Observable { + return try { + pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.tokenBlocking) + .map { editResponse -> editResponse.edit()!!.editSucceeded() } + } catch (throwable: Throwable) { + Observable.just(false) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.java b/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.java deleted file mode 100644 index 8f4faf01f..000000000 --- a/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.java +++ /dev/null @@ -1,76 +0,0 @@ -package fr.free.nrw.commons.actions; - -import androidx.annotation.NonNull; - -import org.wikipedia.edit.Edit; - -import io.reactivex.Observable; -import retrofit2.http.Field; -import retrofit2.http.FormUrlEncoded; -import retrofit2.http.Headers; -import retrofit2.http.POST; - -import static org.wikipedia.dataclient.Service.MW_API_PREFIX; - -/** - * This interface facilitates wiki commons page editing services to the Networking module - * which provides all network related services used by the app. - * - * This interface posts a form encoded request to the wikimedia API - * with editing action as argument to edit a particular page - */ -public interface PageEditInterface { - - /** - * This method posts such that the Content which the page - * has will be completely replaced by the value being passed to the - * "text" field of the encoded form data - * @param title Title of the page to edit. Cannot be used together with pageid. - * @param summary Edit summary. Also section title when section=new and sectiontitle is not set - * @param text Holds the page content - * @param token A "csrf" token - */ - @FormUrlEncoded - @Headers("Cache-Control: no-cache") - @POST(MW_API_PREFIX + "action=edit") - @NonNull - Observable postEdit(@NonNull @Field("title") String title, - @NonNull @Field("summary") String summary, - @NonNull @Field("text") String text, - // NOTE: This csrf shold always be sent as the last field of form data - @NonNull @Field("token") String token); - - /** - * This method posts such that the Content which the page - * has will be completely replaced by the value being passed to the - * "text" field of the encoded form data - * @param title Title of the page to edit. Cannot be used together with pageid. - * @param summary Edit summary. Also section title when section=new and sectiontitle is not set - * @param text The received page content is added to beginning of the page - * @param token A "csrf" token - */ - @FormUrlEncoded - @Headers("Cache-Control: no-cache") - @POST(MW_API_PREFIX + "action=edit") - @NonNull Observable postAppendEdit(@NonNull @Field("title") String title, - @NonNull @Field("summary") String summary, - @NonNull @Field("appendtext") String text, - @NonNull @Field("token") String token); - - /** - * This method posts such that the Content which the page - * has will be completely replaced by the value being passed to the - * "text" field of the encoded form data - * @param title Title of the page to edit. Cannot be used together with pageid. - * @param summary Edit summary. Also section title when section=new and sectiontitle is not set - * @param text The received page content is added to beginning of the page - * @param token A "csrf" token - */ - @FormUrlEncoded - @Headers("Cache-Control: no-cache") - @POST(MW_API_PREFIX + "action=edit") - @NonNull Observable postPrependEdit(@NonNull @Field("title") String title, - @NonNull @Field("summary") String summary, - @NonNull @Field("prependtext") String text, - @NonNull @Field("token") String token); -} diff --git a/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.kt b/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.kt new file mode 100644 index 000000000..8b27d1288 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.kt @@ -0,0 +1,76 @@ +package fr.free.nrw.commons.actions + +import io.reactivex.Observable +import org.wikipedia.dataclient.Service +import org.wikipedia.edit.Edit +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.Headers +import retrofit2.http.POST + +/** + * This interface facilitates wiki commons page editing services to the Networking module + * which provides all network related services used by the app. + * + * This interface posts a form encoded request to the wikimedia API + * with editing action as argument to edit a particular page + */ +interface PageEditInterface { + /** + * This method posts such that the Content which the page + * has will be completely replaced by the value being passed to the + * "text" field of the encoded form data + * @param title Title of the page to edit. Cannot be used together with pageid. + * @param summary Edit summary. Also section title when section=new and sectiontitle is not set + * @param text Holds the page content + * @param token A "csrf" token + */ + @FormUrlEncoded + @Headers("Cache-Control: no-cache") + @POST(Service.MW_API_PREFIX + "action=edit") + fun postEdit( + @Field("title") title: String, + @Field("summary") summary: String, + @Field("text") text: String, + // NOTE: This csrf shold always be sent as the last field of form data + @Field("token") token: String + ): Observable + + /** + * This method posts such that the Content which the page + * has will be appended with the value being passed to the + * "appendText" field of the encoded form data + * @param title Title of the page to edit. Cannot be used together with pageid. + * @param summary Edit summary. Also section title when section=new and sectiontitle is not set + * @param appendText Text to add to the end of the page + * @param token A "csrf" token + */ + @FormUrlEncoded + @Headers("Cache-Control: no-cache") + @POST(Service.MW_API_PREFIX + "action=edit") + fun postAppendEdit( + @Field("title") title: String, + @Field("summary") summary: String, + @Field("appendtext") appendText: String, + @Field("token") token: String + ): Observable + + /** + * This method posts such that the Content which the page + * has will be prepended with the value being passed to the + * "prependText" field of the encoded form data + * @param title Title of the page to edit. Cannot be used together with pageid. + * @param summary Edit summary. Also section title when section=new and sectiontitle is not set + * @param prependText Text to add to the beginning of the page + * @param token A "csrf" token + */ + @FormUrlEncoded + @Headers("Cache-Control: no-cache") + @POST(Service.MW_API_PREFIX + "action=edit") + fun postPrependEdit( + @Field("title") title: String, + @Field("summary") summary: String, + @Field("prependtext") prependText: String, + @Field("token") token: String + ): Observable +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.java b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.java deleted file mode 100644 index aa606a93a..000000000 --- a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.java +++ /dev/null @@ -1,48 +0,0 @@ -package fr.free.nrw.commons.actions; - -import org.wikipedia.csrf.CsrfTokenClient; -import org.wikipedia.dataclient.Service; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - -import fr.free.nrw.commons.CommonsApplication; -import io.reactivex.Observable; - -/** - * Facilitates the Wkikimedia Thanks api extention, as described in the - * api documentation: "The Thanks extension includes an API for sending thanks" - * - * In simple terms this class is used by a user to thank someone for adding - * contribution to the commons platform - */ -@Singleton -public class ThanksClient { - - private final CsrfTokenClient csrfTokenClient; - private final Service service; - - @Inject - public ThanksClient(@Named("commons-csrf") CsrfTokenClient csrfTokenClient, - @Named("commons-service") Service service) { - this.csrfTokenClient = csrfTokenClient; - this.service = service; - } - - /** - * Handles the Thanking logic - * @param revesionID The revision ID you would like to thank someone for - * @return if thanks was successfully sent to intended recepient, returned as a boolean observable - */ - public Observable thank(long revisionId) { - try { - return service.thank(String.valueOf(revisionId), null, - csrfTokenClient.getTokenBlocking(), - CommonsApplication.getInstance().getUserAgent()) - .map(mwQueryResponse -> mwQueryResponse.getSuccessVal() == 1); - } catch (Throwable throwable) { - return Observable.just(false); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt new file mode 100644 index 000000000..d0ff6629c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.actions + +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF +import io.reactivex.Observable +import org.wikipedia.csrf.CsrfTokenClient +import org.wikipedia.dataclient.Service +import org.wikipedia.dataclient.mwapi.MwPostResponse +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +/** + * Client for the Wkikimedia Thanks API extension + * Thanks are used by a user to show gratitude to another user for their contributions + */ +@Singleton +class ThanksClient @Inject constructor( + @param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient, + @param:Named("commons-service") private val service: Service +) { + /** + * Thanks a user for a particular revision + * @param revisionId The revision ID the user would like to thank someone for + * @return if thanks was successfully sent to intended recipient + */ + fun thank(revisionId: Long): Observable { + return try { + service.thank(revisionId.toString(), null, csrfTokenClient.tokenBlocking, CommonsApplication.getInstance().userAgent) + .map { mwThankPostResponse -> mwThankPostResponse.result.success== 1 } + } catch (throwable: Throwable) { + Observable.just(false) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index 8b260baa6..64563ca0e 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -30,6 +30,7 @@ import androidx.core.content.ContextCompat; import com.google.android.material.textfield.TextInputLayout; import fr.free.nrw.commons.utils.ActivityUtils; +import java.util.Locale; import org.wikipedia.AppAdapter; import org.wikipedia.dataclient.ServiceFactory; import org.wikipedia.dataclient.WikiSite; @@ -265,7 +266,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { public void onResponse(Call call, Response response) { loginClient.login(commonsWikiSite, username, password, null, twoFactorCode, - response.body().query().loginToken(), new LoginCallback() { + response.body().query().loginToken(), Locale.getDefault().getLanguage(), new LoginCallback() { @Override public void success(@NonNull LoginResult result) { Timber.d("Login Success"); @@ -338,6 +339,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { // no longer attached to activity! return; } + compositeDisposable.clear(); sessionManager.setUserLoggedIn(true); AppAdapter.get().updateAccount(loginResult); progressDialog.dismiss(); diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java index 9861a3007..a7905f8ea 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java @@ -136,4 +136,14 @@ public class SessionManager { currentAccount = null; }); } + + /** + * Return a corresponding boolean preference + * + * @param key + * @return + */ + public boolean getPreference(String key) { + return defaultKvStore.getBoolean(key); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.java index c7738ddc1..c3b2604ec 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkFragment.java @@ -4,35 +4,33 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentManager; -import androidx.viewpager.widget.ViewPager; import com.google.android.material.tabs.TabLayout; import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.explore.ParentViewPager; +import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.theme.BaseActivity; import javax.inject.Inject; import butterknife.BindView; import butterknife.ButterKnife; -import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.category.CategoryImagesCallback; import fr.free.nrw.commons.contributions.ContributionController; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; +import javax.inject.Named; public class BookmarkFragment extends CommonsDaggerSupportFragment { private FragmentManager supportFragmentManager; private BookmarksPagerAdapter adapter; @BindView(R.id.viewPagerBookmarks) - ViewPager viewPager; + ParentViewPager viewPager; @BindView(R.id.tab_layout) TabLayout tabLayout; @BindView(R.id.fragmentContainer) @@ -40,6 +38,13 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { @Inject ContributionController controller; + /** + * To check if the user is loggedIn or not. + */ + @Inject + @Named("default_preferences") + public + JsonKvStore applicationKvStore; @NonNull public static BookmarkFragment newInstance() { @@ -48,6 +53,10 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { return fragment; } + public void setScroll(boolean canScroll){ + viewPager.setCanScroll(canScroll); + } + @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -66,15 +75,36 @@ public class BookmarkFragment extends CommonsDaggerSupportFragment { // reference to the Fragment from FragmentManager, using findFragmentById() supportFragmentManager = getChildFragmentManager(); - adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext()); + adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(), + applicationKvStore.getBoolean("login_skipped")); viewPager.setAdapter(adapter); tabLayout.setupWithViewPager(viewPager); + + ((MainActivity)getActivity()).showTabs(); + ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); + + setupTabLayout(); return view; } + /** + * This method sets up the tab layout. + * If the adapter has only one element it sets the visibility of tabLayout to gone. + */ + public void setupTabLayout(){ + tabLayout.setVisibility(View.VISIBLE); + if (adapter.getCount() == 1) { + tabLayout.setVisibility(View.GONE); + } + } + + public void onBackPressed() { - ((BookmarkListRootFragment) (adapter.getItem(tabLayout.getSelectedTabPosition()))) - .backPressed(); + if(((BookmarkListRootFragment)(adapter.getItem(tabLayout.getSelectedTabPosition()))).backPressed()) { + // The event is handled internally by the adapter , no further action required. + return; + } + // Event is not handled by the adapter ( performed back action ) change action bar. ((BaseActivity)getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); } } diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java index 3ee03218a..1202a6bef 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java @@ -19,10 +19,13 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; import fr.free.nrw.commons.category.CategoryImagesCallback; +import fr.free.nrw.commons.category.GridViewAdapter; import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.navtab.NavTab; +import java.util.ArrayList; +import java.util.Iterator; public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements FragmentManager.OnBackStackChangedListener, @@ -166,6 +169,22 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple return null; } + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + @Override + public void refreshNominatedMedia(int index) { + if(mediaDetails != null && !listFragment.isVisible()) { + removeFragment(mediaDetails); + mediaDetails = new MediaDetailPagerFragment(false, true); + ((BookmarkFragment) getParentFragment()).setScroll(false); + setFragment(mediaDetails, listFragment); + mediaDetails.showImage(index); + } + } + /** * This method is called on success of API call for featured images or mobile uploads. The * viewpager will notified that number of items have changed. @@ -189,12 +208,17 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple ((MainActivity) getActivity()).showTabs(); } + void moveToContributionsFragment(){ + ((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code()); + ((MainActivity) getActivity()).showTabs(); + } @Override public void onItemClick(AdapterView parent, View view, int position, long id) { Log.d("deneme8","on media clicked"); container.setVisibility(View.VISIBLE); ((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE); mediaDetails = new MediaDetailPagerFragment(false, true); + ((BookmarkFragment) getParentFragment()).setScroll(false); setFragment(mediaDetails, listFragment); mediaDetails.showImage(position); } diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java index 1f5529105..1c308d365 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java @@ -19,7 +19,14 @@ public class BookmarksPagerAdapter extends FragmentPagerAdapter { private ArrayList pages; - BookmarksPagerAdapter(FragmentManager fm, Context context) { + /** + * Default Constructor + * @param fm + * @param context + * @param onlyPictures is true if the fragment requires only BookmarkPictureFragment + * (i.e. when no user is logged in). + */ + BookmarksPagerAdapter(FragmentManager fm, Context context,boolean onlyPictures) { super(fm); pages = new ArrayList<>(); Bundle picturesBundle = new Bundle(); @@ -28,13 +35,16 @@ public class BookmarksPagerAdapter extends FragmentPagerAdapter { pages.add(new BookmarkPages( new BookmarkListRootFragment(picturesBundle, this), context.getString(R.string.title_page_bookmarks_pictures))); - - Bundle locationBundle = new Bundle(); - locationBundle.putString("categoryName", context.getString(R.string.title_page_bookmarks_locations)); - locationBundle.putInt("order", 1); - pages.add(new BookmarkPages( - new BookmarkListRootFragment(locationBundle, this), - context.getString(R.string.title_page_bookmarks_locations))); + if (!onlyPictures) { + // if onlyPictures is false we also add the location fragment. + Bundle locationBundle = new Bundle(); + locationBundle.putString("categoryName", + context.getString(R.string.title_page_bookmarks_locations)); + locationBundle.putInt("order", 1); + pages.add(new BookmarkPages( + new BookmarkListRootFragment(locationBundle, this), + context.getString(R.string.title_page_bookmarks_locations))); + } notifyDataSetChanged(); } diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java index ff97b5119..e1a8b5b5c 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java @@ -157,6 +157,7 @@ public class BookmarkLocationsDao { builder.setCommonsLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_COMMONS_LINK))); return new Place( + cursor.getString(cursor.getColumnIndex(Table.COLUMN_LANGUAGE)), cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)), Label.fromText((cursor.getString(cursor.getColumnIndex(Table.COLUMN_LABEL_TEXT)))), cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)), @@ -164,13 +165,14 @@ public class BookmarkLocationsDao { cursor.getString(cursor.getColumnIndex(Table.COLUMN_CATEGORY)), builder.build(), cursor.getString(cursor.getColumnIndex(Table.COLUMN_PIC)), - cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESTROYED)) + Boolean.parseBoolean(cursor.getString(cursor.getColumnIndex(Table.COLUMN_EXISTS))) ); } private ContentValues toContentValues(Place bookmarkLocation) { ContentValues cv = new ContentValues(); cv.put(BookmarkLocationsDao.Table.COLUMN_NAME, bookmarkLocation.getName()); + cv.put(BookmarkLocationsDao.Table.COLUMN_LANGUAGE, bookmarkLocation.getLanguage()); cv.put(BookmarkLocationsDao.Table.COLUMN_DESCRIPTION, bookmarkLocation.getLongDescription()); cv.put(BookmarkLocationsDao.Table.COLUMN_CATEGORY, bookmarkLocation.getCategory()); cv.put(BookmarkLocationsDao.Table.COLUMN_LABEL_TEXT, bookmarkLocation.getLabel().getText()); @@ -181,7 +183,7 @@ public class BookmarkLocationsDao { cv.put(BookmarkLocationsDao.Table.COLUMN_LAT, bookmarkLocation.location.getLatitude()); cv.put(BookmarkLocationsDao.Table.COLUMN_LONG, bookmarkLocation.location.getLongitude()); cv.put(BookmarkLocationsDao.Table.COLUMN_PIC, bookmarkLocation.pic); - cv.put(BookmarkLocationsDao.Table.COLUMN_DESTROYED, bookmarkLocation.destroyed); + cv.put(BookmarkLocationsDao.Table.COLUMN_EXISTS, bookmarkLocation.exists.toString()); return cv; } @@ -189,6 +191,7 @@ public class BookmarkLocationsDao { public static final String TABLE_NAME = "bookmarksLocations"; static final String COLUMN_NAME = "location_name"; + static final String COLUMN_LANGUAGE = "location_language"; static final String COLUMN_DESCRIPTION = "location_description"; static final String COLUMN_LAT = "location_lat"; static final String COLUMN_LONG = "location_long"; @@ -200,11 +203,12 @@ public class BookmarkLocationsDao { static final String COLUMN_WIKIDATA_LINK = "location_wikidata_link"; static final String COLUMN_COMMONS_LINK = "location_commons_link"; static final String COLUMN_PIC = "location_pic"; - static final String COLUMN_DESTROYED = "location_destroyed"; + static final String COLUMN_EXISTS = "location_exists"; // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. public static final String[] ALL_FIELDS = { COLUMN_NAME, + COLUMN_LANGUAGE, COLUMN_DESCRIPTION, COLUMN_CATEGORY, COLUMN_LABEL_TEXT, @@ -216,13 +220,14 @@ public class BookmarkLocationsDao { COLUMN_WIKIDATA_LINK, COLUMN_COMMONS_LINK, COLUMN_PIC, - COLUMN_DESTROYED + COLUMN_EXISTS }; static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" + COLUMN_NAME + " STRING PRIMARY KEY," + + COLUMN_LANGUAGE + " STRING," + COLUMN_DESCRIPTION + " STRING," + COLUMN_CATEGORY + " STRING," + COLUMN_LABEL_TEXT + " STRING," @@ -234,7 +239,7 @@ public class BookmarkLocationsDao { + COLUMN_WIKIDATA_LINK + " STRING," + COLUMN_COMMONS_LINK + " STRING," + COLUMN_PIC + " STRING," - + COLUMN_DESTROYED + " STRING" + + COLUMN_EXISTS + " STRING" + ");"; public static void onCreate(SQLiteDatabase db) { @@ -287,6 +292,20 @@ public class BookmarkLocationsDao { Timber.e(exception); } } + if (from == 13){ + try { + db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_language STRING;"); + } catch (SQLiteException exception){ + Timber.e(exception); + } + } + if (from == 14){ + try { + db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_exists STRING;"); + } catch (SQLiteException exception){ + Timber.e(exception); + } + } } } } diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java index f644d75e6..fed6617b4 100644 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java @@ -57,8 +57,8 @@ public class CampaignView extends SwipableCardView { @Override public boolean onSwipe(View view) { view.setVisibility(View.GONE); - /*((MainActivity) getContext()).defaultKvStore - .putBoolean("displayCampaignsCardView", false);*/ + ((MainActivity) getContext()).defaultKvStore + .putBoolean("displayCampaignsCardView", false); ViewUtil.showLongToast(getContext(), getResources().getString(R.string.nearby_campaign_dismiss_message)); return true; diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.java index 87704bf35..3dd3bce52 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.java @@ -11,6 +11,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.FrameLayout; +import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.viewpager.widget.ViewPager; @@ -20,7 +21,7 @@ import com.google.android.material.tabs.TabLayout; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.explore.ViewPagerAdapter; +import fr.free.nrw.commons.ViewPagerAdapter; import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment; import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment; import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment; @@ -47,7 +48,7 @@ public class CategoryDetailsActivity extends BaseActivity @BindView(R.id.mediaContainer) FrameLayout mediaContainer; @BindView(R.id.tab_layout) TabLayout tabLayout; @BindView(R.id.viewPager) ViewPager viewPager; - + @BindView(R.id.toolbar) Toolbar toolbar; ViewPagerAdapter viewPagerAdapter; @Override @@ -60,6 +61,8 @@ public class CategoryDetailsActivity extends BaseActivity viewPager.setAdapter(viewPagerAdapter); viewPager.setOffscreenPageLimit(2); tabLayout.setupWithViewPager(viewPager); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); setTabs(); setPageTitle(); } @@ -162,6 +165,19 @@ public class CategoryDetailsActivity extends BaseActivity return null; } + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + @Override + public void refreshNominatedMedia(int index) { + if (getSupportFragmentManager().getBackStackEntryCount() == 1) { + onBackPressed(); + onMediaClicked(index); + } + } + /** * This method inflates the menu in the toolbar */ @@ -185,6 +201,9 @@ public class CategoryDetailsActivity extends BaseActivity PageTitle title = Utils.getPageTitle(CATEGORY_PREFIX + categoryName); Utils.handleWebUrl(this, Uri.parse(title.getCanonicalUri())); return true; + case android.R.id.home: + onBackPressed(); + return true; default: return super.onOptionsItemSelected(item); } @@ -197,7 +216,12 @@ public class CategoryDetailsActivity extends BaseActivity @Override public void onBackPressed() { if (supportFragmentManager.getBackStackEntryCount() == 1){ - // back to search so show search toolbar and hide navigation toolbar + + // the back press is handled by the mediaDetails , no further action required. + if(mediaDetails.backButtonClicked()){ + return; + } + tabLayout.setVisibility(View.VISIBLE); viewPager.setVisibility(View.VISIBLE); mediaContainer.setVisibility(View.GONE); diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java deleted file mode 100644 index de4fa1ebc..000000000 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java +++ /dev/null @@ -1,197 +0,0 @@ -package fr.free.nrw.commons.category; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.AdapterView; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; -import butterknife.ButterKnife; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.explore.SearchActivity; -import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.ActivityUtils; - -/** - * This activity displays pictures of a particular category - * Its generic and simply takes the name of category name in its start intent to load all images in - * a particular category. This activity is currently being used to display a list of featured images, - * which is nothing but another category on wikimedia commons. - */ - -public class CategoryImagesActivity - extends BaseActivity - implements FragmentManager.OnBackStackChangedListener, - MediaDetailPagerFragment.MediaDetailProvider, - AdapterView.OnItemClickListener, CategoryImagesCallback { - - - private FragmentManager supportFragmentManager; - private CategoriesMediaFragment categoriesMediaFragment; - private MediaDetailPagerFragment mediaDetails; - - /** - * This method is called on backPressed of anyFragment in the activity. - * We are changing the icon here from back to hamburger icon. - */ - @Override - public void onBackPressed() { - super.onBackPressed(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_category_images); - ButterKnife.bind(this); - - // Activity can call methods in the fragment by acquiring a - // reference to the Fragment from FragmentManager, using findFragmentById() - supportFragmentManager = getSupportFragmentManager(); - setCategoryImagesFragment(); - supportFragmentManager.addOnBackStackChangedListener(this); - setPageTitle(); - } - - /** - * Gets the categoryName from the intent and initializes the fragment for showing images of that category - */ - private void setCategoryImagesFragment() { - categoriesMediaFragment = new CategoriesMediaFragment(); - String categoryName = getIntent().getStringExtra("categoryName"); - if (getIntent() != null && categoryName != null) { - Bundle arguments = new Bundle(); - arguments.putString("categoryName", categoryName); - categoriesMediaFragment.setArguments(arguments); - FragmentTransaction transaction = supportFragmentManager.beginTransaction(); - transaction - .add(R.id.fragmentContainer, categoriesMediaFragment) - .commit(); - } - } - - /** - * Gets the passed title from the intents and displays it as the page title - */ - private void setPageTitle() { - if (getIntent() != null && getIntent().getStringExtra("title") != null) { - setTitle(getIntent().getStringExtra("title")); - } - } - - @Override - public void onBackStackChanged() { - } - - /** - * This method is called onClick of media inside category details (CategoryImageListFragment). - */ - @Override - public void onItemClick(AdapterView adapterView, View view, int i, long l) { - if (mediaDetails == null || !mediaDetails.isVisible()) { - // set isFeaturedImage true for featured images, to include author field on media detail - mediaDetails = new MediaDetailPagerFragment(false, true); - FragmentManager supportFragmentManager = getSupportFragmentManager(); - supportFragmentManager - .beginTransaction() - .hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount())) - .add(R.id.fragmentContainer, mediaDetails) - .addToBackStack(null) - .commit(); - // Reason for using hide, add instead of replace is to maintain scroll position after - // coming back to the search activity. See https://github.com/commons-app/apps-android-commons/issues/1631 - // https://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack/19022550#19022550 supportFragmentManager.executePendingTransactions(); - } - mediaDetails.showImage(i); - } - - /** - * Consumers should be simply using this method to use this activity. - * @param context A Context of the application package implementing this class. - * @param title Page title - * @param categoryName Name of the category for displaying its images - */ - public static void startYourself(Context context, String title, String categoryName) { - Intent intent = new Intent(context, CategoryImagesActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtra("title", title); - intent.putExtra("categoryName", categoryName); - context.startActivity(intent); - } - - /** - * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index - * @param i It is the index of which media object is to be returned which is same as - * current index of viewPager. - * @return Media Object - */ - @Override - public Media getMediaAtPosition(int i) { - return categoriesMediaFragment.getMediaAtPosition(i); - } - - /** - * This method is called on success of API call for featured Images. - * The viewpager will notified that number of items have changed. - */ - @Override - public void viewPagerNotifyDataSetChanged() { - if (mediaDetails!=null){ - mediaDetails.notifyDataSetChanged(); - } - } - - /** - * This method is called on from getCount of MediaDetailPagerFragment - * The viewpager will contain same number of media items as that of media elements in adapter. - * @return Total Media count in the adapter - */ - @Override - public int getTotalMediaCount() { - return categoriesMediaFragment.getTotalMediaCount(); - } - - @Override - public Integer getContributionStateAt(int position) { - return null; - } - - /** - * This method inflates the menu in the toolbar - */ - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_search, menu); - return super.onCreateOptionsMenu(menu); - } - - /** - * This method handles the logic on ItemSelect in toolbar menu - * Currently only 1 choice is available to open search page of the app - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - - // Handle item selection - switch (item.getItemId()) { - case R.id.action_search: - ActivityUtils.startActivityWithFlags(this, SearchActivity.class); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - public void onMediaClicked(int position) { - // this class is unused and will be deleted - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/category/ContinuationClient.kt b/app/src/main/java/fr/free/nrw/commons/category/ContinuationClient.kt index cbf9017b2..a85eee79d 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/ContinuationClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/ContinuationClient.kt @@ -38,4 +38,15 @@ abstract class ContinuationClient { continuationStore.remove("$prefix$category") } + /** + * Remove the existing the key from continuationExists and continuationStore + * + * @param prefix + * @param userName the username + */ + protected fun resetUserContinuation(prefix: String, userName: String) { + continuationExists.remove("$prefix$userName") + continuationStore.remove("$prefix$userName") + } + } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ChunkInfo.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ChunkInfo.kt index 8312820d9..0ef4066a2 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ChunkInfo.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ChunkInfo.kt @@ -5,7 +5,7 @@ import android.os.Parcelable import fr.free.nrw.commons.upload.UploadResult data class ChunkInfo( - val uploadResult: UploadResult, + val uploadResult: UploadResult?, val indexOfNextChunkToUpload: Int, val totalChunks: Int ) : Parcelable { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt index 613eb7046..00f002176 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt @@ -27,6 +27,9 @@ class ContributionBoundaryCallback @Inject constructor( * network */ override fun onZeroItemsLoaded() { + if (sessionManager.userName != null) { + mediaClient.resetUserNameContinuation(sessionManager.userName!!) + } fetchContributions() } @@ -50,21 +53,25 @@ class ContributionBoundaryCallback @Inject constructor( * Fetches contributions using the MediaWiki API */ fun fetchContributions() { - compositeDisposable.add( - mediaClient.getMediaListForUser(sessionManager.userName!!) - .map { mediaList -> - mediaList.map { - Contribution(media=it, state=Contribution.STATE_COMPLETED) + if (sessionManager.userName != null) { + compositeDisposable.add( + mediaClient.getMediaListForUser(sessionManager.userName!!) + .map { mediaList -> + mediaList.map { + Contribution(media = it, state = Contribution.STATE_COMPLETED) + } } - } - .subscribeOn(ioThreadScheduler) - .subscribe(::saveContributionsToDB) { error: Throwable -> - Timber.e( - "Failed to fetch contributions: %s", - error.message - ) - } - ) + .subscribeOn(ioThreadScheduler) + .subscribe(::saveContributionsToDB) { error: Throwable -> + Timber.e( + "Failed to fetch contributions: %s", + error.message + ) + } + ) + }else { + compositeDisposable.clear() + } } /** diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index 6f5fca967..15c61d836 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -1,13 +1,11 @@ package fr.free.nrw.commons.contributions; -import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES; import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; import android.Manifest; import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.util.Log; import androidx.annotation.NonNull; import fr.free.nrw.commons.R; import fr.free.nrw.commons.filepicker.DefaultCallback; @@ -29,7 +27,6 @@ import javax.inject.Singleton; public class ContributionController { public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads"; - private final JsonKvStore defaultKvStore; @Inject @@ -55,20 +52,16 @@ public class ContributionController { } /** - * Check for permissions and initiate gallery picker + * Initiate gallery picker */ - public void initiateGalleryPick(Activity activity, boolean allowMultipleUploads) { - PermissionUtils.checkPermissionsAndPerformAction(activity, - Manifest.permission.READ_EXTERNAL_STORAGE, - () -> initiateGalleryUpload(activity, allowMultipleUploads), - R.string.storage_permission_title, - R.string.read_storage_permission_rationale); + public void initiateGalleryPick(final Activity activity, final boolean allowMultipleUploads) { + initiateGalleryUpload(activity, allowMultipleUploads); } /** * Open chooser for gallery uploads */ - private void initiateGalleryUpload(Activity activity, boolean allowMultipleUploads) { + private void initiateGalleryUpload(final Activity activity, final boolean allowMultipleUploads) { setPickerConfiguration(activity, allowMultipleUploads); FilePicker.openGallery(activity, 0); } @@ -130,7 +123,8 @@ public class ContributionController { List imagesFiles) { Intent shareIntent = new Intent(context, UploadActivity.class); shareIntent.setAction(ACTION_INTERNAL_UPLOADS); - shareIntent.putParcelableArrayListExtra(EXTRA_FILES, new ArrayList<>(imagesFiles)); + shareIntent + .putParcelableArrayListExtra(UploadActivity.EXTRA_FILES, new ArrayList<>(imagesFiles)); Place place = defaultKvStore.getJson(PLACE_OBJECT, Place.class); if (place != null) { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java index 7aeb7558b..a5b3c6a1b 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java @@ -39,12 +39,6 @@ public abstract class ContributionDao { saveSynchronous(newContribution); } - public Completable saveAndDelete(final Contribution oldContribution, - final Contribution newContribution) { - return Completable - .fromAction(() -> deleteAndSaveContribution(oldContribution, newContribution)); - } - @Insert(onConflict = OnConflictStrategy.REPLACE) public abstract Single> save(List contribution); @@ -62,11 +56,8 @@ public abstract class ContributionDao { @Query("SELECT * from contribution WHERE pageId=:pageId") public abstract Contribution getContribution(String pageId); - @Query("SELECT * from contribution WHERE state=:state") - public abstract Single> getContribution(int state); - - @Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)") - public abstract Single updateStates(int state, int[] toUpdateStates); + @Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC") + public abstract Single> getContribution(List states); @Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)") public abstract Single getPendingUploads(int[] toUpdateStates); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java index f73ce5501..b8a2488b2 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.contributions; +import android.content.Context; import fr.free.nrw.commons.BasePresenter; /** @@ -10,6 +11,8 @@ public class ContributionsContract { public interface View { void showMessage(String localizedMessage); + + Context getContext(); } public interface UserActionListener extends BasePresenter { @@ -18,5 +21,6 @@ public class ContributionsContract { void deleteUpload(Contribution contribution); + void saveContribution(Contribution contribution); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java index 3a2e29853..aee11028a 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -8,10 +8,7 @@ import android.Manifest; import android.annotation.SuppressLint; import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; import android.os.Bundle; -import android.os.IBinder; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -28,21 +25,20 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager.OnBackStackChangedListener; import androidx.fragment.app.FragmentTransaction; - import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.notification.Notification; import fr.free.nrw.commons.notification.NotificationController; import fr.free.nrw.commons.theme.BaseActivity; import java.util.List; - import javax.inject.Inject; import javax.inject.Named; - import butterknife.BindView; import butterknife.ButterKnife; +import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.campaigns.Campaign; import fr.free.nrw.commons.campaigns.CampaignView; import fr.free.nrw.commons.campaigns.CampaignsPresenter; @@ -60,8 +56,10 @@ import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; import fr.free.nrw.commons.nearby.NearbyController; import fr.free.nrw.commons.nearby.NearbyNotificationCardView; import fr.free.nrw.commons.nearby.Place; +import fr.free.nrw.commons.notification.Notification; import fr.free.nrw.commons.notification.NotificationActivity; -import fr.free.nrw.commons.upload.UploadService; +import fr.free.nrw.commons.notification.NotificationController; +import fr.free.nrw.commons.theme.BaseActivity; import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.NetworkUtils; @@ -71,6 +69,9 @@ import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; +import java.util.List; +import javax.inject.Inject; +import javax.inject.Named; import timber.log.Timber; public class ContributionsFragment @@ -79,7 +80,7 @@ public class ContributionsFragment OnBackStackChangedListener, LocationUpdateListener, MediaDetailProvider, - ICampaignsView, ContributionsContract.View, Callback { + ICampaignsView, ContributionsContract.View, Callback{ @Inject @Named("default_preferences") JsonKvStore store; @Inject NearbyController nearbyController; @Inject OkHttpJsonApiClient okHttpJsonApiClient; @@ -87,8 +88,6 @@ public class ContributionsFragment @Inject LocationServiceManager locationManager; @Inject NotificationController notificationController; - private UploadService uploadService; - private boolean isUploadServiceConnected; private CompositeDisposable compositeDisposable = new CompositeDisposable(); private ContributionsListFragment contributionsListFragment; @@ -99,6 +98,7 @@ public class ContributionsFragment @BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView; @BindView(R.id.campaigns_view) CampaignView campaignView; @BindView(R.id.limited_connection_enabled_layout) LinearLayout limitedConnectionEnabledLayout; + @BindView(R.id.limited_connection_description_text_view) TextView limitedConnectionDescriptionTextView; @Inject ContributionsPresenter contributionsPresenter; @@ -121,32 +121,7 @@ public class ContributionsFragment return fragment; } - /** - * Since we will need to use parent activity on onAuthCookieAcquired, we have to wait - * fragment to be attached. Latch will be responsible for this sync. - */ - private ServiceConnection uploadServiceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName componentName, IBinder binder) { - uploadService = (UploadService) ((UploadService.UploadServiceLocalBinder) binder) - .getService(); - isUploadServiceConnected = true; - } - - @Override - public void onServiceDisconnected(ComponentName componentName) { - // this should never happen - Timber.e(new RuntimeException("UploadService died but the rest of the process did not!")); - isUploadServiceConnected = false; - } - - @Override - public void onBindingDied(final ComponentName name) { - isUploadServiceConnected = false; - } - }; private boolean shouldShowMediaDetailsFragment; - private boolean isAuthCookieAcquired; @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -183,6 +158,9 @@ public class ContributionsFragment if(shouldShowMediaDetailsFragment){ showMediaDetailPagerFragment(); }else{ + if (mediaDetailPagerFragment != null) { + removeFragment(mediaDetailPagerFragment); + } showContributionsListFragment(); } @@ -190,6 +168,7 @@ public class ContributionsFragment && sessionManager.getCurrentAccount() != null) { setUploadCount(); } + limitedConnectionEnabledLayout.setOnClickListener(toggleDescriptionListener); setHasOptionsMenu(true); return view; } @@ -265,7 +244,6 @@ public class ContributionsFragment until fragment life time ends. */ if (!isFragmentAttachedBefore && getActivity() != null) { - onAuthCookieAcquired(); isFragmentAttachedBefore = true; } } @@ -305,19 +283,6 @@ public class ContributionsFragment fetchCampaigns(); } - /** - * Called when onAuthCookieAcquired is called on authenticated parent activity - */ - void onAuthCookieAcquired() { - // Since we call onAuthCookieAcquired method from onAttach, isAdded is still false. So don't use it - isAuthCookieAcquired=true; - if (getActivity() != null) { // If fragment is attached to parent activity - getActivity().bindService(getUploadServiceIntent(), uploadServiceConnection, Context.BIND_AUTO_CREATE); - isUploadServiceConnected = true; - } - - } - private void initFragments() { if (null == contributionsListFragment) { contributionsListFragment = new ContributionsListFragment(); @@ -374,7 +339,6 @@ public class ContributionsFragment getChildFragmentManager().executePendingTransactions(); } - public Intent getUploadServiceIntent(){ Intent intent = new Intent(getActivity(), UploadService.class); intent.setAction(UploadService.ACTION_START_SERVICE); @@ -424,19 +388,22 @@ public class ContributionsFragment showNearbyCardPermissionRationale(); }); - if (store.getBoolean("displayNearbyCardView", true)) { - checkPermissionsAndShowNearbyCardView(); - if (nearbyNotificationCardView.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) { - nearbyNotificationCardView.setVisibility(View.VISIBLE); + // Notification cards should only be seen on contributions list, not in media details + if (mediaDetailPagerFragment == null) { + if (store.getBoolean("displayNearbyCardView", true)) { + checkPermissionsAndShowNearbyCardView(); + if (nearbyNotificationCardView.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) { + nearbyNotificationCardView.setVisibility(View.VISIBLE); + } + + } else { + // Hide nearby notification card view if related shared preferences is false + nearbyNotificationCardView.setVisibility(View.GONE); } - } else { - // Hide nearby notification card view if related shared preferences is false - nearbyNotificationCardView.setVisibility(View.GONE); + setNotificationCount(); + fetchCampaigns(); } - - setNotificationCount(); - fetchCampaigns(); } private void checkPermissionsAndShowNearbyCardView() { @@ -444,6 +411,7 @@ public class ContributionsFragment onLocationPermissionGranted(); } else if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) && store.getBoolean("displayLocationPermissionForCardView", true) + && !store.getBoolean("doNotAskForLocationPermission", false) && (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) { nearbyNotificationCardView.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION; showNearbyCardPermissionRationale(); @@ -476,6 +444,7 @@ public class ContributionsFragment private void displayYouWontSeeNearbyMessage() { ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.unable_to_display_nearest_place)); + store.putBoolean("doNotAskForLocationPermission", true); } @@ -512,13 +481,6 @@ public class ContributionsFragment locationManager.unregisterLocationManager(); locationManager.removeLocationListener(this); super.onDestroy(); - - if (isUploadServiceConnected) { - if (getActivity() != null) { - getActivity().unbindService(uploadServiceConnection); - isUploadServiceConnected = false; - } - } } catch (IllegalArgumentException | IllegalStateException exception) { Timber.e(exception); } @@ -564,6 +526,9 @@ public class ContributionsFragment if (store.getBoolean("displayCampaignsCardView", true)) { presenter.getCampaigns(); } + else{ + campaignView.setVisibility(View.GONE); + } } @Override public void showMessage(String message) { @@ -578,7 +543,6 @@ public class ContributionsFragment @Override public void onDestroyView() { super.onDestroyView(); - isUploadServiceConnected = false; presenter.onDetachView(); } @@ -597,8 +561,9 @@ public class ContributionsFragment @Override public void retryUpload(Contribution contribution) { if (NetworkUtils.isInternetConnectionEstablished(getContext())) { - if (contribution.getState() == STATE_FAILED || contribution.getState() == STATE_PAUSED || contribution.getState()==Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE && null != uploadService) { - uploadService.queue(contribution); + if (contribution.getState() == STATE_FAILED || contribution.getState() == STATE_PAUSED || contribution.getState()==Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) { + contribution.setState(Contribution.STATE_QUEUED); + contributionsPresenter.saveContribution(contribution); Timber.d("Restarting for %s", contribution.toString()); } else { Timber.d("Skipping re-upload for non-failed %s", contribution.toString()); @@ -615,7 +580,21 @@ public class ContributionsFragment */ @Override public void pauseUpload(Contribution contribution) { - uploadService.pauseUpload(contribution); + //Pause the upload in the global singleton + CommonsApplication.pauseUploads.put(contribution.getPageId(), true); + //Retain the paused state in DB + contribution.setState(STATE_PAUSED); + contributionsPresenter.saveContribution(contribution); + } + + /** + * Notify the viewpager that number of items have changed. + */ + @Override + public void viewPagerNotifyDataSetChanged() { + if (mediaDetailPagerFragment != null) { + mediaDetailPagerFragment.notifyDataSetChanged(); + } } /** @@ -665,5 +644,41 @@ public class ContributionsFragment } return false; } + + // Getter for mediaDetailPagerFragment + public MediaDetailPagerFragment getMediaDetailPagerFragment() { + return mediaDetailPagerFragment; + } + + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + @Override + public void refreshNominatedMedia(int index) { + if(mediaDetailPagerFragment != null && !contributionsListFragment.isVisible()) { + removeFragment(mediaDetailPagerFragment); + mediaDetailPagerFragment = new MediaDetailPagerFragment(false, true); + mediaDetailPagerFragment.showImage(index); + showMediaDetailPagerFragment(); + } + } + + // click listener to toggle description that means uses can press the limited connection + // banner and description will hide. Tap again to show description. + private View.OnClickListener toggleDescriptionListener = new View.OnClickListener() { + + @Override + public void onClick(View view) { + View view2 = limitedConnectionDescriptionTextView; + if (view2.getVisibility() == View.GONE) { + view2.setVisibility(View.VISIBLE); + } else { + view2.setVisibility(View.GONE); + } + } + }; + } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index 3147c73ac..5b04468d8 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -10,6 +10,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; @@ -24,6 +25,7 @@ import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver; import androidx.recyclerview.widget.RecyclerView.ItemAnimator; +import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; import androidx.recyclerview.widget.SimpleItemAnimator; import butterknife.BindView; import butterknife.ButterKnife; @@ -161,6 +163,60 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl } } } + + /** + * Called whenever items in the list have changed + * Calls viewPagerNotifyDataSetChanged() that will notify the viewpager + */ + @Override + public void onItemRangeChanged(final int positionStart, final int itemCount) { + super.onItemRangeChanged(positionStart, itemCount); + callback.viewPagerNotifyDataSetChanged(); + } + }); + + //Fab close on touch outside (Scrolling or taping on item triggers this action). + rvContributionsList.addOnItemTouchListener(new OnItemTouchListener() { + + /** + * Silently observe and/or take over touch events sent to the RecyclerView before + * they are handled by either the RecyclerView itself or its child views. + */ + @Override + public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { + if (e.getAction() == MotionEvent.ACTION_DOWN) { + if (isFabOpen) { + animateFAB(isFabOpen); + } + } + return false; + } + + /** + * Process a touch event as part of a gesture that was claimed by returning true + * from a previous call to {@link #onInterceptTouchEvent}. + * + * @param rv + * @param e MotionEvent describing the touch event. All coordinates are in the + * RecyclerView's coordinate system. + */ + @Override + public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { + //required abstract method DO NOT DELETE + } + + /** + * Called when a child of RecyclerView does not want RecyclerView and its ancestors + * to intercept touch events with {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}. + * + * @param disallowIntercept True if the child does not want the parent to intercept + * touch events. + */ + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + //required abstract method DO NOT DELETE + } + }); } @@ -330,7 +386,10 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl public Media getMediaAtPosition(final int i) { - return adapter.getContributionForPosition(i).getMedia(); + if(adapter.getContributionForPosition(i) != null) { + return adapter.getContributionForPosition(i).getMedia(); + } + return null; } public int getTotalMediaCount() { @@ -368,5 +427,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl void showDetail(int position, boolean isWikipediaButtonDisplayed); void pauseUpload(Contribution contribution); + + // Notify the viewpager that number of items have changed. + void viewPagerNotifyDataSetChanged(); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsLocalDataSource.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsLocalDataSource.java index 93499e363..dcfca2519 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsLocalDataSource.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsLocalDataSource.java @@ -3,7 +3,6 @@ package fr.free.nrw.commons.contributions; import androidx.paging.DataSource.Factory; import io.reactivex.Completable; import java.util.ArrayList; -import java.util.Date; import java.util.List; import javax.inject.Inject; @@ -22,8 +21,8 @@ class ContributionsLocalDataSource { @Inject public ContributionsLocalDataSource( - @Named("default_preferences") JsonKvStore defaultKVStore, - ContributionDao contributionDao) { + @Named("default_preferences") final JsonKvStore defaultKVStore, + final ContributionDao contributionDao) { this.defaultKVStore = defaultKVStore; this.contributionDao = contributionDao; } @@ -31,14 +30,14 @@ class ContributionsLocalDataSource { /** * Fetch default number of contributions to be show, based on user preferences */ - public String getString(String key) { + public String getString(final String key) { return defaultKVStore.getString(key); } /** * Fetch default number of contributions to be show, based on user preferences */ - public long getLong(String key) { + public long getLong(final String key) { return defaultKVStore.getLong(key); } @@ -47,8 +46,8 @@ class ContributionsLocalDataSource { * @param uri * @return */ - public Contribution getContributionWithFileName(String uri) { - List contributionWithUri = contributionDao.getContributionWithTitle(uri); + public Contribution getContributionWithFileName(final String uri) { + final List contributionWithUri = contributionDao.getContributionWithTitle(uri); if(!contributionWithUri.isEmpty()){ return contributionWithUri.get(0); } @@ -60,7 +59,7 @@ class ContributionsLocalDataSource { * @param contribution * @return */ - public Completable deleteContribution(Contribution contribution) { + public Completable deleteContribution(final Contribution contribution) { return contributionDao.delete(contribution); } @@ -68,10 +67,10 @@ class ContributionsLocalDataSource { return contributionDao.fetchContributions(); } - public Single> saveContributions(List contributions) { - List contributionList = new ArrayList<>(); - for(Contribution contribution: contributions) { - Contribution oldContribution = contributionDao.getContribution(contribution.getPageId()); + public Single> saveContributions(final List contributions) { + final List contributionList = new ArrayList<>(); + for(final Contribution contribution: contributions) { + final Contribution oldContribution = contributionDao.getContribution(contribution.getPageId()); if(oldContribution != null) { contribution.setWikidataPlace(oldContribution.getWikidataPlace()); } @@ -80,11 +79,15 @@ class ContributionsLocalDataSource { return contributionDao.save(contributionList); } - public void set(String key, long value) { + public Completable saveContributions(Contribution contribution) { + return contributionDao.save(contribution); + } + + public void set(final String key, final long value) { defaultKVStore.putLong(key,value); } - public Completable updateContribution(Contribution contribution) { + public Completable updateContribution(final Contribution contribution) { return contributionDao.update(contribution); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java index 2cae4f04c..002e8bc95 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java @@ -1,10 +1,18 @@ package fr.free.nrw.commons.contributions; +import androidx.work.ExistingWorkPolicy; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; import fr.free.nrw.commons.MediaDataExtractor; import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener; import fr.free.nrw.commons.di.CommonsApplicationModule; +import fr.free.nrw.commons.upload.worker.UploadWorker; import io.reactivex.Scheduler; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; +import java.util.Collections; +import java.util.List; import javax.inject.Inject; import javax.inject.Named; @@ -14,7 +22,6 @@ import javax.inject.Named; public class ContributionsPresenter implements UserActionListener { private final ContributionsRepository repository; - private final Scheduler mainThreadScheduler; private final Scheduler ioThreadScheduler; private CompositeDisposable compositeDisposable; private ContributionsContract.View view; @@ -23,9 +30,9 @@ public class ContributionsPresenter implements UserActionListener { MediaDataExtractor mediaDataExtractor; @Inject - ContributionsPresenter(ContributionsRepository repository, @Named(CommonsApplicationModule.MAIN_THREAD) Scheduler mainThreadScheduler,@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) { + ContributionsPresenter(ContributionsRepository repository, + @Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) { this.repository = repository; - this.mainThreadScheduler=mainThreadScheduler; this.ioThreadScheduler=ioThreadScheduler; } @@ -57,4 +64,23 @@ public class ContributionsPresenter implements UserActionListener { .subscribeOn(ioThreadScheduler) .subscribe()); } + + /** + * Update the contribution's state in the databse, upon completion, trigger the workmanager to + * process this contribution + * + * @param contribution + */ + @Override + public void saveContribution(Contribution contribution) { + compositeDisposable.add(repository + .save(contribution) + .subscribeOn(ioThreadScheduler) + .subscribe(() -> { + WorkManager.getInstance(view.getContext().getApplicationContext()) + .enqueueUniqueWork( + UploadWorker.class.getSimpleName(), + ExistingWorkPolicy.KEEP, OneTimeWorkRequest.from(UploadWorker.class)); + })); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRepository.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRepository.java index 17b004802..8054cfb4a 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRepository.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRepository.java @@ -53,6 +53,10 @@ public class ContributionsRepository { return localDataSource.saveContributions(contributions); } + public Completable save(Contribution contributions){ + return localDataSource.saveContributions(contributions); + } + public void set(String key, long value) { localDataSource.set(key,value); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index 24c2a1b16..059448031 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -1,7 +1,9 @@ package fr.free.nrw.commons.contributions; +import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -13,13 +15,15 @@ import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; +import androidx.work.ExistingWorkPolicy; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.bookmarks.BookmarkFragment; -import fr.free.nrw.commons.category.CategoryImagesCallback; import fr.free.nrw.commons.explore.ExploreFragment; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.location.LocationServiceManager; @@ -29,15 +33,15 @@ import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment; import fr.free.nrw.commons.navtab.NavTab; import fr.free.nrw.commons.navtab.NavTabLayout; import fr.free.nrw.commons.navtab.NavTabLoggedOut; -import fr.free.nrw.commons.nearby.NearbyNotificationCardView; import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment; import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.NearbyParentFragmentInstanceReadyCallback; import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.notification.NotificationController; import fr.free.nrw.commons.quiz.QuizChecker; +import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.upload.UploadService; +import fr.free.nrw.commons.upload.worker.UploadWorker; import fr.free.nrw.commons.utils.ViewUtilWrapper; import javax.inject.Inject; import javax.inject.Named; @@ -64,6 +68,7 @@ public class MainActivity extends BaseActivity private ExploreFragment exploreFragment; private BookmarkFragment bookmarkFragment; public ActiveFragment activeFragment; + private MediaDetailPagerFragment mediaDetailPagerFragment; @Inject public LocationServiceManager locationManager; @@ -107,6 +112,7 @@ public class MainActivity extends BaseActivity @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + loadLocale(); setContentView(R.layout.main); ButterKnife.bind(this); setSupportActionBar(toolbar); @@ -114,7 +120,7 @@ public class MainActivity extends BaseActivity onSupportNavigateUp(); }); if (applicationKvStore.getBoolean("login_skipped") == true) { - setTitle(getString(R.string.explore_tab_title_mobile)); + setTitle(getString(R.string.navigation_item_explore)); setUpLoggedOutPager(); } else { if(savedInstanceState == null){ @@ -123,7 +129,6 @@ public class MainActivity extends BaseActivity loadFragment(ContributionsFragment.newInstance(),false); } setUpPager(); - initMain(); } } @@ -236,13 +241,26 @@ public class MainActivity extends BaseActivity protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt("viewPagerCurrentItem", viewPager.getCurrentItem()); + outState.putString("activeFragment", activeFragment.name()); } - private void initMain() { - //Do not remove this, this triggers the sync service - Intent uploadServiceIntent = new Intent(this, UploadService.class); - uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); - startService(uploadServiceIntent); + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + String currentFragmentName = savedInstanceState.getString("activeFragment"); + if(currentFragmentName == ActiveFragment.CONTRIBUTIONS.name()) { + setTitle(getString(R.string.contributions_fragment)); + loadFragment(ContributionsFragment.newInstance(),false); + }else if(currentFragmentName == ActiveFragment.NEARBY.name()) { + setTitle(getString(R.string.nearby_fragment)); + loadFragment(NearbyParentFragment.newInstance(),false); + }else if(currentFragmentName == ActiveFragment.EXPLORE.name()) { + setTitle(getString(R.string.navigation_item_explore)); + loadFragment(ExploreFragment.newInstance(),false); + }else if(currentFragmentName == ActiveFragment.BOOKMARK.name()) { + setTitle(getString(R.string.favorites)); + loadFragment(BookmarkFragment.newInstance(),false); + } } @Override @@ -298,13 +316,10 @@ public class MainActivity extends BaseActivity viewUtilWrapper .showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled)); } else { - Intent intent = new Intent(this, UploadService.class); - intent.setAction(UploadService.PROCESS_PENDING_LIMITED_CONNECTION_MODE_UPLOADS); - if (VERSION.SDK_INT >= VERSION_CODES.O) { - startForegroundService(intent); - } else { - startService(intent); - } + WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork( + UploadWorker.class.getSimpleName(), + ExistingWorkPolicy.KEEP, OneTimeWorkRequest.from(UploadWorker.class)); + viewUtilWrapper .showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled)); } @@ -350,4 +365,14 @@ public class MainActivity extends BaseActivity BOOKMARK, MORE } + + /** + * Load default language in onCreate from SharedPreferences + */ + private void loadLocale(){ + final SharedPreferences preferences = getSharedPreferences("Settings", Activity.MODE_PRIVATE); + final String language = preferences.getString("language", ""); + final SettingsFragment settingsFragment = new SettingsFragment(); + settingsFragment.setLocale(this, language); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt b/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt index 21f2c26d4..67f4024e0 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt @@ -39,7 +39,7 @@ class WikipediaInstructionsDialogFragment : DialogFragment() { callback?.onConfirmClicked(contribution, checkbox_copy_wikicode.isChecked) } - dialog!!.window.setSoftInputMode( + dialog!!.window?.setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN ) } diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java index 04e103cd0..e37f1942b 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java @@ -50,6 +50,7 @@ public class DBOpenHelper extends SQLiteOpenHelper { public void deleteTable(SQLiteDatabase db, String tableName) { try { db.execSQL(String.format(DROP_TABLE_STATEMENT, tableName)); + onCreate(db); } catch (SQLiteException e) { e.printStackTrace(); } diff --git a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt index e83869395..f06e15da8 100644 --- a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt +++ b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt @@ -5,13 +5,16 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.ContributionDao +import fr.free.nrw.commons.upload.depicts.Depicts +import fr.free.nrw.commons.upload.depicts.DepictsDao /** * The database for accessing the respective DAOs * */ -@Database(entities = [Contribution::class], version = 7, exportSchema = false) +@Database(entities = [Contribution::class,Depicts::class], version = 7, exportSchema = false) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun contributionDao(): ContributionDao + abstract fun DepictsDao (): DepictsDao; } diff --git a/app/src/main/java/fr/free/nrw/commons/db/Converters.java b/app/src/main/java/fr/free/nrw/commons/db/Converters.java index 3156f5e2d..59b06a675 100644 --- a/app/src/main/java/fr/free/nrw/commons/db/Converters.java +++ b/app/src/main/java/fr/free/nrw/commons/db/Converters.java @@ -23,6 +23,27 @@ public class Converters { return ApplicationlessInjection.getInstance(CommonsApplication.getInstance()).getCommonsApplicationComponent().gson(); } + /** + * convert DepictedItem object to string + * input Example -> DepictedItem depictedItem=new DepictedItem () + * output Example -> string + */ + @TypeConverter + public static String depictsItemToString(DepictedItem objects) { + return writeObjectToString(objects); + } + + /** + * convert string to DepictedItem object + * output Example -> DepictedItem depictedItem=new DepictedItem () + * input Example -> string + */ + @TypeConverter + public static DepictedItem stringToDepicts(String objectList) { + return readObjectWithTypeToken(objectList, new TypeToken() { + }); + } + @TypeConverter public static Date fromTimestamp(Long value) { return value == null ? null : new Date(value); diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java index 325a5a5e5..28c79c612 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -7,7 +7,6 @@ import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.SignupActivity; import fr.free.nrw.commons.category.CategoryDetailsActivity; -import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity; import fr.free.nrw.commons.explore.SearchActivity; @@ -47,9 +46,6 @@ public abstract class ActivityBuilderModule { @ContributesAndroidInjector abstract NotificationActivity bindNotificationActivity(); - @ContributesAndroidInjector - abstract CategoryImagesActivity bindFeaturedImagesActivity(); - @ContributesAndroidInjector abstract UploadActivity bindUploadActivity(); diff --git a/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java b/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java index b4f4e5db8..f2bff5db7 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java @@ -8,6 +8,7 @@ import android.content.Context; import androidx.fragment.app.Fragment; +import dagger.android.HasAndroidInjector; import javax.inject.Inject; import dagger.android.AndroidInjector; @@ -25,6 +26,7 @@ import dagger.android.support.HasSupportFragmentInjector; */ public class ApplicationlessInjection implements + HasAndroidInjector, HasActivityInjector, HasFragmentInjector, HasSupportFragmentInjector, @@ -34,6 +36,7 @@ public class ApplicationlessInjection private static ApplicationlessInjection instance = null; + @Inject DispatchingAndroidInjector androidInjector; @Inject DispatchingAndroidInjector activityInjector; @Inject DispatchingAndroidInjector broadcastReceiverInjector; @Inject DispatchingAndroidInjector fragmentInjector; @@ -49,6 +52,11 @@ public class ApplicationlessInjection commonsApplicationComponent.inject(this); } + @Override + public AndroidInjector androidInjector() { + return androidInjector; + } + @Override public DispatchingAndroidInjector activityInjector() { return activityInjector; @@ -94,5 +102,4 @@ public class ApplicationlessInjection return instance; } - } diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java index 888967b48..33896f768 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -6,6 +6,7 @@ import fr.free.nrw.commons.explore.categories.CategoriesModule; import fr.free.nrw.commons.navtab.MoreBottomSheetFragment; import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment; import fr.free.nrw.commons.navtab.NavTabLayout; +import fr.free.nrw.commons.upload.worker.UploadWorker; import javax.inject.Singleton; import dagger.Component; @@ -47,6 +48,8 @@ import fr.free.nrw.commons.widget.PicOfDayAppWidget; public interface CommonsApplicationComponent extends AndroidInjector { void inject(CommonsApplication application); + void inject(UploadWorker worker); + void inject(LoginActivity activity); void inject(SettingsFragment fragment); diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index 753836a6c..1e19de5f4 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -23,6 +23,7 @@ import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.upload.UploadController; +import fr.free.nrw.commons.upload.depicts.DepictsDao; import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.wikidata.WikidataEditListener; import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl; @@ -165,7 +166,7 @@ public class CommonsApplicationModule { @Provides public UploadController providesUploadController(SessionManager sessionManager, @Named("default_preferences") JsonKvStore kvStore, - Context context) { + Context context, ContributionDao contributionDao) { return new UploadController(sessionManager, context, kvStore); } @@ -242,6 +243,14 @@ public class CommonsApplicationModule { return appDatabase.contributionDao(); } + /** + * Get the reference of DepictsDao class + */ + @Provides + public DepictsDao providesDepictDao(AppDatabase appDatabase) { + return appDatabase.DepictsDao(); + } + @Provides public ContentResolver providesContentResolver(Context context){ return context.getContentResolver(); diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java index 33291c6cd..ec68c3eae 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java @@ -216,9 +216,8 @@ public class NetworkingModule { @Provides @Singleton public PageEditClient provideCommonsPageEditClient(@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient, - @Named("commons-page-edit-service") PageEditInterface pageEditInterface, - @Named("commons-service") Service service) { - return new PageEditClient(csrfTokenClient, pageEditInterface, service); + @Named("commons-page-edit-service") PageEditInterface pageEditInterface) { + return new PageEditClient(csrfTokenClient, pageEditInterface); } @Provides diff --git a/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java index 89e65e1d8..1fb52c937 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java @@ -3,7 +3,6 @@ package fr.free.nrw.commons.di; import dagger.Module; import dagger.android.ContributesAndroidInjector; import fr.free.nrw.commons.auth.WikiAccountAuthenticatorService; -import fr.free.nrw.commons.upload.UploadService; /** * This Class Represents the Module for dependency injection (using dagger) @@ -14,9 +13,6 @@ import fr.free.nrw.commons.upload.UploadService; @SuppressWarnings({"WeakerAccess", "unused"}) public abstract class ServiceBuilderModule { - @ContributesAndroidInjector - abstract UploadService bindUploadService(); - @ContributesAndroidInjector abstract WikiAccountAuthenticatorService bindWikiAccountAuthenticatorService(); diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java index 6a6a23e20..4fd27eb5a 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java @@ -10,11 +10,12 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import androidx.viewpager.widget.ViewPager; import butterknife.BindView; import butterknife.ButterKnife; import com.google.android.material.tabs.TabLayout; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.ViewPagerAdapter; +import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.theme.BaseActivity; import fr.free.nrw.commons.utils.ActivityUtils; @@ -30,11 +31,15 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { @BindView(R.id.tab_layout) TabLayout tabLayout; @BindView(R.id.viewPager) - ViewPager viewPager; + ParentViewPager viewPager; ViewPagerAdapter viewPagerAdapter; private ExploreListRootFragment featuredRootFragment; private ExploreListRootFragment mobileRootFragment; + public void setScroll(boolean canScroll){ + viewPager.setCanScroll(canScroll); + } + @NonNull public static ExploreFragment newInstance() { ExploreFragment fragment = new ExploreFragment(); @@ -83,16 +88,26 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { fragmentList.add(mobileRootFragment); titleList.add(getString(R.string.explore_tab_title_mobile).toUpperCase()); + ((MainActivity)getActivity()).showTabs(); + ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); + viewPagerAdapter.setTabData(fragmentList, titleList); viewPagerAdapter.notifyDataSetChanged(); } public void onBackPressed() { if (tabLayout.getSelectedTabPosition() == 0) { - featuredRootFragment.backPressed(); + if(featuredRootFragment.backPressed()){ + // Event is handled by the Fragment we need not do anything. + return; + } } else { - mobileRootFragment.backPressed(); + if(mobileRootFragment.backPressed()){ + // Event is handled by the Fragment we need not do anything. + return; + } } + // Event is not handled by the fragment ( i.e performed back action ) therefore change action bar. ((BaseActivity)getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java index e8967d9e8..ca005207a 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java @@ -2,7 +2,7 @@ package fr.free.nrw.commons.explore; import android.content.Context; import android.os.Bundle; -import android.util.Log; +import android.os.Handler; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -10,20 +10,16 @@ import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; import butterknife.BindView; import butterknife.ButterKnife; -import com.google.android.material.tabs.TabLayout; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; import fr.free.nrw.commons.category.CategoryImagesCallback; -import fr.free.nrw.commons.contributions.ContributionsListFragment; import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.navtab.NavTab; -import fr.free.nrw.commons.settings.SettingsFragment; public class ExploreListRootFragment extends CommonsDaggerSupportFragment implements MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback { @@ -80,7 +76,7 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") .commit(); getChildFragmentManager().executePendingTransactions(); - }else if (!fragment.isAdded() && otherFragment != null ) { + } else if (!fragment.isAdded() && otherFragment != null ) { getChildFragmentManager() .beginTransaction() .hide(otherFragment) @@ -113,7 +109,6 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem @Override public void onMediaClicked(int position) { - Log.d("deneme8","on media clicked"); container.setVisibility(View.VISIBLE); ((ExploreFragment)getParentFragment()).tabLayout.setVisibility(View.GONE); mediaDetails = new MediaDetailPagerFragment(false, true); @@ -157,6 +152,19 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem return null; } + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + @Override + public void refreshNominatedMedia(int index) { + if(mediaDetails != null && !listFragment.isVisible()) { + removeFragment(mediaDetails); + onMediaClicked(index); + } + } + /** * This method is called on success of API call for featured images or mobile uploads. The * viewpager will notified that number of items have changed. @@ -168,15 +176,26 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem } } - public void backPressed() { + /** + * Performs back pressed action on the fragment. + * Return true if the event was handled by the mediaDetails otherwise returns false. + * @return + */ + public boolean backPressed() { if (null!=mediaDetails && mediaDetails.isVisible()) { // todo add get list fragment + if(mediaDetails.backButtonClicked()) { + // MediaDetails handled the event no further action required. + return true; + } ((ExploreFragment)getParentFragment()).tabLayout.setVisibility(View.VISIBLE); removeFragment(mediaDetails); + ((ExploreFragment) getParentFragment()).setScroll(true); setFragment(listFragment, mediaDetails); } else { ((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code()); } ((MainActivity)getActivity()).showTabs(); + return false; } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.java b/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.java new file mode 100644 index 000000000..5b72bdbe4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/ParentViewPager.java @@ -0,0 +1,69 @@ +package fr.free.nrw.commons.explore; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import androidx.viewpager.widget.ViewPager; + +/** + * ParentViewPager + * A custom viewPager whose scrolling can be enabled and disabled. + */ +public class ParentViewPager extends ViewPager { + + /** + * Boolean variable that stores the current state of pager scroll i.e(enabled or disabled) + */ + private boolean canScroll = true; + + + /** + * Default constructors + */ + public ParentViewPager(Context context) { + super(context); + } + + public ParentViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + + /** + * Setter method for canScroll. + */ + public void setCanScroll(boolean canScroll) { + this.canScroll = canScroll; + } + + + /** + * Getter method for canScroll. + */ + public boolean isCanScroll() { + return canScroll; + } + + + /** + * Method that prevents scrolling if canScroll is set to false. + */ + @Override + public boolean onTouchEvent(MotionEvent ev) { + return canScroll && super.onTouchEvent(ev); + } + + + /** + * A facilitator method that allows parent to intercept touch events before its children. + * thus making it possible to prevent swiping parent on child end. + */ + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return canScroll && super.onInterceptTouchEvent(ev); + } + + + + +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java index 487364dfc..f525eec91 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java @@ -18,6 +18,7 @@ import com.jakewharton.rxbinding2.view.RxView; import com.jakewharton.rxbinding2.widget.RxSearchView; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.ViewPagerAdapter; import fr.free.nrw.commons.category.CategoryImagesCallback; import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment; import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment; @@ -182,6 +183,19 @@ public class SearchActivity extends BaseActivity return null; } + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + @Override + public void refreshNominatedMedia(int index) { + if (getSupportFragmentManager().getBackStackEntryCount() == 1) { + onBackPressed(); + onMediaClicked(index); + } + } + /** * This method is called on success of API call for image Search. * The viewpager will notified that number of items have changed. @@ -244,6 +258,12 @@ public class SearchActivity extends BaseActivity @Override public void onBackPressed() { if (getSupportFragmentManager().getBackStackEntryCount() == 1){ + + // the back press is handled by the mediaDetails , no further action required. + if(mediaDetails.backButtonClicked()){ + return; + } + // back to search so show search toolbar and hide navigation toolbar searchView.setVisibility(View.VISIBLE);//set the searchview tabLayout.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java index 8c5874ec9..00afa0df0 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java @@ -2,9 +2,14 @@ package fr.free.nrw.commons.explore.depictions; import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.widget.FrameLayout; +import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.viewpager.widget.ViewPager; @@ -13,11 +18,12 @@ import butterknife.ButterKnife; import com.google.android.material.tabs.TabLayout; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.ViewPagerAdapter; import fr.free.nrw.commons.category.CategoryImagesCallback; import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment; import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment; import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment; -import fr.free.nrw.commons.explore.ViewPagerAdapter; import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.theme.BaseActivity; import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; @@ -43,6 +49,8 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe TabLayout tabLayout; @BindView(R.id.viewPager) ViewPager viewPager; + @BindView(R.id.toolbar) + Toolbar toolbar; ViewPagerAdapter viewPagerAdapter; @@ -56,6 +64,8 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe viewPager.setAdapter(viewPagerAdapter); viewPager.setOffscreenPageLimit(2); tabLayout.setupWithViewPager(viewPager); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); setTabs(); setPageTitle(); } @@ -153,7 +163,12 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe @Override public void onBackPressed() { if (supportFragmentManager.getBackStackEntryCount() == 1){ - // back to search so show search toolbar and hide navigation toolbar + + // back pressed is handled by the mediaDetails , no further action required. + if(mediaDetailPagerFragment.backButtonClicked()){ + return; + } + tabLayout.setVisibility(View.VISIBLE); viewPager.setVisibility(View.VISIBLE); mediaContainer.setVisibility(View.GONE); @@ -176,6 +191,19 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe return null; } + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + @Override + public void refreshNominatedMedia(int index) { + if (getSupportFragmentManager().getBackStackEntryCount() == 1) { + onBackPressed(); + onMediaClicked(index); + } + } + /** * Consumers should be simply using this method to use this activity. * @@ -188,4 +216,34 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe intent.putExtra("entityId", depictedItem.getId()); context.startActivity(intent); } + + /** + * This function inflates the menu + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater menuInflater=getMenuInflater(); + menuInflater.inflate(R.menu.menu_wikidata_item,menu); + return super.onCreateOptionsMenu(menu); + } + + /** + * This method handles the logic on item select in toolbar menu + * Currently only 1 choice is available to open Wikidata item details page in browser + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()){ + case R.id.browser_actions_menu_items: + String entityId=getIntent().getStringExtra("entityId"); + Uri uri = Uri.parse("https://www.wikidata.org/wiki/" + entityId); + Utils.handleWebUrl(this, uri); + return true; + case android.R.id.home: + onBackPressed(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt index ea97cfa75..321a5812d 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt @@ -55,4 +55,14 @@ abstract class PageableMediaFragment : BasePagingFragment(), MediaDetailP override fun getTotalMediaCount(): Int = pagedListAdapter.itemCount override fun getContributionStateAt(position: Int) = null + + /** + * Reload media detail fragment once media is nominated + * + * @param index item position that has been nominated + */ + override fun refreshNominatedMedia(index: Int) { + activity?.onBackPressed() + categoryImagesCallback.onMediaClicked(index) + } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/paging/BasePagingFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/paging/BasePagingFragment.kt index fcb7d2ee7..a1b2b976f 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/paging/BasePagingFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/paging/BasePagingFragment.kt @@ -48,6 +48,16 @@ abstract class BasePagingFragment : CommonsDaggerSupportFragment(), ) } + /** + * Called on configuration change, update the spanCount according to the orientation state. + */ + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + paginatedSearchResultsList.apply { + layoutManager = GridLayoutManager(context, if (isPortrait) 1 else 2) + } + } + override fun observePagingResults(searchResults: LiveData>) { this.searchResults?.removeObservers(viewLifecycleOwner) this.searchResults = searchResults diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java b/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java index 897fc23d0..aa9379a20 100644 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java @@ -124,8 +124,8 @@ public class UploadableFile implements Parcelable { private DateTimeWithSource getDateTimeFromExif() { try { ExifInterface exif = new ExifInterface(file.getAbsolutePath()); - @SuppressLint("RestrictedApi") long dateTime = exif.getDateTime(); - if (dateTime != -1) { + @SuppressLint("RestrictedApi") Long dateTime = exif.getDateTime(); + if(dateTime != null){ Date date = new Date(dateTime); return new DateTimeWithSource(date, DateTimeWithSource.EXIF_SOURCE); } diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java index 777ad8dce..9d8d99bdb 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java @@ -1,11 +1,16 @@ package fr.free.nrw.commons.location; +import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext; +import android.Manifest.permission; import android.app.Activity; import android.content.Context; +import android.content.pm.PackageManager; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; +import android.util.Log; +import androidx.core.app.ActivityCompat; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -37,11 +42,42 @@ public class LocationServiceManager implements LocationListener { public LatLng getLastLocation() { if (lastLocation == null) { - return null; + lastLocation = getLastKnownLocation(); + if(lastLocation != null) { + return LatLng.from(lastLocation); + } + else { + return null; + } } return LatLng.from(lastLocation); } + private Location getLastKnownLocation() { + List providers = locationManager.getProviders(true); + Location bestLocation = null; + for (String provider : providers) { + Location l=null; + if (ActivityCompat.checkSelfPermission(getApplicationContext(), permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED + && ActivityCompat.checkSelfPermission(getApplicationContext(), permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + l = locationManager.getLastKnownLocation(provider); + } + if (l == null) { + continue; + } + if (bestLocation == null + || l.getAccuracy() < bestLocation.getAccuracy()) { + bestLocation = l; + } + } + if (bestLocation == null) { + return null; + } + return bestLocation; + } + /** * Registers a LocationManager to listen for current location. */ @@ -58,11 +94,10 @@ public class LocationServiceManager implements LocationListener { * @param locationProvider the location provider * @return true if successful */ - private boolean requestLocationUpdatesFromProvider(String locationProvider) { + public boolean requestLocationUpdatesFromProvider(String locationProvider) { try { // If both providers are not available - if (locationManager == null || !(locationManager.getAllProviders().contains(LocationManager.NETWORK_PROVIDER)) - || !(locationManager.getAllProviders().contains(LocationManager.GPS_PROVIDER))) { + if (locationManager == null || !(locationManager.getAllProviders().contains(locationProvider))) { return false; } locationManager.requestLocationUpdates(locationProvider, diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.kt index 0db413466..5ca20b946 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaClient.kt @@ -149,6 +149,14 @@ class MediaClient @Inject constructor( resetContinuation(CATEGORY_CONTINUATION_PREFIX, category) } + /** + * Call the resetUserContinuation method + * + * @param userName the username + */ + fun resetUserNameContinuation(userName: String) = + resetUserContinuation("user_", userName) + override fun responseMapper( networkResult: Single, key: String? diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 4c4c5cf67..6c9472ddd 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -16,14 +16,18 @@ import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; +import android.util.Log; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnKeyListener; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; +import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.ScrollView; @@ -51,6 +55,7 @@ import fr.free.nrw.commons.MediaDataExtractor; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.AccountUtil; +import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.category.CategoryClient; import fr.free.nrw.commons.category.CategoryDetailsActivity; import fr.free.nrw.commons.category.CategoryEditHelper; @@ -61,6 +66,7 @@ import fr.free.nrw.commons.delete.DeleteHelper; import fr.free.nrw.commons.delete.ReasonBuilder; import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.nearby.Label; import fr.free.nrw.commons.ui.widget.HtmlTextView; import fr.free.nrw.commons.utils.ViewUtilWrapper; @@ -74,6 +80,7 @@ import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.Map; import javax.inject.Inject; +import javax.inject.Named; import org.apache.commons.lang3.StringUtils; import org.wikipedia.util.DateUtil; import timber.log.Timber; @@ -87,6 +94,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements private int index; private boolean isDeleted = false; private boolean isWikipediaButtonDisplayed; + private Callback callback; public static MediaDetailFragment forMedia(int index, boolean editable, boolean isCategoryImage, boolean isWikipediaButtonDisplayed) { @@ -105,6 +113,9 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements return mf; } + @Inject + SessionManager sessionManager; + @Inject MediaDataExtractor mediaDataExtractor; @Inject @@ -117,13 +128,16 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements ViewUtilWrapper viewUtil; @Inject CategoryClient categoryClient; + @Inject + @Named("default_preferences") + JsonKvStore applicationKvStore; private int initialListTop = 0; + @BindView(R.id.mediaDetailFrameLayout) + FrameLayout frameLayout; @BindView(R.id.mediaDetailImageView) SimpleDraweeView image; - @BindView(R.id.mediaDetailImageViewLandscape) - SimpleDraweeView imageLandscape; @BindView(R.id.mediaDetailImageViewSpacer) LinearLayout imageSpacer; @BindView(R.id.mediaDetailTitle) @@ -182,6 +196,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements TextView existingCategories; @BindView(R.id.no_results_found) TextView noResultsFound; + @BindView(R.id.progressBarDeletion) + ProgressBar progressBarDeletion; private ArrayList categoryNames = new ArrayList<>(); private String categorySearchQuery; @@ -202,6 +218,20 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements private Media media; private ArrayList reasonList; + /** + * Height stores the height of the frame layout as soon as it is initialised and updates itself on + * configuration changes. + * Used to adjust aspect ratio of image when length of the image is too large. + */ + private int frameLayoutHeight; + + /** + * Minimum height of the metadata, in pixels. + * Images with a very narrow aspect ratio will be reduced so that the metadata information panel always has at least this height. + */ + private int minimumHeightOfMetadata = 200; + + final static String NOMINATING_FOR_DELETION_MEDIA = "Nominating for deletion %s"; @Override public void onSaveInstanceState(Bundle outState) { @@ -258,9 +288,36 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements authorLayout.setVisibility(GONE); } + if (!sessionManager.isUserLoggedIn()) { + categoryEditButton.setVisibility(GONE); + } + + if(applicationKvStore.getBoolean("login_skipped")){ + delete.setVisibility(GONE); + } + /** + * Gets the height of the frame layout as soon as the view is ready and updates aspect ratio + * of the picture. + */ + view.post(new Runnable() { + @Override + public void run() { + frameLayoutHeight = frameLayout.getMeasuredHeight(); + updateAspectRatio(scrollView.getWidth()); + } + }); + return view; } + @Override + public void onAttach(final Context context) { + super.onAttach(context); + if (getParentFragment() != null) { + callback = (Callback) getParentFragment(); + } + } + @OnClick(R.id.mediaDetailImageViewSpacer) public void launchZoomActivity(View view) { if (media.getImageUrl() != null) { @@ -288,8 +345,19 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements Label.valuesAsList()), categoryRecyclerView, categoryClient, this); categoryRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); categoryRecyclerView.setAdapter(categoryEditSearchRecyclerViewAdapter); + // detail provider is null when fragment is shown in review activity + if (detailProvider != null) { + media = detailProvider.getMediaAtPosition(index); + } else { + media = getArguments().getParcelable("media"); + } media = detailProvider.getMediaAtPosition(index); + + if(media != null && applicationKvStore.getBoolean(String.format(NOMINATING_FOR_DELETION_MEDIA, media.getImageUrl()), false)) { + enableProgressBar(); + } + scrollView.getViewTreeObserver().addOnGlobalLayoutListener( new OnGlobalLayoutListener() { @Override @@ -298,11 +366,10 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements return; } scrollView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { - imageLandscape.setVisibility(VISIBLE); - } oldWidthOfImageView = scrollView.getWidth(); - displayMediaDetails(); + if(media != null) { + displayMediaDetails(); + } } } ); @@ -315,6 +382,16 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { + /** + * We update the height of the frame layout as the configuration changes. + */ + frameLayout.post(new Runnable() { + @Override + public void run() { + frameLayoutHeight = frameLayout.getMeasuredHeight(); + updateAspectRatio(scrollView.getWidth()); + } + }); if (scrollView.getWidth() != oldWidthOfImageView) { if (newWidthOfImageView == 0) { newWidthOfImageView = scrollView.getWidth(); @@ -325,13 +402,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements } } ); - // check orientation - if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { - imageLandscape.setVisibility(VISIBLE); - } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { - imageLandscape.setVisibility(GONE); - } - // ensuring correct aspect ratio for landscape mode + // Ensuring correct aspect ratio for landscape mode if (heightVerifyingBoolean) { updateAspectRatio(newWidthOfImageView); heightVerifyingBoolean = false; @@ -381,6 +452,10 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements private void onDeletionPageExists(Boolean deletionPageExists) { if (deletionPageExists){ + if(applicationKvStore.getBoolean(String.format(NOMINATING_FOR_DELETION_MEDIA, media.getImageUrl()), false)) { + applicationKvStore.remove(String.format(NOMINATING_FOR_DELETION_MEDIA, media.getImageUrl())); + progressBarDeletion.setVisibility(GONE); + } delete.setVisibility(GONE); nominatedForDeletion.setVisibility(VISIBLE); } else if (!isCategoryImage) { @@ -397,6 +472,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements * The imageSpacer is Basically a transparent overlay for the SimpleDraweeView * which holds the image to be displayed( moreover this image is out of * the scroll view ) + * + * + * If the image is sufficiently large i.e. the image height extends the view height, we reduce + * the height and change the width to maintain the aspect ratio, otherwise image takes up the + * total possible width and height is adjusted accordingly. + * * @param scrollWidth the current width of the scrollView */ private void updateAspectRatio(int scrollWidth) { @@ -404,11 +485,19 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements int finalHeight = (scrollWidth*imageInfoCache.getHeight()) / imageInfoCache.getWidth(); ViewGroup.LayoutParams params = image.getLayoutParams(); ViewGroup.LayoutParams spacerParams = imageSpacer.getLayoutParams(); + params.width = scrollWidth; + if(finalHeight > frameLayoutHeight - minimumHeightOfMetadata) { + + // Adjust the height and width of image. + int temp = frameLayoutHeight - minimumHeightOfMetadata; + params.width = (scrollWidth*temp) / finalHeight; + finalHeight = temp; + + } params.height = finalHeight; spacerParams.height = finalHeight; image.setLayoutParams(params); imageSpacer.setLayoutParams(spacerParams); - imageLandscape.setLayoutParams(params); } } @@ -435,23 +524,13 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements image.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder); image.getHierarchy().setFailureImage(R.drawable.image_placeholder); - imageLandscape.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder); - imageLandscape.getHierarchy().setFailureImage(R.drawable.image_placeholder); - DraweeController controller = Fresco.newDraweeControllerBuilder() .setLowResImageRequest(ImageRequest.fromUri(media.getThumbUrl())) .setImageRequest(ImageRequest.fromUri(media.getImageUrl())) .setControllerListener(aspectRatioListener) .setOldController(image.getController()) .build(); - DraweeController controllerLandscape = Fresco.newDraweeControllerBuilder() - .setLowResImageRequest(ImageRequest.fromUri(media.getThumbUrl())) - .setImageRequest(ImageRequest.fromUri(media.getImageUrl())) - .setControllerListener(aspectRatioListener) - .setOldController(imageLandscape.getController()) - .build(); image.setController(controller); - imageLandscape.setController(controllerLandscape); } /** @@ -598,15 +677,27 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements */ private void buildDepictionList(List idAndCaptions) { depictionContainer.removeAllViews(); + String locale = Locale.getDefault().getLanguage(); for (IdAndCaptions idAndCaption : idAndCaptions) { depictionContainer.addView(buildDepictLabel( - idAndCaption.getCaptions().values().iterator().next(), + getDepictionCaption(idAndCaption, locale), idAndCaption.getId(), depictionContainer )); } } + private String getDepictionCaption(IdAndCaptions idAndCaption, String locale) { + //Check if the Depiction Caption is available in user's locale if not then check for english, else show any available. + if(idAndCaption.getCaptions().get(locale) != null) { + return idAndCaption.getCaptions().get(locale); + } + if(idAndCaption.getCaptions().get("en") != null) { + return idAndCaption.getCaptions().get("en"); + } + return idAndCaption.getCaptions().values().iterator().next(); + } + @OnClick(R.id.mediaDetailLicense) public void onMediaDetailLicenceClicked(){ String url = media.getLicenseUrl(); @@ -645,6 +736,22 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements displayHideCategorySearch(); } + /** + * Hides the categoryEditContainer. + * returns true after closing the categoryEditContainer if open, implying that event was handled. + * else returns false + * @return + */ + public boolean hideCategoryEditContainerIfOpen(){ + if (dummyCategoryEditContainer.getVisibility() == VISIBLE) { + // editCategory is open, close it and return true as the event was handled. + dummyCategoryEditContainer.setVisibility(GONE); + return true; + } + // Event was not handled. + return false; + } + public void displayHideCategorySearch() { if (dummyCategoryEditContainer.getVisibility() != VISIBLE) { dummyCategoryEditContainer.setVisibility(VISIBLE); @@ -722,7 +829,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements input.addTextChangedListener(new TextWatcher() { private void handleText() { final Button okButton = d.getButton(AlertDialog.BUTTON_POSITIVE); - if (input.getText().length() == 0) { + if (input.getText().length() == 0 || isDeleted) { okButton.setEnabled(false); } else { okButton.setEnabled(true); @@ -749,35 +856,37 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements @SuppressLint("CheckResult") private void onDeleteClicked(Spinner spinner) { + applicationKvStore.putBoolean(String.format(NOMINATING_FOR_DELETION_MEDIA, media.getImageUrl()), true); + enableProgressBar(); String reason = spinner.getSelectedItem().toString(); Single resultSingle = reasonBuilder.getReason(media, reason) .flatMap(reasonString -> deleteHelper.makeDeletion(getContext(), media, reason)); - compositeDisposable.add(resultSingle - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(s -> { - if (getActivity() != null) { - isDeleted = true; - enableDeleteButton(false); - } - })); - + resultSingle + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(s -> { + if(applicationKvStore.getBoolean(String.format(NOMINATING_FOR_DELETION_MEDIA, media.getImageUrl()), false)) { + applicationKvStore.remove(String.format(NOMINATING_FOR_DELETION_MEDIA, media.getImageUrl())); + callback.nominatingForDeletion(index); + } + }); } @SuppressLint("CheckResult") private void onDeleteClickeddialogtext(String reason) { + applicationKvStore.putBoolean(String.format(NOMINATING_FOR_DELETION_MEDIA, media.getImageUrl()), true); + enableProgressBar(); Single resultSingletext = reasonBuilder.getReason(media, reason) .flatMap(reasonString -> deleteHelper.makeDeletion(getContext(), media, reason)); - compositeDisposable.add(resultSingletext - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(s -> { - if (getActivity() != null) { - isDeleted = true; - enableDeleteButton(false); - } - })); - + resultSingletext + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(s -> { + if(applicationKvStore.getBoolean(String.format(NOMINATING_FOR_DELETION_MEDIA, media.getImageUrl()), false)) { + applicationKvStore.remove(String.format(NOMINATING_FOR_DELETION_MEDIA, media.getImageUrl())); + callback.nominatingForDeletion(index); + } + }); } @OnClick(R.id.seeMore) @@ -787,13 +896,13 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements } } - private void enableDeleteButton(boolean visibility) { - delete.setEnabled(visibility); - if (visibility) { - delete.setTextColor(getResources().getColor(R.color.primaryTextColor)); - } else { - delete.setTextColor(getResources().getColor(R.color.deleteButtonLight)); - } + /** + * Enable Progress Bar and Update delete button text. + */ + private void enableProgressBar() { + progressBarDeletion.setVisibility(VISIBLE); + delete.setText("Nominating for Deletion"); + isDeleted = true; } private void rebuildCatList(List categories) { @@ -923,4 +1032,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements return true; } } + + public interface Callback { + void nominatingForDeletion(int index); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index 7756b9e40..328f50e3b 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -12,6 +12,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentStatePagerAdapter; @@ -35,11 +36,12 @@ import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.disposables.CompositeDisposable; +import java.util.ArrayList; import java.util.Objects; import javax.inject.Inject; import timber.log.Timber; -public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener { +public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener, MediaDetailFragment.Callback { @Inject BookmarkPicturesDao bookmarkDao; @@ -61,6 +63,15 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple private boolean isFromFeaturedRootFragment; private int position; + private ArrayList removedItems=new ArrayList(); + + public void clearRemoved(){ + removedItems.clear(); + } + public ArrayList getRemovedItems() { + return removedItems; + } + public MediaDetailPagerFragment() { this(false, false); } @@ -89,8 +100,6 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple pager.addOnPageChangeListener(this); adapter = new MediaDetailAdapter(getChildFragmentManager()); - ((BaseActivity)getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true); - pager.setAdapter(adapter); if (savedInstanceState != null) { final int pageNumber = savedInstanceState.getInt("current-page"); @@ -279,12 +288,27 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false) .setVisible(false); } + + if (!sessionManager.isUserLoggedIn()) { + menu.findItem(R.id.menu_set_as_avatar).setVisible(false); + } + } } } private void updateBookmarkState(MenuItem item) { boolean isBookmarked = bookmarkDao.findBookmark(bookmark); + if(isBookmarked) { + if(removedItems.contains(pager.getCurrentItem())) { + removedItems.remove(new Integer(pager.getCurrentItem())); + } + } + else { + if(!removedItems.contains(pager.getCurrentItem())) { + removedItems.add(pager.getCurrentItem()); + } + } int icon = isBookmarked ? R.drawable.menu_ic_round_star_filled_24px : R.drawable.menu_ic_round_star_border_24px; item.setIcon(icon); } @@ -351,17 +375,45 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple } } + /** + * Called after the media is nominated for deletion + * + * @param index item position that has been nominated + */ + @Override + public void nominatingForDeletion(int index) { + provider.refreshNominatedMedia(index); + } + + /** + * backButtonClicked is called on a back event in the media details pager. + * returns true after closing the categoryEditContainer if open, implying that event was handled. + * else returns false + * @return + */ + public boolean backButtonClicked(){ + return ((MediaDetailFragment)(adapter.getCurrentFragment())).hideCategoryEditContainerIfOpen(); + } + public interface MediaDetailProvider { Media getMediaAtPosition(int i); int getTotalMediaCount(); Integer getContributionStateAt(int position); + + // Reload media detail fragment once media is nominated + void refreshNominatedMedia(int index); } //FragmentStatePagerAdapter allows user to swipe across collection of images (no. of images undetermined) private class MediaDetailAdapter extends FragmentStatePagerAdapter { + /** + * Keeps track of the current displayed fragment. + */ + private Fragment mCurrentFragment; + public MediaDetailAdapter(FragmentManager fm) { super(fm); } @@ -391,5 +443,30 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple } return provider.getTotalMediaCount(); } + + /** + * Get the currently displayed fragment. + * @return + */ + public Fragment getCurrentFragment() { + return mCurrentFragment; + } + + /** + * Called to inform the adapter of which item is currently considered to be the "primary", + * that is the one show to the user as the current page. + * @param container + * @param position + * @param object + */ + @Override + public void setPrimaryItem(@NonNull final ViewGroup container, final int position, + @NonNull final Object object) { + // Update the current fragment if changed + if(getCurrentFragment() != object) { + mCurrentFragment = ((Fragment)object); + } + super.setPrimaryItem(container, position, object); + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java index b749ac5e2..2bd22666f 100644 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java @@ -26,11 +26,13 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.di.ApplicationlessInjection; +import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.logging.CommonsLogSender; import fr.free.nrw.commons.profile.ProfileActivity; import fr.free.nrw.commons.review.ReviewActivity; import fr.free.nrw.commons.settings.SettingsActivity; import javax.inject.Inject; +import javax.inject.Named; import timber.log.Timber; public class MoreBottomSheetFragment extends BottomSheetDialogFragment { @@ -40,6 +42,11 @@ public class MoreBottomSheetFragment extends BottomSheetDialogFragment { @BindView(R.id.more_profile) TextView moreProfile; + @BindView((R.id.more_peer_review)) TextView morePeerReview; + + @Inject @Named("default_preferences") + JsonKvStore store; + @Nullable @Override public View onCreateView(@NonNull final LayoutInflater inflater, @@ -47,6 +54,9 @@ public class MoreBottomSheetFragment extends BottomSheetDialogFragment { super.onCreateView(inflater, container, savedInstanceState); final View view = inflater.inflate(R.layout.fragment_more_bottom_sheet, container, false); ButterKnife.bind(this, view); + if(store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED)){ + morePeerReview.setVisibility(View.GONE); + } setUserName(); return view; } diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java index 2220bd09c..4f5324a9b 100644 --- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java +++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java @@ -5,6 +5,7 @@ import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.fragment.app.Fragment; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.bookmarks.BookmarkFragment; import fr.free.nrw.commons.explore.ExploreFragment; import org.wikipedia.model.EnumCode; import org.wikipedia.model.EnumCodeMap; @@ -19,6 +20,13 @@ public enum NavTabLoggedOut implements EnumCode { return ExploreFragment.newInstance(); } }, + FAVORITES(R.string.favorites, R.drawable.ic_round_star_border_24px) { + @NonNull + @Override + public Fragment newInstance() { + return BookmarkFragment.newInstance(); + } + }, MORE(R.string.more, R.drawable.ic_menu_black_24dp) { @NonNull @Override diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index 52f963af8..9915bf84b 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -201,7 +201,7 @@ public class NearbyController { nearbyBaseMarker.icon(IconFactory.getInstance(context) .fromBitmap(iconGreen)); } - } else if (!place.destroyed.trim().isEmpty()) { // Means place is destroyed + } else if (!place.exists) { // Means that the topic of the Wikidata item does not exist in the real world anymore, for instance it is a past event, or a place that was destroyed if (iconGrey != null) { nearbyBaseMarker.icon(IconFactory.getInstance(context) .fromBitmap(iconGrey)); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java index 11f670420..a3e24635b 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java @@ -18,19 +18,23 @@ import timber.log.Timber; */ public class Place implements Parcelable { + public final String language; public final String name; private final Label label; private final String longDescription; public final LatLng location; private final String category; public final String pic; - public final String destroyed; + // exists boolean will tell whether the place exists or not, + // For a place to be existing both destroyed and endTime property should be null but it is also not necessary for a non-existing place to have both properties either one property is enough (in such case that not given property will be considered as null). + public final Boolean exists; public String distance; public final Sitelinks siteLinks; - public Place(String name, Label label, String longDescription, LatLng location, String category, Sitelinks siteLinks, String pic, String destroyed) { + public Place(String language,String name, Label label, String longDescription, LatLng location, String category, Sitelinks siteLinks, String pic, Boolean exists) { + this.language = language; this.name = name; this.label = label; this.longDescription = longDescription; @@ -38,10 +42,10 @@ public class Place implements Parcelable { this.category = category; this.siteLinks = siteLinks; this.pic = (pic == null) ? "":pic; - this.destroyed = (destroyed == null) ? "":destroyed; + this.exists = exists; } - public Place(Parcel in) { + this.language = in.readString(); this.name = in.readString(); this.label = (Label) in.readSerializable(); this.longDescription = in.readString(); @@ -50,10 +54,9 @@ public class Place implements Parcelable { this.siteLinks = in.readParcelable(Sitelinks.class.getClassLoader()); String picString = in.readString(); this.pic = (picString == null) ? "":picString; - String destroyedString = in.readString(); - this.destroyed = (destroyedString == null) ? "":destroyedString; + String existString = in.readString(); + this.exists = Boolean.parseBoolean(existString); } - public static Place from(NearbyResultItem item) { String itemClass = item.getClassName().getValue(); String classEntityId = ""; @@ -77,6 +80,7 @@ public class Place implements Parcelable { ? " (" + description + ")" : "") : description); return new Place( + item.getLabel().getLanguage(), item.getLabel().getValue(), Label.fromText(classEntityId), // list description, // description and label of Wikidata item @@ -88,7 +92,16 @@ public class Place implements Parcelable { .setWikidataLink(item.getItem().getValue()) .build(), item.getPic().getValue(), - item.getDestroyed().getValue()); + // Checking if the place exists or not + (item.getDestroyed().getValue() == "") && (item.getEndTime().getValue() == "")); + } + + /** + * Gets the language of the caption ie name. + * @return language + */ + public String getLanguage() { + return language; } /** @@ -192,6 +205,7 @@ public class Place implements Parcelable { public String toString() { return "Place{" + "name='" + name + '\'' + + ", lang='" + language + '\'' + ", label='" + label + '\'' + ", longDescription='" + longDescription + '\'' + ", location='" + location + '\'' + @@ -199,7 +213,7 @@ public class Place implements Parcelable { ", distance='" + distance + '\'' + ", siteLinks='" + siteLinks.toString() + '\'' + ", pic='" + pic + '\'' + - ", destroyed='" + destroyed + '\'' + + ", exists='" + exists.toString() + '\'' + '}'; } @@ -210,6 +224,7 @@ public class Place implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString(language); dest.writeString(name); dest.writeSerializable(label); dest.writeString(longDescription); @@ -217,7 +232,7 @@ public class Place implements Parcelable { dest.writeString(category); dest.writeParcelable(siteLinks, 0); dest.writeString(pic); - dest.writeString(destroyed); + dest.writeString(exists.toString()); } public static final Creator CREATOR = new Creator() { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java b/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java index 69d9f58cb..b3b5b5d9f 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/contract/NearbyParentFragmentContract.java @@ -22,7 +22,7 @@ public interface NearbyParentFragmentContract { void listOptionMenuItemClicked(); void populatePlaces(LatLng curlatLng); boolean isListBottomSheetExpanded(); - void checkPermissionsAndPerformAction(Runnable runnable); + void checkPermissionsAndPerformAction(); void displayLoginSkippedWarning(); void setFABPlusAction(android.view.View.OnClickListener onClickListener); void setFABRecenterAction(android.view.View.OnClickListener onClickListener); @@ -89,6 +89,7 @@ public interface NearbyParentFragmentContract { void detachView(); void setActionListeners(JsonKvStore applicationKvStore); + void removeNearbyPreferences(JsonKvStore applicationKvStore); boolean backButtonClicked(); void onCameraMove(com.mapbox.mapboxsdk.geometry.LatLng latLng); void filterByMarkerType(List