Merge remote-tracking branch 'origin/master' into structured-data

# Conflicts:
#	app/build.gradle
#	app/src/main/java/fr/free/nrw/commons/CommonsApplication.java
#	app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java
#	app/src/main/java/fr/free/nrw/commons/contributions/model/DisplayableContribution.java
#	app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java
#	app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java
#	app/src/main/java/fr/free/nrw/commons/upload/UploadService.java
#	app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java
#	app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java
This commit is contained in:
Sean Mac Gillicuddy 2020-03-20 08:46:39 +00:00
commit 719f32c27e
127 changed files with 1946 additions and 1520 deletions

7
.gitignore vendored
View file

@ -1,10 +1,13 @@
# IDEA files
*.iml
*.iws
*.ipr
.idea
*.iws
/.idea/*
app/src/main/gen/*
# IDEA/Android Studio Ignore exceptions
!/.idea/codeStyles/
# Gradle
.gradle
local.properties

544
.idea/codeStyles/Project.xml generated Normal file
View file

@ -0,0 +1,544 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</value>
</option>
<AndroidXmlCodeStyleSettings>
<option name="LAYOUT_SETTINGS">
<value>
<option name="INSERT_BLANK_LINE_BEFORE_TAG" value="false" />
</value>
</option>
</AndroidXmlCodeStyleSettings>
<JSCodeStyleSettings>
<option name="INDENT_CHAINED_CALLS" value="false" />
</JSCodeStyleSettings>
<JavaCodeStyleSettings>
<option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<Objective-C>
<option name="INDENT_NAMESPACE_MEMBERS" value="0" />
<option name="INDENT_C_STRUCT_MEMBERS" value="2" />
<option name="INDENT_CLASS_MEMBERS" value="2" />
<option name="INDENT_VISIBILITY_KEYWORDS" value="1" />
<option name="INDENT_INSIDE_CODE_BLOCK" value="2" />
<option name="KEEP_STRUCTURES_IN_ONE_LINE" value="true" />
<option name="FUNCTION_PARAMETERS_WRAP" value="5" />
<option name="FUNCTION_CALL_ARGUMENTS_WRAP" value="5" />
<option name="TEMPLATE_CALL_ARGUMENTS_WRAP" value="5" />
<option name="TEMPLATE_CALL_ARGUMENTS_ALIGN_MULTILINE" value="true" />
<option name="ALIGN_INIT_LIST_IN_COLUMNS" value="false" />
<option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" />
</Objective-C>
<Objective-C-extensions>
<extensions>
<pair source="cc" header="h" fileNamingConvention="NONE" />
<pair source="c" header="h" fileNamingConvention="NONE" />
</extensions>
</Objective-C-extensions>
<Python>
<option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" />
</Python>
<TypeScriptCodeStyleSettings>
<option name="INDENT_CHAINED_CALLS" value="false" />
</TypeScriptCodeStyleSettings>
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="CSS">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="ECMA Script Level 4">
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<option name="PARENT_SETTINGS_INSTALLED" value="true" />
</codeStyleSettings>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_RESOURCES" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="THROWS_KEYWORD_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="WRAP_COMMENTS" value="true" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="RIGHT_MARGIN" value="80" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<option name="PARENT_SETTINGS_INSTALLED" value="true" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="ObjectiveC">
<option name="RIGHT_MARGIN" value="80" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
<option name="BLANK_LINES_BEFORE_IMPORTS" value="0" />
<option name="BLANK_LINES_AFTER_IMPORTS" value="0" />
<option name="BLANK_LINES_AROUND_CLASS" value="0" />
<option name="BLANK_LINES_AROUND_METHOD" value="0" />
<option name="BLANK_LINES_AROUND_METHOD_IN_INTERFACE" value="0" />
<option name="ALIGN_MULTILINE_BINARY_OPERATION" value="false" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="PROTO">
<option name="RIGHT_MARGIN" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Python">
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="RIGHT_MARGIN" value="80" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="PARENT_SETTINGS_INSTALLED" value="true" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="SASS">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="SCSS">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:.*Style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_width</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_height</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_weight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_margin</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginTop</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginBottom</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginStart</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginEnd</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginLeft</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_marginRight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:padding</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingTop</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingBottom</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingStart</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingEnd</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingLeft</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:paddingRight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/tools</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
<codeStyleSettings language="protobuf">
<option name="RIGHT_MARGIN" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View file

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View file

@ -8,7 +8,7 @@ What changes did you make and why?
Tested {build variant, e.g. ProdDebug} on {name of device or emulator} with API level {API level}.
**Screenshots showing what changed (optional - for UI changes)**
**Screenshots (for UI changes only)**
Need help? See https://support.google.com/android/answer/9075928

View file

@ -5,6 +5,7 @@ apply from: '../gitutils.gradle'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: "com.hiya.jacoco-android"
apply from: 'quality.gradle'
@ -38,6 +39,7 @@ dependencies {
implementation 'com.github.pedrovgs:renderers:3.3.3'
implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:8.6.2'
implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-localization-v8:0.11.0'
implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-scalebar-v9:0.4.0'
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
implementation 'com.karumi:dexter:5.0.0'
@ -105,13 +107,18 @@ dependencies {
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'
//Room
def room_version= '2.2.3'
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor
implementation "androidx.room:room-runtime:$ROOM_VERSION"
implementation "androidx.room:room-ktx:$ROOM_VERSION"
implementation "androidx.room:room-rxjava2:$ROOM_VERSION"
kapt "androidx.room:room-compiler:$ROOM_VERSION" // For Kotlin use kapt instead of annotationProcessor
implementation 'com.squareup.retrofit2:retrofit:2.7.1'
implementation "androidx.room:room-rxjava2:$room_version"
testImplementation "androidx.arch.core:core-testing:2.1.0"
// Pref
// Java language implementation
implementation "androidx.preference:preference:$PREFERENCE_VERSION"
// Kotlin
implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION"
}
android {
@ -293,3 +300,7 @@ if(isRunningOnTravisAndIsNotPRBuild) {
}
}
}
androidExtensions {
experimental = true
}

View file

@ -1 +0,0 @@
[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":90,"versionName":"2.8.3","enabled":true,"outputFile":"app-commons-v2.8.3-acra-prod-release.apk","fullName":"prodRelease","baseName":"prod-release"},"path":"app-commons-v2.8.3-acra-prod-release.apk","properties":{}}]

View file

@ -76,6 +76,10 @@
-keepattributes SourceFile,LineNumberTable
-keepattributes *Annotation*
# --- /recycler view ---
-keep class androidx.recyclerview.widget.RecyclerView {
public androidx.recyclerview.widget.RecyclerView$ViewHolder findViewHolderForPosition(int);
}
# --- Parcelable ---
-keepclassmembers class * implements android.os.Parcelable {
static ** CREATOR;

View file

@ -0,0 +1,107 @@
package fr.free.nrw.commons
import android.app.Activity
import android.app.Instrumentation
import android.content.Intent
import androidx.test.InstrumentationRegistry
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import fr.free.nrw.commons.utils.ConfigUtils
import org.hamcrest.CoreMatchers
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AboutActivityTest {
@get:Rule
var activityRule: ActivityTestRule<*> = ActivityTestRule(AboutActivity::class.java)
@Before
fun setup() {
Intents.init()
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
}
@Test
fun testBuildNumber() {
Espresso.onView(ViewMatchers.withId(R.id.about_version))
.check(ViewAssertions.matches(withText(ConfigUtils.getVersionNameWithSha(getApplicationContext()))))
}
@Test
fun testLaunchWebsite() {
Espresso.onView(ViewMatchers.withId(R.id.website_launch_icon)).perform(ViewActions.click())
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.WEBSITE_URL)))
}
@Test
fun testLaunchFacebook() {
Espresso.onView(ViewMatchers.withId(R.id.facebook_launch_icon)).perform(ViewActions.click())
Intents.intended(IntentMatchers.hasAction(Intent.ACTION_VIEW))
Intents.intended(CoreMatchers.anyOf(IntentMatchers.hasData(Urls.FACEBOOK_WEB_URL),
IntentMatchers.hasPackage(Urls.FACEBOOK_PACKAGE_NAME)))
}
@Test
fun testLaunchGithub() {
Espresso.onView(ViewMatchers.withId(R.id.github_launch_icon)).perform(ViewActions.click())
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.GITHUB_REPO_URL)))
}
@Test
fun testLaunchRateUs() {
val appPackageName = InstrumentationRegistry.getInstrumentation().targetContext.packageName
Espresso.onView(ViewMatchers.withId(R.id.about_rate_us)).perform(ViewActions.click())
Intents.intended(IntentMatchers.hasAction(Intent.ACTION_VIEW))
Intents.intended(CoreMatchers.anyOf(IntentMatchers.hasData("${Urls.PLAY_STORE_URL_PREFIX}$appPackageName"),
IntentMatchers.hasData("${Urls.PLAY_STORE_URL_PREFIX}$appPackageName")))
}
@Test
fun testLaunchAboutPrivacyPolicy() {
Espresso.onView(ViewMatchers.withId(R.id.about_privacy_policy)).perform(ViewActions.click())
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(BuildConfig.PRIVACY_POLICY_URL)))
}
@Test
fun testLaunchTranslate() {
Espresso.onView(ViewMatchers.withId(R.id.about_translate)).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(android.R.id.button1)).perform(ViewActions.click())
val langCode = CommonsApplication.getInstance().languageLookUpTable.codes[0]
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData("${Urls.TRANSLATE_WIKI_URL}$langCode")))
}
@Test
fun testLaunchAboutCredits() {
Espresso.onView(ViewMatchers.withId(R.id.about_credits)).perform(ViewActions.click())
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.CREDITS_URL)))
}
@Test
fun testLaunchAboutFaq() {
Espresso.onView(ViewMatchers.withId(R.id.about_faq)).perform(ViewActions.click())
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.FAQ_URL)))
}
@Test
fun orientationChange() {
UITestHelper.changeOrientation(activityRule)
}
}

View file

@ -9,8 +9,12 @@ import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.rule.ActivityTestRule
import org.apache.commons.lang3.StringUtils
import org.hamcrest.BaseMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher
import timber.log.Timber
class UITestHelper {
companion object {
fun skipWelcome() {
@ -34,7 +38,7 @@ class UITestHelper {
closeSoftKeyboard()
onView(ViewMatchers.withId(R.id.login_button))
.perform(ViewActions.click())
sleep(5000)
sleep(10000)
} catch (ignored: NoMatchingViewException) {
}
@ -68,5 +72,22 @@ class UITestHelper {
activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
assert(activityRule.activity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
}
fun <T> first(matcher: Matcher<T>): Matcher<T>? {
return object : BaseMatcher<T>() {
var isFirst = true
override fun matches(item: Any): Boolean {
if (isFirst && matcher.matches(item)) {
isFirst = false
return true
}
return false
}
override fun describeTo(description: Description) {
description.appendText("should return first matching item")
}
}
}
}
}

View file

@ -8,11 +8,13 @@ import android.graphics.Bitmap
import android.net.Uri
import android.os.Environment
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.intending
@ -24,6 +26,8 @@ import androidx.test.rule.ActivityTestRule
import androidx.test.rule.GrantPermissionRule
import androidx.test.runner.AndroidJUnit4
import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.upload.DescriptionsAdapter
import fr.free.nrw.commons.util.MyViewAction
import fr.free.nrw.commons.utils.ConfigUtils
import org.hamcrest.core.AllOf.allOf
import org.junit.After
@ -65,7 +69,6 @@ class UploadTest {
}
UITestHelper.skipWelcome()
UITestHelper.loginUser()
saveToInternalStorage()
}
@After
@ -73,59 +76,15 @@ class UploadTest {
Intents.release()
}
private fun saveToInternalStorage() {
val bitmapImage = randomBitmap
// path to /data/data/yourapp/app_data/imageDir
val mypath = File(Environment.getExternalStorageDirectory(), "image.jpg")
Timber.d("Filepath: %s", mypath.path)
Timber.d("Absolute Filepath: %s", mypath.absolutePath)
var fos: FileOutputStream? = null
try {
fos = FileOutputStream(mypath)
// Use the compress method on the BitMap object to write image to the OutputStream
bitmapImage.compress(Bitmap.CompressFormat.JPEG, 100, fos)
} catch (e: Exception) {
e.printStackTrace()
} finally {
try {
fos?.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
@Test
fun uploadTest() {
fun testUploadWithDescription() {
if (!ConfigUtils.isBetaFlavour()) {
throw Error("This test should only be run in Beta!")
}
// Uri to return by our mock gallery selector
// Requires file 'image.jpg' to be placed at root of file structure
val imageUri = Uri.parse("file://mnt/sdcard/image.jpg")
setupSingleUpload("image.jpg")
// Build a result to return from the Camera app
val intent = Intent()
intent.data = imageUri
val result = ActivityResult(Activity.RESULT_OK, intent)
// Stub out the File picker. When an intent is sent to the File picker, this tells
// Espresso to respond with the ActivityResult we just created
intending(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*"))).respondWith(result)
// Open FAB
onView(allOf<View>(withId(R.id.fab_plus), isDisplayed()))
.perform(click())
// Click gallery
onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed()))
.perform(click())
openGallery()
// Validate that an intent to get an image is sent
intended(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*")))
@ -158,7 +117,7 @@ class UploadTest {
UITestHelper.sleep(3000)
try {
onView(allOf(isDisplayed(), withParent(withId(R.id.rv_categories))))
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
.perform(click())
} catch (ignored: NoMatchingViewException) {
}
@ -188,4 +147,206 @@ class UploadTest {
} catch (ignored: NoMatchingViewException) {
}
}
@Test
fun testUploadWithoutDescription() {
if (!ConfigUtils.isBetaFlavour()) {
throw Error("This test should only be run in Beta!")
}
setupSingleUpload("image.jpg")
openGallery()
// Validate that an intent to get an image is sent
intended(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*")))
// Create filename with the current time (to prevent overwrites)
val dateFormat = SimpleDateFormat("yyMMdd-hhmmss")
val commonsFileName = "MobileTest " + dateFormat.format(Date())
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
dismissWarning("Yes")
onView(allOf<View>(isDisplayed(), withId(R.id.et_title)))
.perform(replaceText(commonsFileName))
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click())
UITestHelper.sleep(10000)
dismissWarning("Yes")
UITestHelper.sleep(3000)
onView(allOf(isDisplayed(), withId(R.id.et_search)))
.perform(replaceText("Test"))
UITestHelper.sleep(3000)
try {
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
.perform(click())
} catch (ignored: NoMatchingViewException) {
}
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click())
dismissWarning("Yes, Submit")
UITestHelper.sleep(500)
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
.perform(click())
UITestHelper.sleep(10000)
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
commonsFileName.replace(' ', '_') + ".jpg"
Timber.i("File should be uploaded to $fileUrl")
}
@Test
fun testUploadWithMultilingualDescription() {
if (!ConfigUtils.isBetaFlavour()) {
throw Error("This test should only be run in Beta!")
}
setupSingleUpload("image.jpg")
openGallery()
// Validate that an intent to get an image is sent
intended(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*")))
// Create filename with the current time (to prevent overwrites)
val dateFormat = SimpleDateFormat("yyMMdd-hhmmss")
val commonsFileName = "MobileTest " + dateFormat.format(Date())
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
dismissWarningDialog()
onView(allOf<View>(isDisplayed(), withId(R.id.et_title)))
.perform(replaceText(commonsFileName))
onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(0,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))
onView(withId(R.id.btn_add_description))
.perform(click())
onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(1,
MyViewAction.selectSpinnerItemInChildViewWithId(R.id.spinner_description_languages, 2)))
onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(1,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description")))
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click())
UITestHelper.sleep(5000)
dismissWarning("Yes")
UITestHelper.sleep(3000)
onView(allOf(isDisplayed(), withId(R.id.et_search)))
.perform(replaceText("Test"))
UITestHelper.sleep(3000)
try {
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
.perform(click())
} catch (ignored: NoMatchingViewException) {
}
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click())
dismissWarning("Yes, Submit")
UITestHelper.sleep(500)
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
.perform(click())
UITestHelper.sleep(10000)
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
commonsFileName.replace(' ', '_') + ".jpg"
Timber.i("File should be uploaded to $fileUrl")
}
private fun setupSingleUpload(imageName: String) {
saveToInternalStorage(imageName)
singleImageIntent(imageName)
}
private fun saveToInternalStorage(imageName: String) {
val bitmapImage = randomBitmap
// path to /data/data/yourapp/app_data/imageDir
val mypath = File(Environment.getExternalStorageDirectory(), imageName)
Timber.d("Filepath: %s", mypath.path)
Timber.d("Absolute Filepath: %s", mypath.absolutePath)
var fos: FileOutputStream? = null
try {
fos = FileOutputStream(mypath)
// Use the compress method on the BitMap object to write image to the OutputStream
bitmapImage.compress(Bitmap.CompressFormat.JPEG, 100, fos)
} catch (e: Exception) {
e.printStackTrace()
} finally {
try {
fos?.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
private fun singleImageIntent(imageName: String) {
// Uri to return by our mock gallery selector
// Requires file 'image.jpg' to be placed at root of file structure
val imageUri = Uri.parse("file://mnt/sdcard/$imageName")
// Build a result to return from the Camera app
val intent = Intent()
intent.data = imageUri
val result = ActivityResult(Activity.RESULT_OK, intent)
// Stub out the File picker. When an intent is sent to the File picker, this tells
// Espresso to respond with the ActivityResult we just created
intending(allOf(hasAction(Intent.ACTION_GET_CONTENT), hasType("image/*"))).respondWith(result)
}
private fun dismissWarningDialog() {
try {
onView(withText("Yes"))
.check(matches(isDisplayed()))
.perform(click())
} catch (ignored: NoMatchingViewException) {
}
}
private fun openGallery() {
// Open FAB
onView(allOf<View>(withId(R.id.fab_plus), isDisplayed()))
.perform(click())
// Click gallery
onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed()))
.perform(click())
}
}

View file

@ -1,25 +0,0 @@
package fr.free.nrw.commons
import android.view.View
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import org.hamcrest.Matcher
object ViewActions {
fun clickChildViewWithId(id: Int): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return null
}
override fun getDescription(): String {
return "Click on a child view with specified id."
}
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<View>(id)
v.performClick()
}
}
}
}

View file

@ -0,0 +1,64 @@
package fr.free.nrw.commons.util
import android.view.View
import android.widget.EditText
import androidx.appcompat.widget.AppCompatSpinner
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import org.hamcrest.Matcher
class MyViewAction {
companion object {
fun typeTextInChildViewWithId(id: Int, textToBeTyped: String): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return null
}
override fun getDescription(): String {
return "Click on a child view with specified id."
}
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<View>(id) as EditText
v.setText(textToBeTyped)
}
}
}
fun selectSpinnerItemInChildViewWithId(id: Int, position: Int): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return null
}
override fun getDescription(): String {
return "Click on a child view with specified id."
}
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<View>(id) as AppCompatSpinner
v.setSelection(position)
}
}
}
fun clickItemWithId(id: Int, position: Int): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return null
}
override fun getDescription(): String {
return "Click on a child view with specified id."
}
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<View>(id) as View
v.performClick()
}
}
}
}
}

View file

@ -122,7 +122,7 @@ public class AboutActivity extends NavigationBaseActivity {
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.share_app_icon:
String shareText = String.format(getString(R.string.share_text), Urls.PLAY_STORE_URL);
String shareText = String.format(getString(R.string.share_text), Urls.PLAY_STORE_URL_PREFIX + this.getPackageName());
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, shareText);

View file

@ -1,5 +1,13 @@
package fr.free.nrw.commons;
import static fr.free.nrw.commons.data.DBOpenHelper.CONTRIBUTIONS_TABLE;
import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE;
import static org.acra.ReportField.APP_VERSION_NAME;
import static org.acra.ReportField.PHONE_MODEL;
import static org.acra.ReportField.STACK_TRACE;
import static org.acra.ReportField.USER_COMMENT;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.NotificationChannel;
@ -9,32 +17,13 @@ import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import android.os.Process;
import android.util.Log;
import androidx.annotation.NonNull;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.core.ImagePipelineConfig;
import com.facebook.imagepipeline.producers.NetworkFetcher;
import com.mapbox.mapboxsdk.Mapbox;
import com.squareup.leakcanary.LeakCanary;
import com.squareup.leakcanary.RefWatcher;
import org.acra.ACRA;
import org.acra.annotation.AcraCore;
import org.acra.annotation.AcraDialog;
import org.acra.annotation.AcraMailSender;
import org.acra.data.StringFormat;
import org.wikipedia.AppAdapter;
import org.wikipedia.language.AppLanguageLookUpTable;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
@ -54,17 +43,20 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.internal.functions.Functions;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers;
import okhttp3.OkHttpClient;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import org.acra.ACRA;
import org.acra.annotation.AcraCore;
import org.acra.annotation.AcraDialog;
import org.acra.annotation.AcraMailSender;
import org.acra.data.StringFormat;
import org.wikipedia.AppAdapter;
import org.wikipedia.language.AppLanguageLookUpTable;
import timber.log.Timber;
import static fr.free.nrw.commons.data.DBOpenHelper.CONTRIBUTIONS_TABLE;
import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE;
import static org.acra.ReportField.APP_VERSION_NAME;
import static org.acra.ReportField.PHONE_MODEL;
import static org.acra.ReportField.STACK_TRACE;
import static org.acra.ReportField.USER_COMMENT;
@AcraCore(
buildConfigClass = BuildConfig.class,
resReportSendSuccessToast = R.string.crash_dialog_ok_toast,
@ -90,9 +82,6 @@ public class CommonsApplication extends Application {
@Inject @Named("default_preferences") JsonKvStore defaultPrefs;
@Inject
OkHttpClient okHttpClient;
/**
* Constants begin
*/
@ -157,15 +146,9 @@ public class CommonsApplication extends Application {
}
// Set DownsampleEnabled to True to downsample the image in case it's heavy
ImagePipelineConfig.Builder imagePipelineConfigBuilder = ImagePipelineConfig.newBuilder(this)
.setDownsampleEnabled(true);
if(ConfigUtils.isBetaFlavour()){
NetworkFetcher networkFetcher=new CustomNetworkFetcher(okHttpClient);
imagePipelineConfigBuilder.setNetworkFetcher(networkFetcher);
}
ImagePipelineConfig config = imagePipelineConfigBuilder.build();
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this)
.setDownsampleEnabled(true)
.build();
try {
Fresco.initialize(this, config);
} catch (Exception e) {

View file

@ -1,206 +0,0 @@
package fr.free.nrw.commons;
import android.net.Uri;
import android.os.Looper;
import android.os.SystemClock;
import com.facebook.imagepipeline.common.BytesRange;
import com.facebook.imagepipeline.image.EncodedImage;
import com.facebook.imagepipeline.producers.BaseNetworkFetcher;
import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks;
import com.facebook.imagepipeline.producers.Consumer;
import com.facebook.imagepipeline.producers.FetchState;
import com.facebook.imagepipeline.producers.NetworkFetcher;
import com.facebook.imagepipeline.producers.ProducerContext;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/** Network fetcher that uses OkHttp 3 as a backend. */
public class CustomNetworkFetcher
extends BaseNetworkFetcher<CustomNetworkFetcher.OkHttpNetworkFetchState> {
public static class OkHttpNetworkFetchState extends FetchState {
public long submitTime;
public long responseTime;
public long fetchCompleteTime;
public OkHttpNetworkFetchState(
Consumer<EncodedImage> consumer, ProducerContext producerContext) {
super(consumer, producerContext);
}
}
private static final String QUEUE_TIME = "queue_time";
private static final String FETCH_TIME = "fetch_time";
private static final String TOTAL_TIME = "total_time";
private static final String IMAGE_SIZE = "image_size";
private final Call.Factory mCallFactory;
private final CacheControl mCacheControl;
private Executor mCancellationExecutor;
/** @param okHttpClient client to use */
public CustomNetworkFetcher(OkHttpClient okHttpClient) {
this(okHttpClient, okHttpClient.dispatcher().executorService());
}
/**
* @param callFactory custom {@link Call.Factory} for fetching image from the network
* @param cancellationExecutor executor on which fetching cancellation is performed if
* cancellation is requested from the UI Thread
*/
public CustomNetworkFetcher(Call.Factory callFactory, Executor cancellationExecutor) {
this(callFactory, cancellationExecutor, true);
}
/**
* @param callFactory custom {@link Call.Factory} for fetching image from the network
* @param cancellationExecutor executor on which fetching cancellation is performed if
* cancellation is requested from the UI Thread
* @param disableOkHttpCache true if network requests should not be cached by OkHttp
*/
public CustomNetworkFetcher(
Call.Factory callFactory, Executor cancellationExecutor, boolean disableOkHttpCache) {
mCallFactory = callFactory;
mCancellationExecutor = cancellationExecutor;
mCacheControl = disableOkHttpCache ? new CacheControl.Builder().noStore().build() : null;
}
@Override
public OkHttpNetworkFetchState createFetchState(
Consumer<EncodedImage> consumer, ProducerContext context) {
return new OkHttpNetworkFetchState(consumer, context);
}
@Override
public void fetch(
final OkHttpNetworkFetchState fetchState, final NetworkFetcher.Callback callback) {
fetchState.submitTime = SystemClock.elapsedRealtime();
final Uri uri = fetchState.getUri();
try {
final Request.Builder requestBuilder = new Request.Builder().url(uri.toString()).get();
if (mCacheControl != null) {
requestBuilder.cacheControl(mCacheControl);
}
final BytesRange bytesRange = fetchState.getContext().getImageRequest().getBytesRange();
if (bytesRange != null) {
requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue());
}
fetchWithRequest(fetchState, callback, requestBuilder.build());
} catch (Exception e) {
// handle error while creating the request
callback.onFailure(e);
}
}
@Override
public void onFetchCompletion(OkHttpNetworkFetchState fetchState, int byteSize) {
fetchState.fetchCompleteTime = SystemClock.elapsedRealtime();
}
@Override
public Map<String, String> getExtraMap(OkHttpNetworkFetchState fetchState, int byteSize) {
Map<String, String> extraMap = new HashMap<>(4);
extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime));
extraMap.put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime));
extraMap.put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime));
extraMap.put(IMAGE_SIZE, Integer.toString(byteSize));
return extraMap;
}
protected void fetchWithRequest(
final OkHttpNetworkFetchState fetchState,
final NetworkFetcher.Callback callback,
final Request request) {
final Call call = mCallFactory.newCall(request);
fetchState
.getContext()
.addCallbacks(
new BaseProducerContextCallbacks() {
@Override
public void onCancellationRequested() {
if (Looper.myLooper() != Looper.getMainLooper()) {
call.cancel();
} else {
mCancellationExecutor.execute(
new Runnable() {
@Override
public void run() {
call.cancel();
}
});
}
}
});
call.enqueue(
new okhttp3.Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
fetchState.responseTime = SystemClock.elapsedRealtime();
final ResponseBody body = response.body();
try {
if (!response.isSuccessful()) {
handleException(
call, new IOException("Unexpected HTTP code " + response), callback);
return;
}
BytesRange responseRange =
BytesRange.fromContentRangeHeader(response.header("Content-Range"));
if (responseRange != null
&& !(responseRange.from == 0
&& responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
// Only treat as a partial image if the range is not all of the content
fetchState.setResponseBytesRange(responseRange);
fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT);
}
long contentLength = body.contentLength();
if (contentLength < 0) {
contentLength = 0;
}
callback.onResponse(body.byteStream(), (int) contentLength);
} catch (Exception e) {
handleException(call, e, callback);
} finally {
body.close();
}
}
@Override
public void onFailure(Call call, IOException e) {
handleException(call, e, callback);
}
});
}
/**
* Handles exceptions.
*
* <p>OkHttp notifies callers of cancellations via an IOException. If IOException is caught after
* request cancellation, then the exception is interpreted as successful cancellation and
* onCancellation is called. Otherwise onFailure is called.
*/
private void handleException(final Call call, final Exception e, final Callback callback) {
if (call.isCanceled()) {
callback.onCancellation();
} else {
callback.onFailure(e);
}
}
}

