Merge branch 'master' into 3.0-release

This commit is contained in:
Josephine Lim 2021-06-09 20:18:42 +10:00 committed by GitHub
commit 3ce1343e64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
326 changed files with 11357 additions and 9719 deletions

View file

@ -25,13 +25,13 @@ We try to have an extensive documentation at our [documentation repository][4]:
Thank you all for your work!
| [<img src="https://avatars1.githubusercontent.com/u/3611199?v=4" width="100px;"/><br /><sub><b>misaochan</b></sub>](https://github.com/misaochan) | [<img src="https://avatars2.githubusercontent.com/u/30430?v=4" width="100px;"/><br /><sub><b>yuvipanda</b></sub>](https://github.com/yuvipanda) | [<img src="https://avatars1.githubusercontent.com/u/3127881?v=4" width="100px;"/><br /><sub><b>neslihanturan</b></sub>](https://github.com/neslihanturan) | [<img src="https://avatars2.githubusercontent.com/u/3069373?v=4" width="100px;"/><br /><sub><b>maskaravivek</b></sub>](https://github.com/maskaravivek) | [<img src="https://avatars3.githubusercontent.com/u/24829418?v=4" width="100px;"/><br /><sub><b>translatewiki</b></sub>](https://github.com/translatewiki) |
| [<img src="https://avatars.githubusercontent.com/u/3611199?v=4" width="100px;"/><br /><sub><b>misaochan</b></sub>](https://github.com/misaochan) | [<img src="https://avatars.githubusercontent.com/u/24829418?v=4" width="100px;"/><br /><sub><b>translatewiki</b></sub>](https://github.com/translatewiki) | [<img src="https://avatars.githubusercontent.com/u/3127881?v=4" width="100px;"/><br /><sub><b>neslihanturan</b></sub>](https://github.com/neslihanturan) | [<img src="https://avatars.githubusercontent.com/u/30430?v=4" width="100px;"/><br /><sub><b>yuvipanda</b></sub>](https://github.com/yuvipanda) | [<img src="https://avatars.githubusercontent.com/u/99590?v=4" width="100px;"/><br /><sub><b>nicolas-raoul</b></sub>](https://github.com/nicolas-raoul) |
| :---: | :---: | :---: | :---: | :---: |
| [<img src="https://avatars1.githubusercontent.com/u/99590?v=4" width="100px;"/><br /><sub><b>nicolas-raoul</b></sub>](https://github.com/nicolas-raoul) | [<img src="https://avatars0.githubusercontent.com/u/407647?v=4" width="100px;"/><br /><sub><b>psh</b></sub>](https://github.com/psh) | [<img src="https://avatars2.githubusercontent.com/u/103075?v=4" width="100px;"/><br /><sub><b>brion</b></sub>](https://github.com/brion) | [<img src="https://avatars3.githubusercontent.com/u/10674?v=4" width="100px;"/><br /><sub><b>whym</b></sub>](https://github.com/whym) | [<img src="https://avatars0.githubusercontent.com/u/4953590?v=4" width="100px;"/><br /><sub><b>domdomegg</b></sub>](https://github.com/domdomegg) |
| [<img src="https://avatars2.githubusercontent.com/u/10153800?v=4" width="100px;"/><br /><sub><b>akaita</b></sub>](https://github.com/akaita) | [<img src="https://avatars0.githubusercontent.com/u/6900601?v=4" width="100px;"/><br /><sub><b>veyndan</b></sub>](https://github.com/veyndan) | [<img src="https://avatars0.githubusercontent.com/u/19607555?v=4" width="100px;"/><br /><sub><b>ujjwalagrawal17</b></sub>](https://github.com/ujjwalagrawal17) | [<img src="https://avatars1.githubusercontent.com/u/1682214?v=4" width="100px;"/><br /><sub><b>dbrant</b></sub>](https://github.com/dbrant) | [<img src="https://avatars3.githubusercontent.com/u/1345681?v=4" width="100px;"/><br /><sub><b>sandarumk</b></sub>](https://github.com/sandarumk) |
| [<img src="https://avatars0.githubusercontent.com/u/6953323?v=4" width="100px;"/><br /><sub><b>tobias47n9e</b></sub>](https://github.com/tobias47n9e) | [<img src="https://avatars0.githubusercontent.com/u/29161745?v=4" width="100px;"/><br /><sub><b>tanvidadu</b></sub>](https://github.com/tanvidadu) | [<img src="https://avatars1.githubusercontent.com/u/25305892?v=4" width="100px;"/><br /><sub><b>hismaeel</b></sub>](https://github.com/hismaeel) | [<img src="https://avatars0.githubusercontent.com/u/12574756?v=4" width="100px;"/><br /><sub><b>tshradheya</b></sub>](https://github.com/tshradheya) | [<img src="https://avatars1.githubusercontent.com/u/27244688?v=4" width="100px;"/><br /><sub><b>diddypod</b></sub>](https://github.com/diddypod) |
| [<img src="https://avatars0.githubusercontent.com/u/32291277?v=4" width="100px;"/><br /><sub><b>prxjeen</b></sub>](https://github.com/prxjeen) | [<img src="https://avatars2.githubusercontent.com/u/3308769?v=4" width="100px;"/><br /><sub><b>addshore</b></sub>](https://github.com/addshore) | [<img src="https://avatars3.githubusercontent.com/u/20313518?v=4" width="100px;"/><br /><sub><b>knight-shade</b></sub>](https://github.com/knight-shade) | [<img src="https://avatars3.githubusercontent.com/u/17375274?v=4" width="100px;"/><br /><sub><b>ashishkumar468</b></sub>](https://github.com/ashishkumar468) | [<img src="https://avatars0.githubusercontent.com/u/210297?v=4" width="100px;"/><br /><sub><b>siebrand</b></sub>](https://github.com/siebrand) |
| [<img src="https://avatars0.githubusercontent.com/u/5329780?v=4" width="100px;"/><br /><sub><b>Bluesir9</b></sub>](https://github.com/Bluesir9) | [<img src="https://avatars1.githubusercontent.com/u/17095817?v=4" width="100px;"/><br /><sub><b>Jatin0312</b></sub>](https://github.com/Jatin0312) | [<img src="https://avatars3.githubusercontent.com/u/3415851?v=4" width="100px;"/><br /><sub><b>mashawan</b></sub>](https://github.com/mashawan) | [<img src="https://avatars3.githubusercontent.com/u/594179?v=4" width="100px;"/><br /><sub><b>ford-prefect</b></sub>](https://github.com/ford-prefect) | [<img src="https://avatars1.githubusercontent.com/u/21229885?v=4" width="100px;"/><br /><sub><b>seantnemann</b></sub>](https://github.com/seantnemann) |
| [<img src="https://avatars.githubusercontent.com/u/4953590?v=4" width="100px;"/><br /><sub><b>domdomegg</b></sub>](https://github.com/domdomegg) | [<img src="https://avatars.githubusercontent.com/u/3069373?v=4" width="100px;"/><br /><sub><b>maskaravivek</b></sub>](https://github.com/maskaravivek) | [<img src="https://avatars.githubusercontent.com/u/407647?v=4" width="100px;"/><br /><sub><b>psh</b></sub>](https://github.com/psh) | [<img src="https://avatars.githubusercontent.com/u/103075?v=4" width="100px;"/><br /><sub><b>brion</b></sub>](https://github.com/brion) | [<img src="https://avatars.githubusercontent.com/u/17375274?v=4" width="100px;"/><br /><sub><b>ashishkumar468</b></sub>](https://github.com/ashishkumar468) |
| [<img src="https://avatars.githubusercontent.com/u/10674?v=4" width="100px;"/><br /><sub><b>whym</b></sub>](https://github.com/whym) | [<img src="https://avatars.githubusercontent.com/u/10153800?v=4" width="100px;"/><br /><sub><b>akaita</b></sub>](https://github.com/akaita) | [<img src="https://avatars.githubusercontent.com/u/30932899?v=4" width="100px;"/><br /><sub><b>madhurgupta10</b></sub>](https://github.com/madhurgupta10) | [<img src="https://avatars.githubusercontent.com/u/6900601?v=4" width="100px;"/><br /><sub><b>veyndan</b></sub>](https://github.com/veyndan) | [<img src="https://avatars.githubusercontent.com/u/19607555?v=4" width="100px;"/><br /><sub><b>ujjwalagrawal17</b></sub>](https://github.com/ujjwalagrawal17) |
| [<img src="https://avatars.githubusercontent.com/u/3358282?v=4" width="100px;"/><br /><sub><b>macgills</b></sub>](https://github.com/macgills) | [<img src="https://avatars.githubusercontent.com/u/1682214?v=4" width="100px;"/><br /><sub><b>dbrant</b></sub>](https://github.com/dbrant) | [<img src="https://avatars.githubusercontent.com/u/34261945?v=4" width="100px;"/><br /><sub><b>vanshikaarora</b></sub>](https://github.com/vanshikaarora) | [<img src="https://avatars.githubusercontent.com/u/1345681?v=4" width="100px;"/><br /><sub><b>sandarumk</b></sub>](https://github.com/sandarumk) | [<img src="https://avatars.githubusercontent.com/u/29161745?v=4" width="100px;"/><br /><sub><b>tanvidadu</b></sub>](https://github.com/tanvidadu) |
| [<img src="https://avatars.githubusercontent.com/u/39745544?v=4" width="100px;"/><br /><sub><b>cypherop</b></sub>](https://github.com/cypherop) | [<img src="https://avatars.githubusercontent.com/u/6953323?v=4" width="100px;"/><br /><sub><b>tobias47n9e</b></sub>](https://github.com/tobias47n9e) | [<img src="https://avatars.githubusercontent.com/u/25305892?v=4" width="100px;"/><br /><sub><b>hismaeel</b></sub>](https://github.com/hismaeel) | [<img src="https://avatars.githubusercontent.com/u/12574756?v=4" width="100px;"/><br /><sub><b>tshradheya</b></sub>](https://github.com/tshradheya) | [<img src="https://avatars.githubusercontent.com/u/3308769?v=4" width="100px;"/><br /><sub><b>addshore</b></sub>](https://github.com/addshore) |
| [<img src="https://avatars.githubusercontent.com/u/20313518?v=4" width="100px;"/><br /><sub><b>knight-shade</b></sub>](https://github.com/knight-shade) | [<img src="https://avatars.githubusercontent.com/u/210297?v=4" width="100px;"/><br /><sub><b>siebrand</b></sub>](https://github.com/siebrand) | [<img src="https://avatars.githubusercontent.com/u/12448084?v=4" width="100px;"/><br /><sub><b>sivaraam</b></sub>](https://github.com/sivaraam) | [<img src="https://avatars.githubusercontent.com/u/5329780?v=4" width="100px;"/><br /><sub><b>Bluesir9</b></sub>](https://github.com/Bluesir9) | [<img src="https://avatars.githubusercontent.com/u/44129798?v=4" width="100px;"/><br /><sub><b>kbhardwaj123</b></sub>](https://github.com/kbhardwaj123) |
.. and [many more](https://github.com/commons-app/apps-android-commons/graphs/contributors).

View file

@ -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())

View file

@ -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))
}

View file

@ -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)
}
}

View file

@ -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))

View file

@ -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)
}
}

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<shortcut
android:enabled="true"
android:icon="@mipmap/ic_settings_black"
android:shortcutId="Setting"
android:shortcutLongLabel="@string/title_app_shortcut_setting"
android:shortcutShortLabel="@string/title_app_shortcut_setting"
tools:targetApi="n_mr1">
<intent
android:action="android.intent.action.VIEW"
android:targetClass="fr.free.nrw.commons.settings.SettingsActivity"
android:targetPackage="fr.free.nrw.commons.beta" />
</shortcut>
</shortcuts>

View file

@ -108,20 +108,16 @@
<activity android:name=".quiz.QuizResultActivity"
android:label="@string/result"/>
<activity
android:name=".category.CategoryImagesActivity"
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.MainActivity"
android:configChanges="orientation|screenSize|keyboard"/>
<activity
android:name=".category.CategoryDetailsActivity"
android:label="@string/title_activity_featured_images"
android:configChanges="screenSize|keyboard|orientation"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".explore.depictions.WikidataItemDetailsActivity"
android:label="@string/title_activity_featured_images"
android:configChanges="screenSize|keyboard|orientation"
android:parentActivityName=".contributions.MainActivity" />
<activity
@ -141,8 +137,7 @@
android:name=".review.ReviewActivity"
android:label="@string/title_activity_review" />
<service android:name=".upload.UploadService" />
<service
<service
android:name=".auth.WikiAccountAuthenticatorService"
android:exported="true"
android:process=":auth">

View file

@ -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<String, Boolean> pauseUploads = new HashMap<>();
/**
* Used to declare and initialize various components and dependencies
*/
@Override

View file

@ -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.

View file

@ -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<Fragment> fragmentList = new ArrayList<>();
private List<String> 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<Fragment> fragmentList, List<String> 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<Fragment> fragmentList = new ArrayList<>();
private List<String> 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<Fragment> fragmentList, List<String> 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);
}
}

View file

@ -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<Boolean> 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<Boolean> appendEdit(String pageTitle, String appendText, String summary) {
return Single.create((SingleOnSubscribe<String>) 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<Boolean> 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);
}
}
}

View file

@ -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<Boolean> {
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<Boolean> {
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<Boolean> {
return try {
pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.tokenBlocking)
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
} catch (throwable: Throwable) {
Observable.just(false)
}
}
}

View file