View file

@ -45,6 +45,7 @@ public class Media implements Parcelable {
};
// Primary metadata fields
@Nullable
public Uri localUri;
public String thumbUrl;
public String imageUrl;

View file

@ -8,8 +8,6 @@ import org.wikipedia.dataclient.okhttp.HttpStatusException;
import java.io.File;
import java.io.IOException;
import fr.free.nrw.commons.di.SslUtils;
import fr.free.nrw.commons.utils.ConfigUtils;
import okhttp3.Cache;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
@ -31,17 +29,13 @@ public final class OkHttpConnectionFactory {
@NonNull
private static OkHttpClient createClient() {
OkHttpClient.Builder builder = new OkHttpClient.Builder()
return new OkHttpClient.Builder()
.cookieJar(SharedPreferenceCookieManager.getInstance())
.cache(NET_CACHE)
.addInterceptor(getLoggingInterceptor())
.addInterceptor(new UnsuccessfulResponseInterceptor())
.addInterceptor(new CommonHeaderRequestInterceptor());
if(ConfigUtils.isBetaFlavour()){
builder.sslSocketFactory(SslUtils.INSTANCE.getTrustAllHostsSSLSocketFactory());
}
return builder.build();
.addInterceptor(new CommonHeaderRequestInterceptor())
.build();
}
private static HttpLoggingInterceptor getLoggingInterceptor() {

View file

@ -6,7 +6,8 @@ internal object Urls {
const val WEBSITE_URL = "https://commons-app.github.io"
const val CREDITS_URL = "https://github.com/commons-app/apps-android-commons/blob/master/CREDITS"
const val FAQ_URL = "https://github.com/commons-app/apps-android-commons/wiki/Frequently-Asked-Questions"
const val PLAY_STORE_URL = "https://play.google.com/store/apps/details?id=fr.free.nrw.commons"
const val PLAY_STORE_PREFIX = "market://details?id="
const val PLAY_STORE_URL_PREFIX = "https://play.google.com/store/apps/details?id="
const val TRANSLATE_WIKI_URL = "https://translatewiki.net/w/i.php?title=Special:Translate&group=commons-android-strings&filter=%21translated&action=translate&language="
const val FACEBOOK_WEB_URL = "https://www.facebook.com/1921335171459985"
const val FACEBOOK_APP_URL = "fb://page/1921335171459985"

View file

@ -115,12 +115,12 @@ public class Utils {
* @param context
*/
public static void rateApp(Context context) {
final String appPackageName = BuildConfig.class.getPackage().getName();
final String appPackageName = context.getPackageName();
try {
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + appPackageName)));
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Urls.PLAY_STORE_PREFIX + appPackageName)));
}
catch (android.content.ActivityNotFoundException anfe) {
handleWebUrl(context, Uri.parse("https://play.google.com/store/apps/details?id=" + appPackageName));
handleWebUrl(context, Uri.parse(Urls.PLAY_STORE_URL_PREFIX + appPackageName));
}
}

View file