@ -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<Edit> 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<Edit> 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<Edit> postPrependEdit(@NonNull @Field("title") String title,
@NonNull @Field("summary") String summary,
@NonNull @Field("prependtext") String text,
@NonNull @Field("token") String token);
}

View file

@ -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<Edit>
/**
* 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<Edit>
/**
* 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<Edit>
}

View file

@ -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<Boolean> 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);
}
}
}

View file

@ -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<Boolean> {
return try {
service.thank(revisionId.toString(), null, csrfTokenClient.tokenBlocking, CommonsApplication.getInstance().userAgent)
.map { mwThankPostResponse -> mwThankPostResponse.result.success== 1 }
} catch (throwable: Throwable) {
Observable.just(false)
}
}
}

View file

@ -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<MwQueryResponse> call,
Response<MwQueryResponse> 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();

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -19,7 +19,14 @@ public class BookmarksPagerAdapter extends FragmentPagerAdapter {
private ArrayList<BookmarkPages> 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();
}

View file

@ -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);
}
}
}
}
}

View file

@ -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;

View file

@ -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);

View file

@ -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
}
}

View file

@ -38,4 +38,15 @@ abstract class ContinuationClient<Network, Domain> {
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")
}
}

View file

@ -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 {

View file

@ -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()
}
}
/**

View file

@ -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<UploadableFile> 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) {

View file

@ -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<List<Long>> save(List<Contribution> 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<List<Contribution>> getContribution(int state);
@Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)")
public abstract Single<Integer> updateStates(int state, int[] toUpdateStates);
@Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
public abstract Single<List<Contribution>> getContribution(List<Integer> states);
@Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)")
public abstract Single<Integer> getPendingUploads(int[] toUpdateStates);

View file

@ -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<ContributionsContract.View> {
@ -18,5 +21,6 @@ public class ContributionsContract {
void deleteUpload(Contribution contribution);
void saveContribution(Contribution contribution);
}
}

View file

@ -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);
}
}
};
}

View file

@ -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();
}
}

View file

@ -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<Contribution> contributionWithUri = contributionDao.getContributionWithTitle(uri);
public Contribution getContributionWithFileName(final String uri) {
final List<Contribution> 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<List<Long>> saveContributions(List<Contribution> contributions) {
List<Contribution> contributionList = new ArrayList<>();
for(Contribution contribution: contributions) {
Contribution oldContribution = contributionDao.getContribution(contribution.getPageId());
public Single<List<Long>> saveContributions(final List<Contribution> contributions) {
final List<Contribution> 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);
}
}

View file

@ -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));
}));
}
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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
)
}

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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<DepictedItem>() {
});
}
@TypeConverter
public static Date fromTimestamp(Long value) {
return value == null ? null : new Date(value);

View file

@ -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();

View file

@ -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<Object> androidInjector;
@Inject DispatchingAndroidInjector<Activity> activityInjector;
@Inject DispatchingAndroidInjector<BroadcastReceiver> broadcastReceiverInjector;
@Inject DispatchingAndroidInjector<android.app.Fragment> fragmentInjector;
@ -49,6 +52,11 @@ public class ApplicationlessInjection
commonsApplicationComponent.inject(this);
}
@Override
public AndroidInjector<Object> androidInjector() {
return androidInjector;
}
@Override
public DispatchingAndroidInjector<Activity> activityInjector() {
return activityInjector;
@ -94,5 +102,4 @@ public class ApplicationlessInjection
return instance;
}
}

View file

@ -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<ApplicationlessInjection> {
void inject(CommonsApplication application);
void inject(UploadWorker worker);
void inject(LoginActivity activity);
void inject(SettingsFragment fragment);

View file

@ -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();

View file

@ -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

View file

@ -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();

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -55,4 +55,14 @@ abstract class PageableMediaFragment : BasePagingFragment<Media>(), 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)
}
}

View file

@ -48,6 +48,16 @@ abstract class BasePagingFragment<T> : 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<PagedList<T>>) {
this.searchResults?.removeObservers(viewLifecycleOwner)
this.searchResults = searchResults

View file

@ -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);
}

View file

@ -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<String> 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,

View file

@ -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<MwQueryResponse>,
key: String?

View file

@ -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<String> categoryNames = new ArrayList<>();
private String categorySearchQuery;
@ -202,6 +218,20 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
private Media media;
private ArrayList<String> 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> 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<Boolean> 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<Boolean> 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<String> categories) {
@ -923,4 +1032,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
return true;
}
}
public interface Callback {
void nominatingForDeletion(int index);
}
}

View file

@ -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<Integer> removedItems=new ArrayList<Integer>();
public void clearRemoved(){
removedItems.clear();
}
public ArrayList<Integer> 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);
}
}
}

View file

@ -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;
}

View file

@ -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

View file

@ -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));

View file

@ -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<Place> CREATOR = new Creator<Place>() {

View file

@ -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<Label> selectedLabels, int state, boolean filterForPlaceState, boolean filterForAllNoneType);

View file

@ -8,6 +8,7 @@ import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -16,6 +17,7 @@ import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.drawable.VectorDrawable;
import android.location.LocationManager;
import android.os.Bundle;
import android.provider.Settings;
import android.text.Html;
@ -27,6 +29,7 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
@ -36,13 +39,13 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.SearchView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
import androidx.fragment.app.Fragment;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.recyclerview.widget.DividerItemDecoration;
@ -57,7 +60,7 @@ import com.google.android.material.chip.ChipGroup;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxSearchView;
import com.jakewharton.rxbinding3.appcompat.RxSearchView;
import com.mapbox.mapboxsdk.annotations.Icon;
import com.mapbox.mapboxsdk.annotations.IconFactory;
import com.mapbox.mapboxsdk.annotations.Marker;
@ -188,6 +191,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
private Animation rotate_forward;
private static final float ZOOM_LEVEL = 14f;
private static final float ZOOM_OUT = 0f;
private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
private BroadcastReceiver broadcastReceiver;
private boolean isNetworkErrorOccurred;
@ -203,6 +207,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.004;
private boolean isMapBoxReady;
private boolean isPermissionDenied;
private boolean recenterToUserLocation;
private MapboxMap mapBox;
IntentFilter intentFilter = new IntentFilter(NETWORK_INTENT_ACTION);
private Marker currentLocationMarker;
@ -256,6 +262,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
cameraMoveListener= () -> presenter.onCameraMove(mapBox.getCameraPosition().target);
addCheckBoxCallback();
presenter.attachView(this);
isPermissionDenied = false;
recenterToUserLocation = false;
initRvNearbyList();
initThemePreferences();
mapView.onCreate(savedInstanceState);
@ -278,7 +286,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
performMapReadyActions();
final CameraPosition cameraPosition = new CameraPosition.Builder()
.target(new LatLng(51.50550, -0.07520))
.zoom(ZOOM_LEVEL)
.zoom(ZOOM_OUT)
.build();
mapBoxMap.setCameraPosition(cameraPosition);
@ -339,28 +347,45 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
private void performMapReadyActions() {
if (((MainActivity)getActivity()).activeFragment == ActiveFragment.NEARBY && isMapBoxReady) {
checkPermissionsAndPerformAction(() -> {
lastKnownLocation = locationManager.getLastLocation();
fr.free.nrw.commons.location.LatLng target=lastFocusLocation;
if(null==lastFocusLocation){
target=lastKnownLocation;
}
if (lastKnownLocation != null) {
final CameraPosition position = new CameraPosition.Builder()
.target(LocationUtils.commonsLatLngToMapBoxLatLng(target)) // Sets the new camera position
.zoom(ZOOM_LEVEL) // Same zoom level
.build();
mapBox.moveCamera(CameraUpdateFactory.newCameraPosition(position));
} else {
Toast.makeText(getContext(), getString(R.string.nearby_location_not_available), Toast.LENGTH_LONG).show();
}
presenter.onMapReady();
registerUnregisterLocationListener(false);
if(!applicationKvStore.getBoolean("doNotAskForLocationPermission", false) ||
PermissionUtils.hasPermission(getActivity(), Manifest.permission.ACCESS_FINE_LOCATION)){
checkPermissionsAndPerformAction();
}else{
isPermissionDenied = true;
addOnCameraMoveListener();
});
}
}
}
private void locationPermissionGranted() {
isPermissionDenied = false;
applicationKvStore.putBoolean("doNotAskForLocationPermission", false);
lastKnownLocation = locationManager.getLastLocation();
fr.free.nrw.commons.location.LatLng target=lastFocusLocation;
if(null==lastFocusLocation){
target=lastKnownLocation;
}
if (lastKnownLocation != null) {
final CameraPosition position = new CameraPosition.Builder()
.target(LocationUtils.commonsLatLngToMapBoxLatLng(target)) // Sets the new camera position
.zoom(ZOOM_LEVEL) // Same zoom level
.build();
mapBox.moveCamera(CameraUpdateFactory.newCameraPosition(position));
}
else if(locationManager.isGPSProviderEnabled()||locationManager.isNetworkProviderEnabled()){
locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER);
locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
setProgressBarVisibility(true);
}
else {
Toast.makeText(getContext(), getString(R.string.nearby_location_not_available), Toast.LENGTH_LONG).show();
}
presenter.onMapReady();
registerUnregisterLocationListener(false);
addOnCameraMoveListener();
}
@Override
public void onResume() {
super.onResume();
@ -368,10 +393,31 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
presenter.attachView(this);
registerNetworkReceiver();
if (isResumed() && ((MainActivity)getActivity()).activeFragment == ActiveFragment.NEARBY) {
startTheMap();
if(!isPermissionDenied && !applicationKvStore.getBoolean("doNotAskForLocationPermission", false)){
startTheMap();
}else{
startMapWithoutPermission();
}
}
}
private void startMapWithoutPermission() {
mapView.onStart();
applicationKvStore.putBoolean("doNotAskForLocationPermission", true);
lastKnownLocation = new fr.free.nrw.commons.location.LatLng(51.50550,-0.07520,1f);
final CameraPosition position = new CameraPosition.Builder()
.target(LocationUtils.commonsLatLngToMapBoxLatLng(lastKnownLocation))
.zoom(ZOOM_OUT)
.build();
if(mapBox != null){
mapBox.moveCamera(CameraUpdateFactory.newCameraPosition(position));
addOnCameraMoveListener();
}
presenter.onMapReady();
removeCurrentLocationMarker();
}
private void registerNetworkReceiver() {
if (getActivity() != null) {
getActivity().registerReceiver(broadcastReceiver, intentFilter);
@ -426,6 +472,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
public void onDestroyView() {
super.onDestroyView();
mapView.onDestroy();
presenter.removeNearbyPreferences(applicationKvStore);
}
private void initViews() {
@ -438,14 +485,41 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
}
/**
* Creates bottom sheet behaviours from bottom sheets, sets initial states and visibility
* a) Creates bottom sheet behaviours from bottom sheets, sets initial states and visibility
* b) Gets the touch event on the map to perform following actions:
* if fab is open then close fab.
* if bottom sheet details are expanded then collapse bottom sheet details.
* if bottom sheet details are collapsed then hide the bottom sheet details.
* if listBottomSheet is open then hide the list bottom sheet.
*/
@SuppressLint("ClickableViewAccessibility")
private void initBottomSheets() {
bottomSheetListBehavior = BottomSheetBehavior.from(rlBottomSheet);
bottomSheetDetailsBehavior = BottomSheetBehavior.from(bottomSheetDetails);
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetDetails.setVisibility(View.VISIBLE);
bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
mapView.setOnTouchListener((v, event) -> {
// Motion event is triggered two times on a touch event, one as ACTION_UP
// and other as ACTION_DOWN, we only want one trigger per touch event.
if(event.getAction() == MotionEvent.ACTION_DOWN) {
if (isFABsExpanded) {
collapseFABs(true);
} else if (bottomSheetDetailsBehavior.getState()
== BottomSheetBehavior.STATE_EXPANDED) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else if (bottomSheetDetailsBehavior.getState()
== BottomSheetBehavior.STATE_COLLAPSED) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
} else if (isListBottomSheetExpanded()) {
bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
}
return false;
});
}
public void initNearbyFilter() {
@ -758,11 +832,14 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
@Override
public void populatePlaces(final fr.free.nrw.commons.location.LatLng curlatLng) {
if (curlatLng.equals(lastFocusLocation)|| lastFocusLocation==null) { // Means we are checking around current location
if (curlatLng.equals(lastFocusLocation) || lastFocusLocation == null || recenterToUserLocation) { // Means we are checking around current location
populatePlacesForCurrentLocation(lastKnownLocation, curlatLng);
} else {
populatePlacesForAnotherLocation(lastKnownLocation, curlatLng);
}
if(recenterToUserLocation) {
recenterToUserLocation = false;
}
}
private void populatePlacesForCurrentLocation(final fr.free.nrw.commons.location.LatLng curlatLng,
@ -881,12 +958,12 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
}
@Override
public void checkPermissionsAndPerformAction(final Runnable runnable) {
public void checkPermissionsAndPerformAction() {
Timber.d("Checking permission and perfoming action");
PermissionUtils.checkPermissionsAndPerformAction(getActivity(),
Manifest.permission.ACCESS_FINE_LOCATION,
runnable,
() -> ((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code()),
() -> locationPermissionGranted(),
() -> isPermissionDenied = true,
R.string.location_permission_title,
R.string.location_permission_rationale_nearby);
}
@ -1058,7 +1135,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
*/
@Override
public void addCurrentLocationMarker(final fr.free.nrw.commons.location.LatLng curLatLng) {
if (null != curLatLng) {
if (null != curLatLng && !isPermissionDenied) {
ExecutorUtils.get().submit(() -> {
mapView.post(() -> removeCurrentLocationMarker());
Timber.d("Adds current location marker");
@ -1104,8 +1181,15 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
@Override
public void updateMapToTrackPosition(final fr.free.nrw.commons.location.LatLng curLatLng) {
Timber.d("Updates map camera to track user position");
final CameraPosition cameraPosition = new CameraPosition.Builder().target
final CameraPosition cameraPosition;
if(isPermissionDenied){
cameraPosition = new CameraPosition.Builder().target
(LocationUtils.commonsLatLngToMapBoxLatLng(curLatLng)).build();
}else{
cameraPosition = new CameraPosition.Builder().target
(LocationUtils.commonsLatLngToMapBoxLatLng(curLatLng))
.zoom(ZOOM_LEVEL).build();
}
if(null!=mapBox) {
mapBox.setCameraPosition(cameraPosition);
mapBox.animateCamera(CameraUpdateFactory
@ -1170,12 +1254,12 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
if (displayExists && displayNeedsPhoto) {
// Exists and needs photo
if (place.destroyed.trim().isEmpty() && place.pic.trim().isEmpty()) {
if (place.exists && place.pic.trim().isEmpty()) {
updateMarker(markerPlaceGroup.getIsBookmarked(), place, NearbyController.currentLocation);
}
} else if (displayExists && !displayNeedsPhoto) {
// Exists and all included needs and doesn't needs photo
if (place.destroyed.trim().isEmpty()) {
if (place.exists) {
updateMarker(markerPlaceGroup.getIsBookmarked(), place, NearbyController.currentLocation);
}
} else if (!displayExists && displayNeedsPhoto) {
@ -1233,7 +1317,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
return (isBookmarked ?
R.drawable.ic_custom_map_marker_green_bookmarked :
R.drawable.ic_custom_map_marker_green);
} 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
return (isBookmarked ?
R.drawable.ic_custom_map_marker_grey_bookmarked :
R.drawable.ic_custom_map_marker_grey);
@ -1289,8 +1373,10 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
@Override
public void recenterMap(final fr.free.nrw.commons.location.LatLng curLatLng) {
if (curLatLng == null) {
if (!(locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled())) {
if (isPermissionDenied || curLatLng == null) {
recenterToUserLocation = true;
checkPermissionsAndPerformAction();
if (!isPermissionDenied && !(locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled())) {
showLocationOffDialog();
}
return;
@ -1548,4 +1634,12 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
public void setNearbyParentFragmentInstanceReadyCallback(NearbyParentFragmentInstanceReadyCallback nearbyParentFragmentInstanceReadyCallback) {
this.nearbyParentFragmentInstanceReadyCallback = nearbyParentFragmentInstanceReadyCallback;
}
@Override
public void onConfigurationChanged(@NonNull final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
ViewGroup.LayoutParams rlBottomSheetLayoutParams = rlBottomSheet.getLayoutParams();
rlBottomSheetLayoutParams.height = getActivity().getWindowManager().getDefaultDisplay().getHeight() / 16 * 9;
rlBottomSheet.setLayoutParams(rlBottomSheetLayoutParams);
}
}

View file

@ -12,7 +12,8 @@ class NearbyResultItem(private val item: ResultTuple?,
@field:SerializedName("commonsCategory") private val commonsCategory: ResultTuple?,
@field:SerializedName("pic") private val pic: ResultTuple?,
@field:SerializedName("destroyed") private val destroyed: ResultTuple?,
@field:SerializedName("description") private val description: ResultTuple?) {
@field:SerializedName("description") private val description: ResultTuple?,
@field:SerializedName("endTime") private val endTime: ResultTuple?) {
fun getItem(): ResultTuple {
return item ?: ResultTuple()
@ -60,6 +61,9 @@ class NearbyResultItem(private val item: ResultTuple?,
fun getDescription(): ResultTuple {
return description ?: ResultTuple()
fun getEndTime(): ResultTuple {
return endTime ?: ResultTuple()
}
}

View file

@ -1,15 +1,21 @@
package fr.free.nrw.commons.nearby.model
import com.google.gson.annotations.SerializedName
class ResultTuple {
@SerializedName("xml:lang")
val language: String
val type: String
val value: String
constructor(type: String, value: String) {
constructor(lang: String, type: String, value: String) {
this.language = lang
this.type = type
this.value = value
}
constructor() {
language = ""
type = ""
value = ""
}

View file

@ -33,6 +33,7 @@ import static fr.free.nrw.commons.location.LocationServiceManager.LocationChange
import static fr.free.nrw.commons.nearby.CheckBoxTriStates.CHECKED;
import static fr.free.nrw.commons.nearby.CheckBoxTriStates.UNCHECKED;
import static fr.free.nrw.commons.nearby.CheckBoxTriStates.UNKNOWN;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
public class NearbyParentFragmentPresenter
implements NearbyParentFragmentContract.UserActions,
@ -83,6 +84,12 @@ public class NearbyParentFragmentPresenter
this.nearbyParentFragmentView=DUMMY;
}
@Override
public void removeNearbyPreferences(JsonKvStore applicationKvStore) {
Timber.d("Remove place objects");
applicationKvStore.remove(PLACE_OBJECT);
}
public void initializeMapOperations() {
lockUnlockNearby(false);
updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED);

View file

@ -65,10 +65,15 @@ public class NotificationActivity extends BaseActivity {
private NotificatinAdapter adapter;
private List<Notification> notificationList;
MenuItem notificationMenuItem;
/**
* Boolean isRead is true if this notification activity is for read section of notification.
*/
private boolean isRead;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
isRead = getIntent().getStringExtra("title").equals("read");
setContentView(R.layout.activity_notification);
ButterKnife.bind(this);
mNotificationWorkerFragment = (NotificationWorkerFragment) getFragmentManager()
@ -85,8 +90,21 @@ public class NotificationActivity extends BaseActivity {
return true;
}
/**
* If this is unread section of the notifications, removeNotification method
* Marks the notification as read,
* Removes the notification from unread,
* Displays the Snackbar.
*
* Otherwise returns (read section).
*
* @param notification
*/
@SuppressLint("CheckResult")
public void removeNotification(Notification notification) {
if (isRead) {
return;
}
Disposable disposable = Observable.defer((Callable<ObservableSource<Boolean>>)
() -> controller.markAsRead(notification))
.subscribeOn(Schedulers.io())
@ -126,7 +144,7 @@ public class NotificationActivity extends BaseActivity {
recyclerView.setLayoutManager(new LinearLayoutManager(this));
DividerItemDecoration itemDecor = new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL);
recyclerView.addItemDecoration(itemDecor);
if (getIntent().getStringExtra("title").equals("read")) {
if (isRead) {
refresh(true);
} else {
refresh(false);
@ -240,7 +258,7 @@ public class NotificationActivity extends BaseActivity {
private void setPageTitle() {
if (getSupportActionBar() != null) {
if (getIntent().getStringExtra("title").equals("read")) {
if (isRead) {
getSupportActionBar().setTitle(R.string.read_notifications);
} else {
getSupportActionBar().setTitle(R.string.notifications);
@ -249,7 +267,7 @@ public class NotificationActivity extends BaseActivity {
}
private void setEmptyView() {
if (getIntent().getStringExtra("title").equals("read")) {
if (isRead) {
noNotificationText.setText(R.string.no_read_notification);
}else {
noNotificationText.setText(R.string.no_notification);
@ -257,7 +275,7 @@ public class NotificationActivity extends BaseActivity {
}
private void setMenuItemTitle() {
if (getIntent().getStringExtra("title").equals("read")) {
if (isRead) {
notificationMenuItem.setTitle(R.string.menu_option_unread);
}else {

View file

@ -10,6 +10,7 @@ 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.profile.achievements.AchievementsFragment;
import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment;
import fr.free.nrw.commons.theme.BaseActivity;

View file

@ -1,57 +0,0 @@
package fr.free.nrw.commons.profile;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import java.util.ArrayList;
import java.util.List;
/**
* This View Pager Adapter will set the fragments for profile activity
*/
public class ViewPagerAdapter extends FragmentPagerAdapter {
private List<Fragment> fragmentList = new ArrayList<>();
private List<String> 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<Fragment> fragmentList, List<String> 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);
}
}

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.profile.achievements;
import android.accounts.Account;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
@ -36,7 +37,6 @@ import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.dinuscxj.progressbar.CircleProgressBar;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager;
@ -46,12 +46,6 @@ import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Objects;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import timber.log.Timber;
/**
@ -62,6 +56,17 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
private static final double BADGE_IMAGE_WIDTH_RATIO = 0.4;
private static final double BADGE_IMAGE_HEIGHT_RATIO = 0.3;
/**
* Help link URLs
*/
private static final String IMAGES_UPLOADED_URL = "https://commons.wikimedia.org/wiki/Commons:Project_scope";
private static final String IMAGES_REVERT_URL = "https://commons.wikimedia.org/wiki/Commons:Deletion_policy#Reasons_for_deletion";
private static final String IMAGES_USED_URL = "https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images";
private static final String IMAGES_NEARBY_PLACES_URL = "https://www.wikidata.org/wiki/Property:P18";
private static final String IMAGES_FEATURED_URL = "https://commons.wikimedia.org/wiki/Commons:Featured_pictures";
private static final String QUALITY_IMAGE_URL = "https://commons.wikimedia.org/wiki/Commons:Quality_images";
private static final String THANKS_URL = "https://www.mediawiki.org/wiki/Extension:Thanks";
private LevelController.LevelInfo levelInfo;
@BindView(R.id.achievement_badge_image)
@ -242,8 +247,9 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
*/
@OnClick(R.id.achievement_info)
public void showInfoDialog(){
launchAlert(getResources().getString(R.string.Achievements)
,getResources().getString(R.string.achievements_info_message));
launchAlert(
getResources().getString(R.string.Achievements),
getResources().getString(R.string.achievements_info_message));
}
/**
@ -456,44 +462,58 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
@OnClick(R.id.images_upload_info)
public void showUploadInfo(){
launchAlert(getResources().getString(R.string.images_uploaded)
,getResources().getString(R.string.images_uploaded_explanation));
launchAlertWithHelpLink(
getResources().getString(R.string.images_uploaded),
getResources().getString(R.string.images_uploaded_explanation),
IMAGES_UPLOADED_URL);
}
@OnClick(R.id.images_reverted_info)
public void showRevertedInfo(){
launchAlert(getResources().getString(R.string.image_reverts)
,getResources().getString(R.string.images_reverted_explanation));
launchAlertWithHelpLink(
getResources().getString(R.string.image_reverts),
getResources().getString(R.string.images_reverted_explanation),
IMAGES_REVERT_URL);
}
@OnClick(R.id.images_used_by_wiki_info)
public void showUsedByWikiInfo(){
launchAlert(getResources().getString(R.string.images_used_by_wiki)
,getResources().getString(R.string.images_used_explanation));
launchAlertWithHelpLink(
getResources().getString(R.string.images_used_by_wiki),
getResources().getString(R.string.images_used_explanation),
IMAGES_USED_URL);
}
@OnClick(R.id.images_nearby_info)
public void showImagesViaNearbyInfo(){
launchAlert(getResources().getString(R.string.statistics_wikidata_edits)
,getResources().getString(R.string.images_via_nearby_explanation));
launchAlertWithHelpLink(
getResources().getString(R.string.statistics_wikidata_edits),
getResources().getString(R.string.images_via_nearby_explanation),
IMAGES_NEARBY_PLACES_URL);
}
@OnClick(R.id.images_featured_info)
public void showFeaturedImagesInfo(){
launchAlert(getResources().getString(R.string.statistics_featured)
,getResources().getString(R.string.images_featured_explanation));
launchAlertWithHelpLink(
getResources().getString(R.string.statistics_featured),
getResources().getString(R.string.images_featured_explanation),
IMAGES_FEATURED_URL);
}
@OnClick(R.id.thanks_received_info)
public void showThanksReceivedInfo(){
launchAlert(getResources().getString(R.string.statistics_thanks)
,getResources().getString(R.string.thanks_received_explanation));
launchAlertWithHelpLink(
getResources().getString(R.string.statistics_thanks),
getResources().getString(R.string.thanks_received_explanation),
THANKS_URL);
}
@OnClick(R.id.quality_images_info)
public void showQualityImagesInfo() {
launchAlert(getResources().getString(R.string.statistics_quality)
, getResources().getString(R.string.quality_images_info));
launchAlertWithHelpLink(
getResources().getString(R.string.statistics_quality),
getResources().getString(R.string.quality_images_info),
QUALITY_IMAGE_URL);
}
/**
@ -511,6 +531,22 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
.show();
}
/**
* Launch Alert with a READ MORE button and clicking it open a custom webpage
*/
private void launchAlertWithHelpLink(String title, String message, String helpLinkUrl){
new Builder(getActivity())
.setTitle(title)
.setMessage(message)
.setCancelable(true)
.setPositiveButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
.setNegativeButton(R.string.read_help_link, (dialog ,id) ->{
Utils.handleWebUrl(requireContext(), Uri.parse(helpLinkUrl));;
})
.create()
.show();
}
/**
* check to ensure that user is logged in
* @return

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.repository;
import fr.free.nrw.commons.category.CategoriesModel;
import fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.NearbyPlaces;
@ -36,18 +37,21 @@ public class UploadRepository {
private final DepictModel depictModel;
private static final double NEARBY_RADIUS_IN_KILO_METERS = 0.1; //100 meters
private final ContributionDao contributionDao;
@Inject
public UploadRepository(UploadModel uploadModel,
UploadController uploadController,
CategoriesModel categoriesModel,
NearbyPlaces nearbyPlaces,
DepictModel depictModel) {
DepictModel depictModel,
ContributionDao contributionDao) {
this.uploadModel = uploadModel;
this.uploadController = uploadController;
this.categoriesModel = categoriesModel;
this.nearbyPlaces = nearbyPlaces;
this.depictModel = depictModel;
this.contributionDao=contributionDao;
}
/**
@ -64,8 +68,14 @@ public class UploadRepository {
*
* @param contribution
*/
public void startUpload(Contribution contribution) {
uploadController.startUpload(contribution);
public void prepareMedia(Contribution contribution) {
uploadController.prepareMedia(contribution);
}
public void saveContribution(Contribution contribution) {
contributionDao.save(contribution).blockingAwait();
}
/**
@ -77,13 +87,6 @@ public class UploadRepository {
return uploadModel.getUploads();
}
/**
* asks the RemoteDataSource to prepare the Upload Service
*/
public void prepareService() {
uploadController.prepareService();
}
/**
*Prepare for a fresh upload
*/
@ -205,16 +208,16 @@ public class UploadRepository {
}
/**
* fetches and returns the previous upload item
* fetches and returns the upload item
*
* @param index
* @return
*/
public UploadItem getPreviousUploadItem(int index) {
if (index - 1 >= 0) {
return uploadModel.getItems().get(index - 1);
public UploadItem getUploadItem(int index) {
if (index >= 0) {
return uploadModel.getItems().get(index);
}
return null; //There is no previous item to copy details
return null; //There is no item to copy details
}
/**

View file

@ -12,10 +12,14 @@ import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.widget.Toolbar;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.facebook.drawee.view.SimpleDraweeView;
@ -23,6 +27,7 @@ import com.viewpagerindicator.CirclePageIndicator;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.delete.DeleteHelper;
import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ViewUtil;
@ -49,12 +54,22 @@ public class ReviewActivity extends BaseActivity {
ProgressBar progressBar;
@BindView(R.id.tv_image_caption)
TextView imageCaption;
@BindView(R.id.mediaDetailContainer)
FrameLayout mediaDetailContainer;
MediaDetailFragment mediaDetailFragment;
@BindView(R.id.reviewActivityContainer)
LinearLayout reviewContainer;
public ReviewPagerAdapter reviewPagerAdapter;
public ReviewController reviewController;
@Inject
ReviewHelper reviewHelper;
@Inject
DeleteHelper deleteHelper;
/**
* Represent fragment for ReviewImage
* Use to call some methods of ReviewImage fragment
*/
private ReviewImageFragment reviewImageFragment;
final String SAVED_MEDIA = "saved_media";
private Media media;
@ -98,25 +113,27 @@ public class ReviewActivity extends BaseActivity {
reviewPagerAdapter = new ReviewPagerAdapter(getSupportFragmentManager());
reviewPager.setAdapter(reviewPagerAdapter);
reviewPagerAdapter.getItem(0);
pagerIndicator.setViewPager(reviewPager);
progressBar.setVisibility(View.VISIBLE);
Drawable d[]=btnSkipImage.getCompoundDrawablesRelative();
d[2].setColorFilter(getApplicationContext().getResources().getColor(R.color.button_blue), PorterDuff.Mode.SRC_IN);
if (savedInstanceState != null && savedInstanceState.getParcelable(SAVED_MEDIA)!=null) {
if (savedInstanceState != null && savedInstanceState.getParcelable(SAVED_MEDIA) != null) {
updateImage(savedInstanceState.getParcelable(SAVED_MEDIA)); // Use existing media if we have one
setUpMediaDetailOnOrientation();
} else {
runRandomizer(); //Run randomizer whenever everything is ready so that a first random image will be added
}
btnSkipImage.setOnClickListener(view -> {
reviewPagerAdapter.disableButtons();
reviewImageFragment = getInstanceOfReviewImageFragment();
reviewImageFragment.disableButtons();
runRandomizer();
});
simpleDraweeView.setOnClickListener(view ->setUpMediaDetailFragment());
btnSkipImage.setOnTouchListener((view, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP && event.getRawX() >= (
btnSkipImage.getRight() - btnSkipImage
@ -142,7 +159,8 @@ public class ReviewActivity extends BaseActivity {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> {
reviewPagerAdapter.disableButtons();
reviewImageFragment = getInstanceOfReviewImageFragment();
reviewImageFragment.disableButtons();
updateImage(media);
}));
return true;
@ -169,7 +187,8 @@ public class ReviewActivity extends BaseActivity {
String caption = String.format(getString(R.string.review_is_uploaded_by), fileName, revision.getUser());
imageCaption.setText(caption);
progressBar.setVisibility(View.GONE);
reviewPagerAdapter.enableButtons();
reviewImageFragment = getInstanceOfReviewImageFragment();
reviewImageFragment.enableButtons();
}));
reviewPager.setCurrentItem(0);
}
@ -226,4 +245,57 @@ public class ReviewActivity extends BaseActivity {
}
return super.onOptionsItemSelected(item);
}
/**
* this function return the instance of reviewImageFragment
*/
public ReviewImageFragment getInstanceOfReviewImageFragment(){
int currentItemOfReviewPager = reviewPager.getCurrentItem();
reviewImageFragment = (ReviewImageFragment) reviewPagerAdapter.instantiateItem(reviewPager, currentItemOfReviewPager);
return reviewImageFragment;
}
/**
* set up the media detail fragment when click on the review image
*/
private void setUpMediaDetailFragment() {
if (mediaDetailContainer.getVisibility() == View.GONE && media != null) {
mediaDetailContainer.setVisibility(View.VISIBLE);
reviewContainer.setVisibility(View.INVISIBLE);
FragmentManager fragmentManager = getSupportFragmentManager();
mediaDetailFragment = new MediaDetailFragment();
Bundle bundle = new Bundle();
bundle.putParcelable("media", media);
mediaDetailFragment.setArguments(bundle);
fragmentManager.beginTransaction().add(R.id.mediaDetailContainer, mediaDetailFragment).
addToBackStack("MediaDetail").commit();
}
}
/**
* handle the back pressed event of this activity
* this function call every time when back button is pressed
*/
@Override
public void onBackPressed() {
if (mediaDetailContainer.getVisibility() == View.VISIBLE) {
mediaDetailContainer.setVisibility(View.GONE);
reviewContainer.setVisibility(View.VISIBLE);
}
super.onBackPressed();
}
/**
* set up media detail fragment after orientation change
*/
private void setUpMediaDetailOnOrientation() {
Fragment mediaDetailFragment = getSupportFragmentManager()
.findFragmentById(R.id.mediaDetailContainer);
if (mediaDetailFragment != null) {
mediaDetailContainer.setVisibility(View.VISIBLE);
reviewContainer.setVisibility(View.INVISIBLE);
getSupportFragmentManager().beginTransaction()
.replace(R.id.mediaDetailContainer, mediaDetailFragment).commit();
}
}
}

View file

@ -43,7 +43,7 @@ public class ReviewImageFragment extends CommonsDaggerSupportFragment {
// Constant variable used to store user's key name for onSaveInstanceState method
private final String SAVED_USER = "saved_user";
//Variable that stores the value of user
// Variable that stores the value of user
private String user;
public void update(int position) {
@ -81,18 +81,18 @@ public class ReviewImageFragment extends CommonsDaggerSupportFragment {
case SPAM:
question = getString(R.string.review_spam);
explanation = getString(R.string.review_spam_explanation);
yesButtonText = getString(R.string.review_spam_yes_button_text);
noButtonText = getString(R.string.review_spam_no_button_text);
yesButton.setOnClickListener(view -> getReviewActivity()
yesButtonText = getString(R.string.yes);
noButtonText = getString(R.string.no);
noButton.setOnClickListener(view -> getReviewActivity()
.reviewController.reportSpam(requireActivity(), getReviewCallback()));
break;
case COPYRIGHT:
enableButtons();
question = getString(R.string.review_copyright);
explanation = getString(R.string.review_copyright_explanation);
yesButtonText = getString(R.string.review_copyright_yes_button_text);
noButtonText = getString(R.string.review_copyright_no_button_text);
yesButton.setOnClickListener(view -> getReviewActivity()
yesButtonText = getString(R.string.yes);
noButtonText = getString(R.string.no);
noButton.setOnClickListener(view -> getReviewActivity()
.reviewController
.reportPossibleCopyRightViolation(requireActivity(), getReviewCallback()));
break;
@ -100,9 +100,9 @@ public class ReviewImageFragment extends CommonsDaggerSupportFragment {
enableButtons();
question = getString(R.string.review_category);
explanation = updateCategoriesQuestion();
yesButtonText = getString(R.string.review_category_yes_button_text);
noButtonText = getString(R.string.review_category_no_button_text);
yesButton.setOnClickListener(view -> {
yesButtonText = getString(R.string.yes);
noButtonText = getString(R.string.no);
noButton.setOnClickListener(view -> {
getReviewActivity()
.reviewController
.reportWrongCategory(requireActivity(), getReviewCallback());
@ -113,7 +113,7 @@ public class ReviewImageFragment extends CommonsDaggerSupportFragment {
enableButtons();
question = getString(R.string.review_thanks);
//Get existing user name if it is already saved using savedInstanceState else get from reviewController
// Get existing user name if it is already saved using savedInstanceState else get from reviewController
if (savedInstanceState == null) {
if (getReviewActivity().reviewController.firstRevision != null) {
user = getReviewActivity().reviewController.firstRevision.getUser();
@ -127,11 +127,12 @@ public class ReviewImageFragment extends CommonsDaggerSupportFragment {
explanation = getString(R.string.review_thanks_explanation, user);
}
// Note that the yes and no buttons are swapped in this section
yesButtonText = getString(R.string.review_thanks_yes_button_text);
noButtonText = getString(R.string.review_thanks_no_button_text);
yesButton.setTextColor(Color.parseColor("#228b22"));
noButton.setTextColor(Color.parseColor("#116aaa"));
yesButton.setOnClickListener(view -> {
yesButton.setTextColor(Color.parseColor("#116aaa"));
noButton.setTextColor(Color.parseColor("#228b22"));
noButton.setOnClickListener(view -> {
getReviewActivity().reviewController.sendThanks(getReviewActivity());
getReviewActivity().swipeToNext();
});
@ -202,8 +203,8 @@ public class ReviewImageFragment extends CommonsDaggerSupportFragment {
noButton.setAlpha(0.5f);
}
@OnClick(R.id.button_no)
void onNoButtonClicked() {
@OnClick(R.id.button_yes)
void onYesButtonClicked() {
getReviewActivity().swipeToNext();
}

View file

@ -2,6 +2,8 @@ package fr.free.nrw.commons.review;
import android.os.Bundle;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
@ -9,6 +11,13 @@ import androidx.fragment.app.FragmentStatePagerAdapter;
public class ReviewPagerAdapter extends FragmentStatePagerAdapter {
private ReviewImageFragment[] reviewImageFragments;
/**
* this function return the instance of ReviewviewPage current item
*/
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
return super.instantiateItem(container, position);
}
ReviewPagerAdapter(FragmentManager fm) {
super(fm);
@ -32,25 +41,6 @@ public class ReviewPagerAdapter extends FragmentStatePagerAdapter {
}
}
/**
* This function is called when an image has
* been loaded to enable the review buttons.
*/
public void enableButtons() {
if (reviewImageFragments != null){
reviewImageFragments[0].enableButtons();
}
}
/**
* This function is called when an image is being loaded
* to disable the review buttons
*/
public void disableButtons() {
if (reviewImageFragments != null){
reviewImageFragments[0].disableButtons();
}
}
@Override
public Fragment getItem(int position) {

View file

@ -7,7 +7,8 @@ public class Prefs {
public static final String DEFAULT_LICENSE = "defaultLicense";
public static final String UPLOADS_SHOWING = "uploadsshowing";
public static final String MANAGED_EXIF_TAGS = "managed_exif_tags";
public static final String KEY_LANGUAGE_VALUE = "languageDescription";
public static final String DESCRIPTION_LANGUAGE = "descriptionLanguage";
public static final String APP_UI_LANGUAGE = "appUiLanguage";
public static final String KEY_THEME_VALUE = "appThemePref";
public static final String TELEMETRY_PREFERENCE = "telemetryPref";

View file

@ -1,16 +1,23 @@
package fr.free.nrw.commons.settings;
import static android.content.Context.MODE_PRIVATE;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.text.InputFilter;
import android.text.InputType;
import androidx.preference.EditTextPreference;
import android.view.View;
import androidx.preference.ListPreference;
import androidx.preference.MultiSelectListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.google.android.material.snackbar.Snackbar;
import androidx.preference.PreferenceGroupAdapter;
import androidx.preference.PreferenceScreen;
import androidx.preference.PreferenceViewHolder;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import com.karumi.dexter.Dexter;
import com.karumi.dexter.listener.PermissionGrantedResponse;
import com.karumi.dexter.listener.single.BasePermissionListener;
@ -18,19 +25,20 @@ import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.maps.TelemetryDefinition;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.MainActivity;
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.upload.Language;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
import org.wikipedia.language.AppLanguageLookUpTable;
public class SettingsFragment extends PreferenceFragmentCompat {
@ -42,7 +50,9 @@ public class SettingsFragment extends PreferenceFragmentCompat {
CommonsLogSender commonsLogSender;
private ListPreference themeListPreference;
private ListPreference langListPreference;
private ListPreference descriptionLanguageListPreference;
private ListPreference appUiLanguageListPreference;
private String keyLanguageListPreference;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@ -67,8 +77,16 @@ public class SettingsFragment extends PreferenceFragmentCompat {
});
}
langListPreference = findPreference("descriptionDefaultLanguagePref");
prepareLanguages();
appUiLanguageListPreference = findPreference("appUiDefaultLanguagePref");
assert appUiLanguageListPreference != null;
keyLanguageListPreference = appUiLanguageListPreference.getKey();
prepareAppLanguages(keyLanguageListPreference);
descriptionLanguageListPreference = findPreference("descriptionDefaultLanguagePref");
assert descriptionLanguageListPreference != null;
keyLanguageListPreference = descriptionLanguageListPreference.getKey();
prepareAppLanguages(keyLanguageListPreference);
Preference betaTesterPreference = findPreference("becomeBetaTester");
betaTesterPreference.setOnPreferenceClickListener(preference -> {
Utils.handleWebUrl(getActivity(), Uri.parse(getResources().getString(R.string.beta_opt_in_link)));
@ -84,6 +102,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
findPreference("useExternalStorage").setEnabled(false);
findPreference("useAuthorName").setEnabled(false);
findPreference("displayNearbyCardView").setEnabled(false);
findPreference("descriptionDefaultLanguagePref").setEnabled(false);
findPreference("displayLocationPermissionForCardView").setEnabled(false);
findPreference("displayCampaignsCardView").setEnabled(false);
}
@ -107,6 +126,21 @@ public class SettingsFragment extends PreferenceFragmentCompat {
}
}
@Override
protected Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
return new PreferenceGroupAdapter(preferenceScreen) {
@Override
public void onBindViewHolder(PreferenceViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
Preference preference = getItem(position);
View iconFrame = holder.itemView.findViewById(R.id.icon_frame);
if (iconFrame != null) {
iconFrame.setVisibility(View.GONE);
}
}
};
}
/**
* Sets the theme pref
*/
@ -119,65 +153,118 @@ public class SettingsFragment extends PreferenceFragmentCompat {
/**
* Prepares language summary and language codes list and adds them to list preference as pairs.
* Uses previously saved language if there is any, if not uses phone local as initial language.
* Uses previously saved language if there is any, if not uses phone locale as initial language.
* Get ListPreference key and act accordingly for each ListPreference.
* Adds preference changed listener and saves value chosen by user to shared preferences
* to remember later
* to remember later and recall MainActivity to reflect language changes
* @param keyListPreference
*/
private void prepareLanguages() {
List<String> languageNamesList = new ArrayList<>();
List<String> languageCodesList = new ArrayList<>();
List<Language> languages = getLanguagesSupportedByDevice();
private void prepareAppLanguages(final String keyListPreference) {
final List<String> languageNamesList;
final List<String> languageCodesList;
final AppLanguageLookUpTable appLanguageLookUpTable = new AppLanguageLookUpTable(
Objects.requireNonNull(getContext()));
languageNamesList = appLanguageLookUpTable.getLocalizedNames();
languageCodesList = appLanguageLookUpTable.getCodes();
List<String> languageNameWithCodeList = new ArrayList<>();
for(Language language: languages) {
// Go through all languages and add them to lists
if(!languageCodesList.contains(language.getLocale().getLanguage())) {
// This if prevents us from adding same language twice
languageNamesList.add(language.getLocale().getDisplayName());
languageCodesList.add(language.getLocale().getLanguage());
}
for (int i = 0; i < languageNamesList.size(); i++) {
languageNameWithCodeList.add(languageNamesList.get(i) + "[" + languageCodesList.get(i) + "]");
}
CharSequence[] languageNames = languageNamesList.toArray(new CharSequence[0]);
CharSequence[] languageCodes = languageCodesList.toArray(new CharSequence[0]);
final CharSequence[] languageNames = languageNamesList.toArray(new CharSequence[0]);
final CharSequence[] languageCodes = languageCodesList.toArray(new CharSequence[0]);
// Add all languages and languages codes to lists preference as pair
langListPreference.setEntries(languageNames);
langListPreference.setEntryValues(languageCodes);
// Gets current language code from shared preferences
String languageCode = getCurrentLanguageCode();
if (languageCode.equals("")){
// If current language code is empty, means none selected by user yet so use phone local
langListPreference.setValue(Locale.getDefault().getLanguage());
} else {
// If any language is selected by user previously, use it
langListPreference.setValue(languageCode);
final String languageCode = getCurrentLanguageCode(keyListPreference);
if (keyListPreference.equals("appUiDefaultLanguagePref")) {
appUiLanguageListPreference.setEntries(languageNames);
appUiLanguageListPreference.setEntryValues(languageCodes);
assert languageCode != null;
if (languageCode.equals("")) {
// If current language code is empty, means none selected by user yet so use phone local
appUiLanguageListPreference.setValue(Locale.getDefault().getLanguage());
} else {
// If any language is selected by user previously, use it
appUiLanguageListPreference.setValue(languageCode);
}
appUiLanguageListPreference.setOnPreferenceChangeListener((preference, newValue) -> {
final String userSelectedValue = (String) newValue;
setLocale(Objects.requireNonNull(getActivity()), userSelectedValue);
saveLanguageValue(userSelectedValue, keyListPreference);
getActivity().recreate();
final Intent intent = new Intent(getActivity(), MainActivity.class);
startActivity(intent);
return true;
});
} else if (keyListPreference.equals("descriptionDefaultLanguagePref")) {
descriptionLanguageListPreference.setEntries(languageNames);
descriptionLanguageListPreference.setEntryValues(languageCodes);
assert languageCode != null;
if (languageCode.equals("")) {
// If current language code is empty, means none selected by user yet so use phone local
descriptionLanguageListPreference.setValue(Locale.getDefault().getLanguage());
} else {
// If any language is selected by user previously, use it
descriptionLanguageListPreference.setValue(languageCode);
}
descriptionLanguageListPreference.setOnPreferenceChangeListener((preference, newValue) -> {
final String userSelectedValue = (String) newValue;
saveLanguageValue(userSelectedValue, keyListPreference);
return true;
});
}
langListPreference.setOnPreferenceChangeListener((preference, newValue) -> {
String userSelectedValue = (String) newValue;
saveLanguageValue(userSelectedValue);
return true;
});
}
private void saveLanguageValue(String userSelectedValue) {
defaultKvStore.putString(Prefs.KEY_LANGUAGE_VALUE, userSelectedValue);
/**
* Changing the default app language with selected one and save it to SharedPreferences
*/
public void setLocale(final Activity activity, final String userSelectedValue) {
final Locale locale = new Locale(userSelectedValue);
Locale.setDefault(locale);
final Configuration configuration = new Configuration();
configuration.locale = locale;
activity.getBaseContext().getResources().updateConfiguration(configuration,
activity.getBaseContext().getResources().getDisplayMetrics());
final SharedPreferences.Editor editor = activity.getSharedPreferences("Settings", MODE_PRIVATE).edit();
editor.putString("language", userSelectedValue);
editor.apply();
}
private String getCurrentLanguageCode() {
return defaultKvStore.getString(Prefs.KEY_LANGUAGE_VALUE, "");
}
private List<Language> getLanguagesSupportedByDevice() {
List<Language> languages = new ArrayList<>();
Locale[] localesArray = Locale.getAvailableLocales();
for (Locale locale : localesArray) {
languages.add(new Language(locale));
/**
* Save userselected language in List Preference
* @param userSelectedValue
* @param preferenceKey
*/
private void saveLanguageValue(final String userSelectedValue, final String preferenceKey) {
if (preferenceKey.equals("appUiDefaultLanguagePref")) {
defaultKvStore.putString(Prefs.APP_UI_LANGUAGE, userSelectedValue);
} else if (preferenceKey.equals("descriptionDefaultLanguagePref")) {
defaultKvStore.putString(Prefs.DESCRIPTION_LANGUAGE, userSelectedValue);
}
}
Collections.sort(languages, (language, t1) -> language.getLocale().getDisplayName()
.compareTo(t1.getLocale().getDisplayName()));
return languages;
/**
* Gets current language code from shared preferences
* @param preferenceKey
* @return
*/
private String getCurrentLanguageCode(final String preferenceKey) {
if (preferenceKey.equals("appUiDefaultLanguagePref")) {
return defaultKvStore.getString(Prefs.APP_UI_LANGUAGE, "");
}
if (preferenceKey.equals("descriptionDefaultLanguagePref")) {
return defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, "");
}
return null;
}
/**

View file

@ -1,10 +1,11 @@
package fr.free.nrw.commons.theme;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity;
import fr.free.nrw.commons.kvstore.JsonKvStore;
@ -27,6 +28,11 @@ public abstract class BaseActivity extends CommonsDaggerAppCompatActivity {
super.onCreate(savedInstanceState);
wasPreviouslyDarkTheme = systemThemeUtils.isDeviceInNightMode();
setTheme(wasPreviouslyDarkTheme ? R.style.DarkAppTheme : R.style.LightAppTheme);
float fontScale = android.provider.Settings.System.getFloat(
getBaseContext().getContentResolver(),
android.provider.Settings.System.FONT_SCALE,
1f);
adjustFontScale(getResources().getConfiguration(), fontScale);
}
@Override
@ -44,4 +50,17 @@ public abstract class BaseActivity extends CommonsDaggerAppCompatActivity {
super.onDestroy();
compositeDisposable.clear();
}
/**
* Apply fontScale on device
*/
public void adjustFontScale(Configuration configuration, float scale) {
configuration.fontScale = scale;
final DisplayMetrics metrics = getResources().getDisplayMetrics();
final WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
wm.getDefaultDisplay().getMetrics(metrics);
metrics.scaledDensity = configuration.fontScale * metrics.density;
getBaseContext().getResources().updateConfiguration(configuration, metrics);
}
}

View file

@ -4,7 +4,7 @@ import timber.log.Timber;
import static androidx.exifinterface.media.ExifInterface.TAG_ARTIST;
import static androidx.exifinterface.media.ExifInterface.TAG_BODY_SERIAL_NUMBER;
import static androidx.exifinterface.media.ExifInterface.TAG_CAMARA_OWNER_NAME;
import static androidx.exifinterface.media.ExifInterface.TAG_CAMERA_OWNER_NAME;
import static androidx.exifinterface.media.ExifInterface.TAG_COPYRIGHT;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_ALTITUDE;
import static androidx.exifinterface.media.ExifInterface.TAG_GPS_ALTITUDE_REF;
@ -36,7 +36,7 @@ public class FileMetadataUtils {
Timber.d("Retuning tags for pref:%s", pref);
switch (pref) {
case "Author":
return new String[]{TAG_ARTIST, TAG_CAMARA_OWNER_NAME};
return new String[]{TAG_ARTIST, TAG_CAMERA_OWNER_NAME};
case "Copyright":
return new String[]{TAG_COPYRIGHT};
case "Location":

View file

@ -1,7 +1,6 @@
package fr.free.nrw.commons.upload;
import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
import android.Manifest;
@ -24,6 +23,9 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
@ -42,6 +44,7 @@ import fr.free.nrw.commons.upload.depicts.DepictsFragment;
import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback;
import fr.free.nrw.commons.upload.worker.UploadWorker;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -55,6 +58,7 @@ import javax.inject.Named;
import timber.log.Timber;
public class UploadActivity extends BaseActivity implements UploadContract.View, UploadBaseFragment.Callback {
@Inject
ContributionController contributionController;
@Inject
@ -104,6 +108,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
private List<UploadableFile> uploadableFiles = Collections.emptyList();
private int currentSelectedPosition = 0;
public static final String EXTRA_FILES = "commons_image_exta";
@SuppressLint("CheckResult")
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -279,6 +285,13 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
.getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size()));
}
@Override
public void makeUploadRequest() {
WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork(
UploadWorker.class.getSimpleName(),
ExistingWorkPolicy.KEEP, OneTimeWorkRequest.from(UploadWorker.class));
}
@Override
public void askUserToLogIn() {
Timber.d("current session is null, asking user to login");

View file

@ -9,12 +9,11 @@ import com.google.gson.Gson;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.contributions.ChunkInfo;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.upload.UploadService.NotificationUpdateProgressListener;
import fr.free.nrw.commons.upload.worker.UploadWorker.NotificationUpdateProgressListener;
import io.reactivex.Observable;
import io.reactivex.disposables.CompositeDisposable;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Date;
import java.util.HashMap;
@ -48,8 +47,6 @@ public class UploadClient {
private final FileUtilsWrapper fileUtilsWrapper;
private final Gson gson;
private Map<String, Boolean> pauseUploads;
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
@Inject
@ -62,14 +59,13 @@ public class UploadClient {
this.pageContentsCreator = pageContentsCreator;
this.fileUtilsWrapper = fileUtilsWrapper;
this.gson = gson;
this.pauseUploads = new HashMap<>();
}
/**
* Upload file to stash in chunks of specified size. Uploading files in chunks will make handling
* of large files easier. Also, it will be useful in supporting pause/resume of uploads
*/
Observable<StashUploadResult> uploadFileToStash(
public Observable<StashUploadResult> uploadFileToStash(
final Context context, final String filename, final Contribution contribution,
final NotificationUpdateProgressListener notificationUpdater) throws IOException {
if (contribution.getChunkInfo() != null
@ -79,7 +75,7 @@ public class UploadClient {
contribution.getChunkInfo().getUploadResult().getFilekey()));
}
pauseUploads.put(contribution.getPageId(), false);
CommonsApplication.pauseUploads.put(contribution.getPageId(), false);
final File file = new File(contribution.getLocalUri().getPath());
final List<File> fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE);
@ -102,7 +98,7 @@ public class UploadClient {
final AtomicBoolean failures = new AtomicBoolean();
compositeDisposable.add(Observable.fromIterable(fileChunks).forEach(chunkFile -> {
if (pauseUploads.get(contribution.getPageId()) || failures.get()) {
if (CommonsApplication.pauseUploads.get(contribution.getPageId()) || failures.get()) {
return;
}
@ -141,7 +137,7 @@ public class UploadClient {
}));
}));
if (pauseUploads.get(contribution.getPageId())) {
if (CommonsApplication.pauseUploads.get(contribution.getPageId())) {
Timber.d("Upload stash paused %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.PAUSED, null));
} else if (failures.get()) {
@ -201,18 +197,6 @@ public class UploadClient {
}
}
/**
* Dispose the active disposable and sets the pause variable
* @param pageId
*/
public void pauseUpload(String pageId) {
pauseUploads.put(pageId, true);
if (!compositeDisposable.isDisposed()) {
compositeDisposable.dispose();
}
compositeDisposable.clear();
}
/**
* Converts string value to request body
*/
@ -222,7 +206,7 @@ public class UploadClient {
}
Observable<UploadResult> uploadFileFromStash(final Context context,
public Observable<UploadResult> uploadFileFromStash(
final Contribution contribution,
final String uniqueFileName,
final String fileKey) {

View file

@ -29,6 +29,8 @@ public interface UploadContract {
void onUploadMediaDeleted(int index);
void updateTopCardTitle();
void makeUploadRequest();
}
public interface UserActionListener extends BasePresenter<View> {

View file

@ -13,12 +13,16 @@ import android.net.Uri;
import android.os.IBinder;
import android.provider.MediaStore;
import android.text.TextUtils;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.worker.UploadWorker;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -35,62 +39,27 @@ import timber.log.Timber;
@Singleton
public class UploadController {
private UploadService uploadService;
private final SessionManager sessionManager;
private final Context context;
private final JsonKvStore store;
@Inject
public UploadController(final SessionManager sessionManager,
final Context context,
final JsonKvStore store) {
final Context context,
final JsonKvStore store) {
this.sessionManager = sessionManager;
this.context = context;
this.store = store;
}
private boolean isUploadServiceConnected;
public ServiceConnection uploadServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(final ComponentName componentName, final IBinder binder) {
uploadService = ((UploadService.UploadServiceLocalBinder) binder).getService();
isUploadServiceConnected = true;
}
@Override
public void onServiceDisconnected(final ComponentName componentName) {
// this should never happen
isUploadServiceConnected = false;
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
}
};
/**
* Prepares the upload service.
*/
public void prepareService() {
final Intent uploadServiceIntent = new Intent(context, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
context.startService(uploadServiceIntent);
context.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
}
/**
* Disconnects the upload service.
*/
public void cleanup() {
if (isUploadServiceConnected) {
context.unbindService(uploadServiceConnection);
}
}
/**
* Starts a new upload task.
*
* @param contribution the contribution object
*/
@SuppressLint("StaticFieldLeak")
public void startUpload(final Contribution contribution) {
public void prepareMedia(final Contribution contribution) {
//Set creator, desc, and license
// If author name is enabled and set, use it
@ -118,20 +87,7 @@ public class UploadController {
final String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
media.setLicense(license);
uploadTask(contribution);
}
/**
* Initiates the upload task
* @param contribution
* @return
*/
private Disposable uploadTask(final Contribution contribution) {
return Single.just(contribution)
.map(this::buildUpload)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::upload);
buildUpload(contribution);
}
/**
@ -139,7 +95,7 @@ public class UploadController {
* @param contribution
* @return
*/
private Contribution buildUpload(final Contribution contribution) {
private void buildUpload(final Contribution contribution) {
final ContentResolver contentResolver = context.getContentResolver();
contribution.setDataLength(resolveDataLength(contentResolver, contribution));
@ -153,8 +109,6 @@ public class UploadController {
contribution.setDateCreated(resolveDateTakenOrNow(contentResolver, contribution));
}
}
return contribution;
}
private String resolveMimeType(final ContentResolver contentResolver, final Contribution contribution) {
@ -202,15 +156,6 @@ public class UploadController {
new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null);
}
/**
* When the contribution object is completely formed, the item is queued to the upload service
* @param contribution
*/
private void upload(final Contribution contribution) {
//Starts the upload. If commented out, user can proceed to next Fragment but upload doesn't happen
uploadService.queue(contribution);
}
/**
* Counts the number of bytes in {@code stream}.

View file

@ -18,7 +18,7 @@ public class UploadItem {
private final String mimeType;
private ImageCoordinates gpsCoords;
private List<UploadMediaDetail> uploadMediaDetails;
private final Place place;
private Place place;
private final long createdTimestamp;
private final String createdTimestampSource;
private final BehaviorSubject<Integer> imageQuality;
@ -70,6 +70,14 @@ public class UploadItem {
this.imageQuality.onNext(imageQuality);
}
/**
* Sets the corresponding place to the uploadItem
* @param place geolocated Wikidata item
*/
public void setPlace(Place place) {
this.place = place;
}
public Place getPlace() {
return place;
}

View file

@ -1,7 +1,6 @@
package fr.free.nrw.commons.upload
import fr.free.nrw.commons.nearby.Place
import java.util.*
/**
* Holds a description of an item being uploaded by [UploadActivity]
@ -20,7 +19,7 @@ data class UploadMediaDetail constructor(
fun javaCopy() = copy()
constructor(place: Place) : this(
Locale.getDefault().language,
place.language,
place.longDescription,
place.name
)

View file

@ -6,6 +6,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ImageView;
import android.widget.Spinner;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatEditText;
@ -13,6 +15,7 @@ import androidx.appcompat.widget.AppCompatSpinner;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.AbstractTextWatcher;
@ -72,24 +75,38 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
notifyItemInserted(uploadMediaDetails.size());
}
/**
* Remove description based on position from the list and notifies the RecyclerView Adapter that
* data in adapter has been removed at that particular position.
* @param uploadMediaDetail
* @param position
*/
public void removeDescription(final UploadMediaDetail uploadMediaDetail, final int position) {
this.uploadMediaDetails.remove(uploadMediaDetail);
notifyItemRemoved(position);
}
public class ViewHolder extends RecyclerView.ViewHolder {
@Nullable
@BindView(R.id.spinner_description_languages)
AppCompatSpinner spinnerDescriptionLanguages;
Spinner spinnerDescriptionLanguages;
@BindView(R.id.description_item_edit_text)
AppCompatEditText descItemEditText;
TextInputEditText descItemEditText;
@BindView(R.id.description_item_edit_text_input_layout)
TextInputLayout descInputLayout;
@BindView(R.id.caption_item_edit_text)
AppCompatEditText captionItemEditText;
TextInputEditText captionItemEditText;
@BindView(R.id.caption_item_edit_text_input_layout)
TextInputLayout captionInputLayout;
@BindView(R.id.btn_remove)
ImageView removeButton;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
@ -110,6 +127,7 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
descItemEditText.setText(uploadMediaDetail.getDescriptionText());
if (position == 0) {
removeButton.setVisibility(View.GONE);
captionInputLayout.setEndIconMode(TextInputLayout.END_ICON_CUSTOM);
captionInputLayout.setEndIconDrawable(R.drawable.mapbox_info_icon_default);
captionInputLayout.setEndIconOnClickListener(v ->
@ -121,10 +139,13 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
callback.showAlert(R.string.media_detail_description, R.string.description_info));
} else {
removeButton.setVisibility(View.VISIBLE);
captionInputLayout.setEndIconDrawable(null);
descInputLayout.setEndIconDrawable(null);
}
removeButton.setOnClickListener(v -> removeDescription(uploadMediaDetail, position));
captionItemEditText.addTextChangedListener(new AbstractTextWatcher(
captionText -> uploadMediaDetails.get(position).setCaptionText(captionText)));
initLanguageSpinner(position, uploadMediaDetail);
@ -174,24 +195,30 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
}
});
if (description.getSelectedLanguageIndex() == -1) {
if (!TextUtils.isEmpty(savedLanguageValue)) {
// If user has chosen a default language from settings activity savedLanguageValue is not null
spinnerDescriptionLanguages.setSelection(languagesAdapter.getIndexOfLanguageCode(savedLanguageValue));
// If user has chosen a default language from settings activity
// savedLanguageValue is not null
spinnerDescriptionLanguages.setSelection(languagesAdapter
.getIndexOfLanguageCode(savedLanguageValue));
} else {
//Checking whether Language Code attribute is null or not.
if(uploadMediaDetails.get(position).getLanguageCode() != null){
//If it is not null that means it is fetching details from the previous upload (i.e. when user has pressed copy previous caption & description)
if (uploadMediaDetails.get(position).getLanguageCode() != null) {
//If it is not null that means it is fetching details from the previous
// upload (i.e. when user has pressed copy previous caption & description)
//hence providing same language code for the current upload.
spinnerDescriptionLanguages.setSelection(languagesAdapter
.getIndexOfLanguageCode(uploadMediaDetails.get(position).getLanguageCode()), true);
.getIndexOfLanguageCode(uploadMediaDetails.get(position)
.getLanguageCode()), true);
} else {
if (position == 0) {
int defaultLocaleIndex = languagesAdapter
.getIndexOfUserDefaultLocale(spinnerDescriptionLanguages.getContext());
final int defaultLocaleIndex = languagesAdapter
.getIndexOfUserDefaultLocale(spinnerDescriptionLanguages
.getContext());
spinnerDescriptionLanguages.setSelection(defaultLocaleIndex, true);
} else {
spinnerDescriptionLanguages.setSelection(0,true);
spinnerDescriptionLanguages.setSelection(0, true);
}
}
}

View file

@ -8,6 +8,7 @@ import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.depicts.DepictsFragment;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import io.reactivex.Observable;
import io.reactivex.Single;

View file

@ -69,7 +69,9 @@ public class UploadPresenter implements UploadContract.UserActionListener {
@Override
public void onNext(Contribution contribution) {
repository.startUpload(contribution);
repository.prepareMedia(contribution);
contribution.setState(Contribution.STATE_QUEUED);
repository.saveContribution(contribution);
}
@Override
@ -83,6 +85,7 @@ public class UploadPresenter implements UploadContract.UserActionListener {
@Override
public void onComplete() {
view.makeUploadRequest();
repository.cleanup();
view.finish();
compositeDisposable.clear();
@ -119,7 +122,6 @@ public class UploadPresenter implements UploadContract.UserActionListener {
@Override
public void onAttachView(UploadContract.View view) {
this.view = view;
repository.prepareService();
}
@Override

View file

@ -14,10 +14,10 @@ data class UploadResult(
val filename: String
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
parcel.readInt(),
parcel.readString()
parcel.readString()?:"",
parcel.readString()?:"",
parcel.readInt()?:0,
parcel.readString()?:""
) {
}

View file

@ -1,474 +0,0 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import fr.free.nrw.commons.BuildConfig;
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.contributions.ChunkInfo;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.di.CommonsDaggerService;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.ViewUtil;
import fr.free.nrw.commons.wikidata.WikidataEditService;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.processors.PublishProcessor;
import io.reactivex.schedulers.Schedulers;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
public class UploadService extends CommonsDaggerService {
private static final String EXTRA_PREFIX = "fr.free.nrw.commons.upload";
private static final List<String> STASH_ERROR_CODES = Arrays
.asList("uploadstash-file-not-found", "stashfailed", "verification-error", "chunk-too-small");
public static final String ACTION_START_SERVICE = EXTRA_PREFIX + ".upload";
public static final String PROCESS_PENDING_LIMITED_CONNECTION_MODE_UPLOADS = EXTRA_PREFIX + "process_limited_connection_mode_uploads";
public static final String EXTRA_FILES = EXTRA_PREFIX + ".files";
@Inject
WikidataEditService wikidataEditService;
@Inject
SessionManager sessionManager;
@Inject
ContributionDao contributionDao;
@Inject
UploadClient uploadClient;
@Inject
MediaClient mediaClient;
@Inject
@Named(CommonsApplicationModule.MAIN_THREAD)
Scheduler mainThreadScheduler;
@Inject
@Named(CommonsApplicationModule.IO_THREAD)
Scheduler ioThreadScheduler;
@Inject
@Named("default_preferences")
public JsonKvStore defaultKvStore;
private NotificationManagerCompat notificationManager;
private NotificationCompat.Builder curNotification;
private int toUpload;
private CompositeDisposable compositeDisposable;
/**
* The filePath names of unfinished uploads, used to prevent overwriting
*/
private Set<String> unfinishedUploads = new HashSet<>();
// DO NOT HAVE NOTIFICATION ID OF 0 FOR ANYTHING
// See http://stackoverflow.com/questions/8725909/startforeground-does-not-show-my-notification
// Seriously, Android?
public static final int NOTIFICATION_UPLOAD_IN_PROGRESS = 1;
public static final int NOTIFICATION_UPLOAD_FAILED = 3;
public static final int NOTIFICATION_UPLOAD_PAUSED = 4;
protected class NotificationUpdateProgressListener {
String notificationTag;
boolean notificationTitleChanged;
Contribution contribution;
String notificationProgressTitle;
String notificationFinishingTitle;
NotificationUpdateProgressListener(String notificationTag, String notificationProgressTitle,
String notificationFinishingTitle, Contribution contribution) {
this.notificationTag = notificationTag;
this.notificationProgressTitle = notificationProgressTitle;
this.notificationFinishingTitle = notificationFinishingTitle;
this.contribution = contribution;
}
public void onProgress(long transferred, long total) {
if (!notificationTitleChanged) {
curNotification.setContentTitle(notificationProgressTitle);
notificationTitleChanged = true;
contribution.setState(Contribution.STATE_IN_PROGRESS);
}
if (transferred == total) {
// Completed!
curNotification.setContentTitle(notificationFinishingTitle)
.setTicker(notificationFinishingTitle)
.setProgress(0, 100, true);
} else {
curNotification
.setProgress(100, (int) (((double) transferred / (double) total) * 100), false);
}
notificationManager
.notify(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
contribution.setTransferred(transferred);
compositeDisposable.add(contributionDao.update(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
public void onChunkUploaded(Contribution contribution, ChunkInfo chunkInfo) {
contribution.setChunkInfo(chunkInfo);
compositeDisposable.add(contributionDao.update(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
}
/**
* Sets contribution state to paused and disposes the active disposable
* @param contribution
*/
public void pauseUpload(Contribution contribution) {
uploadClient.pauseUpload(contribution.getPageId());
contribution.setState(Contribution.STATE_PAUSED);
compositeDisposable.add(contributionDao.update(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
@Override
public void onDestroy() {
super.onDestroy();
compositeDisposable.dispose();
Timber.d("UploadService.onDestroy; %s are yet to be uploaded", unfinishedUploads);
}
public class UploadServiceLocalBinder extends Binder {
public UploadService getService() {
return UploadService.this;
}
}
private final IBinder localBinder = new UploadServiceLocalBinder();
private PublishProcessor<Contribution> contributionsToUpload;
@Override
public IBinder onBind(Intent intent) {
return localBinder;
}
@Override
public void onCreate() {
super.onCreate();
CommonsApplication.createNotificationChannel(getApplicationContext());
compositeDisposable = new CompositeDisposable();
notificationManager = NotificationManagerCompat.from(this);
curNotification = getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL);
contributionsToUpload = PublishProcessor.create();
compositeDisposable.add(contributionsToUpload.subscribe(this::handleUpload));
}
public void handleUpload(Contribution contribution) {
contribution.setState(Contribution.STATE_QUEUED);
contribution.setTransferred(0);
toUpload++;
if (curNotification != null && toUpload != 1) {
curNotification.setContentText(getResources()
.getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload));
Timber.d("%d uploads left", toUpload);
notificationManager
.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_IN_PROGRESS,
curNotification.build());
}
compositeDisposable.add(contributionDao
.save(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe(() -> uploadContribution(contribution)));
}
private boolean freshStart = true;
public void queue(Contribution contribution) {
if (defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
contribution.setState(Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE);
contributionDao.save(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe();
return;
}
contributionsToUpload.offer(contribution);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
showUploadNotification();
if (ACTION_START_SERVICE.equals(intent.getAction()) && freshStart) {
compositeDisposable.add(contributionDao.updateStates(Contribution.STATE_FAILED,
new int[]{Contribution.STATE_QUEUED, Contribution.STATE_IN_PROGRESS})
.observeOn(mainThreadScheduler)
.subscribeOn(ioThreadScheduler)
.subscribe());
freshStart = false;
} else if (PROCESS_PENDING_LIMITED_CONNECTION_MODE_UPLOADS.equals(intent.getAction())) {
contributionDao.getContribution(Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE)
.flatMapObservable(
(Function<List<Contribution>, ObservableSource<Contribution>>) contributions -> Observable
.fromIterable(contributions))
.concatMapCompletable(contribution -> Completable.fromAction(() -> queue(contribution)))
.subscribeOn(ioThreadScheduler)
.subscribe();
}
return START_REDELIVER_INTENT;
}
private void showUploadNotification() {
compositeDisposable.add(contributionDao
.getPendingUploads(new int[]{Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED})
.subscribe(count -> {
if (count > 0) {
startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS,
curNotification.setContentText(getText(R.string.starting_uploads)).build());
}
}));
}
@SuppressLint("StringFormatInvalid")
private NotificationCompat.Builder getNotificationBuilder(String channelId) {
return new NotificationCompat.Builder(this, channelId)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher))
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setProgress(100, 0, true)
.setOngoing(true)
.setContentIntent(
PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0));
}
@SuppressLint("CheckResult")
private void uploadContribution(Contribution contribution) {
if (contribution.getLocalUri() == null || contribution.getLocalUri().getPath() == null) {
Timber.d("localUri/path is null");
return;
}
String notificationTag = contribution.getLocalUri().toString();
Timber.d("Before execution!");
final Media media = contribution.getMedia();
final String displayTitle = media.getDisplayTitle();
curNotification.setContentTitle(getString(R.string.upload_progress_notification_title_start,
displayTitle))
.setContentText(getResources()
.getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload,
toUpload))
.setTicker(getString(R.string.upload_progress_notification_title_in_progress,
displayTitle))
.setOngoing(true);
notificationManager
.notify(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
String filename = media.getFilename();
NotificationUpdateProgressListener notificationUpdater = new NotificationUpdateProgressListener(
notificationTag,
getString(R.string.upload_progress_notification_title_in_progress,
displayTitle),
getString(R.string.upload_progress_notification_title_finishing,
displayTitle),
contribution
);
Observable.fromCallable(() -> "Temp_" + contribution.hashCode() + filename)
.flatMap(stashFilename -> uploadClient
.uploadFileToStash(getApplicationContext(), stashFilename, contribution,
notificationUpdater))
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.doFinally(() -> {
if (filename != null) {
unfinishedUploads.remove(filename);
}
toUpload--;
if (toUpload == 0) {
// Sync modifications right after all uploads are processed
ContentResolver
.requestSync(sessionManager.getCurrentAccount(), BuildConfig.MODIFICATION_AUTHORITY,
new Bundle());
stopForeground(true);
}
})
.flatMap(uploadStash -> {
Timber.d("Upload stash result %s", uploadStash.toString());
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
if (uploadStash.getState() == StashUploadState.SUCCESS) {
Timber.d("making sure of uniqueness of name: %s", filename);
String uniqueFilename = findUniqueFilename(filename);
unfinishedUploads.add(uniqueFilename);
return uploadClient.uploadFileFromStash(
getApplicationContext(),
contribution,
uniqueFilename,
uploadStash.getFileKey()).doOnError(new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
Timber.e(throwable, "Error occurred in uploading file from stash");
if (STASH_ERROR_CODES.contains(throwable.getMessage())) {
clearChunks(contribution);
}
}
});
} else if (uploadStash.getState() == StashUploadState.PAUSED) {
showPausedNotification(contribution);
return Observable.never();
} else {
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution);
return Observable.never();
}
})
.subscribe(
uploadResult -> onUpload(contribution, notificationTag, uploadResult),
throwable -> {
Timber.w(throwable, "Exception during upload");
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
showFailedNotification(contribution);
});
}
private void clearChunks(Contribution contribution) {
contribution.setChunkInfo(null);
compositeDisposable.add(contributionDao.update(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
private void onUpload(Contribution contribution, String notificationTag,
UploadResult uploadResult) {
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
if (uploadResult.isSuccessful()) {
onSuccessfulUpload(contribution, uploadResult);
} else {
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution);
}
}
private void onSuccessfulUpload(Contribution contribution, UploadResult uploadResult) {
compositeDisposable
.add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution));
WikidataPlace wikidataPlace = contribution.getWikidataPlace();
if (wikidataPlace != null && wikidataPlace.getImageValue() == null) {
if (!contribution.hasInvalidLocation()) {
wikidataEditService.createClaim(wikidataPlace, uploadResult.getFilename(),
contribution.getMedia().getCaptions());
} else {
ViewUtil.showShortToast(this, getString(R.string.wikidata_edit_failure));
Timber
.d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
}
}
saveCompletedContribution(contribution, uploadResult);
}
private void saveCompletedContribution(Contribution contribution, UploadResult uploadResult) {
compositeDisposable.add(mediaClient.getMedia("File:" + uploadResult.getFilename())
.map(contribution::completeWith)
.flatMapCompletable(
newContribution -> {
newContribution.setDateModified(new Date());
return contributionDao.saveAndDelete(contribution, newContribution);
})
.subscribe());
}
@SuppressLint("StringFormatInvalid")
@SuppressWarnings("deprecation")
private void showFailedNotification(final Contribution contribution) {
final String displayTitle = contribution.getMedia().getDisplayTitle();
curNotification.setTicker(getString(R.string.upload_failed_notification_title, displayTitle))
.setContentTitle(getString(R.string.upload_failed_notification_title, displayTitle))
.setContentText(getString(R.string.upload_failed_notification_subtitle))
.setProgress(0, 0, false)
.setOngoing(false);
notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_FAILED,
curNotification.build());
contribution.setState(Contribution.STATE_FAILED);
contribution.setChunkInfo(null);
compositeDisposable.add(contributionDao
.update(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
private void showPausedNotification(final Contribution contribution) {
final String displayTitle = contribution.getMedia().getDisplayTitle();
curNotification.setTicker(getString(R.string.upload_paused_notification_title, displayTitle))
.setContentTitle(getString(R.string.upload_paused_notification_title, displayTitle))
.setContentText(getString(R.string.upload_paused_notification_subtitle))
.setProgress(0, 0, false)
.setOngoing(false);
notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_PAUSED,
curNotification.build());
contribution.setState(Contribution.STATE_PAUSED);
compositeDisposable.add(contributionDao
.update(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
private String findUniqueFilename(String fileName) throws IOException {
String sequenceFileName;
for (int sequenceNumber = 1; true; sequenceNumber++) {
if (sequenceNumber == 1) {
sequenceFileName = fileName;
} else {
if (fileName.indexOf('.') == -1) {
// We really should have appended a filePath type suffix already.
// But... we might not.
sequenceFileName = fileName + " " + sequenceNumber;
} else {
Pattern regex = Pattern.compile("^(.*)(\\..+?)$");
Matcher regexMatcher = regex.matcher(fileName);
sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2");
}
}
if (!mediaClient.checkPageExistsUsingTitle(String.format("File:%s", sequenceFileName))
.blockingGet()
&& !unfinishedUploads.contains(sequenceFileName)) {
break;
}
}
return sequenceFileName;
}
}

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.upload.depicts
import androidx.room.Entity
import androidx.room.PrimaryKey
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import java.util.*
/**
* entity class for DepictsRoomDateBase
*/
@Entity(tableName = "depicts_table")
data class Depicts (@PrimaryKey val item: DepictedItem, val lastUsed:Date)

View file

@ -0,0 +1,112 @@
package fr.free.nrw.commons.upload.depicts
import androidx.room.*
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.*
/**
* Dao class for DepictsRoomDataBase
*/
@Dao
abstract class DepictsDao {
/**
* insert Depicts in DepictsRoomDataBase
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun insert(depictedItem: Depicts)
/**
* get all Depicts from roomdatabase
*/
@Query("Select * From depicts_table order by lastUsed DESC")
abstract suspend fun getAllDepict(): List<Depicts>
/**
* get all Depicts which need to delete from roomdatabase
*/
@Query("Select * From depicts_table order by lastUsed DESC LIMIT :n OFFSET 10")
abstract suspend fun getItemToDelete(n: Int): List<Depicts>
/**
* Delete Depicts from roomdatabase
*/
@Delete
abstract suspend fun delete(depicts: Depicts)
lateinit var allDepict: List<Depicts>
lateinit var listOfDelete: List<Depicts>
/**
* get all depicts from DepictsRoomDatabase
*/
fun depictsList(): List<Depicts> {
runBlocking {
launch(Dispatchers.IO) {
allDepict = getAllDepict()
}
}
return allDepict
}
/**
* insert Depicts in DepictsRoomDataBase
*/
fun insertDepict(depictes: Depicts) {
runBlocking {
launch(Dispatchers.IO) {
insert(depictes)
}
}
}
/**
* get all Depicts item which need to delete
*/
fun getItemTodelete(number: Int): List<Depicts> {
runBlocking {
launch(Dispatchers.IO) {
listOfDelete = getItemToDelete(number)
}
}
return listOfDelete
}
/**
* delete Depicts in DepictsRoomDataBase
*/
fun deleteDepicts(depictes: Depicts) {
runBlocking {
launch(Dispatchers.IO) {
delete(depictes)
}
}
}
/**
* save Depicts in DepictsRoomDataBase
*/
fun savingDepictsInRoomDataBase(listDepictedItem: List<DepictedItem>) {
var numberofItemInRoomDataBase: Int
val maxNumberOfItemSaveInRoom = 10
for (depictsItem in listDepictedItem) {
depictsItem.isSelected = false
insertDepict(Depicts(depictsItem, Date()))
}
numberofItemInRoomDataBase = depictsList().size
// delete the depictItem from depictsroomdataBase when number of element in depictsroomdataBase is greater than 10
if (numberofItemInRoomDataBase > maxNumberOfItemSaveInRoom) {
val listOfDepictsToDelete: List<Depicts> =
getItemTodelete(numberofItemInRoomDataBase)
for (i in listOfDepictsToDelete) {
deleteDepicts(i)
}
}
}
}

View file

@ -21,10 +21,12 @@ import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxTextView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.upload.UploadBaseFragment;
import fr.free.nrw.commons.upload.UploadModel;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.DialogUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
@ -54,7 +56,6 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra
DepictsContract.UserActionListener presenter;
private UploadDepictsAdapter adapter;
private Disposable subscribe;
@Nullable
@Override
public android.view.View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@ -174,8 +175,7 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra
*
* @param query query string
*/
private void searchForDepictions(String query) {
private void searchForDepictions(final String query) {
presenter.searchForDepictions(query);
}
}

View file

@ -12,6 +12,7 @@ import io.reactivex.disposables.CompositeDisposable
import io.reactivex.processors.PublishProcessor
import timber.log.Timber
import java.lang.reflect.Proxy
import java.util.*
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
@ -34,6 +35,8 @@ class DepictsPresenter @Inject constructor(
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
private val searchTerm: PublishProcessor<String> = PublishProcessor.create()
private val depictedItems: MutableLiveData<List<DepictedItem>> = MutableLiveData()
@Inject
lateinit var depictsDao: DepictsDao;
override fun onAttachView(view: DepictsContract.View) {
this.view = view
@ -62,10 +65,15 @@ class DepictsPresenter @Inject constructor(
return searchResults(term).map { Pair(it, term) }
}
private fun searchResults(it: String): Flowable<List<DepictedItem>> {
return repository.searchAllEntities(it)
private fun searchResults(querystring: String): Flowable<List<DepictedItem>> {
var recentDepictedItemList: MutableList<DepictedItem> = ArrayList();
//show recentDepictedItemList when queryString is empty
if (querystring.isEmpty()) {
recentDepictedItemList = getRecentDepictedItems();
}
return repository.searchAllEntities(querystring)
.subscribeOn(ioScheduler)
.map { repository.selectedDepictions + it }
.map { repository.selectedDepictions + it + recentDepictedItemList }
.map { it.filterNot { item -> WikidataDisambiguationItems.isDisambiguationItem(item.instanceOfs) } }
.map { it.distinctBy(DepictedItem::id) }
}
@ -102,11 +110,28 @@ class DepictsPresenter @Inject constructor(
*/
override fun verifyDepictions() {
if (repository.selectedDepictions.isNotEmpty()) {
if (::depictsDao.isInitialized) {
//save all the selected Depicted item in room Database
depictsDao.savingDepictsInRoomDataBase(repository.selectedDepictions)
}
view.goToNextScreen()
} else {
view.noDepictionSelected()
}
}
/**
* Get the depicts from DepictsRoomdataBase
*/
fun getRecentDepictedItems(): MutableList<DepictedItem> {
val depictedItemList: MutableList<DepictedItem> = ArrayList()
val depictsList = depictsDao.depictsList()
for (i in depictsList.indices) {
val depictedItem = depictsList[i].item
depictedItemList.add(depictedItem)
}
return depictedItemList
}
}
/**

View file

@ -12,6 +12,7 @@ import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatButton;
@ -38,8 +39,6 @@ import fr.free.nrw.commons.upload.UploadItem;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
@ -69,8 +68,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
@BindView(R.id.tooltip)
ImageView tooltip;
private UploadMediaDetailAdapter uploadMediaDetailAdapter;
@BindView(R.id.btn_copy_prev_title_desc)
AppCompatButton btnCopyPreviousTitleDesc;
@BindView(R.id.btn_copy_subsequent_media)
AppCompatButton btnCopyToSubsequentMedia;
@Inject
UploadMediaDetailsContract.UserActionListener presenter;
@ -84,6 +83,19 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
private boolean isExpanded = true;
/**
* showNearbyFound will be true, if any nearby location found that needs pictures and the nearby popup is yet to be shown
* Used to show and check if the nearby found popup is already shown
*/
private boolean showNearbyFound;
/**
* nearbyPlace holds the detail of nearby place that need pictures, if any found
*/
private Place nearbyPlace;
private UploadItem uploadItem;
private UploadMediaDetailFragmentCallback callback;
public void setCallback(UploadMediaDetailFragmentCallback callback) {
@ -123,9 +135,9 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
showInfoAlert(R.string.media_detail_step_title, R.string.media_details_tooltip);
}
});
initRecyclerView();
initPresenter();
presenter.receiveImage(uploadableFile, place);
initRecyclerView();
if (callback.getIndexInViewFlipper(this) == 0) {
btnPrevious.setEnabled(false);
@ -135,11 +147,11 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
btnPrevious.setAlpha(1.0f);
}
//If this is the first media, we have nothing to copy, lets not show the button
if (callback.getIndexInViewFlipper(this) == 0) {
btnCopyPreviousTitleDesc.setVisibility(View.GONE);
//If this is the last media, we have nothing to copy, lets not show the button
if (callback.getIndexInViewFlipper(this) == callback.getTotalNumberOfSteps()-4) {
btnCopyToSubsequentMedia.setVisibility(View.GONE);
} else {
btnCopyPreviousTitleDesc.setVisibility(View.VISIBLE);
btnCopyToSubsequentMedia.setVisibility(View.VISIBLE);
}
attachImageViewScaleChangeListener();
@ -168,7 +180,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
* init the description recycler veiw and caption recyclerview
*/
private void initRecyclerView() {
uploadMediaDetailAdapter = new UploadMediaDetailAdapter(defaultKvStore.getString(Prefs.KEY_LANGUAGE_VALUE, ""));
uploadMediaDetailAdapter = new UploadMediaDetailAdapter(defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""));
uploadMediaDetailAdapter.setCallback(this::showInfoAlert);
uploadMediaDetailAdapter.setEventListener(this);
rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
@ -199,7 +211,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
UploadMediaDetail uploadMediaDetail = new UploadMediaDetail();
uploadMediaDetail.setManuallyAdded(true);//This was manually added by the user
uploadMediaDetailAdapter.addDescription(uploadMediaDetail);
rvDescriptions.scrollToPosition(uploadMediaDetailAdapter.getItemCount()-1);
rvDescriptions.smoothScrollToPosition(uploadMediaDetailAdapter.getItemCount()-1);
}
@Override
@ -231,13 +243,30 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
}
/**
* Shows popup if any nearby location needing pictures matches uploadable picture's GPS location
* Sets variables to Show popup if any nearby location needing pictures matches uploadable picture's GPS location
* @param uploadItem
* @param place
*/
@SuppressLint("StringFormatInvalid")
@Override
public void onNearbyPlaceFound(UploadItem uploadItem, Place place) {
nearbyPlace = place;
this.uploadItem = uploadItem;
showNearbyFound = true;
if(callback.getIndexInViewFlipper(this) == 0) {
showNearbyPlaceFound(nearbyPlace);
showNearbyFound = false;
}
}
/**
* Shows nearby place found popup
* @param place
*/
@SuppressLint("StringFormatInvalid") // To avoid the unwanted lint warning that string 'upload_nearby_place_found_description' is not of a valid format
private void showNearbyPlaceFound(Place place) {
final View customLayout = getLayoutInflater().inflate(R.layout.custom_nearby_found, null);
ImageView nearbyFoundImage = customLayout.findViewById(R.id.nearbyItemImage);
nearbyFoundImage.setImageURI(uploadItem.getMediaUri());
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.upload_nearby_place_found_title),
String.format(Locale.getDefault(),
@ -248,7 +277,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
},
() -> {
});
},
customLayout, true);
}
@Override
@ -261,6 +291,19 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
}
/**
* This method gets called whenever the next/previous button is pressed
*/
@Override
protected void onBecameVisible() {
super.onBecameVisible();
presenter.fetchTitleAndDescription(callback.getIndexInViewFlipper(this));
if(showNearbyFound) {
showNearbyPlaceFound(nearbyPlace);
showNearbyFound = false;
}
}
@Override
public void showMessage(int stringResourceId, int colorResourceId) {
ViewUtil.showLongToast(getContext(), stringResourceId);
@ -322,6 +365,14 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
//If the error message is null, we will probably not show anything
}
@Override
public void showConnectionErrorPopup() {
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.upload_connection_error_alert_title),
getString(R.string.upload_connection_error_alert_detail), getString(R.string.ok),
() -> {}, true);
}
@Override public void showMapWithImageCoordinates(boolean shouldShow) {
ibMap.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
}
@ -369,6 +420,9 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
@Override
public void onPrimaryCaptionTextChange(boolean isNotEmpty) {
btnCopyToSubsequentMedia.setEnabled(isNotEmpty);
btnCopyToSubsequentMedia.setClickable(isNotEmpty);
btnCopyToSubsequentMedia.setAlpha(isNotEmpty ? 1.0f: 0.5f);
btnNext.setEnabled(isNotEmpty);
btnNext.setClickable(isNotEmpty);
btnNext.setAlpha(isNotEmpty ? 1.0f: 0.5f);
@ -380,9 +434,10 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
}
@OnClick(R.id.btn_copy_prev_title_desc)
public void onButtonCopyPreviousTitleDesc(){
presenter.fetchPreviousTitleAndDescription(callback.getIndexInViewFlipper(this));
@OnClick(R.id.btn_copy_subsequent_media)
public void onButtonCopyTitleDescToSubsequentMedia(){
presenter.copyTitleAndDescriptionToSubsequentMedia(callback.getIndexInViewFlipper(this));
Toast.makeText(getContext(), getResources().getString(R.string.copied_successfully), Toast.LENGTH_SHORT).show();
}
}

View file

@ -32,6 +32,8 @@ public interface UploadMediaDetailsContract {
void showBadImagePopup(Integer errorCode, UploadItem uploadItem);
void showConnectionErrorPopup();
void showMapWithImageCoordinates(boolean shouldShow);
void showExternalMap(UploadItem uploadItem);
@ -45,7 +47,9 @@ public interface UploadMediaDetailsContract {
void verifyImageQuality(int uploadItemIndex);
void fetchPreviousTitleAndDescription(int indexInViewFlipper);
void copyTitleAndDescriptionToSubsequentMedia(int indexInViewFlipper);
void fetchTitleAndDescription(int indexInViewFlipper);
void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex);

View file

@ -23,6 +23,7 @@ import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import java.lang.reflect.Proxy;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
@ -92,7 +93,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
gpsCoords != null && gpsCoords.getImageCoordsExists();
view.showMapWithImageCoordinates(hasImageCoordinates);
view.showProgress(false);
if (hasImageCoordinates) {
if (hasImageCoordinates && place == null) {
checkNearbyPlaces(uploadItem);
}
},
@ -138,8 +139,12 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
},
throwable -> {
view.showProgress(false);
view.showMessage("" + throwable.getLocalizedMessage(),
R.color.color_error);
if (throwable instanceof UnknownHostException) {
view.showConnectionErrorPopup();
} else {
view.showMessage("" + throwable.getLocalizedMessage(),
R.color.color_error);
}
Timber.e(throwable, "Error occurred while handling image");
})
);
@ -147,22 +152,29 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
/**
* Fetches and sets the caption and desctiption of the previous item
* Copies the caption and description of the current item to the subsequent media
*
* @param indexInViewFlipper
*/
@Override
public void fetchPreviousTitleAndDescription(int indexInViewFlipper) {
UploadItem previousUploadItem = repository.getPreviousUploadItem(indexInViewFlipper);
if (null != previousUploadItem) {
final UploadItem currentUploadItem = repository.getUploads().get(indexInViewFlipper);
currentUploadItem.setMediaDetails(deepCopy(previousUploadItem.getUploadMediaDetails()));
view.updateMediaDetails(currentUploadItem.getUploadMediaDetails());
} else {
view.showMessage(R.string.previous_image_title_description_not_found, R.color.color_error);
}
public void copyTitleAndDescriptionToSubsequentMedia(int indexInViewFlipper) {
for(int i = indexInViewFlipper+1; i < repository.getCount(); i++){
final UploadItem subsequentUploadItem = repository.getUploads().get(i);
subsequentUploadItem.setMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails()));
}
}
/**
* Fetches and set the caption and description of the item
*
* @param indexInViewFlipper
*/
@Override
public void fetchTitleAndDescription(int indexInViewFlipper) {
final UploadItem currentUploadItem = repository.getUploads().get(indexInViewFlipper);
view.updateMediaDetails(currentUploadItem.getUploadMediaDetails());
}
@NotNull
private List<UploadMediaDetail> deepCopy(List<UploadMediaDetail> uploadMediaDetails) {
final ArrayList<UploadMediaDetail> newList = new ArrayList<>();
@ -187,6 +199,9 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
final List<UploadMediaDetail> uploadMediaDetails = repository.getUploads()
.get(uploadItemPosition)
.getUploadMediaDetails();
UploadItem uploadItem = repository.getUploads()
.get(uploadItemPosition);
uploadItem.setPlace(place);
uploadMediaDetails.set(0, new UploadMediaDetail(place));
view.updateMediaDetails(uploadMediaDetails);
}

View file

@ -1,6 +1,8 @@
package fr.free.nrw.commons.upload.structure.depictions
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.upload.WikidataItem
import fr.free.nrw.commons.wikidata.WikidataProperties
@ -20,6 +22,7 @@ const val THUMB_IMAGE_SIZE = "70px"
* Model class for Depicted Item in Upload and Explore
*/
@Parcelize
@Entity
data class DepictedItem constructor(
override val name: String,
val description: String?,
@ -27,7 +30,7 @@ data class DepictedItem constructor(
val instanceOfs: List<String>,
val commonsCategories: List<String>,
var isSelected: Boolean,
override val id: String
@PrimaryKey override val id: String
) : WikidataItem, Parcelable {
constructor(entity: Entities.Entity) : this(

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