@ -145,13 +145,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
}
@OnFocusChange(R.id.login_username)
void onUsernameFocusChanged(View view, boolean hasFocus) {
if (!hasFocus) {
ViewUtil.hideKeyboard(view);
}
}
@OnFocusChange(R.id.login_password)
void onPasswordFocusChanged(View view, boolean hasFocus) {
if (!hasFocus) {

View file

@ -111,10 +111,8 @@ public class GridViewAdapter extends ArrayAdapter {
*/
private void setAuthorView(Media item, TextView author) {
if (!TextUtils.isEmpty(item.getCreator())) {
String uploadedByTemplate = getContext().getString(R.string.image_uploaded_by);
String uploadedBy = String.format(Locale.getDefault(), uploadedByTemplate, item.getCreator());
author.setText(uploadedBy);
author.setVisibility(View.VISIBLE);
author.setText(getContext().getString(R.string.image_uploaded_by, item.getCreator()));
} else {
author.setVisibility(View.GONE);
}

View file

@ -70,6 +70,7 @@ public class Contribution extends Media {
public String decimalCoords;
public boolean isMultiple;
public String wikiDataEntityId;
private String p18Value;
public Uri contentProviderUri;
public String dateCreatedSource;
@ -287,6 +288,19 @@ public class Contribution extends Media {
this.wikiDataEntityId = wikiDataEntityId;
}
public String getP18Value() {
return p18Value;
}
/**
* When the corresponding image property of wiki entity is known as in case of nearby uploads,
* it can be set using the setter method
* @param p18Value p18 value, image property of the wikidata item
*/
public void setP18Value(String p18Value) {
this.p18Value = p18Value;
}
public void setContentProviderUri(Uri contentProviderUri) {
this.contentProviderUri = contentProviderUri;
}

View file

@ -8,6 +8,8 @@ import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Transaction;
import androidx.room.Update;
import io.reactivex.disposables.Disposable;
import java.util.List;
import io.reactivex.Completable;
@ -52,4 +54,7 @@ public abstract class ContributionDao {
@Query("Delete FROM contribution WHERE state = :state")
public abstract void deleteAll(int state);
@Update
public abstract Single<Integer> update(Contribution contribution);
}

View file

@ -1,40 +1,36 @@
package fr.free.nrw.commons.contributions;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.collection.LruCache;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.view.SimpleDraweeView;
import org.apache.commons.lang3.StringUtils;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.MediaDataExtractor;
import com.facebook.drawee.view.SimpleDraweeView;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.contributions.model.DisplayableContribution;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.upload.FileUtils;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import timber.log.Timber;
public class ContributionViewHolder extends RecyclerView.ViewHolder {
private static final long TIMEOUT_SECONDS = 15;
private static final String NO_CAPTION = "No caption";
private final Callback callback;
@BindView(R.id.contributionImage)
SimpleDraweeView imageView;
@ -44,21 +40,13 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.contributionProgress) ProgressBar progressView;
@BindView(R.id.failed_image_options) LinearLayout failedImageOptions;
@Inject
MediaDataExtractor mediaDataExtractor;
@Inject
MediaClient mediaClient;
@Inject
@Named("thumbnail-cache")
LruCache<String, String> thumbnailCache;
private DisplayableContribution contribution;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private int position;
private static int TIMEOUT_SECONDS = 15;
private static final String NO_CAPTION = "No caption";
private Contribution contribution;
private Random random = new Random();
private CompositeDisposable compositeDisposable = new CompositeDisposable();
ContributionViewHolder(View parent, Callback callback) {
super(parent);
@ -66,16 +54,22 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
this.callback=callback;
}
public void init(int position, DisplayableContribution contribution) {
ApplicationlessInjection.getInstance(itemView.getContext())
.getCommonsApplicationComponent().inject(this);
this.position=position;
public void init(int position, Contribution contribution) {
this.contribution = contribution;
fetchAndDisplayThumbnail(contribution);
fetchAndDisplayCaption(contribution);
titleView.setText(contribution.getDisplayTitle());
this.position = position;
imageView.getHierarchy().setPlaceholderImage(new ColorDrawable(
Color.argb(100, random.nextInt(256), random.nextInt(256), random.nextInt(256))));
String imageSource = chooseImageSource(contribution.thumbUrl, contribution.getLocalUri());
if (!TextUtils.isEmpty(imageSource)) {
final ImageRequest imageRequest =
ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
.setProgressiveRenderingEnabled(true)
.build();
imageView.setImageRequest(imageRequest);
}
seqNumView.setText(String.valueOf(contribution.getPosition() + 1));
seqNumView.setText(String.valueOf(position + 1));
seqNumView.setVisibility(View.VISIBLE);
switch (contribution.getState()) {
@ -118,7 +112,7 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
*
* @param contribution
*/
private void fetchAndDisplayCaption(DisplayableContribution contribution) {
private void fetchAndDisplayCaption(Contribution contribution) {
if ((contribution.getState() != Contribution.STATE_COMPLETED)) {
titleView.setText(contribution.getDisplayTitle());
} else {
@ -137,40 +131,18 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
}
/**
* This method fetches the thumbnail url from file name
* If the thumbnail url is present in cache, then it is used otherwise API call is made to fetch the thumbnail
* This can be removed once #2904 is in place and contribution contains all metadata beforehand
* @param contribution
* Returns the image source for the image view, first preference is given to thumbUrl if that is
* null, moves to local uri and if both are null return null
*
* @param thumbUrl
* @param localUri
* @return
*/
private void fetchAndDisplayThumbnail(DisplayableContribution contribution) {
String keyForLRUCache = contribution.getFilename();
String cacheUrl = thumbnailCache.get(keyForLRUCache);
if (!StringUtils.isBlank(cacheUrl)) {
imageView.setImageURI(cacheUrl);
return;
}
imageView.setBackground(null);
if ((contribution.getState() != Contribution.STATE_COMPLETED) && FileUtils.fileExists(
contribution.getLocalUri())) {
imageView.setImageURI(contribution.getLocalUri());
} else {
Timber.d("Fetching thumbnail for %s", contribution.getFilename());
Disposable disposable = mediaDataExtractor
.getMediaFromFileName(contribution.getFilename())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> {
thumbnailCache.put(keyForLRUCache, media.getThumbUrl());
imageView.setImageURI(media.getThumbUrl());
});
compositeDisposable.add(disposable);
}
}
public void clear() {
compositeDisposable.clear();
@Nullable
private String chooseImageSource(String thumbUrl, Uri localUri) {
return !TextUtils.isEmpty(thumbUrl) ? thumbUrl :
localUri != null ? localUri.toString() :
null;
}
/**

View file

@ -26,10 +26,15 @@ public class ContributionsContract {
}
public interface UserActionListener extends BasePresenter<ContributionsContract.View> {
Contribution getContributionsWithTitle(String uri);
void deleteUpload(Contribution contribution);
Media getItemAtPosition(int i);
void updateContribution(Contribution contribution);
void fetchMediaDetails(Contribution contribution);
}
}

View file

@ -20,6 +20,8 @@ import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
import androidx.fragment.app.FragmentTransaction;
import fr.free.nrw.commons.MediaDataExtractor;
import io.reactivex.disposables.Disposable;
import java.util.List;
import javax.inject.Inject;
@ -219,6 +221,12 @@ public class ContributionsFragment
public Contribution getContributionForPosition(int position) {
return (Contribution) contributionsPresenter.getItemAtPosition(position);
}
@Override
public void fetchMediaUriFor(Contribution contribution) {
Timber.d("Fetching thumbnail for %s", contribution.filename);
contributionsPresenter.fetchMediaDetails(contribution);
}
});
if(null==mediaDetailPagerFragment){

View file

@ -1,5 +1,8 @@
package fr.free.nrw.commons.contributions;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.ViewGroup;
@ -10,7 +13,6 @@ import java.util.ArrayList;
import java.util.List;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.model.DisplayableContribution;
/**
* Represents The View Adapter for the List of Contributions
@ -22,7 +24,7 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
public ContributionsListAdapter(Callback callback) {
this.callback = callback;
contributions=new ArrayList<>();
contributions = new ArrayList<>();
}
/**
@ -41,9 +43,12 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
@Override
public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) {
final Contribution contribution = contributions.get(position);
DisplayableContribution displayableContribution = new DisplayableContribution(contribution,
position);
holder.init(position, displayableContribution);
if (TextUtils.isEmpty(contribution.getThumbUrl())
&& contribution.getState() == Contribution.STATE_COMPLETED) {
callback.fetchMediaUriFor(contribution);
}
holder.init(position, contribution);
}
@Override
@ -51,12 +56,14 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
return contributions.size();
}
public void setContributions(List<Contribution> contributionList) {
if(null!=contributionList) {
this.contributions.clear();
this.contributions.addAll(contributionList);
public void setContributions(@NonNull List<Contribution> contributionList) {
contributions = contributionList;
notifyDataSetChanged();
}
@Override
public long getItemId(int position) {
return contributions.get(position)._id;
}
public interface Callback {
@ -68,5 +75,7 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
void openMediaDetail(int contribution);
Contribution getContributionForPosition(int position);
void fetchMediaUriFor(Contribution contribution);
}
}

View file

@ -90,6 +90,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
private void initAdapter() {
adapter = new ContributionsListAdapter(callback);
adapter.setHasStableIds(true);
}
@Override

View file

@ -74,4 +74,8 @@ class ContributionsLocalDataSource {
public void set(String key, long value) {
defaultKVStore.putLong(key,value);
}
public Single<Integer> updateContribution(Contribution contribution) {
return contributionDao.update(contribution);
}
}

View file

@ -12,6 +12,7 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import fr.free.nrw.commons.MediaDataExtractor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -63,6 +64,10 @@ public class ContributionsPresenter implements UserActionListener {
@Inject
SessionManager sessionManager;
@Inject
MediaDataExtractor mediaDataExtractor;
private LifecycleOwner lifeCycleOwner;
@Inject
@ -181,4 +186,24 @@ public class ContributionsPresenter implements UserActionListener {
}
return contributionList.get(i);
}
@Override
public void updateContribution(Contribution contribution) {
compositeDisposable.add(repository
.updateContribution(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
@Override
public void fetchMediaDetails(Contribution contribution) {
compositeDisposable.add(mediaDataExtractor
.getMediaFromFileName(contribution.filename)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> {
contribution.thumbUrl=media.thumbUrl;
updateContribution(contribution);
}));
}
}

View file

@ -61,4 +61,8 @@ public class ContributionsRepository {
public long getLong(String key) {
return localDataSource.getLong(key);
}
public Single<Integer> updateContribution(Contribution contribution) {
return localDataSource.updateContribution(contribution);
}
}

View file

@ -1,37 +0,0 @@
package fr.free.nrw.commons.contributions.model;
import fr.free.nrw.commons.contributions.Contribution;
public class DisplayableContribution extends Contribution {
private int position;
public DisplayableContribution(Contribution contribution,
int position) {
super(contribution.getContentUri(),
contribution.getFilename(),
contribution.getLocalUri(),
contribution.getImageUrl(),
contribution.getDateCreated(),
contribution.getState(),
contribution.getDataLength(),
contribution.getDateUploaded(),
contribution.getTransferred(),
contribution.getSource(),
contribution.getCaptions(),
contribution.getDescription(),
contribution.getCreator(),
contribution.getMultiple(),
contribution.getWidth(),
contribution.getHeight(),
contribution.getLicense());
this._id=contribution._id;
this.position = position;
}
public int getPosition() {
return position;
}
public void setPosition(int position) {
this.position = position;
}
}

View file

@ -98,6 +98,12 @@ public class DeleteHelper {
String userPageString = "\n{{subst:idw|" + media.getFilename() +
"}} ~~~~";
String creator = media.getCreator();
if (creator == null || creator.isEmpty()) {
throw new RuntimeException("Failed to nominate for deletion");
}
String creatorName = creator.replace(" (page does not exist)", "");
return pageEditClient.prependEdit(media.filename, fileDeleteString + "\n", summary)
.flatMap(result -> {
if (result) {
@ -111,7 +117,7 @@ public class DeleteHelper {
throw new RuntimeException("Failed to nominate for deletion");
}).flatMap(result -> {
if (result) {
return pageEditClient.appendEdit("User_Talk:" + username, userPageString + "\n", summary);
return pageEditClient.appendEdit("User_Talk:" + creatorName, userPageString + "\n", summary);
}
throw new RuntimeException("Failed to nominate for deletion");
});

View file

@ -55,8 +55,6 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
void inject(PicOfDayAppWidget picOfDayAppWidget);
void inject(ContributionViewHolder viewHolder);
Gson gson();
@Component.Builder

View file

@ -18,8 +18,6 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import dagger.Module;
import dagger.Provides;
@ -34,7 +32,6 @@ import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.mwapi.UserInterface;
import fr.free.nrw.commons.review.ReviewInterface;
import fr.free.nrw.commons.upload.UploadInterface;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.wikidata.WikidataInterface;
import fr.free.nrw.commons.upload.WikiBaseInterface;
import fr.free.nrw.commons.upload.depicts.DepictsInterface;
@ -66,19 +63,13 @@ public class NetworkingModule {
public OkHttpClient provideOkHttpClient(Context context,
HttpLoggingInterceptor httpLoggingInterceptor) {
File dir = new File(context.getCacheDir(), "okHttpCache");
OkHttpClient.Builder builder = new OkHttpClient.Builder().connectTimeout(60, TimeUnit.SECONDS)
return new OkHttpClient.Builder().connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.addInterceptor(httpLoggingInterceptor)
.readTimeout(60, TimeUnit.SECONDS)
.cache(new Cache(dir, OK_HTTP_CACHE_SIZE));
if(ConfigUtils.isBetaFlavour()){
builder.sslSocketFactory(SslUtils.INSTANCE.getTrustAllHostsSSLSocketFactory());
.cache(new Cache(dir, OK_HTTP_CACHE_SIZE))
.build();
}
return builder.build();
}
@Provides
@Singleton

View file

@ -1,47 +0,0 @@
package fr.free.nrw.commons.di
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
object SslUtils {
fun getTrustAllHostsSSLSocketFactory(): SSLSocketFactory? {
try {
// Create a trust manager that does not validate certificate chains
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
@Throws(CertificateException::class)
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
}
@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
}
})
// Install the all-trusting trust manager
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
// Create an ssl socket factory with our all-trusting manager
return sslContext.socketFactory
} catch (e: KeyManagementException) {
e.printStackTrace()
return null
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
return null
}
}
}

View file

@ -8,7 +8,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.preference.PreferenceManager;
import androidx.preference.PreferenceManager;
import android.provider.MediaStore;
import android.text.TextUtils;

View file

@ -1,7 +1,7 @@
package fr.free.nrw.commons.filepicker;
import android.content.Context;
import android.preference.PreferenceManager;
import androidx.preference.PreferenceManager;
public class FilePickerConfiguration implements Constants {

View file

@ -6,7 +6,6 @@ import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -214,6 +213,14 @@ public class LocationServiceManager implements LocationListener {
Timber.d("Provider %s disabled", provider);
}
public boolean isNetworkProviderEnabled() {
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
}
public boolean isGPSProviderEnabled() {
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
}
public enum LocationChangeType{
LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers
LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving

View file

@ -397,31 +397,36 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@OnClick(R.id.nominateDeletion)
public void onDeleteButtonClicked(){
if(AccountUtil.getUserName(getContext()).equals(media.getCreator())){
if (AccountUtil.getUserName(getContext()) != null && AccountUtil.getUserName(getContext()).equals(media.getCreator())) {
final ArrayAdapter<String> languageAdapter = new ArrayAdapter<>(getActivity(),
R.layout.simple_spinner_dropdown_list, reasonList);
final Spinner spinner = new Spinner(getActivity());
spinner.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
spinner.setLayoutParams(
new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
spinner.setAdapter(languageAdapter);
spinner.setGravity(17);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(spinner);
builder.setTitle(R.string.nominate_delete)
.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> onDeleteClicked(spinner));
builder.setNegativeButton(R.string.about_translate_cancel, (dialog, which) -> dialog.dismiss());
.setPositiveButton(R.string.about_translate_proceed,
(dialog, which) -> onDeleteClicked(spinner));
builder.setNegativeButton(R.string.about_translate_cancel,
(dialog, which) -> dialog.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
if(isDeleted) {
if (isDeleted) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}
}
//Reviewer correct me if i have misunderstood something over here
//But how does this if (delete.getVisibility() == View.VISIBLE) {
// enableDeleteButton(true); makes sense ?
else{
else {
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
alert.setMessage(getString(R.string.dialog_box_text_nomination,media.getDisplayTitle()));
alert.setMessage(
getString(R.string.dialog_box_text_nomination, media.getDisplayTitle()));
final EditText input = new EditText(getActivity());
alert.setView(input);
input.requestFocus();
@ -460,6 +465,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
}
@SuppressLint("CheckResult")
private void onDeleteClicked(Spinner spinner) {
String reason = spinner.getSelectedItem().toString();

View file

@ -70,7 +70,7 @@ public class NearbyFilterSearchRecyclerViewAdapter
@NonNull
@Override
public RecyclerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = inflater.inflate(callback.isDarkTheme()?R.layout.nearby_search_list_item_dark:R.layout.nearby_search_list_item, parent, false);
View itemView = inflater.inflate(callback.isDarkTheme() ? R.layout.nearby_search_list_item_dark : R.layout.nearby_search_list_item, parent, false);
return new RecyclerViewHolder(itemView);
}
@ -79,17 +79,20 @@ public class NearbyFilterSearchRecyclerViewAdapter
Label label = displayedLabels.get(position);
holder.placeTypeIcon.setImageResource(label.getIcon());
holder.placeTypeLabel.setText(label.toString());
holder.placeTypeLayout.setSelected(label.isSelected());
holder.placeTypeLayout.setBackgroundColor(label.isSelected() ? ContextCompat.getColor(context, R.color.divider_grey) : callback.isDarkTheme()?Color.BLACK:Color.WHITE);
holder.placeTypeLayout.setOnClickListener(view -> {
callback.setCheckboxUnknown();
if (label.isSelected()) {
selectedLabels.remove(label);
} else {
selectedLabels.add(label);
}
label.setSelected(!label.isSelected());
holder.placeTypeLayout.setBackgroundColor(label.isSelected() ? ContextCompat.getColor(context, R.color.divider_grey) : Color.WHITE);
holder.placeTypeLayout.setSelected(label.isSelected());
callback.filterByMarkerType(selectedLabels, 0, false, false);
});
}
@ -165,7 +168,7 @@ public class NearbyFilterSearchRecyclerViewAdapter
notifyDataSetChanged();
}
public interface Callback{
public interface Callback {
void setCheckboxUnknown();

View file

@ -28,6 +28,8 @@ public interface NearbyParentFragmentContract {
void setFABRecenterAction(android.view.View.OnClickListener onClickListener);
void animateFABs();
void recenterMap(LatLng curLatLng);
void showLocationOffDialog();
void openLocationSettings();
void hideBottomSheet();
void hideBottomDetailsSheet();
void displayBottomSheetWithInfo(Marker marker);

View file

@ -6,9 +6,11 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
@ -52,8 +54,11 @@ import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.maps.UiSettings;
import com.mapbox.pluginscalebar.ScaleBarOptions;
import com.mapbox.pluginscalebar.ScaleBarPlugin;
import com.pedrogomez.renderers.RVRendererAdapter;
import fr.free.nrw.commons.utils.DialogUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@ -229,8 +234,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
UiSettings uiSettings = mapBoxMap.getUiSettings();
uiSettings.setCompassGravity(Gravity.BOTTOM | Gravity.LEFT);
uiSettings.setCompassMargins(12, 0, 0, 24);
uiSettings.setLogoEnabled(false);
uiSettings.setAttributionEnabled(false);
uiSettings.setLogoEnabled(true);
uiSettings.setAttributionEnabled(true);
uiSettings.setRotateGesturesEnabled(false);
NearbyParentFragment.this.isMapBoxReady=true;
performMapReadyActions();
@ -239,8 +244,19 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
.zoom(ZOOM_LEVEL)
.build();
mapBoxMap.setCameraPosition(cameraPosition);
});
ScaleBarPlugin scaleBarPlugin = new ScaleBarPlugin(mapView, mapBoxMap);
int color = isDarkTheme ? R.color.bottom_bar_light : R.color.bottom_bar_dark;
ScaleBarOptions scaleBarOptions = new ScaleBarOptions(getContext())
.setTextColor(color)
.setTextSize(R.dimen.description_text_size)
.setBarHeight(R.dimen.tiny_gap)
.setBorderWidth(R.dimen.miniscule_margin)
.setMarginTop(R.dimen.tiny_padding)
.setMarginLeft(R.dimen.tiny_padding)
.setTextBarMargin(R.dimen.tiny_padding);
scaleBarPlugin.create(scaleBarOptions);
});
});
}
@ -1238,6 +1254,9 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
@Override
public void recenterMap(fr.free.nrw.commons.location.LatLng curLatLng) {
if (curLatLng == null) {
if (!(locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled())) {
showLocationOffDialog();
}
return;
}
addCurrentLocationMarker(curLatLng);
@ -1268,6 +1287,28 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
mapBox.animateCamera(CameraUpdateFactory.newCameraPosition(position), 1000);
}
@Override
public void showLocationOffDialog() {
// This creates a dialog box that prompts the user to enable location
DialogUtil
.showAlertDialog(getActivity(), getString(R.string.ask_to_turn_location_on), getString(R.string.nearby_needs_location),
getString(R.string.yes), getString(R.string.no), this::openLocationSettings, null);
}
@Override
public void openLocationSettings() {
// This method opens the location settings of the device along with a followup toast.
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
PackageManager packageManager = getActivity().getPackageManager();
if (intent.resolveActivity(packageManager)!= null) {
startActivity(intent);
Toast.makeText(getContext(), R.string.recommend_high_accuracy_mode, Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getContext(), R.string.cannot_open_location_settings, Toast.LENGTH_LONG).show();
}
}
@Override
public void hideBottomSheet() {
bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);

View file

@ -182,11 +182,10 @@ public class UploadRemoteDataSource {
* ask the UplaodModel for the image quality of the UploadItem
*
* @param uploadItem
* @param shouldValidateTitle
* @return
*/
public Single<Integer> getImageQuality(UploadItem uploadItem, boolean shouldValidateTitle) {
return uploadModel.getImageQuality(uploadItem, shouldValidateTitle);
public Single<Integer> getImageQuality(UploadItem uploadItem) {
return uploadModel.getImageQuality(uploadItem);
}
/**

View file

@ -193,11 +193,10 @@ public class UploadRepository {
* query the RemoteDataSource for image quality
*
* @param uploadItem
* @param shouldValidateTitle
* @return
*/
public Single<Integer> getImageQuality(UploadItem uploadItem, boolean shouldValidateTitle) {
return remoteDataSource.getImageQuality(uploadItem, shouldValidateTitle);
public Single<Integer> getImageQuality(UploadItem uploadItem) {
return remoteDataSource.getImageQuality(uploadItem);
}
/**

View file

@ -3,28 +3,17 @@ package fr.free.nrw.commons.settings;
import android.Manifest;
import android.net.Uri;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.MultiSelectListPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.SwitchPreference;
import android.text.Editable;
import android.text.TextWatcher;
import android.text.InputFilter;
import android.text.InputType;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
import androidx.preference.MultiSelectListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.google.android.material.snackbar.Snackbar;
import com.karumi.dexter.Dexter;
import com.karumi.dexter.listener.PermissionGrantedResponse;
import com.karumi.dexter.listener.single.BasePermissionListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.ApplicationlessInjection;
@ -33,10 +22,15 @@ 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 javax.inject.Inject;
import javax.inject.Named;
import static fr.free.nrw.commons.utils.SystemThemeUtils.THEME_MODE_DEFAULT;
public class SettingsFragment extends PreferenceFragment {
public class SettingsFragment extends PreferenceFragmentCompat {
@Inject
@Named("default_preferences")
@ -49,29 +43,19 @@ public class SettingsFragment extends PreferenceFragment {
private ListPreference langListPreference;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
ApplicationlessInjection
.getInstance(getActivity().getApplicationContext())
.getCommonsApplicationComponent()
.inject(this);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences);
// Set the preferences from an XML resource
setPreferencesFromResource(R.xml.preferences, rootKey);
themeListPreference = (ListPreference) findPreference(Prefs.KEY_THEME_VALUE);
themeListPreference = findPreference(Prefs.KEY_THEME_VALUE);
prepareTheme();
//Check if the Author Name switch is enabled and appropriately handle the author name usage
SwitchPreference useAuthorName = (SwitchPreference) findPreference("useAuthorName");
EditTextPreference authorName = (EditTextPreference) findPreference("authorName");
authorName.setEnabled(defaultKvStore.getBoolean("useAuthorName", false));
useAuthorName.setOnPreferenceChangeListener((preference, newValue) -> {
authorName.setEnabled((Boolean)newValue);
return true;
});
MultiSelectListPreference multiSelectListPref = (MultiSelectListPreference) findPreference(Prefs.MANAGED_EXIF_TAGS);
MultiSelectListPreference multiSelectListPref = findPreference(Prefs.MANAGED_EXIF_TAGS);
if (multiSelectListPref != null) {
multiSelectListPref.setOnPreferenceChangeListener((preference, newValue) -> {
if (newValue instanceof HashSet && !((HashSet) newValue).contains(getString(R.string.exif_tag_location))) {
@ -81,42 +65,44 @@ public class SettingsFragment extends PreferenceFragment {
});
}
final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads");
final EditTextPreference uploadLimit = findPreference("uploads");
int currentUploadLimit = defaultKvStore.getInt(Prefs.UPLOADS_SHOWING, 100);
uploadLimit.setText(Integer.toString(currentUploadLimit));
uploadLimit.setSummary(Integer.toString(currentUploadLimit));
uploadLimit.getEditText().addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
uploadLimit.setText(String.valueOf(currentUploadLimit));
uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> {
if (newValue.toString().length() == 0) {
return false;
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (s.length() == 0) return;
int value = Integer.parseInt(s.toString());
int value = Integer.parseInt(newValue.toString());
if (value > 500) {
uploadLimit.getEditText().setError(getString(R.string.maximum_limit_alert));
value = 500;
Snackbar error = Snackbar.make(getView(), R.string.maximum_limit_alert, Snackbar.LENGTH_LONG);
error.show();
return false;
} else if (value == 0) {
uploadLimit.getEditText().setError(getString(R.string.cannot_be_zero));
value = 100;
Snackbar error = Snackbar.make(getView(), R.string.cannot_be_zero, Snackbar.LENGTH_LONG);
error.show();
return false;
}
return true;
});
uploadLimit.setOnBindEditTextListener(editText -> {
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
editText.selectAll();
int maxLength = 3; // set maxLength to 3
editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)});
int value = Integer.parseInt(editText.getText().toString());
defaultKvStore.putInt(Prefs.UPLOADS_SHOWING, value);
defaultKvStore.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, true);
uploadLimit.setText(Integer.toString(value));
uploadLimit.setSummary(Integer.toString(value));
}
});
langListPreference = (ListPreference) findPreference("descriptionDefaultLanguagePref");
langListPreference = findPreference("descriptionDefaultLanguagePref");
prepareLanguages();
Preference betaTesterPreference = findPreference("becomeBetaTester");
betaTesterPreference.setOnPreferenceClickListener(preference -> {
@ -129,42 +115,26 @@ public class SettingsFragment extends PreferenceFragment {
return true;
});
// Disable some settings when not logged in.
if (defaultKvStore.getBoolean("login_skipped", false)){
SwitchPreference useExternalStorage = (SwitchPreference) findPreference("useExternalStorage");
SwitchPreference displayNearbyCardView = (SwitchPreference) findPreference("displayNearbyCardView");
SwitchPreference displayLocationPermissionForCardView = (SwitchPreference) findPreference("displayLocationPermissionForCardView");
SwitchPreference displayCampaignsCardView = (SwitchPreference) findPreference("displayCampaignsCardView");
useExternalStorage.setEnabled(false);
if (defaultKvStore.getBoolean("login_skipped", false)) {
findPreference("useExternalStorage").setEnabled(false);
findPreference("useAuthorName").setEnabled(false);
findPreference("displayNearbyCardView").setEnabled(false);
findPreference("displayLocationPermissionForCardView").setEnabled(false);
findPreference("displayCampaignsCardView").setEnabled(false);
uploadLimit.setEnabled(false);
useAuthorName.setEnabled(false);
displayNearbyCardView.setEnabled(false);
displayLocationPermissionForCardView.setEnabled(false);
displayCampaignsCardView.setEnabled(false);
}
}
/**
* Uses previously saved theme if there is any, if not then uses default.
* Sets the theme pref
*/
private void prepareTheme() {
themeListPreference.setSummary(getThemeSummary(getCurrentTheme()));
themeListPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getActivity().recreate();
return true;
});
}
private CharSequence getThemeSummary(String value) {
int prefIndex = themeListPreference.findIndexOfValue(value);
return themeListPreference.getEntries()[prefIndex];
}
private String getCurrentTheme() {
return defaultKvStore.getString(Prefs.KEY_THEME_VALUE, THEME_MODE_DEFAULT);
}
/**
* 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.
@ -195,19 +165,14 @@ public class SettingsFragment extends PreferenceFragment {
String languageCode = getCurrentLanguageCode();
if (languageCode.equals("")){
// If current language code is empty, means none selected by user yet so use phone local
langListPreference.setSummary(Locale.getDefault().getDisplayLanguage());
langListPreference.setValue(Locale.getDefault().getLanguage());
} else {
// If any language is selected by user previously, use it
int prefIndex = langListPreference.findIndexOfValue(languageCode);
langListPreference.setSummary(langListPreference.getEntries()[prefIndex]);
langListPreference.setValue(languageCode);
}
langListPreference.setOnPreferenceChangeListener((preference, newValue) -> {
String userSelectedValue = (String) newValue;
int prefIndex = langListPreference.findIndexOfValue(userSelectedValue);
langListPreference.setSummary(langListPreference.getEntries()[prefIndex]);
saveLanguageValue(userSelectedValue);
return true;
});

View file

@ -1,37 +0,0 @@
package fr.free.nrw.commons.ui.LongTitlePreferences;
import android.content.Context;
import android.preference.EditTextPreference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* Created by seannemann on 6/27/2018.
*/
public class LongTitleEditTextPreference extends EditTextPreference {
public LongTitleEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public LongTitleEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LongTitleEditTextPreference(Context context) {
super(context);
}
@Override
protected void onBindView(View view)
{
super.onBindView(view);
TextView title= view.findViewById(android.R.id.title);
if (title != null) {
title.setSingleLine(false);
}
}
}

View file

@ -1,32 +0,0 @@
package fr.free.nrw.commons.ui.LongTitlePreferences;
import android.content.Context;
import android.preference.ListPreference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* Created by seannemann on 6/27/2018.
*/
public class LongTitleListPreference extends ListPreference {
public LongTitleListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LongTitleListPreference(Context context) {
super(context);
}
@Override
protected void onBindView(View view)
{
super.onBindView(view);
TextView title= view.findViewById(android.R.id.title);
if (title != null) {
title.setSingleLine(false);
}
}
}

View file

@ -1,38 +0,0 @@
package fr.free.nrw.commons.ui.LongTitlePreferences;
import android.content.Context;
import android.preference.MultiSelectListPreference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
public class LongTitleMultiSelectListPreference extends MultiSelectListPreference {
/*
public LongTitleMultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public LongTitleMultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
*/
public LongTitleMultiSelectListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LongTitleMultiSelectListPreference(Context context) {
super(context);
}
@Override
protected void onBindView(View view)
{
super.onBindView(view);
TextView title= view.findViewById(android.R.id.title);
if (title != null) {
title.setSingleLine(false);
}
}
}

View file

@ -1,36 +0,0 @@
package fr.free.nrw.commons.ui.LongTitlePreferences;
import android.content.Context;
import android.preference.Preference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* Created by seannemann on 6/27/2018.
*/
public class LongTitlePreference extends Preference {
public LongTitlePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public LongTitlePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LongTitlePreference(Context context) {
super(context);
}
@Override
protected void onBindView(View view)
{
super.onBindView(view);
TextView title= view.findViewById(android.R.id.title);
if (title != null) {
title.setSingleLine(false);
}
}
}

View file

@ -1,36 +0,0 @@
package fr.free.nrw.commons.ui.LongTitlePreferences;
import android.content.Context;
import android.preference.PreferenceCategory;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* Created by seannemann on 6/27/2018.
*/
public class LongTitlePreferenceCategory extends PreferenceCategory {
public LongTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public LongTitlePreferenceCategory(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LongTitlePreferenceCategory(Context context) {
super(context);
}
@Override
protected void onBindView(View view)
{
super.onBindView(view);
TextView title= view.findViewById(android.R.id.title);
if (title != null) {
title.setSingleLine(false);
}
}
}

View file

@ -1,36 +0,0 @@
package fr.free.nrw.commons.ui.LongTitlePreferences;
import android.content.Context;
import android.preference.SwitchPreference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* Created by seannemann on 6/27/2018.
*/
public class LongTitleSwitchPreference extends SwitchPreference {
public LongTitleSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public LongTitleSwitchPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LongTitleSwitchPreference(Context context) {
super(context);
}
@Override
protected void onBindView(View view)
{
super.onBindView(view);
TextView title= view.findViewById(android.R.id.title);
if (title != null) {
title.setSingleLine(false);
}
}
}

View file

@ -1,25 +1,22 @@
package fr.free.nrw.commons.upload;
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION;
import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
import android.content.Context;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.ImageUtilsWrapper;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import timber.log.Timber;
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION;
import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
/**
* Methods for pre-processing images to be uploaded
*/
@ -43,14 +40,12 @@ public class ImageProcessingService {
this.mediaClient = mediaClient;
}
/**
* Check image quality before upload
* - checks duplicate image
* - checks dark image
* - checks geolocation for image
* - check for valid caption
* Check image quality before upload - checks duplicate image - checks dark image - checks
* geolocation for image - check for valid title
*/
Single<Integer> validateImage(UploadModel.UploadItem uploadItem, boolean checkTitle) {
Single<Integer> validateImage(UploadModel.UploadItem uploadItem) {
int currentImageQuality = uploadItem.getImageQuality();
Timber.d("Current image quality is %d", currentImageQuality);
if (currentImageQuality == ImageUtils.IMAGE_KEEP) {
@ -58,22 +53,20 @@ public class ImageProcessingService {
}
Timber.d("Checking the validity of image");
String filePath = uploadItem.getMediaUri().getPath();
Single<Integer> duplicateImage = checkDuplicateImage(filePath);
Single<Integer> wrongGeoLocation = checkImageGeoLocation(uploadItem.getPlace(), filePath);
Single<Integer> darkImage = checkDarkImage(filePath);
Single<Integer> itemTitle = checkTitle ? validateItemTitle(uploadItem) : Single.just(ImageUtils.IMAGE_OK);
Single<Integer> checkFBMD = checkFBMD(filePath);
Single<Integer> checkEXIF = checkEXIF(filePath);
Single<Integer> zipResult = Single.zip(duplicateImage, wrongGeoLocation, darkImage, itemTitle,
(duplicate, wrongGeo, dark, title) -> {
Timber.d("Result for duplicate: %d, geo: %d, dark: %d, title: %d", duplicate, wrongGeo, dark, title);
return duplicate | wrongGeo | dark | title;
});
return Single.zip(zipResult, checkFBMD , checkEXIF , (zip , fbmd , exif)->{
Timber.d("zip:" + zip + "fbmd:" + fbmd + "exif:" + exif);
return zip | fbmd | exif;
});
return Single.zip(
checkDuplicateImage(filePath),
checkImageGeoLocation(uploadItem.getPlace(), filePath),
checkDarkImage(filePath),
validateItemTitle(uploadItem),
checkFBMD(filePath),
checkEXIF(filePath),
(duplicateImage, wrongGeoLocation, darkImage, itemTitle, fbmd, exif) -> {
Timber.d("duplicate: %d, geo: %d, dark: %d, title: %d" + "fbmd:" + fbmd + "exif:" + exif,
duplicateImage, wrongGeoLocation, darkImage, itemTitle);
return duplicateImage | wrongGeoLocation | darkImage | itemTitle | fbmd | exif;
}
);
}
/**
@ -115,7 +108,8 @@ public class ImageProcessingService {
.map(doesFileExist -> {
Timber.d("Result for valid title is %s", doesFileExist);
return doesFileExist ? FILE_NAME_EXISTS : IMAGE_OK;
});
})
.subscribeOn(Schedulers.io());
}
/**
@ -126,14 +120,14 @@ public class ImageProcessingService {
*/
private Single<Integer> checkDuplicateImage(String filePath) {
Timber.d("Checking for duplicate image %s", filePath);
return Single.fromCallable(() ->
fileUtilsWrapper.getFileInputStream(filePath))
return Single.fromCallable(() -> fileUtilsWrapper.getFileInputStream(filePath))
.map(fileUtilsWrapper::getSHA1)
.flatMap(mediaClient::checkFileExistsUsingSha)
.map(b -> {
Timber.d("Result for duplicate image %s", b);
return b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK;
});
})
.subscribeOn(Schedulers.io());
}
/**
@ -166,7 +160,8 @@ public class ImageProcessingService {
return Single.just(ImageUtils.IMAGE_OK);
}
return imageUtilsWrapper.checkImageGeolocationIsDifferent(geoLocation, place.getLocation());
});
})
.subscribeOn(Schedulers.io());
}
}

View file

@ -1,18 +1,16 @@
package fr.free.nrw.commons.upload;
import java.io.FileInputStream;
import java.io.IOException;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.utils.ImageUtils;
import io.reactivex.Single;
import java.io.FileInputStream;
import java.io.IOException;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* We want to discourage users from uploading images to Commons that were taken from Facebook.
* This attempts to detect whether an image was downloaded from Facebook by heuristically
* searching for metadata that is specific to images that come from Facebook.
* We want to discourage users from uploading images to Commons that were taken from Facebook. This
* attempts to detect whether an image was downloaded from Facebook by heuristically searching for
* metadata that is specific to images that come from Facebook.
*/
@Singleton
public class ReadFBMD {
@ -22,6 +20,7 @@ public class ReadFBMD {
}
public Single<Integer> processMetadata(String path) {
return Single.fromCallable(() -> {
try {
int psBlockOffset;
int fbmdOffset;
@ -37,12 +36,13 @@ public class ReadFBMD {
if (psBlockOffset > 0 && fbmdOffset > 0
&& fbmdOffset > psBlockOffset && fbmdOffset - psBlockOffset < 0x80) {
return Single.just(ImageUtils.FILE_FBMD);
return ImageUtils.FILE_FBMD;
}
} catch (IOException e) {
e.printStackTrace();
}
return Single.just(ImageUtils.IMAGE_OK);
return ImageUtils.IMAGE_OK;
});
}
}

View file

@ -1,185 +0,0 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.BiMap;
import fr.free.nrw.commons.utils.LangCodeUtils;
public class SpinnerLanguagesAdapter extends ArrayAdapter {
private final int resource;
private final LayoutInflater layoutInflater;
private List<String> languageNamesList;
private List<String> languageCodesList;
private final BiMap<AdapterView, String> selectedLanguages;
public String selectedLangCode="";
private Context context;
private boolean dropDownClicked;
private String savedLanguageValue;
public SpinnerLanguagesAdapter(@NonNull Context context,
int resource,
BiMap<AdapterView, String> selectedLanguages,
String savedLanguageValue) {
super(context, resource);
this.resource = resource;
this.layoutInflater = LayoutInflater.from(context);
languageNamesList = new ArrayList<>();
languageCodesList = new ArrayList<>();
prepareLanguages();
this.selectedLanguages = selectedLanguages;
this.context = context;
this.dropDownClicked = false;
this.savedLanguageValue = savedLanguageValue;
}
private void prepareLanguages() {
List<Language> languages = getLocaleSupportedByDevice();
for(Language language: languages) {
if(!languageCodesList.contains(language.getLocale().getLanguage())) {
languageNamesList.add(language.getLocale().getDisplayName());
languageCodesList.add(language.getLocale().getLanguage());
}
}
}
private List<Language> getLocaleSupportedByDevice() {
List<Language> languages = new ArrayList<>();
Locale[] localesArray = Locale.getAvailableLocales();
for (Locale locale : localesArray) {
languages.add(new Language(locale));
}
Collections.sort(languages, (language, t1) -> language.getLocale().getDisplayName()
.compareTo(t1.getLocale().getDisplayName()));
return languages;
}
@Override
public boolean isEnabled(int position) {
return !languageCodesList.get(position).isEmpty()&&
(!selectedLanguages.containsKey(languageCodesList.get(position)) ||
languageCodesList.get(position).equals(selectedLangCode));
}
@Override
public int getCount() {
return languageNamesList.size();
}
@Override
public View getDropDownView(int position, @Nullable View convertView,
@NonNull ViewGroup parent) {
if (convertView == null) {
convertView = layoutInflater.inflate(resource, parent, false);
}
ViewHolder holder = new ViewHolder(convertView);
holder.init(position, true, savedLanguageValue);
dropDownClicked = true;
return convertView;
}
@Override
public @NonNull
View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = layoutInflater.inflate(resource, parent, false);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.init(position, false, savedLanguageValue);
return convertView;
}
public class ViewHolder {
@Nullable
@BindView(R.id.tv_language)
TextView tvLanguage;
@Nullable
@BindView(R.id.view)
View view;
public ViewHolder(View itemView) {
ButterKnife.bind(this, itemView);
}
public void init(int position, boolean isDropDownView, String savedLanguageValue) {
String languageCode = LangCodeUtils.fixLanguageCode(languageCodesList.get(position));
final String languageName = StringUtils.capitalize(languageNamesList.get(position));
if(TextUtils.isEmpty(savedLanguageValue)){
savedLanguageValue = Locale.getDefault().getLanguage();
}
if (!isDropDownView) {
if( !dropDownClicked && savedLanguageValue !=null){
languageCode = LangCodeUtils.fixLanguageCode(savedLanguageValue);
}
if (view != null) {
view.setVisibility(View.GONE);
}
if (languageCode.length() > 2)
tvLanguage.setText(languageCode.substring(0, 2));
else
tvLanguage.setText(languageCode);
} else {
view.setVisibility(View.VISIBLE);
if (languageCodesList.get(position).isEmpty()) {
tvLanguage.setText(languageName);
tvLanguage.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
} else {
tvLanguage.setText(
String.format("%s [%s]", languageName, languageCode));
if (selectedLanguages.containsKey(languageCodesList.get(position)) &&
!languageCodesList.get(position).equals(selectedLangCode)) {
tvLanguage.setTextColor(Color.GRAY);
}
}
}
}
}
String getLanguageCode(int position) {
return languageCodesList.get(position);
}
int getIndexOfUserDefaultLocale(Context context) {
return languageCodesList.indexOf(context.getResources().getConfiguration().locale.getLanguage());
}
int getIndexOfLanguageCode(String languageCode) {
return languageCodesList.indexOf(languageCode);
}
}

View file

@ -0,0 +1,104 @@
package fr.free.nrw.commons.upload
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.annotation.LayoutRes
import androidx.core.os.ConfigurationCompat
import fr.free.nrw.commons.R
import fr.free.nrw.commons.utils.BiMap
import fr.free.nrw.commons.utils.LangCodeUtils
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.row_item_languages_spinner.*
import org.apache.commons.lang3.StringUtils
import java.util.*
/**
* This class handles the display of language spinners and their dropdown views for UploadMediaDetailFragment
*
* @property selectedLanguages - controls the enabled state of dropdown views
*
* @param context - required by super constructor
*/
class SpinnerLanguagesAdapter constructor(
context: Context,
private val selectedLanguages: BiMap<*, String>
) : ArrayAdapter<Any?>(context, -1) {
private val languageNamesList: List<String>
private val languageCodesList: List<String>
init {
val sortedLanguages = Locale.getAvailableLocales()
.map(::Language)
.sortedBy { it.locale.displayName }
languageNamesList = sortedLanguages.map { it.locale.displayName }
languageCodesList = sortedLanguages.map { it.locale.language }
}
var selectedLangCode = ""
override fun isEnabled(position: Int) = languageCodesList[position].let {
it.isNotEmpty() && !selectedLanguages.containsKey(it) && it != selectedLangCode
}
override fun getCount() = languageNamesList.size
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup) =
(convertView ?: parent.inflate(R.layout.row_item_languages_spinner).also {
it.tag = DropDownViewHolder(it)
}).apply {
(tag as DropDownViewHolder).init(
languageCodesList[position],
languageNamesList[position],
isEnabled(position)
)
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup) =
(convertView ?: parent.inflate(R.layout.row_item_languages_spinner).also {
it.tag = SpinnerViewHolder(it)
}).apply { (tag as SpinnerViewHolder).init(languageCodesList[position]) }
class SpinnerViewHolder(override val containerView: View) : LayoutContainer {
fun init(languageCode: String) {
LangCodeUtils.fixLanguageCode(languageCode).let {
tv_language.text = if (it.length > 2) it.take(2) else it
}
}
}
class DropDownViewHolder(override val containerView: View) : LayoutContainer {
fun init(languageCode: String, languageName: String, enabled: Boolean) {
tv_language.isEnabled = enabled
if (languageCode.isEmpty()) {
tv_language.text = StringUtils.capitalize(languageName)
tv_language.textAlignment = View.TEXT_ALIGNMENT_CENTER
} else {
tv_language.text =
"${StringUtils.capitalize(languageName)}" +
" [${LangCodeUtils.fixLanguageCode(languageCode)}]"
}
}
}
fun getLanguageCode(position: Int): String {
return languageCodesList[position]
}
fun getIndexOfUserDefaultLocale(context: Context): Int {
return languageCodesList.indexOf(context.locale.language)
}
fun getIndexOfLanguageCode(languageCode: String): Int {
return languageCodesList.indexOf(languageCode)
}
}
private fun ViewGroup.inflate(@LayoutRes resId: Int) =
LayoutInflater.from(context).inflate(resId, this, false)
private val Context.locale: Locale
get() = ConfigurationCompat.getLocales(resources.configuration)[0]

View file

@ -202,8 +202,8 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
private void initLanguageSpinner(int position, UploadMediaDetail description) {
SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(
spinnerDescriptionLanguages.getContext(),
R.layout.row_item_languages_spinner, selectedLanguages,
savedLanguageValue);
selectedLanguages
);
spinnerDescriptionLanguages.setAdapter(languagesAdapter);
spinnerDescriptionLanguages.setOnItemSelectedListener(new OnItemSelectedListener() {
@ -217,7 +217,7 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
selectedLanguages.remove(adapterView);
selectedLanguages.put(adapterView, languageCode);
((SpinnerLanguagesAdapter) adapterView
.getAdapter()).selectedLangCode = languageCode;
.getAdapter()).setSelectedLangCode(languageCode);
spinnerDescriptionLanguages.setSelection(position);
Timber.d("Description language code is: "+languageCode);
}

View file

@ -3,31 +3,8 @@ package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.filepicker.MimeTypeMapWrapper;
@ -40,6 +17,17 @@ import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.subjects.BehaviorSubject;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import timber.log.Timber;
@Singleton
@ -131,8 +119,8 @@ public class UploadModel {
return Observable.just(getUploadItem(uploadableFile, place, source, similarImageInterface));
}
public Single<Integer> getImageQuality(UploadItem uploadItem, boolean checkTitle) {
return imageProcessingService.validateImage(uploadItem, checkTitle);
public Single<Integer> getImageQuality(UploadItem uploadItem) {
return imageProcessingService.validateImage(uploadItem);
}
private UploadItem getUploadItem(UploadableFile uploadableFile,
@ -213,6 +201,8 @@ public class UploadModel {
CommonsApplication.DEFAULT_EDIT_SUMMARY, selectedDepictions, item.gpsCoords.getCoords());
if (item.place != null) {
contribution.setWikiDataEntityId(item.place.getWikiDataEntityId());
// If item already has an image, we need to know it. We don't want to override existing image later
contribution.setP18Value(item.place.pic);
}
if (null == selectedCategories) {//Just a fail safe, this should never be null
selectedCategories = new ArrayList<>();

View file

@ -289,12 +289,15 @@ public class UploadService extends HandlerService<Contribution> {
Timber.d("Contribution upload success. Initiating Wikidata edit for entity id %s",
contribution.getWikiDataEntityId());
// to perform upload of depictions we pass on depiction entityId of the selected depictions to the wikidataEditService
final String p18Value = contribution.getP18Value();
if (contribution.getDepictionsEntityIds() != null) {
for (String s : contribution.getDepictionsEntityIds()) {
wikidataEditService.createClaimWithLogging(s, canonicalFilename);
wikidataEditService.createClaimWithLogging(s, canonicalFilename,
p18Value);
}
}
wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), canonicalFilename);
wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), canonicalFilename,
p18Value);
wikidataEditService.createLabelforWikidataEntity(contribution.getWikiDataEntityId(), canonicalFilename,
(Map) contribution.getCaptions());
contribution.setFilename(canonicalFilename);

View file

@ -254,7 +254,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
@OnClick(R.id.btn_next)
public void onNextButtonClicked() {
uploadItem.setMediaDetails(uploadMediaDetailAdapter.getUploadMediaDetails());
presenter.verifyImageQuality(uploadItem, true);
presenter.verifyImageQuality(uploadItem);
}
@OnClick(R.id.btn_previous)

View file

@ -43,7 +43,7 @@ public interface UploadMediaDetailsContract {
void receiveImage(UploadableFile uploadableFile, @Contribution.FileSource String source,
Place place);
void verifyImageQuality(UploadItem uploadItem, boolean validateTitle);
void verifyImageQuality(UploadItem uploadItem);
void setUploadItem(int index, UploadItem uploadItem);

View file

@ -111,14 +111,14 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
* asks the repository to verify image quality
*
* @param uploadItem
* @param validateTitle
*/
@Override
public void verifyImageQuality(UploadItem uploadItem, boolean validateTitle) {
public void verifyImageQuality(UploadItem uploadItem) {
view.showProgress(true);
Disposable imageQualityDisposable = repository
.getImageQuality(uploadItem, true)
.subscribeOn(ioScheduler)
compositeDisposable.add(
repository
.getImageQuality(uploadItem)
.observeOn(mainThreadScheduler)
.subscribe(imageResult -> {
view.showProgress(false);
@ -129,9 +129,8 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
view.showMessage("" + throwable.getLocalizedMessage(),
R.color.color_error);
Timber.e(throwable, "Error occurred while handling image");
});
compositeDisposable.add(imageQualityDisposable);
})
);
}
/**

View file

@ -1,11 +1,10 @@
package fr.free.nrw.commons.utils;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.location.LatLng;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class ImageUtilsWrapper {
@ -16,16 +15,16 @@ public class ImageUtilsWrapper {
}
public Single<Integer> checkIfImageIsTooDark(String bitmapPath) {
return Single.just(ImageUtils.checkIfImageIsTooDark(bitmapPath))
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation());
return Single.fromCallable(() -> ImageUtils.checkIfImageIsTooDark(bitmapPath))
.subscribeOn(Schedulers.computation());
}
public Single<Integer> checkImageGeolocationIsDifferent(String geolocationOfFileString, LatLng latLng) {
boolean isImageGeoLocationDifferent = ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng);
return Single.just(isImageGeoLocationDifferent)
public Single<Integer> checkImageGeolocationIsDifferent(String geolocationOfFileString,
LatLng latLng) {
return Single.fromCallable(
() -> ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng))
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation())
.map(isDifferent -> isDifferent ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT : ImageUtils.IMAGE_OK);
.map(isDifferent -> isDifferent ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT
: ImageUtils.IMAGE_OK);
}
}

View file

@ -2,22 +2,9 @@ package fr.free.nrw.commons.wikidata;
import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.NonNull;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.wikipedia.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.Service;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.kvstore.JsonKvStore;
@ -29,6 +16,15 @@ import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.wikipedia.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.Service;
import timber.log.Timber;
/**
@ -75,11 +71,17 @@ public class WikidataEditService {
/**
* Create a P18 claim and log the edit with custom tag
<<<<<<< HEAD
*
* @param wikidataEntityId
* @param fileName
=======
* @param wikidataEntityId a unique id of each Wikidata items
* @param fileName name of the file we will upload
* @param p18Value pic attribute of Wikidata item
>>>>>>> origin/master
*/
public void createClaimWithLogging(String wikidataEntityId, String fileName) {
public void createClaimWithLogging(String wikidataEntityId, String fileName, @NonNull String p18Value) {
if (wikidataEntityId == null) {
Timber.d("Skipping creation of claim as Wikidata entity ID is null");
return;
@ -95,6 +97,11 @@ public class WikidataEditService {
return;
}
if (!p18Value.trim().isEmpty()) {
Timber.d("Skipping creation of claim as p18Value is not empty, we won't override existing image");
return;
}
editWikidataProperty(wikidataEntityId, fileName);
//editWikiBaseDepictsProperty(wikidataEntityId, fileName);
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true"
android:drawable="@color/opak_middle_grey" />
<item android:drawable="@color/black" />
</selector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true"
android:drawable="@color/divider_grey" />
<item android:drawable="@color/white" />
</selector>

View file

@ -1,18 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/search_list_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="@dimen/tiny_margin">
android:padding="@dimen/tiny_margin"
android:background="@drawable/linearlayout_color_selector">
<ImageView
android:id="@+id/place_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/place_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"></TextView>
android:layout_height="wrap_content"/>
</LinearLayout>

View file

@ -4,16 +4,17 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/tiny_margin"
android:orientation="horizontal">
android:orientation="horizontal"
android:background="@drawable/linearlayout_color_dark_selector">
<ImageView
android:id="@+id/place_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/place_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/white"></TextView>
android:textColor="@color/white"/>
</LinearLayout>

View file

@ -22,9 +22,4 @@
tools:text="en"
/>
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="@dimen/tiny_height"
android:background="@color/item_white_background" />
</LinearLayout>

View file

@ -71,7 +71,7 @@
<string name="gps_disabled">GPS is uitgesit in uw apparaat. Wil u die aansit?</string>
<string name="enable_gps">GPS aansit</string>
<string name="contributions_subtitle_zero">Nog geen uploads</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
@ -369,7 +369,6 @@
<string name="preference_author_name_toggle">Gebruik die gepaste outeur se naam</string>
<string name="preference_author_name_toggle_summary">Gebruik \'n pasgemaakte outeurnaam in plaas van u gebruikersnaam terwyl u foto\'s oplaai</string>
<string name="preference_author_name">Pasgemaakte outeurnaam</string>
<string name="preference_author_name_summary">Die pasgemaakte outeur se naam om te gebruik in plaas van u gebruikersnaam in oplaaie</string>
<string name="contributions_fragment">bydraes</string>
<string name="nearby_fragment">Naby</string>
<string name="notifications">Kennisgewings</string>

View file

@ -11,6 +11,7 @@
* Mido
* Monrokhoury
* Mr. Ibrahem
* NEHAOUA
* OsamaK
* ترجمان05
* ديفيد
@ -49,7 +50,7 @@
<string name="upload_progress_notification_title_finishing">اكتمال رفع %1$s</string>
<string name="upload_failed_notification_title">فشل رفع %1$s</string>
<string name="upload_failed_notification_subtitle">انقر لتشاهد</string>
<plurals name="uploads_pending_notification_indicator">
<plurals name="uploads_pending_notification_indicator" fuzzy="true">
<item quantity="one">%1$d رفع ملف</item>
<item quantity="other">%1$d رفع ملفات</item>
</plurals>
@ -85,16 +86,16 @@
<string name="gps_disabled">عطل نظام الملاحة العالمي GPS بجهازك. أترغب في التنشيط؟</string>
<string name="enable_gps">تفعيل GPS</string>
<string name="contributions_subtitle_zero">لا مرفوعات بعد</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
</plurals>
<plurals name="starting_multiple_uploads">
<plurals name="starting_multiple_uploads" fuzzy="true">
<item quantity="one">جارٍ بدء %1$d رفع</item>
<item quantity="other">جارٍ بدء %1$d مرفوعات</item>
</plurals>
<string name="multiple_uploads_title">{{جمع|واحد=%1$d رفع|%1$d رفع}}</string>
<string name="multiple_uploads_title" fuzzy="true">{{جمع|واحد=%1$d رفع|%1$d رفع}}</string>
<string name="categories_not_found">لا توجد تصنيفات تطابق %1$s</string>
<string name="categories_skip_explanation">أضف تصنيفات لجعل صورك أكثر قابلية للاكتشاف على ويكيميديا ​​كومنز.\n\nابدأ الكتابة لإضافة تصنيفات.</string>
<string name="categories_activity_title">تصنيفات</string>
@ -106,8 +107,8 @@
<string name="menu_about">حول</string>
<string name="about_license">تطبيق ويكيميديا ​​كومنز تطبيق مفتوح المصدر تم إنشاؤه وصيانته من قبل المستفيدين والمتطوعين من مجتمع ويكيميديا، لا تشارك مؤسسة ويكيميديا ​​في إنشاء التطبيق أو تطويره أو صيانته.</string>
<string name="about_improve">أنشئ &lt;a href=\"%1$s\"&gt;مشكلة غيت هب&lt;/a&gt; جديدة لتقارير الأخطاء والاقتراحات.</string>
<string name="about_privacy_policy" fuzzy="true">&lt;u&gt;Pسياسة الخصوصية&lt;/u&gt;</string>
<string name="about_credits" fuzzy="true">&lt;u&gt;الإحالات&lt;/u&gt;</string>
<string name="about_privacy_policy">سياسة الخصوصية</string>
<string name="about_credits">الإحالات</string>
<string name="title_activity_about">حول</string>
<string name="menu_feedback">إرسال ملاحظات (عبر البريد الإلكتروني)</string>
<string name="no_email_client">عميل البريد الإلكتروني غير مثبت</string>
@ -116,7 +117,7 @@
<string name="no_uploads_yet">لم ترفع بعد أية صور.</string>
<string name="menu_retry_upload">إعادة المحاولة</string>
<string name="menu_cancel_upload">إلغاء</string>
<plurals name="share_license_summary">
<plurals name="share_license_summary" fuzzy="true">
<item quantity="one">سيتم ترخيص هذه الصورة تحت %1$s</item>
<item quantity="other">سيتم ترخيص هذه الصور تحت %1$s</item>
</plurals>
@ -126,7 +127,7 @@
<string name="use_previous">استخدم العنوان والوصف السابق</string>
<string name="allow_gps">الحصول تلقائيا على الموقع الحالي</string>
<string name="allow_gps_summary">يسترد الموقع الحالي إذا لم يتم وسم الصورة جغرافيا، وكان هناك وسم جغرافي عليها، تحذير: سيكشف هذا عن موقعك الحالي.</string>
<string name="preference_theme" fuzzy="true">الوضع الليلي</string>
<string name="preference_theme">الوضع الليلي</string>
<string name="license_name_cc_by_sa_four">النسبة-الترخيص بالمثل 4.0</string>
<string name="license_name_cc_by_four">الإحالة 4.0</string>
<string name="license_name_cc_by_sa">النسبة-الترخيص بالمثل 3.0</string>
@ -167,7 +168,7 @@
<string name="welcome_copyright_subtext">تجنب المواد محفوظة الحقوق الموجودة على الإنترنت وصور الملصقات وأغلفة الكتب، إلخ.</string>
<string name="welcome_final_text">هل تظن أنك فهمت؟</string>
<string name="welcome_final_button_text">نعم!</string>
<string name="welcome_help_button_text" fuzzy="true">&lt;u&gt;المزيد من المعلومات&lt;/u&gt;</string>
<string name="welcome_help_button_text">مزيد من المعلومات</string>
<string name="detail_panel_cats_label">تصنيفات</string>
<string name="detail_panel_cats_loading">جارٍ التحميل…</string>
<string name="detail_panel_cats_none">لا شيء محدد</string>
@ -269,7 +270,7 @@
<string name="null_url">خطأ! المسار غير موجود</string>
<string name="nominate_deletion">ترشيح للحذف</string>
<string name="nominated_for_deletion">الصورة تم ترشيحها للحذف.</string>
<string name="nominated_see_more" fuzzy="true">&lt;u&gt;انظر صفحة الويب للتفاصيل&lt;/u&gt;</string>
<string name="nominated_see_more">انظر صفحة الويب للتفاصيل</string>
<string name="nominating_file_for_deletion">جارٍ ترشيح %1$s للحذف.</string>
<string name="nominating_for_deletion_status">جارٍ ترشيح الملف للحذف: %1$s</string>
<string name="view_browser">عرض في المتصفح</string>
@ -281,7 +282,7 @@
<string name="copy_wikicode">انسخ نص الويكي إلى الكليب بورد</string>
<string name="wikicode_copied">نص الويكي تم نسخه إلى الكليب بورد</string>
<string name="nearby_location_has_not_changed">الموقع لم يتغير.</string>
<string name="nearby_location_not_available" fuzzy="true">الموقع غير متوفر.</string>
<string name="nearby_location_not_available">قد لا تعمل الأجهزة المجاورة بشكل صحيح، الموقع غير متوفر.</string>
<string name="location_permission_rationale_nearby">صلاحية مطلوبة لعرض قائمة بالأماكن القريبة</string>
<string name="get_directions">الحصول على الاتجاهات</string>
<string name="read_article">قراءة المقالة</string>
@ -294,8 +295,8 @@
<string name="nearby_wikidata">ويكي بيانات</string>
<string name="nearby_wikipedia">ويكيبيديا</string>
<string name="nearby_commons">كومنز</string>
<string name="about_rate_us" fuzzy="true">&lt;u&gt;قيِّمنا&lt;/u&gt;</string>
<string name="about_faq" fuzzy="true">&lt;u&gt;الأسئلة المتكررة&lt;/u&gt;</string>
<string name="about_rate_us">قيِّمنا</string>
<string name="about_faq">الأسئلة المتكررة</string>
<string name="welcome_skip_button">تخطي البرنامج التعليمي</string>
<string name="no_internet">الإنترنت غير متوفر</string>
<string name="internet_established">الإنترنت متاح</string>
@ -303,7 +304,7 @@
<string name="error_review">خطأ في جلب الصورة للمراجعة، اضغط على تحديث للمحاولة مرة أخرى.</string>
<string name="error_review_categories">خطأ في جلب تصنيات الصورة للمراجعة، اضغط على تحديث للمحاولة مرة أخرى.</string>
<string name="no_notifications">لم يتم العثور على إشعارات</string>
<string name="about_translate" fuzzy="true">&lt;u&gt;ترجمة&lt;/u&gt;</string>
<string name="about_translate">ترجمة</string>
<string name="about_translate_title">اللغات</string>
<string name="about_translate_message">اختر اللغة التي ترغب في تقديم ترجمات لها</string>
<string name="about_translate_proceed">تقدم</string>
@ -386,7 +387,6 @@
<string name="preference_author_name_toggle">استخدم اسم مؤلف مخصصا</string>
<string name="preference_author_name_toggle_summary">استخدم اسم مؤلف مخصصا بدلا من اسم المستخدم الخاص بك أثناء رفع الصور</string>
<string name="preference_author_name">اسم المؤلف المخصص</string>
<string name="preference_author_name_summary">اسم المؤلف المخصص لاستخدامه بدلا من اسم المستخدم الخاص بك في المرفوعات</string>
<string name="contributions_fragment">مساهمات</string>
<string name="nearby_fragment">مجاور</string>
<string name="notifications">الإشعارات</string>
@ -405,7 +405,7 @@
<string name="submit">إرسال</string>
<string name="upload_title_duplicate">يوجد ملف باسم الملف %1$s، هل أنت متأكد أنك تريد المتابعة؟</string>
<string name="map_application_missing">لا يمكن العثور على تطبيق خرائط متوافق على جهازك; الرجاء تثبيت تطبيق خرائط لاستخدام هذه الميزة.</string>
<plurals name="upload_count_title">
<plurals name="upload_count_title" fuzzy="true">
<item quantity="one">%1$d رفع</item>
<item quantity="other">%1$d مرفوعات</item>
</plurals>
@ -445,6 +445,7 @@
<string name="display_location_permission_title">عرض إذن الموقع</string>
<string name="display_location_permission_explanation">اطلب إذن الموقع عند الحاجة إلى ميزة عرض بطاقة التنبيه القريبة.</string>
<string name="achievements_fetch_failed">حدث خطأ ما; لم نتمكن من جلب إنجازاتك</string>
<string name="achievements_fetch_failed_ultimate_achievement">لقد قدمت الكثير من المساهمات لا يستطيع نظامنا لحساب الإنجازات التعامل معها. هذا هو الإنجاز النهائي.</string>
<string name="ends_on">ينتهي في:</string>
<string name="display_campaigns">عرض الحملات</string>
<string name="display_campaigns_explanation">انظر الحملات الجارية</string>
@ -495,7 +496,7 @@
<string name="review_thanks_no_button_text">الصورة التالية</string>
<string name="skip_image_explanation">النقر فوق هذا الزر سيمنحك صورة أخرى مرفوعة مؤخرا من ويكيميديا ​​كومنز</string>
<string name="review_image_explanation">يمكنك مراجعة الصور وتحسين جودة ويكيميديا ​​كومنز.\n وسائط اللمراجعة الأربعة هي:\n - هل هذه الصورة في النطاق؟\n - هل تتبع هذه الصورة قواعد حقوق النشر؟\n - هل هذه الصورة مصنفة بشكل صحيح؟\n - إذا سارت الأمور على ما يرام يمكنك أيضا شكر المساهم.</string>
<plurals name="receiving_shared_content">
<plurals name="receiving_shared_content" fuzzy="true">
<item quantity="one">جارٍ تلقي محتوى مشترك، قد تستغرق معالجة الصورة بعض الوقت بناءً على حجم الصور وجهازك</item>
<item quantity="other">جارٍ تلقي محتوى مشترك، قد تستغرق معالجة الصور بعض الوقت بناءً على حجم الصور وجهازك</item>
</plurals>
@ -564,5 +565,17 @@
<string name="place_type">نوع المكان:</string>
<string name="nearby_search_hint">جسر، متحف، فندق، إلخ</string>
<string name="you_must_reset_your_passsword">حدث خطأ ما عند الدخول، يجب عليك إعادة تعيين كلمة المرور!!</string>
<string name="upload_nearby_place_found_title">تم العثور على مكان قريب</string>
<string name="upload_nearby_place_found_description">هل هذه صورة للمكان %1$؟</string>
<string name="title_app_shortcut_explore">استكشف</string>
<string name="title_app_shortcut_bookmark">العلامات</string>
<string name="title_app_shortcut_setting">الإعدادات</string>
<string name="remove_bookmark">أزل من العلامات</string>
<string name="add_bookmark">اضف إلى العلامات</string>
<string name="wallpaper_set_unsuccessfully">هناك خطأ ما. تعذر تعيين الخلفية</string>
<string name="setting_wallpaper_dialog_title">اجعلها خلفية</string>
<string name="setting_wallpaper_dialog_message">إعداد الخلفية. أرجو الإنتظار…</string>
<string name="theme_default_name">افتراضي</string>
<string name="theme_dark_name">غامق</string>
<string name="theme_light_name">فاتح</string>
</resources>

View file

@ -72,7 +72,7 @@
<string name="gps_disabled">El GPS ta desactiváu nel preséu. ¿Quiés activalu?</string>
<string name="enable_gps">Activar GPS</string>
<string name="contributions_subtitle_zero">Inda nun hai xubes</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
@ -376,7 +376,6 @@
<string name="preference_author_name_toggle">Usar nome d\'autor personalizáu</string>
<string name="preference_author_name_toggle_summary">Usar un nome d\'autor personalizáu en cuenta del to nome d\'usuariu al xubir semeyes</string>
<string name="preference_author_name">Nome d\'autor personalizáu</string>
<string name="preference_author_name_summary">El nome d\'autor personalizáu pa usar nes xubíes en cuenta del to nome d\'usuariu</string>
<string name="contributions_fragment">Collaboraciones</string>
<string name="nearby_fragment">Cercanu</string>
<string name="notifications">Avisos</string>

View file

@ -214,7 +214,6 @@
<string name="preference_author_name_toggle">Използване на персонализирано авторско име</string>
<string name="preference_author_name_toggle_summary">При качването използвайте персонализирано авторско име вместо потребителското си име</string>
<string name="preference_author_name">Персонализирано авторско име</string>
<string name="preference_author_name_summary">Персонализираното авторско име, което ще се използва вместо потребителското ви име при качване</string>
<string name="archived_notifications">Известия (архивирани)</string>
<string name="list_sheet">Списък</string>
<string name="next">Следваща</string>

View file

@ -81,7 +81,7 @@
<string name="gps_disabled">GPS আপনার ডিভাইসে অক্ষম করা আছে। আপনি কি এটি সক্ষম করতে চান?</string>
<string name="enable_gps">GPS সক্রিয় করুন</string>
<string name="contributions_subtitle_zero">এখনো কোন আপলোড নেই</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>

View file

@ -38,7 +38,7 @@
<string name="upload_progress_notification_title_finishing">Oc\'h echuiñ enporzhiadenn %1$s</string>
<string name="upload_failed_notification_title">C\'hwitet en deus enporzhiañ %1$s</string>
<string name="upload_failed_notification_subtitle">Pouezit evit diskwel</string>
<plurals name="uploads_pending_notification_indicator">
<plurals name="uploads_pending_notification_indicator" fuzzy="true">
<item quantity="one">%1$d restr o vezañ karget</item>
<item quantity="other">%1$d restr o vezañ karget</item>
</plurals>
@ -74,16 +74,16 @@
<string name="gps_disabled">Diwerededkaet eo ar GPS war hoc\'h ardivink.\nHa c\'hoant ho peus da weredekaat anezhañ ?</string>
<string name="enable_gps">Gweredekaat ar GPS</string>
<string name="contributions_subtitle_zero">Enporzhiadenn ebet c\'hoazh !</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
</plurals>
<plurals name="starting_multiple_uploads">
<plurals name="starting_multiple_uploads" fuzzy="true">
<item quantity="one">%1$d bellgargadenn loc\'het</item>
<item quantity="other">%1$d pellgargadennoù loc\'het</item>
</plurals>
<plurals name="multiple_uploads_title">
<plurals name="multiple_uploads_title" fuzzy="true">
<item quantity="one">%1$d bellgargadenn</item>
<item quantity="other">%1$d pellgargadennoù</item>
</plurals>
@ -107,7 +107,7 @@
<string name="no_uploads_yet">N\'ho peus ket enporzhiet skeudennoù c\'hoazh.</string>
<string name="menu_retry_upload">Klask en-dro</string>
<string name="menu_cancel_upload">Nullañ</string>
<plurals name="share_license_summary">
<plurals name="share_license_summary" fuzzy="true">
<item quantity="one">gant an aotre-implijout %1$s e vo ar skeudenn-mañ</item>
<item quantity="other">gant an aotreoù-implijout %1$s e vo ar skeudenn-mañ</item>
</plurals>

View file

@ -26,7 +26,7 @@
<string name="upload_progress_notification_title_finishing">Postavljanje datoteke %1$s završeno</string>
<string name="upload_failed_notification_title">Postavljanje datoteke %1$s nije uspjelo</string>
<string name="upload_failed_notification_subtitle">Dodirnite da biste vidjeli</string>
<plurals name="uploads_pending_notification_indicator">
<plurals name="uploads_pending_notification_indicator" fuzzy="true">
<item quantity="one">postavlja se %1$d datoteka</item>
<item quantity="other">postavlja se %1$d datoteka</item>
</plurals>

View file

@ -76,7 +76,7 @@
<string name="gps_disabled">El vostre dispositiu no té el GPS habilitat. El voleu habilitar?</string>
<string name="enable_gps">Habilita el GPS</string>
<string name="contributions_subtitle_zero">Encara no hi ha cap càrrega</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
@ -353,7 +353,6 @@
<string name="preference_author_name_toggle">Utilitza un nom d\'autor personalitzat</string>
<string name="preference_author_name_toggle_summary">Utilitza un nom d\'autor personalitzat en comptes del vostre nom d\'usuari a l\'hora de pujar fotos</string>
<string name="preference_author_name">Nom de l\'autor personalitzat</string>
<string name="preference_author_name_summary">El nom d\'autor personalitzat que s\'utilitzarà en comptes del vostre nom d\'usuari en les pujades</string>
<string name="contributions_fragment">Contribucions</string>
<string name="nearby_fragment">A prop</string>
<string name="notifications">Notificacions</string>

View file

@ -52,7 +52,7 @@
<string name="upload_progress_notification_title_finishing">Dokončení nahrávání souboru %1$s</string>
<string name="upload_failed_notification_title">Načítání souboru %1$s se nezdařilo</string>
<string name="upload_failed_notification_subtitle">Klepnutím zobrazíte</string>
<plurals name="uploads_pending_notification_indicator">
<plurals name="uploads_pending_notification_indicator" fuzzy="true">
<item quantity="one">%1$d soubor se nahrává</item>
<item quantity="other">%1$d souborů se nahrává</item>
</plurals>
@ -88,16 +88,16 @@
<string name="gps_disabled">GPS ve vašem zařízení není povoleno. Chtěli byste ho spustit?</string>
<string name="enable_gps">Spustit GPS</string>
<string name="contributions_subtitle_zero">Žádné nahrané soubory</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
</plurals>
<plurals name="starting_multiple_uploads">
<plurals name="starting_multiple_uploads" fuzzy="true">
<item quantity="one">Spouští se nahrávání %1$d souboru</item>
<item quantity="other">Spouští se nahrávání %1$d souborů</item>
</plurals>
<plurals name="multiple_uploads_title">
<plurals name="multiple_uploads_title" fuzzy="true">
<item quantity="one">%1$d nahrávání</item>
<item quantity="other">%1$d nahrávání</item>
</plurals>
@ -122,7 +122,7 @@
<string name="no_uploads_yet">Zatím jste nenahrál žádné fotky.</string>
<string name="menu_retry_upload">Opakovat</string>
<string name="menu_cancel_upload">Zrušit</string>
<plurals name="share_license_summary">
<plurals name="share_license_summary" fuzzy="true">
<item quantity="one">Tento obrázek bude zveřejněn pod licencí %1$s</item>
<item quantity="other">Tyto obrázky budou zveřejněny pod licencí %1$s</item>
</plurals>
@ -132,7 +132,7 @@
<string name="use_previous">Použít předchozí název a popis</string>
<string name="allow_gps">Automaticky získat aktuální polohu</string>
<string name="allow_gps_summary">Nabídnout kategorie na základě aktuální polohy (pokud není obrázek opatřen souřadnicemi). Varování: Tímto krokem poskytujete svou současnou polohu.</string>
<string name="preference_theme" fuzzy="true">Noční režim</string>
<string name="preference_theme">Vzhled</string>
<string name="license_name_cc_by_sa_four">Uveďte autora-Zachovejte licenci 4.0</string>
<string name="license_name_cc_by_four">Uveďte autora 4.0</string>
<string name="license_name_cc_by_sa"> Uveďte autora-Zachovejte licenci 3.0</string>
@ -392,7 +392,6 @@
<string name="preference_author_name_toggle">Použít vlastní název autora</string>
<string name="preference_author_name_toggle_summary">Při nahrávání fotografií používejte vlastní jméno autora namísto uživatelského jména</string>
<string name="preference_author_name">Vlastní název autora</string>
<string name="preference_author_name_summary">Vlastní název autora, který chcete používat místo uživatelského jména v nahrávání</string>
<string name="contributions_fragment">Příspěvky</string>
<string name="nearby_fragment">Poblíž</string>
<string name="notifications">Upozornění</string>
@ -411,7 +410,7 @@
<string name="submit">Odeslat</string>
<string name="upload_title_duplicate">Soubor s názvem %1$s již existuje. Opravdu chcete pokračovat?</string>
<string name="map_application_missing">Na vašem zařízení nebyla nalezena žádná kompatibilní aplikace poskytující mapy. Pro použití této funkce nainstalujte aplikaci poskytující mapy.</string>
<plurals name="upload_count_title">
<plurals name="upload_count_title" fuzzy="true">
<item quantity="one">%1$d nahrání</item>
<item quantity="other">%1$d nahrání</item>
</plurals>
@ -501,7 +500,7 @@
<string name="review_thanks_no_button_text">Další soubor</string>
<string name="skip_image_explanation">Kliknutím na toto tlačítko se zobrazí další nedávno nahraný obrázek z Wikimedia Commons</string>
<string name="review_image_explanation">Můžete kontrolovat obrázky a zlepšit kvalitu Wikimedia Commons.\n\nTři aspekty, které se posuzují:\n  - Je tento obrázek v pořádku?\n  - Dodržuje tento obrázok pravidla autorských práv?\n  - Je tento obrázek správně kategorizován?\n\nPokud je vše v pořádku, autorovi můžete poděkovat.</string>
<plurals name="receiving_shared_content">
<plurals name="receiving_shared_content" fuzzy="true">
<item quantity="one">Probíhá příjem sdíleného obsahu. Zpracování obrázku může chvíli trvat v závislosti na velikosti obrázku a vašem zařízení</item>
<item quantity="other">Probíhá příjem sdíleného obsahu. Zpracování obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení</item>
</plurals>

View file

@ -36,7 +36,7 @@
<string name="upload_progress_notification_title_finishing">Yn gorffen uwchlwytho %1$s</string>
<string name="upload_failed_notification_title">Methwyd uwchlwytho %1$s</string>
<string name="upload_failed_notification_subtitle">Tapiwch i weld</string>
<plurals name="uploads_pending_notification_indicator">
<plurals name="uploads_pending_notification_indicator" fuzzy="true">
<item quantity="one">%1$d ffeil yn uwchlwytho</item>
<item quantity="other">%1$d ffeil yn uwchlwytho</item>
</plurals>
@ -72,16 +72,16 @@
<string name="gps_disabled">Ataliwyd GPS ar eich dyfais. Ydych chi am ei droi\'n weithredol?</string>
<string name="enable_gps">Gweithredu\'r GPS</string>
<string name="contributions_subtitle_zero">Heb uwchlwytho eto</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
</plurals>
<plurals name="starting_multiple_uploads">
<plurals name="starting_multiple_uploads" fuzzy="true">
<item quantity="one">Cychwyn %1$d uwchlwythiad</item>
<item quantity="other">Cychwyn uwchlwytho %1$d ffeil</item>
</plurals>
<plurals name="multiple_uploads_title">
<plurals name="multiple_uploads_title" fuzzy="true">
<item quantity="one">%1$d uwchlwythiad</item>
<item quantity="other">%1$d uwchlwythiad</item>
</plurals>
@ -106,7 +106,7 @@
<string name="no_uploads_yet">Nid ydych wedi uwchlwytho ffotograffau eto.</string>
<string name="menu_retry_upload">Ailgynnig</string>
<string name="menu_cancel_upload">Diddymer</string>
<plurals name="share_license_summary">
<plurals name="share_license_summary" fuzzy="true">
<item quantity="one">Caiff y ddelwedd hon ei thrwyddedu yn ôl termau\'r drwydded %1$s</item>
<item quantity="other">Caiff y delweddau hyn eu trwyddedu dan %1$s</item>
</plurals>

View file

@ -117,7 +117,7 @@
<string name="use_previous">Brug forrige titel og beskrivelse</string>
<string name="allow_gps">Hent automatisk nuværende placering</string>
<string name="allow_gps_summary">Henter den nuværende placering for at tilbyde kategoriforslag hvis billedet ikke er geografisk mærket og geomærker billedet. Advarsel: Dette vil afsløre din nuværende placering.</string>
<string name="preference_theme" fuzzy="true">Nat-tilstand</string>
<string name="preference_theme">Tema</string>
<string name="license_name_cc_by_sa_four"> Attribution-ShareAlike 4.0</string>
<string name="license_name_cc_by_four"> Attribution 4.0</string>
<string name="license_name_cc_by_sa"> Attribution-ShareAlike 3.0</string>
@ -385,4 +385,8 @@
<string name="place_state_exists">Findes</string>
<string name="place_state_needs_photo">Mangler et billede</string>
<string name="nearby_search_hint">Bro, museum, hotel, osv.</string>
<string name="title_app_shortcut_bookmark">Bogmærker</string>
<string name="theme_default_name">Standard</string>
<string name="theme_dark_name">Mørkt</string>
<string name="theme_light_name">Lyst</string>
</resources>

View file

@ -82,7 +82,7 @@
<string name="gps_disabled">GPS ist auf deinem Gerät deaktiviert. Möchtest du es aktivieren?</string>
<string name="enable_gps">GPS aktivieren</string>
<string name="contributions_subtitle_zero">Noch keine hochgeladenen Dateien</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">Eine hochgeladene Datei</item>
<item quantity="other">%1$d hochgeladene Dateien</item>
@ -385,7 +385,6 @@
<string name="preference_author_name_toggle">Benutzerdefinierten Namen des Autors verwenden</string>
<string name="preference_author_name_toggle_summary">Einen benutzerdefinierten Namen des Autors anstelle deines Benutzernamens beim Hochladen von Fotos verwenden</string>
<string name="preference_author_name">Benutzerdefinierter Name des Autors</string>
<string name="preference_author_name_summary">Der benutzerdefinierte Name des Autors, der anstelle deines Benutzernamens in den hochgeladenen Dateien erscheinen soll.</string>
<string name="contributions_fragment">Beiträge</string>
<string name="nearby_fragment">In der Nähe</string>
<string name="notifications">Benachrichtigungen</string>

View file

@ -83,7 +83,7 @@
<string name="gps_disabled">Το GPS στην συσκευή είναι απενεργοποιημένο. Θέλετε να το ενεργοποιήσετε;</string>
<string name="enable_gps"> Ενεργοποιήσετε το GPS</string>
<string name="contributions_subtitle_zero">Δεν έχουν ακόμη επιφορτωθεί αρχεία</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">Δεν υπάρχουν φορτώσεις ακόμη</item>
<item quantity="one">%1$d φόρτωση</item>
<item quantity="other">%1$d φορτώσεις</item>

View file

@ -75,7 +75,7 @@
<string name="gps_disabled">GPS estas malŝaltita en via aparato. Ĉu vi volas ŝalti ĝin?</string>
<string name="enable_gps">Ŝalti la GPS</string>
<string name="contributions_subtitle_zero">Neniuj alŝutaĵoj ĝis nun!</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
@ -378,7 +378,6 @@
<string name="preference_author_name_toggle">Uzi laŭmendan aŭtoran nomon</string>
<string name="preference_author_name_toggle_summary">Uzi laŭmendan aŭtoran nomon anstataŭ via uzantnomon por alŝutado de fotoj</string>
<string name="preference_author_name">Laŭmenda aŭtora nomo</string>
<string name="preference_author_name_summary">La laŭmenda aŭtora nomo uzota anstataŭ via uzantnomo por alŝutoj</string>
<string name="contributions_fragment">Kontribuoj</string>
<string name="nearby_fragment">Apude</string>
<string name="notifications">Sciigoj</string>

View file

@ -97,7 +97,7 @@
<string name="gps_disabled">El GPS está desactivado en tu dispositivo. ¿Quieres activarlo?</string>
<string name="enable_gps">Activar GPS</string>
<string name="contributions_subtitle_zero">No hay subidas aún</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
@ -401,7 +401,6 @@
<string name="preference_author_name_toggle">Usar nombre de autor personalizado</string>
<string name="preference_author_name_toggle_summary">Usar un nombre de autor personalizado en vez de tu nombre de usuario al subir fotos</string>
<string name="preference_author_name">Nombre de autor personalizado</string>
<string name="preference_author_name_summary">El nombre de autor personalizado para usar en las subidas en vez de tu nombre de usuario</string>
<string name="contributions_fragment">Contribuciones</string>
<string name="nearby_fragment">Cercanos</string>
<string name="notifications">Notificaciones</string>

View file

@ -77,7 +77,7 @@
<string name="gps_disabled">GPSa desgaituta dago gailu honetan. Gaitu nahi duzu?</string>
<string name="enable_gps">GPSa gaitu</string>
<string name="contributions_subtitle_zero">Oraindik ez da ezer igo</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">igoera (%1$d)</item>
<item quantity="other">(%1$d) igoera</item>

View file

@ -82,7 +82,7 @@
<string name="gps_disabled">GPS ei ole käytössä. Haluatko ottaa sen käyttöön?</string>
<string name="enable_gps">Ota GPS käyttöön</string>
<string name="contributions_subtitle_zero">Ei tallennuksia vielä</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
@ -287,14 +287,14 @@
<string name="nearby_wikidata">Wikidata</string>
<string name="nearby_wikipedia">Wikipedia</string>
<string name="nearby_commons">Commons</string>
<string name="about_rate_us" fuzzy="true">&lt;u&gt;Arvostele meidät&lt;/u&gt;</string>
<string name="about_faq" fuzzy="true">&lt;u&gt;UKK&lt;/u&gt;</string>
<string name="about_rate_us">Arvostele meidät</string>
<string name="about_faq">UKK</string>
<string name="welcome_skip_button">Ohita opetus</string>
<string name="no_internet">Internet ei saatavissa</string>
<string name="internet_established">Internet saatavana</string>
<string name="error_notifications">Virhe ilmoitusten haussa</string>
<string name="no_notifications">Ilmoituksia ei löytynyt</string>
<string name="about_translate" fuzzy="true">&lt;u&gt;Käännä&lt;/u&gt;</string>
<string name="about_translate">Käännä</string>
<string name="about_translate_title">Kielet</string>
<string name="about_translate_message">Valitse kieli, joksi haluaisit kääntää</string>
<string name="about_translate_proceed">Jatka</string>

View file

@ -96,7 +96,7 @@
<string name="gps_disabled">Le GPS est désactivé sur votre appareil. Voulez-vous lactiver?</string>
<string name="enable_gps">Activer le GPS</string>
<string name="contributions_subtitle_zero">Encore aucun téléversement</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
@ -400,7 +400,6 @@
<string name="preference_author_name_toggle">Utilisez un nom d\'auteur personnalisé</string>
<string name="preference_author_name_toggle_summary">Utilisez un nom d\'auteur personnalisé au lieu de votre nom d\'utilisateur lorsque vous téléversez des photos</string>
<string name="preference_author_name">Nom d\'auteur personnalisé</string>
<string name="preference_author_name_summary">Le nom d\'auteur personnalisé à utiliser au lieu du nom d\'utilisateur dans vos téléversements</string>
<string name="contributions_fragment">Contributions</string>
<string name="nearby_fragment">Proche</string>
<string name="notifications">Notifications</string>
@ -459,6 +458,7 @@
<string name="display_location_permission_title">Afficher l\'autorisation de géolocalisation</string>
<string name="display_location_permission_explanation">Demander le droit de géolocaliser quand cela est nécessaire à la fonction de notification de proximité.</string>
<string name="achievements_fetch_failed">Un problème est survenu, nous n\'avons pas pu récupérer vos accomplissements</string>
<string name="achievements_fetch_failed_ultimate_achievement">Vous avez effectué tellement de contributions que notre système de calcul des accomplissements est débordé. Il s\'agit là de l\'accomplissement ultime.</string>
<string name="ends_on">Se termine le:</string>
<string name="display_campaigns">Campagnes d\'affichage</string>
<string name="display_campaigns_explanation">Voir les campagnes en cours</string>

View file

@ -78,7 +78,7 @@
<string name="gps_disabled">O GPS está desactivado no seu dispositivo. Quere activalo?</string>
<string name="enable_gps">Activar GPS</string>
<string name="contributions_subtitle_zero">Aínda non hai subas</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
@ -382,7 +382,6 @@
<string name="preference_author_name_toggle">Usar nome personalizado de autor</string>
<string name="preference_author_name_toggle_summary">Usar un nome personalizado de autor en lugar do seu nome de usuario á hora de cargar imaxes</string>
<string name="preference_author_name">Nome personalizado de autor</string>
<string name="preference_author_name_summary">O nome personalizado de autor a utilizar nas cargas en lugar do seu nome de usuario</string>
<string name="contributions_fragment">Contribucións</string>
<string name="nearby_fragment">Preto</string>
<string name="notifications">Notificacións</string>

View file

@ -87,7 +87,7 @@
<string name="gps_disabled">आपके डिवाइस में जीपीएस अक्षम है। क्या आप इसे सक्षम करना चाहेंगे?</string>
<string name="enable_gps">जीपीएस सक्षम करें</string>
<string name="contributions_subtitle_zero">अभी तक कोई अपलोड नहीं</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>

View file

@ -35,7 +35,7 @@
<string name="upload_progress_notification_title_finishing">Završeno postavljanje %1$s</string>
<string name="upload_failed_notification_title">Postavljanje %1$s neuspješno</string>
<string name="upload_failed_notification_subtitle">Dodirnite da biste vidjeli</string>
<plurals name="uploads_pending_notification_indicator">
<plurals name="uploads_pending_notification_indicator" fuzzy="true">
<item quantity="one">Postavlja se %1$d datoteka</item>
<item quantity="other">Postavljaju se %1$d datoteke</item>
</plurals>
@ -75,11 +75,11 @@
<item quantity="one">%1$d postavljena datoteka</item>
<item quantity="other">%1$d postavljene datoteke</item>
</plurals>
<plurals name="starting_multiple_uploads">
<plurals name="starting_multiple_uploads" fuzzy="true">
<item quantity="one">Započeto %1$d postavljanje</item>
<item quantity="other">Započeta %1$d postavljanja</item>
</plurals>
<plurals name="multiple_uploads_title">
<plurals name="multiple_uploads_title" fuzzy="true">
<item quantity="one">%1$d postavljanje</item>
<item quantity="other">%1$d postavljanja</item>
</plurals>
@ -280,10 +280,19 @@
<string name="level">Razina</string>
<string name="error_occurred">Došlo je do pogrješke!</string>
<string name="notifications_channel_name_all">Obavijesti Zajedničkoga poslužitelja</string>
<string name="achievements_fetch_failed_ultimate_achievement">Toliko ste pridonijeli projektu da se naš sustav za računanje postignuća ne može nositi s time. To je vrhunsko postignuće.</string>
<string name="error_processing_image">Došlo je do pogrješke tijekom obradbe slike. Molimo Vas, pokušajte ponovo!</string>
<string name="please_wait">Molimo Vas, pričekajte ...</string>
<string name="skip_image">Preskoči ovu sliku</string>
<string name="default_description_language">Zadani jezik za opis</string>
<string name="title_app_shortcut_explore">Istraži</string>
<string name="title_app_shortcut_bookmark">Oznake</string>
<string name="title_app_shortcut_setting">Postavke</string>
<string name="remove_bookmark">Uklonjeno iz oznaka</string>
<string name="add_bookmark">Dodano u oznake</string>
<string name="wallpaper_set_unsuccessfully">Nešto je pošlo po zlu. Ne možemo postaviti pozadinu</string>
<string name="setting_wallpaper_dialog_title">Postavi kao pozadinu</string>
<string name="setting_wallpaper_dialog_message">Postavljanje pozadine. Molimo, pričekajte...</string>
<string name="theme_default_name">Zadano</string>
<string name="theme_dark_name">Tamno</string>
<string name="theme_light_name">Svijetlo</string>

View file

@ -374,7 +374,6 @@
<string name="preference_author_name_toggle">Gunakan nama lain</string>
<string name="preference_author_name_toggle_summary">Gunakan nama lain, bukan nama pengguna Anda saat mengunggah foto</string>
<string name="preference_author_name">Nama lain</string>
<string name="preference_author_name_summary">Nama lain untuk digunakan sebagai ganti nama pengguna Anda pada unggahan</string>
<string name="contributions_fragment">Kontribusi</string>
<string name="nearby_fragment">Sekitar</string>
<string name="notifications">Pemberitahuan</string>

View file

@ -71,7 +71,7 @@
<string name="gps_disabled">GPS er óvirkt í tækinu þínu. Viltu virkja það?</string>
<string name="enable_gps">Virkja GPS</string>
<string name="contributions_subtitle_zero">Engar innsendingar ennþá</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
@ -369,7 +369,6 @@
<string name="preference_author_name_toggle">Nota sérsniðið nafn höfundar</string>
<string name="preference_author_name_toggle_summary">Nota sérsniðið nafn höfundar í stað notandanafnsins þíns þegar ljósmyndir eru sendar inn</string>
<string name="preference_author_name">Sérsniðið nafn höfundar</string>
<string name="preference_author_name_summary">Sérsniðið nafn höfundar sem nota á í stað notandanafnsins þíns þegar myndir eru sendar inn</string>
<string name="contributions_fragment">Framlög</string>
<string name="nearby_fragment">Nálægt</string>
<string name="notifications">Tilkynningar</string>

View file

@ -83,7 +83,7 @@
<string name="gps_disabled">Il GPS è disabilitato nel dispositivo. Vuoi attivarlo?</string>
<string name="enable_gps">Attiva GPS</string>
<string name="contributions_subtitle_zero">Non è stato ancora caricato niente</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
@ -381,7 +381,6 @@
<string name="preference_author_name_toggle">Usa il nome dell\'autore personalizzato</string>
<string name="preference_author_name_toggle_summary">Utilizza un nome dell\'autore personalizzato al posto del tuo nome utente durante il caricamento delle foto</string>
<string name="preference_author_name">Nome dell\'autore personalizzato</string>
<string name="preference_author_name_summary">Il nome dell\'autore personalizzato da usare al posto del tuo nome utente nei caricamenti</string>
<string name="contributions_fragment">Contributi</string>
<string name="nearby_fragment">Nelle vicinanze</string>
<string name="notifications">Notifiche</string>

View file

@ -50,7 +50,7 @@
<string name="upload_progress_notification_title_finishing">העלאת %1$s מסתיימת</string>
<string name="upload_failed_notification_title">העלאת %1$s נכשלה</string>
<string name="upload_failed_notification_subtitle">לחץ כדי להציג</string>
<plurals name="uploads_pending_notification_indicator">
<plurals name="uploads_pending_notification_indicator" fuzzy="true">
<item quantity="one">מועלה קובץ אחד</item>
<item quantity="other">מועלים %1$d קבצים</item>
</plurals>
@ -86,16 +86,16 @@
<string name="gps_disabled">ה־GPS במכשיר שלך אינו מופעל. האם להפעיל אותו?</string>
<string name="enable_gps">הפעלת GPS</string>
<string name="contributions_subtitle_zero">לא הועלה עדיין שום דבר</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
</plurals>
<plurals name="starting_multiple_uploads">
<plurals name="starting_multiple_uploads" fuzzy="true">
<item quantity="one">התחלת העלאה</item>
<item quantity="other">התחלת 1$d% העלאות</item>
</plurals>
<plurals name="multiple_uploads_title">
<plurals name="multiple_uploads_title" fuzzy="true">
<item quantity="one">העלאה אחת</item>
<item quantity="other">%1$d העלאות</item>
</plurals>
@ -120,7 +120,7 @@
<string name="no_uploads_yet">עדיין לא העלית תמונות.</string>
<string name="menu_retry_upload">לנסות שוב</string>
<string name="menu_cancel_upload">ביטול</string>
<plurals name="share_license_summary">
<plurals name="share_license_summary" fuzzy="true">
<item quantity="one">התמונה הזאת תפורסם ברישיון %1$s</item>
<item quantity="other">התמונות האלה תפורסמנה ברישיון %1$s</item>
</plurals>
@ -390,7 +390,6 @@
<string name="preference_author_name_toggle">שימוש בשם יוצר מותאם אישית</string>
<string name="preference_author_name_toggle_summary">שימוש בשם יוצר בהתאמה אישית במקום בשם המשתמש שלך בעת העלאת תמונות</string>
<string name="preference_author_name">שם יוצר מותאם אישית</string>
<string name="preference_author_name_summary">שם יוצר מותאם אישית לשימוש במקום שם המשתמש שלך להעלאות</string>
<string name="contributions_fragment">תרומות</string>
<string name="nearby_fragment">בסביבה</string>
<string name="notifications">התראות</string>
@ -409,7 +408,7 @@
<string name="submit">שליחה</string>
<string name="upload_title_duplicate">כבר קיים קובץ בשם %1$s. להמשיך?</string>
<string name="map_application_missing">לא נמצא יישום מפה תואם במכשיר שלך. נא להתקין יישום מפה כדי להשתמש בתכונה זו.</string>
<plurals name="upload_count_title">
<plurals name="upload_count_title" fuzzy="true">
<item quantity="one">העלאה אחת</item>
<item quantity="other">%1$d העלאות</item>
</plurals>
@ -500,7 +499,7 @@
<string name="review_thanks_no_button_text">התמונה הבאה</string>
<string name="skip_image_explanation">לחיצה על הכפתור הזה תציג בפניך תמונה נוספת שנוספה לאחרונה לוויקישיתוף</string>
<string name="review_image_explanation">ניתן לסקור את התמונות ולשפר את האיכות של ויקישיתוף.\nארבע יסודות הסקירה הם:\n- האם התמונה קשורה לנושא?\n- האם התמונה תואמת לכללי זכויות היוצרים?\n- האם התמונה נמצאת בקטגוריה הנכונה?\n- אם הכול בסדר, ניתן גם להודות למי שתרם אותה.</string>
<plurals name="receiving_shared_content">
<plurals name="receiving_shared_content" fuzzy="true">
<item quantity="one">מתקבל תוכן שיתופי. עיבוד התמונה עשוי לארוך זמן מה כתלות בגודל התמונה והמכשיר שלך</item>
<item quantity="other">מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך</item>
</plurals>
@ -582,4 +581,7 @@
<string name="theme_default_name">ברירת מחדל</string>
<string name="theme_dark_name">כהה</string>
<string name="theme_light_name">בהירה</string>
<string name="cannot_open_location_settings">פתיחת הגדרות המיקום נכשלה. נא להפעיל את איתור המיקום ידנית</string>
<string name="recommend_high_accuracy_mode">לתוצאות המיטביות יש להשתמש במצב דיוק גבוה.</string>
<string name="ask_to_turn_location_on">להפעיל מיקום?</string>
</resources>

View file

@ -386,7 +386,6 @@
<string name="preference_author_name_toggle">専用の投稿者名を使う</string>
<string name="preference_author_name_toggle_summary">画像の投稿にはユーザー名ではなく専用の投稿者名を使う</string>
<string name="preference_author_name">投稿専用の名前</string>
<string name="preference_author_name_summary">ユーザー名ではなく、投稿専用の投稿者名</string>
<string name="contributions_fragment">投稿記録</string>
<string name="nearby_fragment">付近</string>
<string name="notifications">お知らせ</string>

View file

@ -371,7 +371,6 @@
<string name="preference_author_name_toggle">사용자 지정 저자 이름을 사용합니다</string>
<string name="preference_author_name_toggle_summary">사진을 업로드하는 중에 사용자 이름 대신 사용자 지정 저자 이름을 사용합니다</string>
<string name="preference_author_name">저자 이름 사용자 지정</string>
<string name="preference_author_name_summary">업로드할 때 사용자 이름 대신 사용할 사용자 지정 저자 이름</string>
<string name="contributions_fragment">기여</string>
<string name="nearby_fragment">근처</string>
<string name="notifications">알림</string>

View file

@ -69,7 +69,7 @@
<string name="gps_disabled">GPS ass op Ärem Apparat ausgeschalt. Wëllt Dir en aktivéieren?</string>
<string name="enable_gps">GPS aktivéieren</string>
<string name="contributions_subtitle_zero">Nach keng eropgeluede Fichieren</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">N@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
@ -134,7 +134,7 @@
<string name="license_name_cc_zero">CC Zero</string>
<string name="tutorial_1_text">Wikimedia Commons späichert déi meescht Biller, déi op Wikipedia benotzt ginn.</string>
<string name="tutorial_1_subtext">Är Biller hëllefe Leit op der ganzer Welt ze forméieren!</string>
<string name="tutorial_2_text">Lued wgl. Biller erop déi komplett vun Iech opgeholl oder gemaacht goufen:</string>
<string name="tutorial_2_text">Luet wgl. nëmme Biller erop déi komplett vun Iech opgeholl oder gemaach goufen:</string>
<string name="tutorial_2_subtext_1">Natierlech Objeten (Blummen, Déieren, Bierger)</string>
<string name="tutorial_2_subtext_2">Nëtzlech Objeten (Vëloen, Garen)</string>
<string name="tutorial_2_subtext_3">Berühmt Leit (Äre Buergermeeschter, Olympioniken deenen Dir begéint sidd)</string>
@ -222,7 +222,7 @@
<string name="upload_problem_image_duplicate">Bild ass schonn op Commons.</string>
<string name="upload_problem_different_geolocation">Dëst Bild gouf op enger anerer Plaz gemaach.</string>
<string name="upload_problem_do_you_continue">Wëllt Dir dëst Bild nach ëmmer eroplueden?</string>
<string name="internet_downloaded">Lued wgl. nëmme Biller erop déi Dir selwer opgeholl hutt. Lued keng Biller erop déi Dir aus dem Internet erofgelueden hutt.</string>
<string name="internet_downloaded">Luet wgl. nëmme Biller erop déi Dir selwer opgeholl hutt. Luet keng Biller erop déi Dir aus dem Internet erofgelueden hutt.</string>
<string name="give_permission">Autorisatioun ginn</string>
<string name="use_external_storage">Externe Späicher benotzen</string>
<string name="use_external_storage_summary">Biller späicheren déi mat der in-app Kamera vun Ärem Apparat gemaach goufen</string>
@ -322,6 +322,7 @@
<string name="yes_submit">Jo, schécken</string>
<string name="no_go_back">Neen, zréck goen</string>
<string name="never_ask_again">Dëst ni méi froen</string>
<string name="achievements_fetch_failed_ultimate_achievement">Dir hutt esou vill Kontributioune gemaach datt eise Berechnungssystem iwwerfuerdert ass. Dëst ass déi bescht Leeschtung.</string>
<string name="nominate_for_deletion_done">Fäerdeg</string>
<string name="notsure">Net sécher</string>
<string name="send_thank_send">Merci schécken</string>

View file

@ -73,7 +73,7 @@
<string name="gps_disabled">GPS е исклучен на вашиот уред. Дали сакате да го вклучите?</string>
<string name="enable_gps">Вклучи GPS</string>
<string name="contributions_subtitle_zero">Сè уште нема подигања</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
@ -377,7 +377,6 @@
<string name="preference_author_name_toggle">Употреби прилагодено авторско име</string>
<string name="preference_author_name_toggle_summary">При подигањето користете прилагодено авторско име наместо вашето корисничко име</string>
<string name="preference_author_name">Прилагодено авторско име</string>
<string name="preference_author_name_summary">Прилагоденото авторско име што ќе се користи место вашето корисничко име при подигања</string>
<string name="contributions_fragment">Придонеси</string>
<string name="nearby_fragment">Во близина</string>
<string name="notifications">Известувања</string>
@ -569,4 +568,8 @@
<string name="theme_default_name">По основно</string>
<string name="theme_dark_name">Темен</string>
<string name="theme_light_name">Светол</string>
<string name="cannot_open_location_settings">Не можев да ги отворам поставките за местоположба. Вклучете ја местоположбата рачно.</string>
<string name="recommend_high_accuracy_mode">За да добиете најдобар исход, изберете го режимот на висока уточнетост.</string>
<string name="ask_to_turn_location_on">Да ја вклучам местоположбата?</string>
<string name="nearby_needs_location">„Во близина“ бара местоположба за да работи</string>
</resources>

View file

@ -80,7 +80,7 @@
<string name="gps_disabled">GPS er slått av på denne enheten. Ønsker du å slå den på?</string>
<string name="enable_gps">Slå på GPS</string>
<string name="contributions_subtitle_zero">Ingen opplastinger ennå</string>
<plurals name="contributions_subtitle">
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
@ -384,7 +384,6 @@
<string name="preference_author_name_toggle">Bruk egendefinert opphavspersonnavn</string>
<string name="preference_author_name_toggle_summary">Bruk et egendefinert opphavspersonnavn i stedet for brukernavnet ditt når bilder lastes opp</string>
<string name="preference_author_name">Egendefinert opphavspersonnavn</string>
<string name="preference_author_name_summary">Det egendefinerte opphavspersonnavnet som skal brukes i stedet for brukernavnet ditt i opplastinger</string>
<string name="contributions_fragment">Bidrag</string>
<string name="nearby_fragment">I nærheten</string>
<string name="notifications">Varsler</string>

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