diff --git a/.travis.yml b/.travis.yml index 20c5bfaee..5e76e09d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,12 +19,13 @@ android: components: - tools - platform-tools - - build-tools-26.0.2 + - build-tools-27.0.0 - extra-google-m2repository - extra-android-m2repository - ${ANDROID_TARGET} - android-25 - android-26 + - android-27 - sys-img-${ANDROID_ABI}-${ANDROID_TARGET} licenses: - 'android-sdk-license-.+' diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f3dfc30a..1688b3b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Wikimedia Commons for Android +## v2.7.2 +- Modified subtext for "automatically get current location" setting to emphasize that it will reveal user's location + +## v2.7.1 +- Fixed UI and permission issues with Nearby +- Fixed issue with My Recent Uploads being empty +- Fixed blank category issue when uploading directly from Nearby +- Various crash fixes + ## v2.7.0 - New Nearby Places UI with direct uploads (and associated category suggestions) - Added two-factor authentication login diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee7f42e06..caa02a103 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1,34 @@ -Please see our guidelines in the wiki: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21 +Thanks for considering to contribute to this project! A few guidelines for +people who want to contribute their code to this software are documented in +[this project's Wiki](https://github.com/commons-app/apps-android-commons/wiki/Contributing-Guidelines). +If you're not sure where to start head on to [this wiki page](https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome!). + +Here's a gist of the guidelines, + +1. Make separate commits for logically separate changes + +1. Describe your changes well in the commit message + + The first line of the commit message should be a short description of what has +changed. It is also good to prefix the first line with "area: " where the "area" +is a filename or identifier for the general area of the code being modified. +The body should provide a meaningful commit message. + +1. Write Javadocs + + We require contributors to include Javadocs for all new methods and classes + submitted via PRs (after 1 May 2018). This is aimed at making it easier for + new contributors to dive into our codebase, especially those who are new to + Android development. A few things to note: + + - This should not replace the need for code that is easily-readable in + and of itself + - Please make sure that your Javadocs are reasonably descriptive, not just + a copy of the method name + - Please do not use `@author` tags - we aim for collective code ownership, + and if needed, Git allows us to see who wrote something without needing + to add these tags (`git blame`) + +1. Write tests for your code (if possible) + +1. Make sure the Wiki pages don't become stale by updating them (if needed) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 34078f07e..37e104d14 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,15 +1,19 @@ -## Description +## Title (required) -Fixes #{GitHub issue number} +Fixes #{GitHub issue number and title (Please do not forget adding title) } + +## Description (required) + +Fixes #{GitHub issue number and title} {Describe the changes made and why they were made.} -## Tests performed +## Tests performed (required) Tested on {API level & name of device/emulator}, with {build variant, e.g. ProdDebug}. -{Please test your PR at least once before submitting.} - -## Screenshots showing what changed +## Screenshots showing what changed (optional) {Only for user interface changes, otherwise remove this section. See [how to take a screenshot](https://android.stackexchange.com/questions/1759/how-to-take-a-screenshot-with-an-android-device)} + +_Note: Please ensure that you have read CONTRIBUTING.md if this is your first pull request._ \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index b1ee9aef9..68a31f984 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,10 +7,12 @@ apply from: 'quality.gradle' apply plugin: 'com.getkeepsafe.dexcount' dependencies { + implementation 'com.squareup.picasso:picasso:2.71828' + implementation 'com.prof.rssparser:rssparser:1.1' implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07' implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar' implementation 'in.yuvi:http.fluent:1.3' - implementation 'com.android.volley:volley:1.0.0' + implementation 'com.github.chrisbanes:PhotoView:2.0.0' implementation 'ch.acra:acra:4.9.2' implementation 'org.mediawiki:api:1.3' implementation 'commons-codec:commons-codec:1.10' @@ -18,69 +20,59 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.1' implementation 'com.jakewharton.timber:timber:4.5.1' implementation 'info.debatty:java-string-similarity:0.24' - implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.4.1@aar'){ - transitive=true + implementation 'com.borjabravo:readmoretextview:2.1.0' + implementation 'com.android.support.constraint:constraint-layout:1.0.2' + implementation('com.mapbox.mapboxsdk:mapbox-android-sdk:5.4.1@aar') { + transitive = true } - - + implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0' implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION" implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION" implementation "com.android.support:design:$SUPPORT_LIB_VERSION" implementation "com.android.support:customtabs:$SUPPORT_LIB_VERSION" - implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION" - implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" - - implementation 'com.squareup.okhttp3:okhttp:3.8.1' + implementation 'com.squareup.okhttp3:okhttp:3.9.1' implementation 'com.squareup.okio:okio:1.13.0' - implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' // Because RxAndroid releases are few and far between, it is recommended you also // explicitly depend on RxJava's latest version for bug fixes and new features. implementation 'com.android.support:multidex:1.0.3' - - testImplementation "org.robolectric:multidex:3.4.2" - implementation 'io.reactivex.rxjava2:rxjava:2.1.2' implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0' - + implementation 'org.jsoup:jsoup:1.11.3' implementation 'com.facebook.fresco:fresco:1.5.0' implementation 'com.facebook.stetho:stetho:1.5.0' - implementation "com.google.dagger:dagger:$DAGGER_VERSION" implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION" - kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" - + testImplementation 'org.robolectric:multidex:3.4.2' testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" - androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" - + testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testImplementation 'junit:junit:4.12' testImplementation 'org.robolectric:robolectric:3.7.1' - testImplementation 'org.mockito:mockito-all:1.10.19' - + testImplementation 'com.nhaarman:mockito-kotlin:1.5.0' testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' + + implementation 'com.caverock:androidsvg:1.2.1' + implementation 'com.github.bumptech.glide:glide:4.7.1' + kapt 'com.github.bumptech.glide:compiler:4.7.1' + + androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION" - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' + androidTestImplementation 'com.android.support.test:rules:1.0.2' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY" releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" - - implementation "com.google.dagger:dagger:$DAGGER_VERSION" - implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION" - kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" - kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" - - implementation 'com.borjabravo:readmoretextview:2.1.0' - implementation 'com.android.support.constraint:constraint-layout:1.0.2' } android { @@ -91,8 +83,8 @@ android { defaultConfig { applicationId 'fr.free.nrw.commons' - versionCode 83 - versionName '2.7.0' + versionCode 85 + versionName '2.7.2' setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) minSdkVersion project.minSdkVersion @@ -121,7 +113,7 @@ android { buildTypes { release { minifyEnabled false // See https://stackoverflow.com/questions/40232404/google-play-apk-and-android-studio-apk-usb-debug-behaving-differently - proguard.cfg modification alone insufficient. - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt', 'proguard-glide.txt' } debug { applicationIdSuffix ".debug" @@ -133,7 +125,9 @@ android { flavorDimensions 'tier' productFlavors { prod { + buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"" buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\"" + buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"" buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"" buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\"" buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\"" @@ -149,7 +143,9 @@ android { beta { // What values do we need to hit the BETA versions of the site / api ? + buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"" buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\"" + buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"" buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"" buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\"" buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\"" diff --git a/app/libs/java-json.jar b/app/libs/java-json.jar new file mode 100644 index 000000000..2f211e366 Binary files /dev/null and b/app/libs/java-json.jar differ diff --git a/app/proguard-glide.txt b/app/proguard-glide.txt new file mode 100644 index 000000000..ef3437660 --- /dev/null +++ b/app/proguard-glide.txt @@ -0,0 +1,9 @@ +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep public class * extends com.bumptech.glide.module.AppGlideModule +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} + +# for DexGuard only +-keepresourcexmlelements manifest/application/meta-data@value=GlideModule \ No newline at end of file diff --git a/app/proguard-rules.txt b/app/proguard-rules.txt index bbf3a3f0d..39b618718 100644 --- a/app/proguard-rules.txt +++ b/app/proguard-rules.txt @@ -1,5 +1,4 @@ -dontobfuscate -keep class org.apache.http.** { *; } -dontwarn org.apache.http.** --keep class fr.free.nrw.commons.upload.MwVolleyApi$Page {*;} -keep class android.support.v7.widget.ShareActionProvider { *; } \ No newline at end of file diff --git a/app/quality.gradle b/app/quality.gradle index 7ea20916a..1afdf0d68 100644 --- a/app/quality.gradle +++ b/app/quality.gradle @@ -18,7 +18,7 @@ task checkstyle(type: Checkstyle) { reports { html { enabled true - destination "${project.buildDir}/reports/checkstyle/checkstyle.html" + destination file("${project.buildDir}/reports/checkstyle/checkstyle.html") } } } @@ -36,10 +36,10 @@ task pmd(type: Pmd) { xml.enabled = false html.enabled = true xml { - destination "${project.buildDir}/reports/pmd/pmd.xml" + destination file("${project.buildDir}/reports/pmd/pmd.xml") } html { - destination "${project.buildDir}/reports/pmd/pmd.html" + destination file("${project.buildDir}/reports/pmd/pmd.html") } } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 253bdaea8..38fc2e35a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ + @@ -26,10 +27,10 @@ android:theme="@style/LightAppTheme" android:supportsRtl="true" > + android:theme="@android:style/Theme.Dialog" + android:launchMode="singleInstance" + android:excludeFromRecents="true" + android:finishOnTaskLaunch="true" /> @@ -91,6 +92,11 @@ android:name=".notification.NotificationActivity" android:label="@string/navigation_item_notification" /> + + + + + + + + + + diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java index e6bf34736..c8941dcd8 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java @@ -9,9 +9,6 @@ import android.os.Bundle; import android.text.Html; import android.text.SpannableString; import android.text.style.UnderlineSpan; -import android.util.Log; -import android.support.customtabs.CustomTabsIntent; -import android.support.v4.content.ContextCompat; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -20,7 +17,6 @@ import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; -import android.widget.Toast; import butterknife.BindView; import butterknife.ButterKnife; @@ -28,8 +24,6 @@ import butterknife.OnClick; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.ui.widget.HtmlTextView; -import static android.widget.Toast.LENGTH_SHORT; - /** * Represents about screen of this app */ @@ -135,9 +129,10 @@ public class AboutActivity extends NavigationBaseActivity { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.share_app_icon: + String shareText = "Upload photos to Wikimedia Commons on your phone\nDownload the Commons app: http://play.google.com/store/apps/details?id=fr.free.nrw.commons"; Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_TEXT, "http://play.google.com/store/apps/details?id=fr.free.nrw.commons"); + sendIntent.putExtra(Intent.EXTRA_TEXT, shareText); sendIntent.setType("text/plain"); startActivity(Intent.createChooser(sendIntent, "Share app via...")); return true; diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index 57cb5fad1..61eecee00 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -1,6 +1,5 @@ package fr.free.nrw.commons; -import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; @@ -27,7 +26,7 @@ import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.modifications.ModifierSequenceDao; -import fr.free.nrw.commons.utils.FileUtils; +import fr.free.nrw.commons.upload.FileUtils; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index 2d79a6c4f..affb57528 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -61,8 +61,8 @@ public class MediaDataExtractor { } try{ - Timber.d("Nominated for deletion: " + mediaWikiApi.pageExists("Commons:Deletion_requests/"+filename)); - deletionStatus = mediaWikiApi.pageExists("Commons:Deletion_requests/"+filename); + deletionStatus = mediaWikiApi.pageExists("Commons:Deletion_requests/" + filename); + Timber.d("Nominated for deletion: " + deletionStatus); } catch (Exception e){ Timber.d(e.getMessage()); diff --git a/app/src/main/java/fr/free/nrw/commons/Utils.java b/app/src/main/java/fr/free/nrw/commons/Utils.java index 91c23ce26..9f89586c0 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -178,6 +178,7 @@ public class Utils { } public static void handleWebUrl(Context context, Uri url) { + Timber.d("Launching web url %s", url.toString()); Intent browserIntent = new Intent(Intent.ACTION_VIEW, url); if (browserIntent.resolveActivity(context.getPackageManager()) == null) { Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT); diff --git a/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java index 705de23da..bca548632 100644 --- a/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons; +import android.net.Uri; import android.support.annotation.Nullable; import android.support.v4.view.PagerAdapter; import android.view.LayoutInflater; @@ -9,6 +10,7 @@ import android.widget.TextView; import butterknife.ButterKnife; import butterknife.OnClick; +import butterknife.Optional; public class WelcomePagerAdapter extends PagerAdapter { static final int[] PAGE_LAYOUTS = new int[]{ @@ -20,6 +22,7 @@ public class WelcomePagerAdapter extends PagerAdapter { }; private static final int PAGE_FINAL = 4; private Callback callback; + private ViewGroup container; /** * Changes callback to provided one @@ -53,6 +56,7 @@ public class WelcomePagerAdapter extends PagerAdapter { @Override public Object instantiateItem(ViewGroup container, int position) { + this.container=container; LayoutInflater inflater = LayoutInflater.from(container.getContext()); ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false); if( BuildConfig.FLAVOR == "beta"){ @@ -102,5 +106,15 @@ public class WelcomePagerAdapter extends PagerAdapter { } } + @Optional + @OnClick(R.id.welcomeInfo) + void onHelpClicked () { + try { + Utils.handleWebUrl(container.getContext(),Uri.parse("https://commons.wikimedia.org/wiki/Help:Contents" )); + } catch (Exception e) { + e.printStackTrace(); + } + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index 256c7e3b3..27a6f6899 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -4,8 +4,8 @@ import android.accounts.Account; import android.accounts.AccountAuthenticatorActivity; import android.accounts.AccountAuthenticatorResponse; import android.accounts.AccountManager; -import android.app.Activity; import android.app.ProgressDialog; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; @@ -19,18 +19,16 @@ import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatDelegate; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; -import android.widget.Toast; import java.io.IOException; +import java.util.Locale; import javax.inject.Inject; import javax.inject.Named; @@ -48,6 +46,7 @@ import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.ui.widget.HtmlTextView; +import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; @@ -85,6 +84,10 @@ public class LoginActivity extends AccountAuthenticatorActivity { private LoginTextWatcher textWatcher = new LoginTextWatcher(); private Boolean loginCurrentlyInProgress = false; + private Boolean errorMessageShown = false; + private String resultantError; + private static final String RESULTANT_ERROR = "resultantError"; + private static final String ERROR_MESSAGE_SHOWN = "errorMessageShown"; private static final String LOGING_IN = "logingIn"; @Override @@ -106,14 +109,14 @@ public class LoginActivity extends AccountAuthenticatorActivity { usernameEdit.addTextChangedListener(textWatcher); usernameEdit.setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus) { - hideKeyboard(v); + ViewUtil.hideKeyboard(v); } }); passwordEdit.addTextChangedListener(textWatcher); passwordEdit.setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus) { - hideKeyboard(v); + ViewUtil.hideKeyboard(v); } }); @@ -125,13 +128,18 @@ public class LoginActivity extends AccountAuthenticatorActivity { forgotPasswordText.setOnClickListener(view -> forgotPassword()); - if(BuildConfig.FLAVOR == "beta"){ + if(BuildConfig.FLAVOR.equals("beta")){ loginCredentials.setText(getString(R.string.login_credential)); } else { loginCredentials.setVisibility(View.GONE); } } + public static void startYourself(Context context) { + Intent intent = new Intent(context, LoginActivity.class); + context.startActivity(intent); + } + private void forgotPassword() { Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)); } @@ -141,12 +149,6 @@ public class LoginActivity extends AccountAuthenticatorActivity { Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\\")); } - public void hideKeyboard(View view) { - InputMethodManager inputMethodManager =(InputMethodManager)this.getSystemService(Activity.INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); - } - - @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); @@ -160,7 +162,10 @@ public class LoginActivity extends AccountAuthenticatorActivity { WelcomeActivity.startYourself(this); prefs.edit().putBoolean("firstrun", false).apply(); } - if (sessionManager.getCurrentAccount() != null) { + + if (sessionManager.getCurrentAccount() != null + && sessionManager.isUserLoggedIn() + && sessionManager.getCachedAuthCookie() != null) { startMainActivity(); } } @@ -215,6 +220,8 @@ public class LoginActivity extends AccountAuthenticatorActivity { handlePassResult(username, password); } else { loginCurrentlyInProgress = false; + errorMessageShown = true; + resultantError = result; handleOtherResults(result); } } @@ -266,18 +273,18 @@ public class LoginActivity extends AccountAuthenticatorActivity { if (result.equals("NetworkFailure")) { // Matches NetworkFailure which is created by the doInBackground method showMessageAndCancelDialog(R.string.login_failed_network); - } else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { + } else if (result.toLowerCase(Locale.getDefault()).contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { // Matches nosuchuser, nosuchusershort, noname - showMessageAndCancelDialog(R.string.login_failed_username); + showMessageAndCancelDialog(R.string.login_failed_wrong_credentials); emptySensitiveEditFields(); - } else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) { + } else if (result.toLowerCase(Locale.getDefault()).contains("wrongpassword".toLowerCase())) { // Matches wrongpassword, wrongpasswordempty - showMessageAndCancelDialog(R.string.login_failed_password); + showMessageAndCancelDialog(R.string.login_failed_wrong_credentials); emptySensitiveEditFields(); - } else if (result.toLowerCase().contains("throttle".toLowerCase())) { + } else if (result.toLowerCase(Locale.getDefault()).contains("throttle".toLowerCase())) { // Matches unknown throttle error codes showMessageAndCancelDialog(R.string.login_failed_throttled); - } else if (result.toLowerCase().contains("userblocked".toLowerCase())) { + } else if (result.toLowerCase(Locale.getDefault()).contains("userblocked".toLowerCase())) { // Matches login-userblocked showMessageAndCancelDialog(R.string.login_failed_blocked); } else if (result.equals("2FA")) { @@ -341,15 +348,22 @@ public class LoginActivity extends AccountAuthenticatorActivity { protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(LOGING_IN, loginCurrentlyInProgress); + outState.putBoolean(ERROR_MESSAGE_SHOWN, errorMessageShown); + outState.putString(RESULTANT_ERROR, resultantError); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGING_IN, false); + errorMessageShown = savedInstanceState.getBoolean(ERROR_MESSAGE_SHOWN, false); if(loginCurrentlyInProgress){ performLogin(); } + if(errorMessageShown){ + resultantError = savedInstanceState.getString(RESULTANT_ERROR); + handleOtherResults(resultantError); + } } public void askUserForTwoFactorAuth() { @@ -361,7 +375,9 @@ public class LoginActivity extends AccountAuthenticatorActivity { public void showMessageAndCancelDialog(@StringRes int resId) { showMessage(resId, R.color.secondaryDarkColor); - progressDialog.cancel(); + if(progressDialog != null){ + progressDialog.cancel(); + } } public void showSuccessAndDismissDialog() { diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java index a7e62c34e..9ef6b7843 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java @@ -61,13 +61,11 @@ public class SessionManager { } public String getAuthCookie() { - boolean isLoggedIn = sharedPreferences.getBoolean("isUserLoggedIn", false); - - if (!isLoggedIn) { + if (!isUserLoggedIn()) { Timber.e("User is not logged in"); return null; } else { - String authCookie = sharedPreferences.getString("getAuthCookie", null); + String authCookie = getCachedAuthCookie(); if (authCookie == null) { Timber.e("Auth cookie is null even after login"); } @@ -75,6 +73,20 @@ public class SessionManager { } } + public String getCachedAuthCookie() { + return sharedPreferences.getString("getAuthCookie", null); + } + + public boolean isUserLoggedIn() { + return sharedPreferences.getBoolean("isUserLoggedIn", false); + } + + public void forceLogin(Context context) { + if (context != null) { + LoginActivity.startYourself(context); + } + } + public Completable clearAllAccounts() { AccountManager accountManager = AccountManager.get(context); Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE); diff --git a/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java b/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java index ff6ceece4..72de0db70 100644 --- a/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java +++ b/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java @@ -7,18 +7,25 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import fr.free.nrw.commons.upload.MwVolleyApi; +import javax.inject.Inject; +import javax.inject.Singleton; + +import fr.free.nrw.commons.upload.GpsCategoryModel; import timber.log.Timber; +@Singleton public class CacheController { + private final GpsCategoryModel gpsCategoryModel; + private final QuadTree> quadTree; private double x, y; - private QuadTree> quadTree; private double xMinus, xPlus, yMinus, yPlus; private static final int EARTH_RADIUS = 6378137; - public CacheController() { + @Inject + CacheController(GpsCategoryModel gpsCategoryModel) { + this.gpsCategoryModel = gpsCategoryModel; quadTree = new QuadTree<>(-180, -90, +180, +90); } @@ -31,8 +38,8 @@ public class CacheController { public void cacheCategory() { List pointCatList = new ArrayList<>(); - if (MwVolleyApi.GpsCatExists.getGpsCatExists()) { - pointCatList.addAll(MwVolleyApi.getGpsCat()); + if (gpsCategoryModel.getGpsCatExists()) { + pointCatList.addAll(gpsCategoryModel.getCategoryList()); Timber.d("Categories being cached: %s", pointCatList); } else { Timber.d("No categories found, so no categories cached"); @@ -65,7 +72,7 @@ public class CacheController { } //Based on algorithm at http://gis.stackexchange.com/questions/2951/algorithm-for-offsetting-a-latitude-longitude-by-some-amount-of-meters - public void convertCoordRange() { + private void convertCoordRange() { //Position, decimal degrees double lat = y; double lon = x; diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java index a41a52139..93ddb60d5 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java @@ -1,7 +1,6 @@ package fr.free.nrw.commons.category; -import android.app.Activity; import android.content.SharedPreferences; import android.os.Bundle; import android.support.v7.app.AlertDialog; @@ -10,14 +9,12 @@ import android.support.v7.widget.RecyclerView; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; -import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; @@ -42,9 +39,9 @@ import butterknife.ButterKnife; import fr.free.nrw.commons.R; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.mwapi.MediaWikiApi; -import fr.free.nrw.commons.upload.MwVolleyApi; -import fr.free.nrw.commons.upload.SingleUploadFragment; +import fr.free.nrw.commons.upload.GpsCategoryModel; import fr.free.nrw.commons.utils.StringSortingUtils; +import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; @@ -76,6 +73,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment { @Inject @Named("prefs") SharedPreferences prefsPrefs; @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; @Inject CategoryDao categoryDao; + @Inject GpsCategoryModel gpsCategoryModel; private RVRendererAdapter categoriesAdapter; private OnCategoriesSaveHandler onCategoriesSaveHandler; @@ -118,7 +116,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment { categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus) { - hideKeyboard(v); + ViewUtil.hideKeyboard(v); } }); @@ -130,11 +128,6 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment { return rootView; } - public void hideKeyboard(View view) { - InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); - } - @Override public void onDestroyView() { categoriesFilter.removeTextChangedListener(textWatcher); @@ -261,7 +254,6 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment { } private Observable defaultCategories() { - Observable directCat = directCategories(); if (hasDirectCategories) { Timber.d("Image has direct Cat"); @@ -295,9 +287,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment { } private Observable gpsCategories() { - return Observable.fromIterable( - MwVolleyApi.GpsCatExists.getGpsCatExists() - ? MwVolleyApi.getGpsCat() : new ArrayList<>()) + return Observable.fromIterable(gpsCategoryModel.getCategoryList()) .map(name -> new CategoryItem(name, false)); } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java index a5202046b..010e97095 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java @@ -105,6 +105,7 @@ public class CategoryDao { return items; } + @NonNull Category fromCursor(Cursor cursor) { // Hardcoding column positions! return new Category( diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java new file mode 100644 index 000000000..3495d710c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java @@ -0,0 +1,29 @@ +package fr.free.nrw.commons.category; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.mwapi.MediaWikiApi; + +@Singleton +public class CategoryImageController { + + private MediaWikiApi mediaWikiApi; + + @Inject + public CategoryImageController(MediaWikiApi mediaWikiApi) { + this.mediaWikiApi = mediaWikiApi; + } + + /** + * Takes a category name as input and calls the API to get a list of images for that category + * @param categoryName + * @return + */ + public List getCategoryImages(String categoryName) { + return mediaWikiApi.getCategoryImages(categoryName); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java new file mode 100644 index 000000000..18749847e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java @@ -0,0 +1,225 @@ +package fr.free.nrw.commons.category; + +import org.jsoup.Jsoup; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.annotation.Nullable; + +import fr.free.nrw.commons.Media; +import timber.log.Timber; + +public class CategoryImageUtils { + + /** + * The method iterates over the child nodes to return a list of Media objects + * @param childNodes + * @return + */ + public static List getMediaList(NodeList childNodes) { + List categoryImages = new ArrayList<>(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node node = childNodes.item(i); + categoryImages.add(getMediaFromPage(node)); + } + + return categoryImages; + } + + /** + * Creates a new Media object from the XML response as received by the API + * @param node + * @return + */ + private static Media getMediaFromPage(Node node) { + Media media = new Media(null, + getImageUrl(node), + getFileName(node), + getDescription(node), + getDataLength(node), + getDateCreated(node), + getDateCreated(node), + getCreator(node) + ); + + media.setLicense(getLicense(node)); + + return media; + } + + /** + * Extracts the filename of the uploaded image + * @param document + * @return + */ + private static String getFileName(Node document) { + Element element = (Element) document; + return element.getAttribute("title"); + } + + /** + * Extracts the image description for that particular upload + * @param document + * @return + */ + private static String getDescription(Node document) { + return getMetaDataValue(document, "ImageDescription"); + } + + /** + * Extracts license information from the image meta data + * @param document + * @return + */ + private static String getLicense(Node document) { + return getMetaDataValue(document, "License"); + } + + /** + * Returns the parsed value of artist from the response + * The artist information is returned as a HTML string from the API. Jsoup library parses the HTML string + * to extract just the text value + * @param document + * @return + */ + private static String getCreator(Node document) { + String artist = getMetaDataValue(document, "Artist"); + if (artist != null) { + return Jsoup.parse(artist).text(); + } + return null; + } + + /** + * Returns the parsed date of creation of the image + * @param document + * @return + */ + private static Date getDateCreated(Node document) { + String dateTime = getMetaDataValue(document, "DateTime"); + if (dateTime != null && !dateTime.equals("")) { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + try { + return format.parse(dateTime); + } catch (ParseException e) { + Timber.d("Error occurred while parsing date %s", dateTime); + return new Date(); + } + } + return new Date(); + } + + /** + * @param document + * @return Returns the url attribute from the imageInfo node + */ + private static String getImageUrl(Node document) { + Element element = (Element) getImageInfo(document); + if (element != null) { + return element.getAttribute("url"); + } + return null; + } + + /** + * Takes the node document and gives out the attribute length from the node document + * @param document + * @return + */ + private static long getDataLength(Node document) { + Element element = (Element) document; + if (element != null) { + String length = element.getAttribute("length"); + if (length != null && !length.equals("")) { + return Long.parseLong(length); + } + } + return 0L; + } + + /** + * Generic method to get the value of any meta as returned by the getMetaData function + * @param document node document as returned by API + * @param metaName the name of meta node to be returned + * @return + */ + private static String getMetaDataValue(Node document, String metaName) { + Element metaData = getMetaData(document, metaName); + if (metaData != null) { + return metaData.getAttribute("value"); + } + return null; + } + + /** + * Generic method to return an element taking the node document and metaName as input + * @param document node document as returned by API + * @param metaName the name of meta node to be returned + * @return + */ + @Nullable + private static Element getMetaData(Node document, String metaName) { + Node extraMetaData = getExtraMetaData(document); + if (extraMetaData != null) { + Node node = getNode(extraMetaData, metaName); + if (node != null) { + return (Element) node; + } + } + return null; + } + + /** + * Extracts extmetadata from the response XML + * @param document + * @return + */ + @Nullable + private static Node getExtraMetaData(Node document) { + Node imageInfo = getImageInfo(document); + if (imageInfo != null) { + return getNode(imageInfo, "extmetadata"); + } + return null; + } + + /** + * Extracts the ii node from the imageinfo node + * @param document + * @return + */ + @Nullable + private static Node getImageInfo(Node document) { + Node imageInfo = getNode(document, "imageinfo"); + if (imageInfo != null) { + return getNode(imageInfo, "ii"); + } + return null; + } + + /** + * Takes a parent node as input and returns a child node if present + * @param node parent node + * @param nodeName child node name + * @return + */ + @Nullable + public static Node getNode(Node node, String nodeName) { + NodeList childNodes = node.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node nodeItem = childNodes.item(i); + Element item = (Element) nodeItem; + if (item.getTagName().equals(nodeName)) { + return nodeItem; + } + } + return null; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java new file mode 100644 index 000000000..17601151c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java @@ -0,0 +1,159 @@ +package fr.free.nrw.commons.category; + +import android.content.Context; +import android.content.Intent; +import android.database.DataSetObserver; +import android.os.Bundle; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.view.View; +import android.widget.AdapterView; + +import butterknife.ButterKnife; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.auth.AuthenticatedActivity; +import fr.free.nrw.commons.media.MediaDetailPagerFragment; + +/** + * This activity displays pictures of a particular category + * Its generic and simply takes the name of category name in its start intent to load all images in + * a particular category. This activity is currently being used to display a list of featured images, + * which is nothing but another category on wikimedia commons. + */ + +public class CategoryImagesActivity + extends AuthenticatedActivity + implements FragmentManager.OnBackStackChangedListener, + MediaDetailPagerFragment.MediaDetailProvider, + AdapterView.OnItemClickListener{ + + + private FragmentManager supportFragmentManager; + private CategoryImagesListFragment categoryImagesListFragment; + private MediaDetailPagerFragment mediaDetails; + + @Override + protected void onAuthCookieAcquired(String authCookie) { + + } + + @Override + protected void onAuthFailure() { + + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_category_images); + ButterKnife.bind(this); + + // Activity can call methods in the fragment by acquiring a + // reference to the Fragment from FragmentManager, using findFragmentById() + supportFragmentManager = getSupportFragmentManager(); + setCategoryImagesFragment(); + supportFragmentManager.addOnBackStackChangedListener(this); + if (savedInstanceState != null) { + mediaDetails = (MediaDetailPagerFragment) supportFragmentManager + .findFragmentById(R.id.fragmentContainer); + + } + requestAuthToken(); + initDrawer(); + setPageTitle(); + } + + /** + * Gets the categoryName from the intent and initializes the fragment for showing images of that category + */ + private void setCategoryImagesFragment() { + categoryImagesListFragment = new CategoryImagesListFragment(); + String categoryName = getIntent().getStringExtra("categoryName"); + if (getIntent() != null && categoryName != null) { + Bundle arguments = new Bundle(); + arguments.putString("categoryName", categoryName); + categoryImagesListFragment.setArguments(arguments); + FragmentTransaction transaction = supportFragmentManager.beginTransaction(); + transaction + .add(R.id.fragmentContainer, categoryImagesListFragment) + .commit(); + } + } + + /** + * Gets the passed title from the intents and displays it as the page title + */ + private void setPageTitle() { + if (getIntent() != null && getIntent().getStringExtra("title") != null) { + setTitle(getIntent().getStringExtra("title")); + } + } + + @Override + public void onBackStackChanged() { + } + + @Override + public void onItemClick(AdapterView adapterView, View view, int i, long l) { + if (mediaDetails == null || !mediaDetails.isVisible()) { + // set isFeaturedImage true for featured images, to include author field on media detail + mediaDetails = new MediaDetailPagerFragment(false, true); + FragmentManager supportFragmentManager = getSupportFragmentManager(); + supportFragmentManager + .beginTransaction() + .replace(R.id.fragmentContainer, mediaDetails) + .addToBackStack(null) + .commit(); + supportFragmentManager.executePendingTransactions(); + } + mediaDetails.showImage(i); + } + + /** + * Consumers should be simply using this method to use this activity. + * @param context + * @param title Page title + * @param categoryName Name of the category for displaying its images + */ + public static void startYourself(Context context, String title, String categoryName) { + Intent intent = new Intent(context, CategoryImagesActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + intent.putExtra("title", title); + intent.putExtra("categoryName", categoryName); + context.startActivity(intent); + } + + @Override + public Media getMediaAtPosition(int i) { + if (categoryImagesListFragment.getAdapter() == null) { + // not yet ready to return data + return null; + } else { + return (Media) categoryImagesListFragment.getAdapter().getItem(i); + } + } + + @Override + public int getTotalMediaCount() { + if (categoryImagesListFragment.getAdapter() == null) { + return 0; + } + return categoryImagesListFragment.getAdapter().getCount(); + } + + @Override + public void notifyDatasetChanged() { + + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java new file mode 100644 index 000000000..a44e19a29 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java @@ -0,0 +1,237 @@ +package fr.free.nrw.commons.category; + +import android.annotation.SuppressLint; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.GridView; +import android.widget.ListAdapter; +import android.widget.ProgressBar; +import android.widget.TextView; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Named; + +import butterknife.BindView; +import butterknife.ButterKnife; +import dagger.android.support.DaggerFragment; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.utils.NetworkUtils; +import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +/** + * Displays images for a particular category with load more on scrolling incorporated + */ +public class CategoryImagesListFragment extends DaggerFragment { + + private static int TIMEOUT_SECONDS = 15; + + private GridViewAdapter gridAdapter; + + @BindView(R.id.statusMessage) + TextView statusTextView; + @BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar; + @BindView(R.id.categoryImagesList) GridView gridView; + + private boolean hasMoreImages = true; + private boolean isLoading; + private String categoryName = null; + + @Inject CategoryImageController controller; + @Inject @Named("category_prefs") SharedPreferences categoryPreferences; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_category_images, container, false); + ButterKnife.bind(this, v); + return v; + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity()); + initViews(); + } + + /** + * Initializes the UI elements for the fragment + * Setup the grid view to and scroll listener for it + */ + private void initViews() { + String categoryName = getArguments().getString("categoryName"); + if (getArguments() != null && categoryName != null) { + this.categoryName = categoryName; + resetQueryContinueValues(categoryName); + initList(); + setScrollListener(); + } + } + + /** + * Query continue values determine the last page that was loaded for the particular keyword + * This method resets those values, so that the results can be queried from the first page itself + * @param keyword + */ + private void resetQueryContinueValues(String keyword) { + SharedPreferences.Editor editor = categoryPreferences.edit(); + editor.remove(keyword); + editor.apply(); + } + + /** + * Checks for internet connection and then initializes the grid view with first 10 images of that category + */ + @SuppressLint("CheckResult") + private void initList() { + if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { + handleNoInternet(); + return; + } + + isLoading = true; + progressBar.setVisibility(VISIBLE); + Observable.fromCallable(() -> controller.getCategoryImages(categoryName)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .subscribe(this::handleSuccess, this::handleError); + } + + /** + * Handles the UI updates for no internet scenario + */ + private void handleNoInternet() { + progressBar.setVisibility(GONE); + if (gridAdapter == null || gridAdapter.isEmpty()) { + statusTextView.setVisibility(VISIBLE); + statusTextView.setText(getString(R.string.no_internet)); + } else { + ViewUtil.showSnackbar(gridView, R.string.no_internet); + } + } + + /** + * Logs and handles API error scenario + * @param throwable + */ + private void handleError(Throwable throwable) { + Timber.e(throwable, "Error occurred while loading featured images"); + initErrorView(); + } + + /** + * Handles the UI updates for a error scenario + */ + private void initErrorView() { + ViewUtil.showSnackbar(gridView, R.string.error_loading_images); + progressBar.setVisibility(GONE); + if (gridAdapter == null || gridAdapter.isEmpty()) { + statusTextView.setVisibility(VISIBLE); + statusTextView.setText(getString(R.string.no_images_found)); + } else { + statusTextView.setVisibility(GONE); + } + } + + /** + * Initializes the adapter with a list of Media objects + * @param mediaList + */ + private void setAdapter(List mediaList) { + gridAdapter = new GridViewAdapter(this.getContext(), R.layout.layout_category_images, mediaList); + gridView.setAdapter(gridAdapter); + } + + /** + * Sets the scroll listener for the grid view so that more images are fetched when the user scrolls down + * Checks if the category has more images before loading + * Also checks whether images are currently being fetched before triggering another request + */ + private void setScrollListener() { + gridView.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + if (hasMoreImages && !isLoading && (firstVisibleItem + visibleItemCount + 1 >= totalItemCount)) { + isLoading = true; + fetchMoreImages(); + } + } + }); + } + + /** + * Fetches more images for the category and adds it to the grid view adapter + */ + @SuppressLint("CheckResult") + private void fetchMoreImages() { + if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { + handleNoInternet(); + return; + } + + progressBar.setVisibility(VISIBLE); + Observable.fromCallable(() -> controller.getCategoryImages(categoryName)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .subscribe(this::handleSuccess, this::handleError); + } + + /** + * Handles the success scenario + * On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter + * @param collection + */ + private void handleSuccess(List collection) { + if(collection == null || collection.isEmpty()) { + initErrorView(); + hasMoreImages = false; + return; + } + + if(gridAdapter == null) { + setAdapter(collection); + } else { + gridAdapter.addItems(collection); + } + + progressBar.setVisibility(GONE); + isLoading = false; + statusTextView.setVisibility(GONE); + } + + public ListAdapter getAdapter() { + return gridView.getAdapter(); + } + + /** + * This method will be called on back pressed of CategoryImagesActivity. + * It initializes the grid view by setting adapter. + */ + @Override + public void onResume() { + gridView.setAdapter(gridAdapter); + super.onResume(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java new file mode 100644 index 000000000..c8e6066f6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java @@ -0,0 +1,88 @@ +package fr.free.nrw.commons.category; + +import android.app.Activity; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.MediaWikiImageView; +import fr.free.nrw.commons.R; + +/** + * This is created to only display UI implementation. Needs to be changed in real implementation + */ + +public class GridViewAdapter extends ArrayAdapter { + private Context context; + private List data; + + public GridViewAdapter(Context context, int layoutResourceId, List data) { + super(context, layoutResourceId, data); + this.context = context; + this.data = data; + } + + /** + * Adds more item to the list + * Its triggered on scrolling down in the list + * @param images + */ + public void addItems(List images) { + if (data == null) { + data = new ArrayList<>(); + } + data.addAll(images); + notifyDataSetChanged(); + } + + @Override + public boolean isEmpty() { + return data == null || data.isEmpty(); + } + + /** + * Sets up the UI for the category image item + * @param position + * @param convertView + * @param parent + * @return + */ + @Override + public View getView(int position, View convertView, ViewGroup parent) { + + if (convertView == null) { + LayoutInflater inflater = ((Activity) context).getLayoutInflater(); + convertView = inflater.inflate(R.layout.layout_category_images, null); + } + + Media item = data.get(position); + MediaWikiImageView imageView = convertView.findViewById(R.id.categoryImageView); + TextView fileName = convertView.findViewById(R.id.categoryImageTitle); + TextView author = convertView.findViewById(R.id.categoryImageAuthor); + fileName.setText(item.getFilename()); + setAuthorView(item, author); + imageView.setMedia(item); + return convertView; + } + + /** + * Shows author information if its present + * @param item + * @param author + */ + private void setAuthorView(Media item, TextView author) { + if (item.getCreator() != null && !item.getCreator().equals("")) { + String uploadedByTemplate = context.getString(R.string.image_uploaded_by); + author.setText(String.format(uploadedByTemplate, item.getCreator())); + } else { + author.setVisibility(View.GONE); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/category/QueryContinue.java b/app/src/main/java/fr/free/nrw/commons/category/QueryContinue.java new file mode 100644 index 000000000..e12d5a778 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/QueryContinue.java @@ -0,0 +1,24 @@ +package fr.free.nrw.commons.category; + +/** + * For APIs that return paginated responses, MediaWiki APIs uses the QueryContinue to facilitate fetching of subsequent pages + * https://www.mediawiki.org/wiki/API:Raw_query_continue + */ +public class QueryContinue { + private String continueParam; + private String gcmContinueParam; + + public QueryContinue(String continueParam, String gcmContinueParam) { + this.continueParam = continueParam; + this.gcmContinueParam = gcmContinueParam; + } + + public String getGcmContinueParam() { + return gcmContinueParam; + } + + public String getContinueParam() { + return continueParam; + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java index 7861f96de..99009c029 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java @@ -45,6 +45,7 @@ public class Contribution extends Media { private long transferred; private String decimalCoords; private boolean isMultiple; + private String wikiDataEntityId; public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp, int state, long dataLength, Date dateUploaded, long transferred, @@ -222,4 +223,17 @@ public class Contribution extends Media { throw new RuntimeException("Unrecognized license value: " + license); } + + public String getWikiDataEntityId() { + return wikiDataEntityId; + } + + /** + * When the corresponding wikidata entity is known as in case of nearby uploads, it can be set + * using the setter method + * @param wikiDataEntityId + */ + public void setWikiDataEntityId(String wikiDataEntityId) { + this.wikiDataEntityId = wikiDataEntityId; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index 37b3d5377..ed6001f94 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -90,7 +90,7 @@ public class ContributionController { fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY); } - public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload) { + public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload, String wikiDataEntityId) { FragmentActivity activity = fragment.getActivity(); Timber.d("handleImagePicked() called with onActivityResult()"); Intent shareIntent = new Intent(activity, ShareActivity.class); @@ -102,9 +102,6 @@ public class ContributionController { shareIntent.setType(activity.getContentResolver().getType(imageData)); shareIntent.putExtra(EXTRA_STREAM, imageData); shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY); - if (isDirectUpload) { - shareIntent.putExtra("isDirectUpload", true); - } break; case SELECT_FROM_CAMERA: //FIXME: Find out appropriate mime type @@ -113,9 +110,6 @@ public class ContributionController { shareIntent.setType("image/jpeg"); shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri); shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA); - if (isDirectUpload) { - shareIntent.putExtra("isDirectUpload", true); - } break; default: @@ -123,6 +117,10 @@ public class ContributionController { } Timber.i("Image selected"); try { + shareIntent.putExtra("isDirectUpload", isDirectUpload); + if (wikiDataEntityId != null && !wikiDataEntityId.equals("")) { + shareIntent.putExtra("wikiDataEntityId", wikiDataEntityId); + } activity.startActivity(shareIntent); } catch (SecurityException e) { Timber.e(e, "Security Exception"); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java index 079cf6477..6d290b1a5 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java @@ -8,7 +8,6 @@ import android.net.Uri; import android.os.RemoteException; import android.support.annotation.Nullable; import android.text.TextUtils; -import android.util.Log; import java.util.Date; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index ff400a8dd..0b600c5d0 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -117,7 +117,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { if (resultCode == RESULT_OK) { Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); - controller.handleImagePicked(requestCode, data, false); + controller.handleImagePicked(requestCode, data, false, null); } else { Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java index 0cce496f0..37b9a7a82 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java @@ -83,7 +83,7 @@ public class DeleteTask extends AsyncTask { String logPageString = "\n{{Commons:Deletion requests/" + media.getFilename() + "}}\n"; - SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()); String date = sdf.format(calendar.getTime()); String userPageString = "\n{{subst:idw|" + media.getFilename() + diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java index e4fb13427..51aa85903 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -7,6 +7,7 @@ import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.SignupActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.nearby.NearbyActivity; import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.settings.SettingsActivity; @@ -46,4 +47,7 @@ public abstract class ActivityBuilderModule { @ContributesAndroidInjector abstract NotificationActivity bindNotificationActivity(); + + @ContributesAndroidInjector + abstract CategoryImagesActivity bindFeaturedImagesActivity(); } diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java index 91f6d4ccb..43721a217 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -9,17 +9,18 @@ import dagger.android.support.AndroidSupportInjectionModule; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.MediaWikiImageView; import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsSyncAdapter; import fr.free.nrw.commons.delete.DeleteTask; import fr.free.nrw.commons.modifications.ModificationsSyncAdapter; -import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.nearby.PlaceRenderer; +import fr.free.nrw.commons.upload.FileProcessor; +import fr.free.nrw.commons.settings.SettingsFragment; + @Singleton @Component(modules = { CommonsApplicationModule.class, + NetworkingModule.class, AndroidInjectionModule.class, AndroidSupportInjectionModule.class, ActivityBuilderModule.class, @@ -47,6 +48,8 @@ public interface CommonsApplicationComponent extends AndroidInjector provideLruCache() { return new LruCache<>(1024); } + + @Provides + @Singleton + public WikidataEditListener provideWikidataEditListener() { + return new WikidataEditListenerImpl(); + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java index a94f46ca9..dfed64871 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -4,6 +4,7 @@ import dagger.Module; import dagger.android.ContributesAndroidInjector; import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.contributions.ContributionsListFragment; +import fr.free.nrw.commons.category.CategoryImagesListFragment; import fr.free.nrw.commons.media.MediaDetailFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.nearby.NearbyListFragment; @@ -47,4 +48,7 @@ public abstract class FragmentBuilderModule { @ContributesAndroidInjector abstract SingleUploadFragment bindSingleUploadFragment(); + @ContributesAndroidInjector + abstract CategoryImagesListFragment bindFeaturedImagesListFragment(); + } diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java new file mode 100644 index 000000000..cd043e950 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java @@ -0,0 +1,59 @@ +package fr.free.nrw.commons.di; + +import android.content.Context; +import android.content.SharedPreferences; +import android.support.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import javax.inject.Named; +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; + +@Module +@SuppressWarnings({"WeakerAccess", "unused"}) +public class NetworkingModule { + public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024; + + @Provides + @Singleton + public OkHttpClient provideOkHttpClient() { + return new OkHttpClient.Builder().build(); + } + + @Provides + @Singleton + public MediaWikiApi provideMediaWikiApi(Context context, + @Named("default_preferences") SharedPreferences defaultPreferences, + @Named("category_prefs") SharedPreferences categoryPrefs, + Gson gson) { + return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultPreferences, categoryPrefs, gson); + } + + @Provides + @Named("commons_mediawiki_url") + @NonNull + @SuppressWarnings("ConstantConditions") + public HttpUrl provideMwUrl() { + return HttpUrl.parse(BuildConfig.COMMONS_URL); + } + + /** + * Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere. + * @return returns a singleton Gson instance + */ + @Provides + @Singleton + public Gson provideGson() { + return new GsonBuilder().create(); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/glide/SvgDecoder.java b/app/src/main/java/fr/free/nrw/commons/glide/SvgDecoder.java new file mode 100644 index 000000000..9087f9501 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/glide/SvgDecoder.java @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.glide; + +import android.support.annotation.NonNull; + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.engine.Resource; +import com.bumptech.glide.load.resource.SimpleResource; +import com.caverock.androidsvg.SVG; +import com.caverock.androidsvg.SVGParseException; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Decodes an SVG internal representation from an {@link InputStream}. + */ +public class SvgDecoder implements ResourceDecoder { + + @Override + public boolean handles(@NonNull InputStream source, @NonNull Options options) { + // TODO: Can we tell? + return true; + } + + public Resource decode(@NonNull InputStream source, int width, int height, + @NonNull Options options) + throws IOException { + try { + SVG svg = SVG.getFromInputStream(source); + return new SimpleResource<>(svg); + } catch (SVGParseException ex) { + throw new IOException("Cannot load SVG from stream", ex); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/glide/SvgDrawableTranscoder.java b/app/src/main/java/fr/free/nrw/commons/glide/SvgDrawableTranscoder.java new file mode 100644 index 000000000..89910c8fb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/glide/SvgDrawableTranscoder.java @@ -0,0 +1,28 @@ +package fr.free.nrw.commons.glide; + +import android.graphics.Picture; +import android.graphics.drawable.PictureDrawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.engine.Resource; +import com.bumptech.glide.load.resource.SimpleResource; +import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.caverock.androidsvg.SVG; + +/** + * Convert the {@link SVG}'s internal representation to an Android-compatible one + * ({@link Picture}). + */ +public class SvgDrawableTranscoder implements ResourceTranscoder { + @Nullable + @Override + public Resource transcode(@NonNull Resource toTranscode, + @NonNull Options options) { + SVG svg = toTranscode.get(); + Picture picture = svg.renderToPicture(); + PictureDrawable drawable = new PictureDrawable(picture); + return new SimpleResource<>(drawable); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/glide/SvgSoftwareLayerSetter.java b/app/src/main/java/fr/free/nrw/commons/glide/SvgSoftwareLayerSetter.java new file mode 100644 index 000000000..66a3bd6bf --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/glide/SvgSoftwareLayerSetter.java @@ -0,0 +1,51 @@ +package fr.free.nrw.commons.glide; + +import android.graphics.drawable.PictureDrawable; +import android.widget.ImageView; + +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.ImageViewTarget; +import com.bumptech.glide.request.target.Target; + +/** + * Listener which updates the {@link ImageView} to be software rendered, because + * {@link com.caverock.androidsvg.SVG SVG}/{@link android.graphics.Picture Picture} can't render on + * a hardware backed {@link android.graphics.Canvas Canvas}. + */ +public class SvgSoftwareLayerSetter implements RequestListener { + + /** + * Sets the layer type to none if the load fails + * @param e + * @param model + * @param target + * @param isFirstResource + * @return + */ + @Override + public boolean onLoadFailed(GlideException e, Object model, Target target, + boolean isFirstResource) { + ImageView view = ((ImageViewTarget) target).getView(); + view.setLayerType(ImageView.LAYER_TYPE_NONE, null); + return false; + } + + /** + * Sets the layer type to software when the resource is ready + * @param resource + * @param model + * @param target + * @param dataSource + * @param isFirstResource + * @return + */ + @Override + public boolean onResourceReady(PictureDrawable resource, Object model, + Target target, DataSource dataSource, boolean isFirstResource) { + ImageView view = ((ImageViewTarget) target).getView(); + view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null); + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java index f9a171461..cd1082ba5 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.location; import android.Manifest; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; @@ -10,9 +11,10 @@ import android.location.LocationManager; import android.os.Bundle; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; -import android.util.Log; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import timber.log.Timber; @@ -29,6 +31,7 @@ public class LocationServiceManager implements LocationListener { private Location lastLocation; private final List locationListeners = new CopyOnWriteArrayList<>(); private boolean isLocationManagerRegistered = false; + private Set locationExplanationDisplayed = new HashSet<>(); /** * Constructs a new instance of LocationServiceManager. @@ -51,7 +54,6 @@ public class LocationServiceManager implements LocationListener { /** * Returns whether the location permission is granted. - * * @return true if the location permission is granted */ public boolean isLocationPermissionGranted() { @@ -73,10 +75,41 @@ public class LocationServiceManager implements LocationListener { LOCATION_REQUEST); } + /** + * The permission explanation dialog box is now displayed just once for a particular activity. We are subscribing + * to updates from multiple providers so its important to show the dialog just once. Otherwise it will be displayed + * once for every provider, which in our case currently is 2. + * @param activity + * @return + */ public boolean isPermissionExplanationRequired(Activity activity) { - return !activity.isFinishing() && - ActivityCompat.shouldShowRequestPermissionRationale(activity, - Manifest.permission.ACCESS_FINE_LOCATION); + if (activity.isFinishing()) { + return false; + } + boolean showRequestPermissionRationale = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_FINE_LOCATION); + if (showRequestPermissionRationale && !locationExplanationDisplayed.contains(activity)) { + locationExplanationDisplayed.add(activity); + return true; + } + return false; + } + + /** + * Gets the last known location in cases where there wasn't time to register a listener + * (e.g. when Location permission just granted) + * @return last known LatLng + */ + @SuppressLint("MissingPermission") + public LatLng getLKL() { + if (isLocationPermissionGranted()) { + Location lastKL = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (lastKL == null) { + lastKL = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); + } + return LatLng.from(lastKL); + } else { + return null; + } } public LatLng getLastLocation() { @@ -90,9 +123,10 @@ public class LocationServiceManager implements LocationListener { * Registers a LocationManager to listen for current location. */ public void registerLocationManager() { - if (!isLocationManagerRegistered) + if (!isLocationManagerRegistered) { isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) && requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); + } } /** @@ -125,7 +159,7 @@ public class LocationServiceManager implements LocationListener { * @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly * LOCATION_SLIGHTLY_CHANGED if location changed slightly */ - protected LocationChangeType isBetterLocation(Location location, Location currentBestLocation) { + private LocationChangeType isBetterLocation(Location location, Location currentBestLocation) { if (currentBestLocation == null) { // A new location is always better than no location @@ -249,6 +283,8 @@ public class LocationServiceManager implements LocationListener { public enum LocationChangeType{ LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving - LOCATION_NOT_CHANGED + LOCATION_NOT_CHANGED, + PERMISSION_JUST_GRANTED, + MAP_UPDATED } } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index b06869e8d..d5b1cc0ce 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -9,6 +9,7 @@ import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; import android.util.TypedValue; import android.view.LayoutInflater; @@ -22,6 +23,9 @@ import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -45,19 +49,23 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.ui.widget.CompatTextView; import timber.log.Timber; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; import static android.widget.Toast.LENGTH_SHORT; public class MediaDetailFragment extends CommonsDaggerSupportFragment { private boolean editable; + private boolean isCategoryImage; private MediaDetailPagerFragment.MediaDetailProvider detailProvider; private int index; - public static MediaDetailFragment forMedia(int index, boolean editable) { + public static MediaDetailFragment forMedia(int index, boolean editable, boolean isCategoryImage) { MediaDetailFragment mf = new MediaDetailFragment(); Bundle state = new Bundle(); state.putBoolean("editable", editable); + state.putBoolean("isCategoryImage", isCategoryImage); state.putInt("index", index); state.putInt("listIndex", 0); state.putInt("listTop", 0); @@ -72,21 +80,37 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { @Inject MediaWikiApi mwApi; - - private MediaWikiImageView image; - private MediaDetailSpacer spacer; private int initialListTop = 0; - private TextView title; - private TextView desc; - private TextView license; - private TextView coordinates; - private TextView uploadedDate; - private TextView seeMore; - private LinearLayout nominatedforDeletion; - private LinearLayout categoryContainer; - private Button delete; - private ScrollView scrollView; + @BindView(R.id.mediaDetailImage) + MediaWikiImageView image; + @BindView(R.id.mediaDetailSpacer) + MediaDetailSpacer spacer; + @BindView(R.id.mediaDetailTitle) + TextView title; + @BindView(R.id.mediaDetailDesc) + TextView desc; + @BindView(R.id.mediaDetailAuthor) + TextView author; + @BindView(R.id.mediaDetailLicense) + TextView license; + @BindView(R.id.mediaDetailCoordinates) + TextView coordinates; + @BindView(R.id.mediaDetailuploadeddate) + TextView uploadedDate; + @BindView(R.id.seeMore) + TextView seeMore; + @BindView(R.id.nominatedDeletionBanner) + LinearLayout nominatedForDeletion; + @BindView(R.id.mediaDetailCategoryContainer) + LinearLayout categoryContainer; + @BindView(R.id.authorLinearLayout) + LinearLayout authorLayout; + @BindView(R.id.nominateDeletion) + Button delete; + @BindView(R.id.mediaDetailScrollView) + ScrollView scrollView; + private ArrayList categoryNames; private boolean categoriesLoaded = false; private boolean categoriesPresent = false; @@ -96,11 +120,15 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { private AsyncTask detailFetchTask; private LicenseList licenseList; + //Had to make this class variable, to implement various onClicks, which access the media, also I fell why make separate variables when one can serve the purpose + private Media media; + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt("index", index); outState.putBoolean("editable", editable); + outState.putBoolean("isCategoryImage", isCategoryImage); getScrollPosition(); outState.putInt("listTop", initialListTop); @@ -116,32 +144,28 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { if (savedInstanceState != null) { editable = savedInstanceState.getBoolean("editable"); + isCategoryImage = savedInstanceState.getBoolean("isCategoryImage"); index = savedInstanceState.getInt("index"); initialListTop = savedInstanceState.getInt("listTop"); } else { editable = getArguments().getBoolean("editable"); + isCategoryImage = getArguments().getBoolean("isCategoryImage"); index = getArguments().getInt("index"); initialListTop = 0; } + categoryNames = new ArrayList<>(); categoryNames.add(getString(R.string.detail_panel_cats_loading)); final View view = inflater.inflate(R.layout.fragment_media_detail, container, false); - image = (MediaWikiImageView) view.findViewById(R.id.mediaDetailImage); - scrollView = (ScrollView) view.findViewById(R.id.mediaDetailScrollView); + ButterKnife.bind(this,view); - // Detail consists of a list view with main pane in header view, plus category list. - spacer = (MediaDetailSpacer) view.findViewById(R.id.mediaDetailSpacer); - title = (TextView) view.findViewById(R.id.mediaDetailTitle); - desc = (TextView) view.findViewById(R.id.mediaDetailDesc); - license = (TextView) view.findViewById(R.id.mediaDetailLicense); - coordinates = (TextView) view.findViewById(R.id.mediaDetailCoordinates); - uploadedDate = (TextView) view.findViewById(R.id.mediaDetailuploadeddate); - seeMore = (TextView) view.findViewById(R.id.seeMore); - nominatedforDeletion = (LinearLayout) view.findViewById(R.id.nominatedDeletionBanner); - delete = (Button) view.findViewById(R.id.nominateDeletion); - categoryContainer = (LinearLayout) view.findViewById(R.id.mediaDetailCategoryContainer); + if (isCategoryImage){ + authorLayout.setVisibility(VISIBLE); + } else { + authorLayout.setVisibility(GONE); + } licenseList = new LicenseList(getActivity()); @@ -179,7 +203,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { @Override public void onResume() { super.onResume(); - Media media = detailProvider.getMediaAtPosition(index); + media = detailProvider.getMediaAtPosition(index); if (media == null) { // Ask the detail provider to ping us when we're ready Timber.d("MediaDetailFragment not yet ready to display details; registering observer"); @@ -192,17 +216,18 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { Timber.d("MediaDetailFragment ready to display delayed details!"); detailProvider.unregisterDataSetObserver(dataObserver); dataObserver = null; - displayMediaDetails(detailProvider.getMediaAtPosition(index)); + media=detailProvider.getMediaAtPosition(index); + displayMediaDetails(); } }; detailProvider.registerDataSetObserver(dataObserver); } else { Timber.d("MediaDetailFragment ready to display details"); - displayMediaDetails(media); + displayMediaDetails(); } } - private void displayMediaDetails(final Media media) { + private void displayMediaDetails() { //Always load image from Internet to allow viewing the desc, license, and cats image.setMedia(media); @@ -239,7 +264,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { if (success) { extractor.fill(media); setTextFields(media); - setOnClickListeners(media); } else { Timber.d("Failed to load photo details."); } @@ -290,71 +314,99 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { } rebuildCatList(); + if(media.getCreator() == null || media.getCreator().equals("")) { + authorLayout.setVisibility(GONE); + } else { + author.setText(media.getCreator()); + } + checkDeletion(media); } - private void setOnClickListeners(final Media media) { - if (licenseLink(media) != null) { - license.setOnClickListener(v -> openWebBrowser(licenseLink(media))); + @OnClick(R.id.mediaDetailLicense) + public void onMediaDetailLicenceClicked(){ + if (!TextUtils.isEmpty(licenseLink(media))) { + openWebBrowser(licenseLink(media)); } else { - Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT); - toast.show(); + if(isCategoryImage) { + Timber.d("Unable to fetch license URL for %s", media.getLicense()); + } else { + Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT); + toast.show(); + } } + } + + @OnClick(R.id.mediaDetailCoordinates) + public void onMediaDetailCoordinatesClicked(){ if (media.getCoordinates() != null) { - coordinates.setOnClickListener(v -> openMap(media.getCoordinates())); + openMap(media.getCoordinates()); } - if (delete.getVisibility() == View.VISIBLE) { - delete.setOnClickListener(v -> { - delete.setEnabled(false); - delete.setTextColor(getResources().getColor(R.color.deleteButtonLight)); - AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); - alert.setMessage("Why should this file be deleted?"); - final EditText input = new EditText(getActivity()); - alert.setView(input); - input.requestFocus(); - alert.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - String reason = input.getText().toString(); - DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason); - deleteTask.execute(); - } - }); - alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - } - }); - AlertDialog d = alert.create(); - input.addTextChangedListener(new TextWatcher() { - private void handleText() { - final Button okButton = d.getButton(AlertDialog.BUTTON_POSITIVE); - if (input.getText().length() == 0) { - okButton.setEnabled(false); - } else { - okButton.setEnabled(true); - } - } + } - @Override - public void afterTextChanged(Editable arg0) { - handleText(); - } + @OnClick(R.id.nominateDeletion) + public void onDeleteButtonClicked(){ + //Reviewer correct me if i have misunderstood something over here + //But how does this if (delete.getVisibility() == View.VISIBLE) { + // enableDeleteButton(true); makes sense ? + AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); + alert.setMessage("Why should this file be deleted?"); + final EditText input = new EditText(getActivity()); + alert.setView(input); + input.requestFocus(); + alert.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + String reason = input.getText().toString(); + DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason); + deleteTask.execute(); + enableDeleteButton(false); + } + }); + alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + } + }); + AlertDialog d = alert.create(); + input.addTextChangedListener(new TextWatcher() { + private void handleText() { + final Button okButton = d.getButton(AlertDialog.BUTTON_POSITIVE); + if (input.getText().length() == 0) { + okButton.setEnabled(false); + } else { + okButton.setEnabled(true); + } + } - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } + @Override + public void afterTextChanged(Editable arg0) { + handleText(); + } - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - }); - d.show(); - d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - }); + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }); + d.show(); + d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + } + + @OnClick(R.id.seeMore) + public void onSeeMoreClicked(){ + if(nominatedForDeletion.getVisibility()== VISIBLE) { + openWebBrowser(media.getFilePageTitle().getMobileUri().toString()); } - if (nominatedforDeletion.getVisibility() == View.VISIBLE){ - seeMore.setOnClickListener(v -> { - openWebBrowser(media.getFilePageTitle().getMobileUri().toString()); - }); + } + + private void enableDeleteButton(boolean visibility) { + delete.setEnabled(visibility); + if(visibility) { + delete.setTextColor(getResources().getColor(R.color.primaryTextColor)); + } else { + delete.setTextColor(getResources().getColor(R.color.deleteButtonLight)); } } @@ -431,7 +483,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { if (date == null || date.toString() == null || date.toString().isEmpty()) { return "Uploaded date not available"; } - SimpleDateFormat formatter = new SimpleDateFormat("dd MMM yyyy"); + SimpleDateFormat formatter = new SimpleDateFormat("dd MMM yyyy", Locale.getDefault()); return formatter.format(date); } @@ -449,12 +501,11 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { private void checkDeletion(Media media){ if (media.getRequestedDeletion()){ - delete.setVisibility(View.GONE); - nominatedforDeletion.setVisibility(View.VISIBLE); - } - else{ - delete.setVisibility(View.VISIBLE); - nominatedforDeletion.setVisibility(View.GONE); + delete.setVisibility(GONE); + nominatedForDeletion.setVisibility(VISIBLE); + } else if (!isCategoryImage) { + delete.setVisibility(VISIBLE); + nominatedForDeletion.setVisibility(GONE); } } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index be7aea836..bd985ef20 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -26,6 +26,8 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Toast; +import butterknife.BindView; +import butterknife.ButterKnife; import javax.inject.Inject; import javax.inject.Named; @@ -36,6 +38,8 @@ import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.utils.ImageUtils; +import timber.log.Timber; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.content.Context.DOWNLOAD_SERVICE; @@ -53,16 +57,19 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple @Named("default_preferences") SharedPreferences prefs; - private ViewPager pager; + @BindView(R.id.mediaDetailsPager) + ViewPager pager; private Boolean editable; + private boolean isFeaturedImage; public MediaDetailPagerFragment() { - this(false); + this(false, false); } @SuppressLint("ValidFragment") - public MediaDetailPagerFragment(Boolean editable) { + public MediaDetailPagerFragment(Boolean editable, boolean isFeaturedImage) { this.editable = editable; + this.isFeaturedImage = isFeaturedImage; } @Override @@ -70,7 +77,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_media_detail_pager, container, false); - pager = (ViewPager) view.findViewById(R.id.mediaDetailsPager); + ButterKnife.bind(this,view); pager.addOnPageChangeListener(this); final MediaDetailAdapter adapter = new MediaDetailAdapter(getChildFragmentManager()); @@ -96,6 +103,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple super.onSaveInstanceState(outState); outState.putInt("current-page", pager.getCurrentItem()); outState.putBoolean("editable", editable); + outState.putBoolean("isFeaturedImage", isFeaturedImage); } @Override @@ -103,6 +111,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple super.onCreate(savedInstanceState); if (savedInstanceState != null) { editable = savedInstanceState.getBoolean("editable"); + isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage"); } setHasOptionsMenu(true); } @@ -133,6 +142,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple // Download downloadMedia(m); return true; + case R.id.menu_set_as_wallpaper: + // Set wallpaper + setWallpaper(m); + return true; case R.id.menu_retry_current_image: // Retry ((ContributionsActivity) getActivity()).retryUpload(pager.getCurrentItem()); @@ -148,6 +161,19 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple } } + /** + * Set the media as the device's wallpaper if the imageUrl is not null + * Fails silently if setting the wallpaper fails + * @param media + */ + private void setWallpaper(Media media) { + if(media.getImageUrl() == null || media.getImageUrl().isEmpty()) { + Timber.d("Media URL not present"); + return; + } + ImageUtils.setWallpaperFromImageUrl(getActivity(), Uri.parse(media.getImageUrl())); + } + /** * Start the media file downloading to the local SD card/storage. * The file can then be opened in Gallery or other apps. @@ -291,7 +317,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple // See bug https://code.google.com/p/android/issues/detail?id=27526 pager.postDelayed(() -> getActivity().supportInvalidateOptionsMenu(), 5); } - return MediaDetailFragment.forMedia(i, editable); + return MediaDetailFragment.forMedia(i, editable, isFeaturedImage); } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 78051abd8..c79b7b963 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -9,6 +9,8 @@ import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; +import com.google.gson.Gson; + import org.apache.http.HttpResponse; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.scheme.PlainSocketFactory; @@ -23,6 +25,8 @@ import org.apache.http.params.CoreProtocolPNames; import org.apache.http.util.EntityUtils; import org.mediawiki.api.ApiResult; import org.mediawiki.api.MWApi; +import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.IOException; @@ -38,7 +42,10 @@ import java.util.Locale; import java.util.concurrent.Callable; import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.Media; import fr.free.nrw.commons.PageTitle; +import fr.free.nrw.commons.category.CategoryImageUtils; +import fr.free.nrw.commons.category.QueryContinue; import fr.free.nrw.commons.notification.Notification; import fr.free.nrw.commons.notification.NotificationUtils; import in.yuvi.http.fluent.Http; @@ -46,6 +53,8 @@ import io.reactivex.Observable; import io.reactivex.Single; import timber.log.Timber; +import static fr.free.nrw.commons.utils.ContinueUtils.getQueryContinue; + /** * @author Addshore */ @@ -55,10 +64,18 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { private static final String THUMB_SIZE = "640"; private AbstractHttpClient httpClient; private MWApi api; + private MWApi wikidataApi; private Context context; - private SharedPreferences sharedPreferences; + private SharedPreferences defaultPreferences; + private SharedPreferences categoryPreferences; + private Gson gson; - public ApacheHttpClientMediaWikiApi(Context context, String apiURL, SharedPreferences sharedPreferences) { + public ApacheHttpClientMediaWikiApi(Context context, + String apiURL, + String wikidatApiURL, + SharedPreferences defaultPreferences, + SharedPreferences categoryPreferences, + Gson gson) { this.context = context; BasicHttpParams params = new BasicHttpParams(); SchemeRegistry schemeRegistry = new SchemeRegistry(); @@ -69,7 +86,10 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent()); httpClient = new DefaultHttpClient(cm, params); api = new MWApi(apiURL, httpClient); - this.sharedPreferences = sharedPreferences; + wikidataApi = new MWApi(wikidatApiURL, httpClient); + this.defaultPreferences = defaultPreferences; + this.categoryPreferences = categoryPreferences; + this.gson = gson; } @Override @@ -160,7 +180,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { } private void setAuthCookieOnLogin(boolean isLoggedIn) { - SharedPreferences.Editor editor = sharedPreferences.edit(); + SharedPreferences.Editor editor = defaultPreferences.edit(); if (isLoggedIn) { editor.putBoolean("isUserLoggedIn", true); editor.putString("getAuthCookie", api.getAuthCookie()); @@ -191,6 +211,15 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { return api.getEditToken(); } + @Override + public String getCentralAuthToken() throws IOException { + String centralAuthToken = api.action("centralauthtoken") + .get() + .getString("/api/centralauthtoken/@centralauthtoken"); + Timber.d("MediaWiki Central auth token is %s", centralAuthToken); + return centralAuthToken; + } + @Override public boolean fileExistsWithName(String fileName) throws IOException { return api.action("query") @@ -336,6 +365,98 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { }).flatMapObservable(Observable::fromIterable); } + /** + * Get the edit token for making wiki data edits + * https://www.mediawiki.org/wiki/API:Tokens + * @return + * @throws IOException + */ + private String getWikidataEditToken() throws IOException { + return wikidataApi.getEditToken(); + } + + @Override + public String getWikidataCsrfToken() throws IOException { + String wikidataCsrfToken = wikidataApi.action("query") + .param("action", "query") + .param("centralauthtoken", getCentralAuthToken()) + .param("meta", "tokens") + .post() + .getString("/api/query/tokens/@csrftoken"); + Timber.d("Wikidata csrf token is %s", wikidataCsrfToken); + return wikidataCsrfToken; + } + + /** + * Creates a new claim using the wikidata API + * https://www.mediawiki.org/wiki/Wikibase/API + * @param entityId the wikidata entity to be edited + * @param property the property to be edited, for eg P18 for images + * @param snaktype the type of value stored for that property + * @param value the actual value to be stored for the property, for eg filename in case of P18 + * @return returns revisionId if the claim is successfully created else returns null + * @throws IOException + */ + @Nullable + @Override + public String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException { + Timber.d("Filename is %s", value); + ApiResult result = wikidataApi.action("wbcreateclaim") + .param("entity", entityId) + .param("centralauthtoken", getCentralAuthToken()) + .param("token", getWikidataCsrfToken()) + .param("snaktype", snaktype) + .param("property", property) + .param("value", value) + .post(); + + if (result == null || result.getNode("api") == null) { + return null; + } + + Node node = result.getNode("api").getDocument(); + Element element = (Element) node; + + if (element != null && element.getAttribute("success").equals("1")) { + return result.getString("api/pageinfo/@lastrevid"); + } else { + Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info")); + } + return null; + } + + /** + * Adds the wikimedia-commons-app tag to the edits made on wikidata + * @param revisionId + * @return + * @throws IOException + */ + @Nullable + @Override + public boolean addWikidataEditTag(String revisionId) throws IOException { + ApiResult result = wikidataApi.action("tag") + .param("revid", revisionId) + .param("centralauthtoken", getCentralAuthToken()) + .param("token", getWikidataCsrfToken()) + .param("add", "wikimedia-commons-app") + .param("reason", "Add tag for edits made using Android Commons app") + .post(); + + if (result == null || result.getNode("api") == null) { + return false; + } + + Node node = result.getNode("api").getDocument(); + Element element = (Element) node; + + if (element != null && element.getAttribute("status").equals("success")) { + return true; + } else { + Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info")); + } + return false; + } + @Override @NonNull public Observable searchTitles(String title, int searchCatsLimit) { @@ -429,8 +550,8 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { .param("notprop", "list") .param("format", "xml") .param("meta", "notifications") -// .param("meta", "notifications") .param("notformat", "model") + .param("notwikis", "wikidatawiki|commonswiki|enwiki") .get() .getNode("/api/query/notifications/list"); } catch (IOException e) { @@ -448,6 +569,83 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { return NotificationUtils.getNotificationsFromList(context, childNodes); } + /** + * The method takes categoryName as input and returns a List of Media objects + * It uses the generator query API to get the images in a category, 10 at a time. + * Uses the query continue values for fetching paginated responses + * @param categoryName Category name as defined on commons + * @return + */ + @Override + @NonNull + public List getCategoryImages(String categoryName) { + ApiResult apiResult = null; + try { + MWApi.RequestBuilder requestBuilder = api.action("query") + .param("generator", "categorymembers") + .param("format", "xml") + .param("gcmtype", "file") + .param("gcmtitle", categoryName) + .param("gcmsort", "timestamp")//property to sort by;timestamp + .param("gcmdir", "desc")//in which direction to sort;descending + .param("prop", "imageinfo") + .param("gcmlimit", "10") + .param("iiprop", "url|extmetadata"); + + QueryContinue queryContinueValues = getQueryContinueValues(categoryName); + if (queryContinueValues != null) { + requestBuilder.param("continue", queryContinueValues.getContinueParam()); + requestBuilder.param("gcmcontinue", queryContinueValues.getGcmContinueParam()); + } + + apiResult = requestBuilder.get(); + } catch (IOException e) { + Timber.e("Failed to obtain searchCategories", e); + } + + if (apiResult == null) { + return new ArrayList<>(); + } + + ApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); + if (categoryImagesNode == null + || categoryImagesNode.getDocument() == null + || categoryImagesNode.getDocument().getChildNodes() == null + || categoryImagesNode.getDocument().getChildNodes().getLength() == 0) { + return new ArrayList<>(); + } + + QueryContinue queryContinue = getQueryContinue(apiResult.getNode("/api/continue").getDocument()); + setQueryContinueValues(categoryName, queryContinue); + + NodeList childNodes = categoryImagesNode.getDocument().getChildNodes(); + return CategoryImageUtils.getMediaList(childNodes); + } + + /** + * For APIs that return paginated responses, MediaWiki APIs uses the QueryContinue to facilitate fetching of subsequent pages + * https://www.mediawiki.org/wiki/API:Raw_query_continue + * After fetching images a page of image for a particular category, shared prefs are updated with the latest QueryContinue Values + * @param keyword + * @param queryContinue + */ + private void setQueryContinueValues(String keyword, QueryContinue queryContinue) { + SharedPreferences.Editor editor = categoryPreferences.edit(); + editor.putString(keyword, gson.toJson(queryContinue)); + editor.apply(); + } + + /** + * Before making a paginated API call, this method is called to get the latest query continue values to be used + * @param keyword + * @return + */ + @Nullable + private QueryContinue getQueryContinueValues(String keyword) { + String queryContinueString = categoryPreferences.getString(keyword, null); + return gson.fromJson(queryContinueString, QueryContinue.class); + } + @Override public boolean existingFile(String fileSha1) throws IOException { return api.action("query") @@ -496,6 +694,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { String resultStatus = result.getString("/api/upload/@result"); if (!resultStatus.equals("Success")) { String errorCode = result.getString("/api/error/@code"); + Timber.e(errorCode); return new UploadResult(resultStatus, errorCode); } else { Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp")); diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java new file mode 100644 index 000000000..031796745 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java @@ -0,0 +1,101 @@ +package fr.free.nrw.commons.mwapi; + +import com.google.gson.Gson; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Named; + +import fr.free.nrw.commons.mwapi.model.ApiResponse; +import fr.free.nrw.commons.mwapi.model.Page; +import fr.free.nrw.commons.mwapi.model.PageCategory; +import io.reactivex.Single; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import timber.log.Timber; + +/** + * Uses the OkHttp library to implement calls to the Commons MediaWiki API to match GPS coordinates + * with nearby Commons categories. Parses the results using GSON to obtain a list of relevant + * categories. Note: that caller is responsible for executing the request() method on a background + * thread. + */ +public class CategoryApi { + + private final OkHttpClient okHttpClient; + private final HttpUrl mwUrl; + private final Gson gson; + + @Inject + public CategoryApi(OkHttpClient okHttpClient, Gson gson, + @Named("commons_mediawiki_url") HttpUrl mwUrl) { + this.okHttpClient = okHttpClient; + this.mwUrl = mwUrl; + this.gson = gson; + } + + public Single> request(String coords) { + return Single.fromCallable(() -> { + HttpUrl apiUrl = buildUrl(coords); + Timber.d("URL: %s", apiUrl.toString()); + + Request request = new Request.Builder().get().url(apiUrl).build(); + Response response = okHttpClient.newCall(request).execute(); + ResponseBody body = response.body(); + if (body == null) { + return Collections.emptyList(); + } + + ApiResponse apiResponse = gson.fromJson(body.charStream(), ApiResponse.class); + Set categories = new LinkedHashSet<>(); + if (apiResponse != null && apiResponse.hasPages()) { + for (Page page : apiResponse.query.pages) { + for (PageCategory category : page.getCategories()) { + categories.add(category.withoutPrefix()); + } + } + } + return new ArrayList<>(categories); + }); + } + + /** + * Builds URL with image coords for MediaWiki API calls + * Example URL: https://commons.wikimedia.org/w/api.php?action=query&prop=categories|coordinates|pageprops&format=json&clshow=!hidden&coprop=type|name|dim|country|region|globe&codistancefrompoint=38.11386944444445|13.356263888888888&generator=geosearch&redirects=&ggscoord=38.11386944444445|1.356263888888888&ggsradius=100&ggslimit=10&ggsnamespace=6&ggsprop=type|name|dim|country|region|globe&ggsprimary=all&formatversion=2 + * + * @param coords Coordinates to build query with + * @return URL for API query + */ + private HttpUrl buildUrl(String coords) { + return mwUrl.newBuilder() + .addPathSegment("w") + .addPathSegment("api.php") + .addQueryParameter("action", "query") + .addQueryParameter("prop", "categories|coordinates|pageprops") + .addQueryParameter("format", "json") + .addQueryParameter("clshow", "!hidden") + .addQueryParameter("coprop", "type|name|dim|country|region|globe") + .addQueryParameter("codistancefrompoint", coords) + .addQueryParameter("generator", "geosearch") + .addQueryParameter("ggscoord", coords) + .addQueryParameter("ggsradius", "10000") + .addQueryParameter("ggslimit", "10") + .addQueryParameter("ggsnamespace", "6") + .addQueryParameter("ggsprop", "type|name|dim|country|region|globe") + .addQueryParameter("ggsprimary", "all") + .addQueryParameter("formatversion", "2") + .build(); + } + +} + + + diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index fd213455d..b4398319f 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.List; +import fr.free.nrw.commons.Media; import fr.free.nrw.commons.notification.Notification; import io.reactivex.Observable; import io.reactivex.Single; @@ -26,6 +27,10 @@ public interface MediaWikiApi { String getEditToken() throws IOException; + String getWikidataCsrfToken() throws IOException; + + String getCentralAuthToken() throws IOException; + boolean fileExistsWithName(String fileName) throws IOException; boolean pageExists(String pageName) throws IOException; @@ -34,6 +39,8 @@ public interface MediaWikiApi { boolean logEvents(LogBuilder[] logBuilders); + List getCategoryImages(String categoryName); + @NonNull UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener) throws IOException; @@ -46,6 +53,12 @@ public interface MediaWikiApi { @Nullable String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException; + @Nullable + String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException; + + @Nullable + boolean addWikidataEditTag(String revisionId) throws IOException; + @NonNull MediaResult fetchMediaByFilename(String filename) throws IOException; diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/model/ApiResponse.java b/app/src/main/java/fr/free/nrw/commons/mwapi/model/ApiResponse.java new file mode 100644 index 000000000..7feb90251 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/model/ApiResponse.java @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.mwapi.model; + +public class ApiResponse { + public Query query; + + public ApiResponse() { + } + + public boolean hasPages() { + return query != null && query.pages != null; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/model/Page.java b/app/src/main/java/fr/free/nrw/commons/mwapi/model/Page.java new file mode 100644 index 000000000..d01ba658f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/model/Page.java @@ -0,0 +1,17 @@ +package fr.free.nrw.commons.mwapi.model; + +import android.support.annotation.NonNull; + +public class Page { + public String title; + public PageCategory[] categories; + public PageCategory category; + + public Page() { + } + + @NonNull + public PageCategory[] getCategories() { + return categories != null ? categories : new PageCategory[0]; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/model/PageCategory.java b/app/src/main/java/fr/free/nrw/commons/mwapi/model/PageCategory.java new file mode 100644 index 000000000..be4b9fd79 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/model/PageCategory.java @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.mwapi.model; + +public class PageCategory { + public String title; + + public PageCategory() { + } + + public String withoutPrefix() { + return title != null ? title.replace("Category:", "") : ""; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/model/Query.java b/app/src/main/java/fr/free/nrw/commons/mwapi/model/Query.java new file mode 100644 index 000000000..b87f97cc3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/model/Query.java @@ -0,0 +1,10 @@ +package fr.free.nrw.commons.mwapi.model; + +public class Query { + public Page[] pages; + + public Query() { + pages = new Page[0]; + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java index 7ab427b2d..9a12c6d39 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java @@ -1,6 +1,5 @@ package fr.free.nrw.commons.nearby; -import android.content.SharedPreferences; import android.os.Build; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java index 36dee44a9..cb28df947 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java @@ -4,23 +4,24 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.graphics.Typeface; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.Handler; import android.support.annotation.NonNull; import android.support.design.widget.BottomSheetBehavior; - import android.support.v4.app.FragmentTransaction; import android.support.v7.app.AlertDialog; -import android.util.Log; + import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.LinearLayout; import android.widget.ProgressBar; -import android.widget.Toast; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -28,28 +29,34 @@ import com.google.gson.GsonBuilder; import java.util.List; import javax.inject.Inject; +import javax.inject.Named; import butterknife.BindView; import butterknife.ButterKnife; - import fr.free.nrw.commons.R; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationServiceManager; +import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType; import fr.free.nrw.commons.location.LocationUpdateListener; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.UriSerializer; - import fr.free.nrw.commons.utils.ViewUtil; +import fr.free.nrw.commons.wikidata.WikidataEditListener; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; - import timber.log.Timber; +import uk.co.deanwild.materialshowcaseview.IShowcaseListener; +import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView; + +import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.*; +import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED; -public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener { +public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener, + WikidataEditListener.WikidataP18EditListener { private static final int LOCATION_REQUEST = 1; @@ -62,13 +69,18 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp LinearLayout bottomSheetDetails; @BindView(R.id.transparentView) View transparentView; + @BindView(R.id.fab_recenter) + View fabRecenter; @Inject LocationServiceManager locationManager; @Inject NearbyController nearbyController; + @Inject WikidataEditListener wikidataEditListener; - private LatLng curLatLang; + @Inject + @Named("application_preferences") SharedPreferences applicationPrefs; + private LatLng curLatLng; private Bundle bundle; private Disposable placesDisposable; private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed @@ -78,10 +90,18 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp private NearbyListFragment nearbyListFragment; private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName(); private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName(); + private View listButton; // Reference to list button to use in tutorial private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; private BroadcastReceiver broadcastReceiver; + private boolean isListShowcaseAdded = false; + private boolean isMapShowCaseAdded = false; + + private LatLng lastKnownLocation; + + private MaterialShowcaseView secondSingleShowCaseView; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -92,6 +112,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp initBottomSheetBehaviour(); initDrawer(); + wikidataEditListener.setAuthenticationStateListener(this); } private void resumeFragment() { @@ -131,16 +152,55 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_nearby, menu); + new Handler().post(() -> { + + listButton = findViewById(R.id.action_display_list); + + secondSingleShowCaseView = new MaterialShowcaseView.Builder(this) + .setTarget(listButton) + .setDismissText(getString(R.string.showcase_view_got_it_button)) + .setContentText(getString(R.string.showcase_view_list_icon)) + .setDelay(500) // optional but starting animations immediately in onCreate can make them choppy + .singleUse(ViewUtil.SHOWCASE_VIEW_ID_1) // provide a unique ID used to ensure it is only shown once + .setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD)) + .setListener(new IShowcaseListener() { + @Override + public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) { + + } + + // If dismissed, we can inform fragment to start showcase sequence there + @Override + public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) { + nearbyMapFragment.onNearbyMaterialShowcaseDismissed(); + } + }) + .build(); + + isListShowcaseAdded = true; + + if (isMapShowCaseAdded) { // If map showcase is also ready, start ShowcaseSequence + // Probably this case is not possible. Just added to be careful + setMapViewTutorialShowCase(); + } + }); + return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection switch (item.getItemId()) { case R.id.action_display_list: - bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_COLLAPSED || bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_HIDDEN){ + bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + }else if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_EXPANDED){ + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + return true; default: return super.onOptionsItemSelected(item); @@ -158,7 +218,11 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp switch (requestCode) { case LOCATION_REQUEST: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + Timber.d("Location permission granted, refreshing view"); + //Still need to check if GPS is enabled + checkGps(); + lastKnownLocation = locationManager.getLKL(); + refreshView(PERMISSION_JUST_GRANTED); } else { //If permission not granted, go to page that says Nearby Places cannot be displayed hideProgressBar(); @@ -218,7 +282,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp private void checkLocationPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (locationManager.isLocationPermissionGranted()) { - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } else { // Should we show an explanation? if (locationManager.isPermissionExplanationRequired(this)) { @@ -244,7 +308,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp } } } else { - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } } @@ -253,7 +317,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp super.onActivityResult(requestCode, resultCode, data); if (requestCode == 1) { Timber.d("User is back from Settings page"); - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } } @@ -261,7 +325,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp protected void onStart() { super.onStart(); locationManager.addLocationListener(this); - locationManager.registerLocationManager(); + registerLocationUpdates(); } @Override @@ -312,8 +376,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp @Override public void onReceive(Context context, Intent intent) { if (NetworkUtils.isInternetConnectionEstablished(NearbyActivity.this)) { - refreshView(LocationServiceManager - .LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } else { ViewUtil.showLongToast(NearbyActivity.this, getString(R.string.no_internet)); } @@ -329,7 +392,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp * * @param locationChangeType defines if location shanged significantly or slightly */ - private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) { + private void refreshView(LocationChangeType locationChangeType) { if (lockNearbyView) { return; } @@ -339,38 +402,91 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp return; } - locationManager.registerLocationManager(); + registerLocationUpdates(); LatLng lastLocation = locationManager.getLastLocation(); - if (curLatLang != null && curLatLang.equals(lastLocation)) { //refresh view only if location has changed + if (curLatLng != null && curLatLng.equals(lastLocation) + && !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed return; } - curLatLang = lastLocation; + curLatLng = lastLocation; - if (curLatLang == null) { + if (locationChangeType.equals(PERMISSION_JUST_GRANTED)) { + curLatLng = lastKnownLocation; + } + + if (curLatLng == null) { Timber.d("Skipping update of nearby places as location is unavailable"); return; } - if (locationChangeType - .equals(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) { + if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED) + || locationChangeType.equals(PERMISSION_JUST_GRANTED) + || locationChangeType.equals(MAP_UPDATED)) { progressBar.setVisibility(View.VISIBLE); - placesDisposable = Observable.fromCallable(() -> nearbyController - .loadAttractionsFromLocation(curLatLang)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::populatePlaces); - } else if (locationChangeType - .equals(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) { + + //TODO: This hack inserts curLatLng before populatePlaces is called (see #1440). Ideally a proper fix should be found Gson gson = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriSerializer()) .create(); - String gsonCurLatLng = gson.toJson(curLatLang); + String gsonCurLatLng = gson.toJson(curLatLng); + bundle.clear(); + bundle.putString("CurLatLng", gsonCurLatLng); + + placesDisposable = Observable.fromCallable(() -> nearbyController + .loadAttractionsFromLocation(curLatLng)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::populatePlaces, + throwable -> { + Timber.d(throwable); + showErrorMessage(getString(R.string.error_fetching_nearby_places)); + progressBar.setVisibility(View.GONE); + }); + } else if (locationChangeType + .equals(LOCATION_SLIGHTLY_CHANGED)) { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Uri.class, new UriSerializer()) + .create(); + String gsonCurLatLng = gson.toJson(curLatLng); bundle.putString("CurLatLng", gsonCurLatLng); updateMapFragment(true); } } + /** + * This method first checks if the location permissions has been granted and then register the location manager for updates. + */ + private void registerLocationUpdates() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (locationManager.isLocationPermissionGranted()) { + locationManager.registerLocationManager(); + } else { + // Should we show an explanation? + if (locationManager.isPermissionExplanationRequired(this)) { + new AlertDialog.Builder(this) + .setMessage(getString(R.string.location_permission_rationale_nearby)) + .setPositiveButton("OK", (dialog, which) -> { + requestLocationPermissions(); + dialog.dismiss(); + }) + .setNegativeButton("Cancel", (dialog, id) -> { + showLocationPermissionDeniedErrorDialog(); + dialog.cancel(); + }) + .create() + .show(); + + } else { + // No explanation needed, we can request the permission. + requestLocationPermissions(); + } + } + } else { + locationManager.registerLocationManager(); + } + } + private void populatePlaces(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { List placeList = nearbyPlacesInfo.placeList; LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates; @@ -378,20 +494,20 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp .registerTypeAdapter(Uri.class, new UriSerializer()) .create(); String gsonPlaceList = gson.toJson(placeList); - String gsonCurLatLng = gson.toJson(curLatLang); + String gsonCurLatLng = gson.toJson(curLatLng); String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates); if (placeList.size() == 0) { ViewUtil.showSnackbar(findViewById(R.id.container), R.string.no_nearby); } - bundle.clear(); bundle.putString("PlaceList", gsonPlaceList); - bundle.putString("CurLatLng", gsonCurLatLng); + //bundle.putString("CurLatLng", gsonCurLatLng); bundle.putString("BoundaryCoord", gsonBoundaryCoordinates); // First time to init fragments if (nearbyMapFragment == null) { + Timber.d("Init map fragment for the first time"); lockNearbyView(true); setMapFragment(); setListFragment(); @@ -399,9 +515,49 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp lockNearbyView(false); } else { // There are fragments, just update the map and list + Timber.d("Map fragment already exists, just update the map and list"); updateMapFragment(false); updateListFragment(); } + + isMapShowCaseAdded = true; + } + + public void setMapViewTutorialShowCase() { + /* + *This showcase view will be the first step of our nearbyMaterialShowcaseSequence. The reason we use a + * single item instead of adding another step to nearbyMaterialShowcaseSequence is that we are not able to + * call withoutShape() method on steps. For mapView we need an showcase view without + * any circle on it, it should cover the whole page. + * */ + MaterialShowcaseView firstSingleShowCaseView = new MaterialShowcaseView.Builder(this) + .setTarget(nearbyMapFragment.mapView) + .setDismissText(getString(R.string.showcase_view_got_it_button)) + .setContentText(getString(R.string.showcase_view_whole_nearby_activity)) + .setDelay(500) // optional but starting animations immediately in onCreate can make them choppy + .singleUse(ViewUtil.SHOWCASE_VIEW_ID_2) // provide a unique ID used to ensure it is only shown once + .withoutShape() // no shape on map view since there are no view to focus on + .setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD)) + .setListener(new IShowcaseListener() { + @Override + public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) { + + } + + @Override + public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) { + /* Add other nearbyMaterialShowcaseSequence here, it will make the user feel as they are a + * nearbyMaterialShowcaseSequence whole together. + * */ + secondSingleShowCaseView.show(NearbyActivity.this); + } + }) + .build(); + + if (applicationPrefs.getBoolean("firstRunNearby", true)) { + applicationPrefs.edit().putBoolean("firstRunNearby", false).apply(); + firstSingleShowCaseView.show(this); + } } private void lockNearbyView(boolean lock) { @@ -411,7 +567,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp locationManager.removeLocationListener(this); } else { lockNearbyView = false; - locationManager.registerLocationManager(); + registerLocationUpdates(); locationManager.addLocationListener(this); } } @@ -457,34 +613,39 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp NearbyMapFragment nearbyMapFragment = getMapFragment(); - if (nearbyMapFragment != null && curLatLang != null) { + if (nearbyMapFragment != null && curLatLng != null) { hideProgressBar(); // In case it is visible (this happens, not an impossible case) /* * If we are close to nearby places boundaries, we need a significant update to * get new nearby places. Check order is south, north, west, east * */ if (nearbyMapFragment.boundaryCoordinates != null - && (curLatLang.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude() - || curLatLang.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude() - || curLatLang.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude() - || curLatLang.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) { + && (curLatLng.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude() + || curLatLng.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude() + || curLatLng.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude() + || curLatLng.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) { // populate places placesDisposable = Observable.fromCallable(() -> nearbyController - .loadAttractionsFromLocation(curLatLang)) + .loadAttractionsFromLocation(curLatLng)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::populatePlaces); - nearbyMapFragment.setArguments(bundle); + .subscribe(this::populatePlaces, + throwable -> { + Timber.d(throwable); + showErrorMessage(getString(R.string.error_fetching_nearby_places)); + progressBar.setVisibility(View.GONE); + }); + nearbyMapFragment.setBundleForUpdtes(bundle); nearbyMapFragment.updateMapSignificantly(); updateListFragment(); return; } if (isSlightUpdate) { - nearbyMapFragment.setArguments(bundle); + nearbyMapFragment.setBundleForUpdtes(bundle); nearbyMapFragment.updateMapSlightly(); } else { - nearbyMapFragment.setArguments(bundle); + nearbyMapFragment.setBundleForUpdtes(bundle); nearbyMapFragment.updateMapSignificantly(); updateListFragment(); } @@ -498,7 +659,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp } private void updateListFragment() { - nearbyListFragment.setArguments(bundle); + nearbyListFragment.setBundleForUpdates(bundle); nearbyListFragment.updateNearbyListSignificantly(); } @@ -528,15 +689,24 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp @Override public void onLocationChangedSignificantly(LatLng latLng) { - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } @Override public void onLocationChangedSlightly(LatLng latLng) { - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED); + refreshView(LOCATION_SLIGHTLY_CHANGED); } public void prepareViewsForSheetPosition(int bottomSheetState) { // TODO } + + private void showErrorMessage(String message) { + ViewUtil.showLongToast(NearbyActivity.this, message); + } + + @Override + public void onWikidataEditSuccessful() { + refreshView(MAP_UPDATED); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index 015d22135..bd042b4d7 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -7,6 +7,7 @@ import android.support.graphics.drawable.VectorDrawableCompat; import com.mapbox.mapboxsdk.annotations.IconFactory; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -44,7 +45,7 @@ public class NearbyController { * @return NearbyPlacesInfo a variable holds Place list without distance information * and boundary coordinates of current Place List */ - public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) { + public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) throws IOException { Timber.d("Loading attractions near %s", curLatLng); NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo(); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java index e46bd76e7..099792bc5 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java @@ -2,6 +2,7 @@ package fr.free.nrw.commons.nearby; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; @@ -21,6 +22,9 @@ import java.lang.reflect.Type; import java.util.Collections; import java.util.List; +import javax.inject.Inject; +import javax.inject.Named; + import dagger.android.support.AndroidSupportInjection; import dagger.android.support.DaggerFragment; import fr.free.nrw.commons.R; @@ -33,6 +37,8 @@ import static android.app.Activity.RESULT_OK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; public class NearbyListFragment extends DaggerFragment { + private Bundle bundleForUpdates; // Carry information from activity about changed nearby places and current location + private static final Type LIST_TYPE = new TypeToken>() { }.getType(); private static final Type CUR_LAT_LNG_TYPE = new TypeToken() { @@ -45,6 +51,11 @@ public class NearbyListFragment extends DaggerFragment { private RecyclerView recyclerView; private ContributionController controller; + + @Inject + @Named("direct_nearby_upload_prefs") + SharedPreferences directPrefs; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -80,9 +91,11 @@ public class NearbyListFragment extends DaggerFragment { } public void updateNearbyListSignificantly() { - Bundle bundle = this.getArguments(); - adapterFactory.updateAdapterData(getPlaceListFromBundle(bundle), - (RVRendererAdapter) recyclerView.getAdapter()); + try { + adapterFactory.updateAdapterData(getPlaceListFromBundle(bundleForUpdates), (RVRendererAdapter) recyclerView.getAdapter()); + } catch (NullPointerException e) { + Timber.e("Null pointer exception from calling recyclerView.getAdapter()"); + } } private List getPlaceListFromBundle(Bundle bundle) { @@ -133,11 +146,15 @@ public class NearbyListFragment extends DaggerFragment { if (resultCode == RESULT_OK) { Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); - controller.handleImagePicked(requestCode, data, true); + controller.handleImagePicked(requestCode, data, true, directPrefs.getString("WikiDataEntityId", null)); } else { Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); } } + public void setBundleForUpdates(Bundle bundleForUpdates) { + this.bundleForUpdates = bundleForUpdates; + } + } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java index 354dbd8ee..934d74353 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java @@ -7,6 +7,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Color; +import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; @@ -58,13 +59,14 @@ import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.utils.UriDeserializer; import fr.free.nrw.commons.utils.ViewUtil; import timber.log.Timber; +import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView; import static android.app.Activity.RESULT_OK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; public class NearbyMapFragment extends DaggerFragment { - private MapView mapView; + public MapView mapView; private List baseMarkerOptions; private fr.free.nrw.commons.location.LatLng curLatLng; public fr.free.nrw.commons.location.LatLng[] boundaryCoordinates; @@ -111,6 +113,12 @@ public class NearbyMapFragment extends DaggerFragment { private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.06; private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04; + private boolean isSecondMaterialShowcaseDismissed; + private boolean isMapReady; + private MaterialShowcaseView thirdSingleShowCaseView; + + private Bundle bundleForUpdtes;// Carry information from activity about changed nearby places and current location + @Inject @Named("prefs") SharedPreferences prefs; @@ -124,6 +132,7 @@ public class NearbyMapFragment extends DaggerFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + Timber.d("Nearby map fragment created"); controller = new ContributionController(this); directUpload = new DirectUpload(this, controller); @@ -149,17 +158,20 @@ public class NearbyMapFragment extends DaggerFragment { getActivity()); boundaryCoordinates = gson.fromJson(gsonBoundaryCoordinates, gsonBoundaryCoordinatesType); } - Mapbox.getInstance(getActivity(), - getString(R.string.mapbox_commons_app_token)); - MapboxTelemetry.getInstance().setTelemetryEnabled(false); + if (curLatLng != null) { + Mapbox.getInstance(getActivity(), + getString(R.string.mapbox_commons_app_token)); + MapboxTelemetry.getInstance().setTelemetryEnabled(false); + } setRetainInstance(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - + Timber.d("onCreateView called"); if (curLatLng != null) { + Timber.d("curLatLng found, setting up map view..."); setupMapView(savedInstanceState); } @@ -192,14 +204,12 @@ public class NearbyMapFragment extends DaggerFragment { } public void updateMapSlightly() { - // Get arguments from bundle for new location - Bundle bundle = this.getArguments(); if (mapboxMap != null) { Gson gson = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriDeserializer()) .create(); - if (bundle != null) { - String gsonLatLng = bundle.getString("CurLatLng"); + if (bundleForUpdtes != null) { + String gsonLatLng = bundleForUpdtes.getString("CurLatLng"); Type curLatLngType = new TypeToken() {}.getType(); curLatLng = gson.fromJson(gsonLatLng, curLatLngType); } @@ -209,17 +219,15 @@ public class NearbyMapFragment extends DaggerFragment { } public void updateMapSignificantly() { - - Bundle bundle = this.getArguments(); if (mapboxMap != null) { - if (bundle != null) { + if (bundleForUpdtes != null) { Gson gson = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriDeserializer()) .create(); - String gsonPlaceList = bundle.getString("PlaceList"); - String gsonLatLng = bundle.getString("CurLatLng"); - String gsonBoundaryCoordinates = bundle.getString("BoundaryCoord"); + String gsonPlaceList = bundleForUpdtes.getString("PlaceList"); + String gsonLatLng = bundleForUpdtes.getString("CurLatLng"); + String gsonBoundaryCoordinates = bundleForUpdtes.getString("BoundaryCoord"); Type listType = new TypeToken>() {}.getType(); List placeList = gson.fromJson(gsonPlaceList, listType); Type curLatLngType = new TypeToken() {}.getType(); @@ -457,6 +465,8 @@ public class NearbyMapFragment extends DaggerFragment { private void setupMapView(Bundle savedInstanceState) { MapboxMapOptions options = new MapboxMapOptions() + .compassGravity(Gravity.BOTTOM | Gravity.LEFT) + .compassMargins(new int[]{12, 0, 0, 24}) .styleUrl(Style.OUTDOORS) .logoEnabled(false) .attributionEnabled(false) @@ -471,6 +481,7 @@ public class NearbyMapFragment extends DaggerFragment { mapView.getMapAsync(new OnMapReadyCallback() { @Override public void onMapReady(MapboxMap mapboxMap) { + ((NearbyActivity)getActivity()).setMapViewTutorialShowCase(); NearbyMapFragment.this.mapboxMap = mapboxMap; updateMapSignificantly(); } @@ -514,6 +525,7 @@ public class NearbyMapFragment extends DaggerFragment { private void addNearbyMarkerstoMapBoxMap() { mapboxMap.addMarkers(baseMarkerOptions); + mapboxMap.setOnInfoWindowCloseListener(marker -> { if (marker == selected) { bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); @@ -529,6 +541,7 @@ public class NearbyMapFragment extends DaggerFragment { }); mapboxMap.setOnMarkerClickListener(marker -> { + if (marker instanceof NearbyMarker) { this.selected = marker; NearbyMarker nearbyMarker = (NearbyMarker) marker; @@ -536,6 +549,7 @@ public class NearbyMapFragment extends DaggerFragment { passInfoToSheet(place); bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } return false; }); @@ -629,7 +643,19 @@ public class NearbyMapFragment extends DaggerFragment { addAnchorToSmallFABs(fabGallery, getActivity().findViewById(R.id.empty_view).getId()); addAnchorToSmallFABs(fabCamera, getActivity().findViewById(R.id.empty_view1).getId()); + thirdSingleShowCaseView = new MaterialShowcaseView.Builder(this.getActivity()) + .setTarget(fabPlus) + .setDismissText(getString(R.string.showcase_view_got_it_button)) + .setContentText(getString(R.string.showcase_view_plus_fab)) + .setDelay(500) // optional but starting animations immediately in onCreate can make them choppy + .singleUse(ViewUtil.SHOWCASE_VIEW_ID_3) // provide a unique ID used to ensure it is only shown once + .setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD)) + .build(); + isMapReady = true; + if (isSecondMaterialShowcaseDismissed) { + thirdSingleShowCaseView.show(getActivity()); + } } @@ -666,7 +692,7 @@ public class NearbyMapFragment extends DaggerFragment { directionsButton.setOnClickListener(view -> { //Open map app at given position - Intent mapIntent = new Intent(Intent.ACTION_VIEW, place.location.getGmmIntentUri()); + Intent mapIntent = new Intent(Intent.ACTION_VIEW, this.place.location.getGmmIntentUri()); if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) { startActivity(mapIntent); } @@ -705,6 +731,7 @@ public class NearbyMapFragment extends DaggerFragment { editor.putString("Title", place.getName()); editor.putString("Desc", place.getLongDescription()); editor.putString("Category", place.getCategory()); + editor.putString("WikiDataEntityId", place.getWikiDataEntityId()); editor.apply(); } @@ -740,7 +767,7 @@ public class NearbyMapFragment extends DaggerFragment { if (resultCode == RESULT_OK) { Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); - controller.handleImagePicked(requestCode, data, true); + controller.handleImagePicked(requestCode, data, true, directPrefs.getString("WikiDataEntityId", null)); } else { Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); @@ -771,7 +798,7 @@ public class NearbyMapFragment extends DaggerFragment { } } - private void closeFabs ( boolean isFabOpen){ + private void closeFabs ( boolean isFabOpen){ if (isFabOpen) { fabPlus.startAnimation(rotate_backward); fabCamera.startAnimation(fab_close); @@ -782,6 +809,18 @@ public class NearbyMapFragment extends DaggerFragment { } } + public void setBundleForUpdtes(Bundle bundleForUpdtes) { + this.bundleForUpdtes = bundleForUpdtes; + } + + public void onNearbyMaterialShowcaseDismissed() { + isSecondMaterialShowcaseDismissed = true; + if (isMapReady) { + thirdSingleShowCaseView.show(getActivity()); + } + } + + @Override public void onStart() { if (mapView != null) { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMaterialShowcaseSequence.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMaterialShowcaseSequence.java new file mode 100644 index 000000000..c6e46611d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMaterialShowcaseSequence.java @@ -0,0 +1,18 @@ +package fr.free.nrw.commons.nearby; + +import android.app.Activity; + +import uk.co.deanwild.materialshowcaseview.MaterialShowcaseSequence; +import uk.co.deanwild.materialshowcaseview.ShowcaseConfig; + + +public class NearbyMaterialShowcaseSequence extends MaterialShowcaseSequence { + + public NearbyMaterialShowcaseSequence(Activity activity, String sequenceID) { + super(activity, sequenceID); + ShowcaseConfig config = new ShowcaseConfig(); + config.setDelay(500); // half second between each showcase view + this.setConfig(config); + this.singleUse(sequenceID); // Display tutorial only once + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java index a2f4b2352..c8d20f753 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java @@ -17,7 +17,7 @@ import java.util.regex.Pattern; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.utils.FileUtils; +import fr.free.nrw.commons.upload.FileUtils; import timber.log.Timber; public class NearbyPlaces { @@ -40,10 +40,9 @@ public class NearbyPlaces { } } - List getFromWikidataQuery(LatLng curLatLng, String lang) { + List getFromWikidataQuery(LatLng curLatLng, String lang) throws IOException { List places = Collections.emptyList(); - try { // increase the radius gradually to find a satisfactory number of nearby places while (radius <= MAX_RADIUS) { places = getFromWikidataQuery(curLatLng, lang, radius); @@ -54,13 +53,6 @@ public class NearbyPlaces { radius *= RADIUS_MULTIPLIER; } } - } catch (IOException e) { - Timber.d(e.toString()); - // errors tend to be caused by too many results (and time out) - // try a small radius next time - Timber.d("back to initial radius: %f", radius); - radius = INITIAL_RADIUS; - } // make sure we will be able to send at least one request next time if (radius > MAX_RADIUS) { radius = MAX_RADIUS; diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java index 9c5138245..93075e8fe 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java @@ -3,6 +3,7 @@ package fr.free.nrw.commons.nearby; import android.graphics.Bitmap; import android.net.Uri; import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; import java.util.HashMap; import java.util.Map; @@ -50,6 +51,20 @@ public class Place { this.distance = distance; } + /** + * Extracts the entity id from the wikidata link + * @return returns the entity id if wikidata link exists + */ + @Nullable + public String getWikiDataEntityId() { + if (!hasWikidataLink()) { + return null; + } + + String wikiDataLink = siteLinks.getWikidataLink().toString(); + return wikiDataLink.replace("http://www.wikidata.org/entity/", ""); + } + public boolean hasWikipediaLink() { return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getWikipediaLink())); } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java index 9cbe28db4..0d33b4a5e 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java @@ -25,7 +25,6 @@ import javax.inject.Named; import butterknife.BindView; import butterknife.ButterKnife; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.contributions.ContributionController; diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java index dc52f198a..b366c944a 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -16,7 +16,6 @@ import android.widget.RelativeLayout; import com.pedrogomez.renderers.RVRendererAdapter; -import java.lang.ref.WeakReference; import java.util.Collections; import java.util.List; @@ -26,6 +25,7 @@ import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.ViewUtil; @@ -46,6 +46,8 @@ public class NotificationActivity extends NavigationBaseActivity { @BindView(R.id.container) RelativeLayout relativeLayout; @Inject NotificationController controller; + @Inject + MediaWikiApi mediaWikiApi; private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment"; private NotificationWorkerFragment mNotificationWorkerFragment; @@ -81,7 +83,6 @@ public class NotificationActivity extends NavigationBaseActivity { } } - @SuppressLint("CheckResult") private void addNotifications() { Timber.d("Add notifications"); diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java index 17a318e74..6dcfca35d 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.notification; -import android.util.Log; +import android.graphics.drawable.PictureDrawable; +import android.text.Html; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -8,17 +9,23 @@ import android.widget.ImageView; import android.widget.TextView; import com.borjabravo.readmoretextview.ReadMoreTextView; +import com.bumptech.glide.RequestBuilder; import com.pedrogomez.renderers.Renderer; import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.glide.SvgSoftwareLayerSetter; + +import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; /** * Created by root on 19.12.2017. */ public class NotificationRenderer extends Renderer { + private RequestBuilder requestBuilder; + @BindView(R.id.title) ReadMoreTextView title; @BindView(R.id.time) TextView time; @BindView(R.id.icon) ImageView icon; @@ -41,23 +48,32 @@ public class NotificationRenderer extends Renderer { protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) { View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false); ButterKnife.bind(this, inflatedView); + requestBuilder = GlideApp.with(inflatedView.getContext()) + .as(PictureDrawable.class) + .error(R.drawable.round_icon_unknown) + .transition(withCrossFade()) + .listener(new SvgSoftwareLayerSetter()); return inflatedView; } @Override public void render() { Notification notification = getContent(); - String str = notification.notificationText.trim(); - str = str.concat(" "); - title.setText(str); + setTitle(notification.notificationText); time.setText(notification.date); - switch (notification.notificationType) { - case THANK_YOU_EDIT: - icon.setImageResource(R.drawable.ic_edit_black_24dp); - break; - default: - icon.setImageResource(R.drawable.round_icon_unknown); - } + requestBuilder.load(notification.iconUrl).into(icon); + } + + /** + * Cleans up the notification text and sets it as the title + * Clean up is required to fix escaped HTML string and extra white spaces at the beginning of the notification + * @param notificationText + */ + private void setTitle(String notificationText) { + notificationText = notificationText.trim().replaceAll("(^\\h*)|(\\h*$)", ""); + notificationText = Html.fromHtml(notificationText).toString(); + notificationText = notificationText.concat(" "); + title.setText(notificationText); } public interface NotificationClicked{ diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java index 68c3add1c..e7c87d3f4 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java @@ -16,12 +16,13 @@ import javax.annotation.Nullable; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.R; -import static fr.free.nrw.commons.notification.NotificationType.THANK_YOU_EDIT; import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN; public class NotificationUtils { private static final String COMMONS_WIKI = "commonswiki"; + private static final String WIKIDATA_WIKI = "wikidatawiki"; + private static final String WIKIPEDIA_WIKI = "enwiki"; public static boolean isCommonsNotification(Node document) { if (document == null || !document.hasAttributes()) { @@ -31,6 +32,32 @@ public class NotificationUtils { return COMMONS_WIKI.equals(element.getAttribute("wiki")); } + /** + * Returns true if the wiki attribute corresponds to wikidatawiki + * @param document + * @return + */ + public static boolean isWikidataNotification(Node document) { + if (document == null || !document.hasAttributes()) { + return false; + } + Element element = (Element) document; + return WIKIDATA_WIKI.equals(element.getAttribute("wiki")); + } + + /** + * Returns true if the wiki attribute corresponds to enwiki + * @param document + * @return + */ + public static boolean isWikipediaNotification(Node document) { + if (document == null || !document.hasAttributes()) { + return false; + } + Element element = (Element) document; + return WIKIPEDIA_WIKI.equals(element.getAttribute("wiki")); + } + public static NotificationType getNotificationType(Node document) { Element element = (Element) document; String type = element.getAttribute("type"); @@ -68,10 +95,17 @@ public class NotificationUtils { return notifications; } + /** + * Currently the app is interested in showing notifications just from the following three wikis: commons, wikidata, wikipedia + * This function returns true only if the notification belongs to any of the above wikis and is of a known notification type + * @param node + * @return + */ private static boolean isUsefulNotification(Node node) { - return isCommonsNotification(node) - && !getNotificationType(node).equals(UNKNOWN) - && !getNotificationType(node).equals(THANK_YOU_EDIT); + return (isCommonsNotification(node) + || isWikidataNotification(node) + || isWikipediaNotification(node)) + && !getNotificationType(node).equals(UNKNOWN); } public static boolean isBundledNotification(Node document) { @@ -97,7 +131,7 @@ public class NotificationUtils { switch (type) { case THANK_YOU_EDIT: - notificationText = context.getString(R.string.notifications_thank_you_edit); + notificationText = getThankYouEditDescription(document); break; case EDIT_USER_TALK: notificationText = getNotificationText(document); @@ -146,6 +180,16 @@ public class NotificationUtils { return body != null ? body.getTextContent() : ""; } + /** + * Gets the header node returned in the XML document to form the description for thank you edits + * @param document + * @return + */ + private static String getThankYouEditDescription(Node document) { + Node body = getNode(getModel(document), "header"); + return body != null ? body.getTextContent() : ""; + } + private static String getNotificationIconUrl(Node document) { String format = "%s%s"; Node iconUrl = getNode(getModel(document), "iconUrl"); diff --git a/app/src/main/java/fr/free/nrw/commons/notification/SvgModule.java b/app/src/main/java/fr/free/nrw/commons/notification/SvgModule.java new file mode 100644 index 000000000..5a1e8ae63 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/SvgModule.java @@ -0,0 +1,35 @@ +package fr.free.nrw.commons.notification; + +import android.content.Context; +import android.graphics.drawable.PictureDrawable; +import android.support.annotation.NonNull; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.Registry; +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.module.AppGlideModule; +import com.caverock.androidsvg.SVG; + +import java.io.InputStream; + +import fr.free.nrw.commons.glide.SvgDecoder; +import fr.free.nrw.commons.glide.SvgDrawableTranscoder; + +/** + * Module for the SVG sample app. + */ +@GlideModule +public class SvgModule extends AppGlideModule { + @Override + public void registerComponents(@NonNull Context context, @NonNull Glide glide, + @NonNull Registry registry) { + registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder()) + .append(InputStream.class, SVG.class, new SvgDecoder()); + } + + // Disable manifest parsing to avoid adding similar modules twice. + @Override + public boolean isManifestParsingEnabled() { + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 037f0d792..d35170adf 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -3,13 +3,10 @@ package fr.free.nrw.commons.settings; import android.Manifest; import android.app.AlertDialog; import android.content.ActivityNotFoundException; -import android.content.ComponentName; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -24,8 +21,6 @@ import android.support.v4.content.FileProvider; import android.widget.Toast; import java.io.File; -import java.util.ArrayList; -import java.util.List; import javax.inject.Inject; import javax.inject.Named; @@ -35,7 +30,7 @@ import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.utils.FileUtils; +import fr.free.nrw.commons.upload.FileUtils; public class SettingsFragment extends PreferenceFragment { @@ -102,6 +97,11 @@ public class SettingsFragment extends PreferenceFragment { return true; }); + Preference betaTesterPreference = findPreference("becomeBetaTester"); + betaTesterPreference.setOnPreferenceClickListener(preference -> { + Utils.handleWebUrl(getActivity(),Uri.parse(getResources().getString(R.string.beta_opt_in_link))); + return true; + }); Preference sendLogsPreference = findPreference("sendLogFile"); sendLogsPreference.setOnPreferenceClickListener(preference -> { //first we need to check if we have the necessary permissions @@ -128,8 +128,8 @@ public class SettingsFragment extends PreferenceFragment { @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (requestCode == REQUEST_CODE_WRITE_EXTERNAL_STORAGE) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (requestCode == REQUEST_CODE_WRITE_EXTERNAL_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + { sendAppLogsViaEmail(); } } diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index b65d6aa25..4a7322b57 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -23,11 +23,11 @@ import fr.free.nrw.commons.AboutActivity; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.nearby.NearbyActivity; import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.settings.SettingsActivity; @@ -36,6 +36,8 @@ import timber.log.Timber; public abstract class NavigationBaseActivity extends BaseActivity implements NavigationView.OnNavigationItemSelectedListener { + private static final String FEATURED_IMAGES_CATEGORY = "Category:Featured_pictures_on_Wikimedia_Commons"; + @BindView(R.id.toolbar) Toolbar toolbar; @BindView(R.id.navigation_view) @@ -154,6 +156,10 @@ public abstract class NavigationBaseActivity extends BaseActivity drawerLayout.closeDrawer(navigationView); NotificationActivity.startYourself(this); return true; + case R.id.action_featured_images: + drawerLayout.closeDrawer(navigationView); + CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_featured_images), FEATURED_IMAGES_CATEGORY); + return true; default: Timber.e("Unknown option [%s] selected from the navigation menu", itemId); return false; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java index ab9fa5602..5a413e49a 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java @@ -1,10 +1,8 @@ package fr.free.nrw.commons.upload; import android.app.Activity; -import android.content.Context; import android.content.Intent; import android.graphics.BitmapRegionDecoder; -import android.net.Uri; import android.os.AsyncTask; import android.support.v7.app.AlertDialog; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java new file mode 100644 index 000000000..2845b8d1f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java @@ -0,0 +1,263 @@ +package fr.free.nrw.commons.upload; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatActivity; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.Date; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; + +import fr.free.nrw.commons.caching.CacheController; +import fr.free.nrw.commons.di.ApplicationlessInjection; +import fr.free.nrw.commons.mwapi.CategoryApi; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext; + +/** + * Processing of the image file that is about to be uploaded via ShareActivity is done here + */ +public class FileProcessor implements SimilarImageDialogFragment.onResponse { + + @Inject + CacheController cacheController; + @Inject + GpsCategoryModel gpsCategoryModel; + @Inject + CategoryApi apiCall; + @Inject + @Named("default_preferences") + SharedPreferences prefs; + private Uri mediaUri; + private ContentResolver contentResolver; + private GPSExtractor imageObj; + private Context context; + private String decimalCoords; + private boolean haveCheckedForOtherImages = false; + private String filePath; + private boolean useExtStorage; + private boolean cacheFound; + private GPSExtractor tempImageObj; + + FileProcessor(Uri mediaUri, ContentResolver contentResolver, Context context) { + this.mediaUri = mediaUri; + this.contentResolver = contentResolver; + this.context = context; + ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this); + useExtStorage = prefs.getBoolean("useExternalStorage", true); + } + + /** + * Gets file path from media URI. + * In older devices getPath() may fail depending on the source URI, creating and using a copy of the file seems to work instead. + * + * @return file path of media + */ + @Nullable + private String getPathOfMediaOrCopy() { + filePath = FileUtils.getPath(context, mediaUri); + Timber.d("Filepath: " + filePath); + if (filePath == null) { + String copyPath = null; + try { + ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r"); + if (descriptor != null) { + if (useExtStorage) { + copyPath = FileUtils.createCopyPath(descriptor); + return copyPath; + } + copyPath = getApplicationContext().getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + ".jpg"; + FileUtils.copy(descriptor.getFileDescriptor(), copyPath); + Timber.d("Filepath (copied): %s", copyPath); + return copyPath; + } + } catch (IOException e) { + Timber.w(e, "Error in file " + copyPath); + return null; + } + } + return filePath; + } + + /** + * Processes file coordinates, either from EXIF data or user location + * + * @param gpsEnabled if true use GPS + */ + GPSExtractor processFileCoordinates(boolean gpsEnabled) { + Timber.d("Calling GPSExtractor"); + try { + ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (descriptor != null) { + imageObj = new GPSExtractor(descriptor.getFileDescriptor(), context, prefs); + } + } else { + String filePath = getPathOfMediaOrCopy(); + if (filePath != null) { + imageObj = new GPSExtractor(filePath, context, prefs); + } + } + + decimalCoords = imageObj.getCoords(gpsEnabled); + if (decimalCoords == null || !imageObj.imageCoordsExists) { + //Find other photos taken around the same time which has gps coordinates + if (!haveCheckedForOtherImages) + findOtherImages(gpsEnabled);// Do not do repeat the process + } else { + useImageCoords(); + } + + } catch (FileNotFoundException e) { + Timber.w("File not found: " + mediaUri, e); + } + return imageObj; + } + + String getDecimalCoords() { + return decimalCoords; + } + + /** + * Find other images around the same location that were taken within the last 20 sec + * + * @param gpsEnabled True if GPS is enabled + */ + private void findOtherImages(boolean gpsEnabled) { + Timber.d("filePath" + getPathOfMediaOrCopy()); + + long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created + File folder = new File(filePath.substring(0, filePath.lastIndexOf('/'))); + File[] files = folder.listFiles(); + Timber.d("folderTime Number:" + files.length); + + + for (File file : files) { + if (file.lastModified() - timeOfCreation <= (120 * 1000) && file.lastModified() - timeOfCreation >= -(120 * 1000)) { + //Make sure the photos were taken within 20seconds + Timber.d("fild date:" + file.lastModified() + " time of creation" + timeOfCreation); + tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos + ParcelFileDescriptor descriptor = null; + try { + descriptor = contentResolver.openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r"); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (descriptor != null) { + tempImageObj = new GPSExtractor(descriptor.getFileDescriptor(), context, prefs); + } + } else { + if (filePath != null) { + tempImageObj = new GPSExtractor(file.getAbsolutePath(), context, prefs); + } + } + + if (tempImageObj != null) { + Timber.d("not null fild EXIF" + tempImageObj.imageCoordsExists + " coords" + tempImageObj.getCoords(gpsEnabled)); + if (tempImageObj.getCoords(gpsEnabled) != null && tempImageObj.imageCoordsExists) { + // Current image has gps coordinates and it's not current gps locaiton + Timber.d("This file has image coords:" + file.getAbsolutePath()); + SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment(); + Bundle args = new Bundle(); + args.putString("originalImagePath", filePath); + args.putString("possibleImagePath", file.getAbsolutePath()); + newFragment.setArguments(args); + newFragment.show(((AppCompatActivity) context).getSupportFragmentManager(), "dialog"); + break; + } + } + } + } + haveCheckedForOtherImages = true; //Finished checking for other images + } + + /** + * Initiates retrieval of image coordinates or user coordinates, and caching of coordinates. + * Then initiates the calls to MediaWiki API through an instance of CategoryApi. + */ + @SuppressLint("CheckResult") + public void useImageCoords() { + if (decimalCoords != null) { + Timber.d("Decimal coords of image: %s", decimalCoords); + Timber.d("is EXIF data present:" + imageObj.imageCoordsExists + " from findOther image"); + + // Only set cache for this point if image has coords + if (imageObj.imageCoordsExists) { + double decLongitude = imageObj.getDecLongitude(); + double decLatitude = imageObj.getDecLatitude(); + cacheController.setQtPoint(decLongitude, decLatitude); + } + + List displayCatList = cacheController.findCategory(); + boolean catListEmpty = displayCatList.isEmpty(); + + + // If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories + if (catListEmpty) { + cacheFound = false; + apiCall.request(decimalCoords) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe( + gpsCategoryModel::setCategoryList, + throwable -> { + Timber.e(throwable); + gpsCategoryModel.clear(); + } + ); + Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList); + } else { + cacheFound = true; + Timber.d("Cache found, setting categoryList in model to %s", displayCatList); + gpsCategoryModel.setCategoryList(displayCatList); + } + } else { + Timber.d("EXIF: no coords"); + } + } + + boolean isCacheFound() { + return cacheFound; + } + + /** + * Calls the async task that detects if image is fuzzy, too dark, etc + */ + void detectUnwantedPictures() { + String imageMediaFilePath = FileUtils.getPath(context, mediaUri); + DetectUnwantedPicturesAsync detectUnwantedPicturesAsync + = new DetectUnwantedPicturesAsync(new WeakReference((Activity) context), imageMediaFilePath); + detectUnwantedPicturesAsync.execute(); + } + + @Override + public void onPositiveResponse() { + imageObj = tempImageObj; + decimalCoords = imageObj.getCoords(false);// Not necessary to use gps as image already ha EXIF data + Timber.d("EXIF from tempImageObj"); + useImageCoords(); + } + + @Override + public void onNegativeResponse() { + Timber.d("EXIF from imageObj"); + useImageCoords(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index 13056ad4b..0cd45c189 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -15,18 +15,84 @@ import android.provider.MediaStore; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import java.io.BufferedReader; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.math.BigInteger; import java.nio.channels.FileChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Date; import timber.log.Timber; public class FileUtils { + /** + * Get SHA1 of file from input stream + */ + static String getSHA1(InputStream is) { + + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA1"); + } catch (NoSuchAlgorithmException e) { + Timber.e(e, "Exception while getting Digest"); + return ""; + } + + byte[] buffer = new byte[8192]; + int read; + try { + while ((read = is.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + byte[] md5sum = digest.digest(); + BigInteger bigInt = new BigInteger(1, md5sum); + String output = bigInt.toString(16); + // Fill to 40 chars + output = String.format("%40s", output).replace(' ', '0'); + Timber.i("File SHA1: %s", output); + + return output; + } catch (IOException e) { + Timber.e(e, "IO Exception"); + return ""; + } finally { + try { + is.close(); + } catch (IOException e) { + Timber.e(e, "Exception on closing MD5 input stream"); + } + } + } + + /** + * In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead. + * @return path of copy + */ + @Nullable + static String createCopyPath(ParcelFileDescriptor descriptor) { + try { + String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + ".jpg"; + File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp"); + newFile.mkdir(); + FileUtils.copy(descriptor.getFileDescriptor(), copyPath); + Timber.d("Filepath (copied): %s", copyPath); + return copyPath; + } catch (IOException e) { + Timber.e(e); + return null; + } + } + /** * Get a file path from a Uri. This will get the the path for Storage Access * Framework Documents, as well as the _data field for the MediaStore and @@ -59,7 +125,7 @@ public class FileUtils { final String id = DocumentsContract.getDocumentId(uri); final Uri contentUri = ContentUris.withAppendedId( - Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + Uri.parse("content://downloads/document"), Long.valueOf(id)); returnPath = getDataColumn(context, contentUri, null, null); } else if (isMediaDocument(uri)) { // MediaProvider @@ -235,4 +301,80 @@ public class FileUtils { copy(new FileInputStream(source), new FileOutputStream(destination)); } + + /** + * Read and return the content of a resource file as string. + * @param fileName asset file's path (e.g. "/queries/nearby_query.rq") + * @return the content of the file + */ + public static String readFromResource(String fileName) throws IOException { + StringBuilder buffer = new StringBuilder(); + BufferedReader reader = null; + try { + InputStream inputStream = FileUtils.class.getResourceAsStream(fileName); + if (inputStream == null) { + throw new FileNotFoundException(fileName); + } + reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); + String line; + while ((line = reader.readLine()) != null) { + buffer.append(line).append("\n"); + } + } finally { + if (reader != null) { + reader.close(); + } + } + return buffer.toString(); + } + + /** + * Deletes files. + * @param file context + */ + public static boolean deleteFile(File file) { + boolean deletedAll = true; + if (file != null) { + if (file.isDirectory()) { + String[] children = file.list(); + for (String child : children) { + deletedAll = deleteFile(new File(file, child)) && deletedAll; + } + } else { + deletedAll = file.delete(); + } + } + + return deletedAll; + } + + public static File createAndGetAppLogsFile(String logs) { + try { + File commonsAppDirectory = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp"); + if (!commonsAppDirectory.exists()) { + commonsAppDirectory.mkdir(); + } + + File logsFile = new File(commonsAppDirectory,"logs.txt"); + if (logsFile.exists()) { + //old logs file is useless + logsFile.delete(); + } + + logsFile.createNewFile(); + + FileOutputStream outputStream = new FileOutputStream(logsFile); + OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); + outputStreamWriter.append(logs); + outputStreamWriter.close(); + outputStream.flush(); + outputStream.close(); + + return logsFile; + } catch (IOException ioe) { + Timber.e(ioe); + return null; + } + } + } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GpsCategoryModel.java b/app/src/main/java/fr/free/nrw/commons/upload/GpsCategoryModel.java new file mode 100644 index 000000000..841210453 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/GpsCategoryModel.java @@ -0,0 +1,40 @@ +package fr.free.nrw.commons.upload; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class GpsCategoryModel { + private Set categorySet; + + @Inject + public GpsCategoryModel() { + clear(); + } + + public void clear() { + categorySet = new HashSet<>(); + } + + public boolean getGpsCatExists() { + return !categorySet.isEmpty(); + } + + public List getCategoryList() { + return new ArrayList<>(categorySet); + } + + public void setCategoryList(List categoryList) { + clear(); + categorySet.addAll(categoryList != null ? categoryList : new ArrayList<>()); + } + + public void add(String categoryString) { + categorySet.add(categoryString); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java index ac0afa979..58d6d61ca 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java @@ -47,6 +47,8 @@ import fr.free.nrw.commons.modifications.TemplateRemoveModifier; import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; +//TODO: We should use this class to see how multiple uploads are handled, and then REMOVE it. + public class MultipleShareActivity extends AuthenticatedActivity implements MediaDetailPagerFragment.MediaDetailProvider, AdapterView.OnItemClickListener, @@ -166,7 +168,8 @@ public class MultipleShareActivity extends AuthenticatedActivity View target = getCurrentFocus(); if (target != null) { InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(target.getWindowToken(), 0); + if (imm != null) + imm.hideSoftInputFromWindow(target.getWindowToken(), 0); } getSupportFragmentManager().beginTransaction() .add(R.id.uploadsFragmentContainer, categorizationFragment, "categorization") @@ -221,8 +224,8 @@ public class MultipleShareActivity extends AuthenticatedActivity //TODO: 15/10/17 should location permission be explicitly requested if not provided? //check if location permission is enabled - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + { locationPermitted = true; } } @@ -237,7 +240,7 @@ public class MultipleShareActivity extends AuthenticatedActivity private void showDetail(int i) { if (mediaDetails == null || !mediaDetails.isVisible()) { - mediaDetails = new MediaDetailPagerFragment(true); + mediaDetails = new MediaDetailPagerFragment(true, false); getSupportFragmentManager() .beginTransaction() .replace(R.id.uploadsFragmentContainer, mediaDetails) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java index 5b39b92f7..028456bb6 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java @@ -1,6 +1,5 @@ package fr.free.nrw.commons.upload; -import android.app.Activity; import android.content.Context; import android.graphics.Point; import android.net.Uri; @@ -11,14 +10,12 @@ import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.DisplayMetrics; -import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.EditText; @@ -27,6 +24,8 @@ import android.widget.GridView; import android.widget.RelativeLayout; import android.widget.TextView; +import butterknife.BindView; +import butterknife.ButterKnife; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.view.SimpleDraweeView; @@ -34,6 +33,7 @@ import dagger.android.support.AndroidSupportInjection; import fr.free.nrw.commons.R; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.media.MediaDetailPagerFragment; +import fr.free.nrw.commons.utils.ViewUtil; public class MultipleUploadListFragment extends Fragment { @@ -41,9 +41,13 @@ public class MultipleUploadListFragment extends Fragment { void OnMultipleUploadInitiated(); } - private GridView photosGrid; + @BindView(R.id.multipleShareBackground) + GridView photosGrid; + + @BindView(R.id.multipleBaseTitle) + EditText baseTitle; + private PhotoDisplayAdapter photosAdapter; - private EditText baseTitle; private TitleTextWatcher textWatcher = new TitleTextWatcher(); private Point photoSize; @@ -89,9 +93,9 @@ public class MultipleUploadListFragment extends Fragment { if (view == null) { view = LayoutInflater.from(getContext()).inflate(R.layout.layout_upload_item, viewGroup, false); holder = new UploadHolderView(); - holder.image = (SimpleDraweeView) view.findViewById(R.id.uploadImage); - holder.title = (TextView) view.findViewById(R.id.uploadTitle); - holder.overlay = (RelativeLayout) view.findViewById(R.id.uploadOverlay); + holder.image = view.findViewById(R.id.uploadImage); + holder.title = view.findViewById(R.id.uploadTitle); + holder.overlay = view.findViewById(R.id.uploadOverlay); holder.image.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, photoSize.y)); holder.image.setHierarchy(GenericDraweeHierarchyBuilder @@ -129,11 +133,8 @@ public class MultipleUploadListFragment extends Fragment { super.onStop(); // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next - View target = getView().findFocus(); - if (target != null) { - InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(target.getWindowToken(), 0); - } + View target = getActivity().getCurrentFocus(); + ViewUtil.hideKeyboard(target); } // FIXME: Wrong result type @@ -169,9 +170,7 @@ public class MultipleUploadListFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_multiple_uploads_list, container, false); - photosGrid = (GridView) view.findViewById(R.id.multipleShareBackground); - baseTitle = (EditText) view.findViewById(R.id.multipleBaseTitle); - + ButterKnife.bind(this,view); photosAdapter = new PhotoDisplayAdapter(); photosGrid.setAdapter(photosAdapter); photosGrid.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity()); @@ -182,18 +181,13 @@ public class MultipleUploadListFragment extends Fragment { baseTitle.setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus) { - hideKeyboard(v); + ViewUtil.hideKeyboard(v); } }); return view; } - public void hideKeyboard(View view) { - InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); - } - @Override public void onDestroyView() { baseTitle.removeTextChangedListener(textWatcher); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java b/app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java deleted file mode 100644 index a530e79e6..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java +++ /dev/null @@ -1,249 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.content.Context; -import android.net.Uri; - -import com.android.volley.Cache; -import com.android.volley.NetworkResponse; -import com.android.volley.Request; -import com.android.volley.RequestQueue; -import com.android.volley.Response; -import com.android.volley.VolleyError; -import com.android.volley.toolbox.HttpHeaderParser; -import com.android.volley.toolbox.JsonRequest; -import com.android.volley.toolbox.Volley; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import timber.log.Timber; - -/** - * Uses the Volley library to implement asynchronous calls to the Commons MediaWiki API to match - * GPS coordinates with nearby Commons categories. Parses the results using GSON to obtain a list - * of relevant categories. - */ -public class MwVolleyApi { - - private static RequestQueue REQUEST_QUEUE; - private static final Gson GSON = new GsonBuilder().create(); - - private static Set categorySet; - private static List categoryList; - - private static final String MWURL = "https://commons.wikimedia.org/"; - private final Context context; - - public MwVolleyApi(Context context) { - this.context = context; - categorySet = new HashSet<>(); - } - - public static List getGpsCat() { - return categoryList; - } - - public static void setGpsCat(List cachedList) { - categoryList = new ArrayList<>(); - categoryList.addAll(cachedList); - Timber.d("Setting GPS cats from cache: %s", categoryList); - } - - public void request(String coords) { - String apiUrl = buildUrl(coords); - Timber.d("URL: %s", apiUrl); - - JsonRequest request = new QueryRequest(apiUrl, - new LogResponseListener<>(), new LogResponseErrorListener()); - getQueue().add(request); - } - - /** - * Builds URL with image coords for MediaWiki API calls - * Example URL: https://commons.wikimedia.org/w/api.php?action=query&prop=categories|coordinates|pageprops&format=json&clshow=!hidden&coprop=type|name|dim|country|region|globe&codistancefrompoint=38.11386944444445|13.356263888888888&generator=geosearch&redirects=&ggscoord=38.11386944444445|1.356263888888888&ggsradius=100&ggslimit=10&ggsnamespace=6&ggsprop=type|name|dim|country|region|globe&ggsprimary=all&formatversion=2 - * @param coords Coordinates to build query with - * @return URL for API query - */ - private String buildUrl(String coords) { - - Uri.Builder builder = Uri.parse(MWURL).buildUpon(); - - builder.appendPath("w") - .appendPath("api.php") - .appendQueryParameter("action", "query") - .appendQueryParameter("prop", "categories|coordinates|pageprops") - .appendQueryParameter("format", "json") - .appendQueryParameter("clshow", "!hidden") - .appendQueryParameter("coprop", "type|name|dim|country|region|globe") - .appendQueryParameter("codistancefrompoint", coords) - .appendQueryParameter("generator", "geosearch") - .appendQueryParameter("ggscoord", coords) - .appendQueryParameter("ggsradius", "10000") - .appendQueryParameter("ggslimit", "10") - .appendQueryParameter("ggsnamespace", "6") - .appendQueryParameter("ggsprop", "type|name|dim|country|region|globe") - .appendQueryParameter("ggsprimary", "all") - .appendQueryParameter("formatversion", "2"); - - return builder.toString(); - } - - private synchronized RequestQueue getQueue() { - if (REQUEST_QUEUE == null) { - REQUEST_QUEUE = Volley.newRequestQueue(context); - } - return REQUEST_QUEUE; - } - - private static class LogResponseListener implements Response.Listener { - - @Override - public void onResponse(T response) { - Timber.d(response.toString()); - } - } - - private static class LogResponseErrorListener implements Response.ErrorListener { - - @Override - public void onErrorResponse(VolleyError error) { - Timber.e(error.toString()); - } - } - - private static class QueryRequest extends JsonRequest { - - public QueryRequest(String url, - Response.Listener listener, - Response.ErrorListener errorListener) { - super(Request.Method.GET, url, null, listener, errorListener); - } - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - String json = parseString(response); - QueryResponse queryResponse = GSON.fromJson(json, QueryResponse.class); - return Response.success(queryResponse, cacheEntry(response)); - } - - private Cache.Entry cacheEntry(NetworkResponse response) { - return HttpHeaderParser.parseCacheHeaders(response); - } - - private String parseString(NetworkResponse response) { - try { - return new String(response.data, HttpHeaderParser.parseCharset(response.headers)); - } catch (UnsupportedEncodingException e) { - return new String(response.data); - } - } - } - - public static class GpsCatExists { - private static boolean gpsCatExists; - - public static void setGpsCatExists(boolean gpsCat) { - gpsCatExists = gpsCat; - } - - public static boolean getGpsCatExists() { - return gpsCatExists; - } - } - - private static class QueryResponse { - private Query query = new Query(); - - private String printSet() { - if (categorySet == null || categorySet.isEmpty()) { - GpsCatExists.setGpsCatExists(false); - Timber.d("gpsCatExists=%b", GpsCatExists.getGpsCatExists()); - return "No collection of categories"; - } else { - GpsCatExists.setGpsCatExists(true); - Timber.d("gpsCatExists=%b", GpsCatExists.getGpsCatExists()); - return "CATEGORIES FOUND" + categorySet.toString(); - } - - } - - @Override - public String toString() { - if (query != null) { - return "query=" + query.toString() + "\n" + printSet(); - } else { - return "No pages found"; - } - } - } - - private static class Query { - private Page [] pages; - - @Override - public String toString() { - StringBuilder builder = new StringBuilder("pages=" + "\n"); - if (pages != null) { - for (Page page : pages) { - builder.append(page.toString()); - builder.append("\n"); - } - builder.replace(builder.length() - 1, builder.length(), ""); - return builder.toString(); - } else { - return "No pages found"; - } - } - } - - public static class Page { - private int pageid; - private int ns; - private String title; - private Category[] categories; - private Category category; - - public Page() { - } - - @Override - public String toString() { - - StringBuilder builder = new StringBuilder("PAGEID=" + pageid + " ns=" + ns + " title=" + title + "\n" + " CATEGORIES= "); - - if (categories == null || categories.length == 0) { - builder.append("no categories exist\n"); - } else { - for (Category category : categories) { - builder.append(category.toString()); - builder.append("\n"); - if (category != null) { - String categoryString = category.toString().replace("Category:", ""); - categorySet.add(categoryString); - } - } - } - - categoryList = new ArrayList<>(categorySet); - builder.replace(builder.length() - 1, builder.length(), ""); - return builder.toString(); - } - } - - private static class Category { - private String title; - - @Override - public String toString() { - return title; - } - } -} - - - diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java index aca17601c..5db101862 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java @@ -1,50 +1,53 @@ package fr.free.nrw.commons.upload; import android.Manifest; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; -import android.os.ParcelFileDescriptor; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; +import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.graphics.drawable.VectorDrawableCompat; import android.support.v4.app.ActivityCompat; -import android.support.v4.app.FragmentManager; import android.support.v4.content.ContextCompat; -import android.support.v7.app.AlertDialog; +import android.view.KeyEvent; import android.view.MenuItem; import android.view.View; -import android.widget.TextView; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; import android.widget.Toast; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.view.SimpleDraweeView; +import com.github.chrisbanes.photoview.PhotoView; -import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; -import java.math.BigInteger; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Date; import java.util.List; import javax.inject.Inject; import javax.inject.Named; +import butterknife.BindView; import butterknife.ButterKnife; +import butterknife.OnClick; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.AuthenticatedActivity; import fr.free.nrw.commons.auth.SessionManager; @@ -52,19 +55,19 @@ import fr.free.nrw.commons.caching.CacheController; import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.category.OnCategoriesSaveHandler; import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.modifications.CategoryModifier; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.ModifierSequenceDao; import fr.free.nrw.commons.modifications.TemplateRemoveModifier; - -import fr.free.nrw.commons.utils.ImageUtils; +import fr.free.nrw.commons.mwapi.CategoryApi; import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.utils.ViewUtil; import timber.log.Timber; import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.DUPLICATE_PROCEED; import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE; +import static fr.free.nrw.commons.upload.FileUtils.getSHA1; /** * Activity for the title/desc screen after image is selected. Also starts processing image @@ -73,14 +76,13 @@ import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE; public class ShareActivity extends AuthenticatedActivity implements SingleUploadFragment.OnUploadActionInitiated, - OnCategoriesSaveHandler,SimilarImageDialogFragment.onResponse { - - private static final int REQUEST_PERM_ON_CREATE_STORAGE = 1; + OnCategoriesSaveHandler { private static final int REQUEST_PERM_ON_CREATE_LOCATION = 2; - private static final int REQUEST_PERM_ON_CREATE_STORAGE_AND_LOCATION = 3; private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4; - private CategorizationFragment categorizationFragment; - + //Had to make them class variables, to extract out the click listeners, also I see no harm in this + final Rect startBounds = new Rect(); + final Rect finalBounds = new Rect(); + final Point globalOffset = new Point(); @Inject MediaWikiApi mwApi; @Inject @@ -92,36 +94,55 @@ public class ShareActivity @Inject ModifierSequenceDao modifierSequenceDao; @Inject + CategoryApi apiCall; + @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject + GpsCategoryModel gpsCategoryModel; + + @BindView(R.id.container) + FrameLayout flContainer; + @BindView(R.id.backgroundImage) + SimpleDraweeView backgroundImageView; + @BindView(R.id.media_map) + FloatingActionButton mapButton; + @BindView(R.id.media_upload_zoom_in) + FloatingActionButton zoomInButton; + @BindView(R.id.media_upload_zoom_out) + FloatingActionButton zoomOutButton; + @BindView(R.id.main_fab) + FloatingActionButton mainFab; + @BindView(R.id.expanded_image) + PhotoView expandedImageView; private String source; private String mimeType; - + private CategorizationFragment categorizationFragment; private Uri mediaUri; private Contribution contribution; - private SimpleDraweeView backgroundImageView; - - private boolean cacheFound; - - private GPSExtractor imageObj; - private GPSExtractor tempImageObj; + private GPSExtractor gpsObj; private String decimalCoords; - + private FileProcessor fileObj; private boolean useNewPermissions = false; private boolean storagePermitted = false; private boolean locationPermitted = false; - private String title; private String description; + private String wikiDataEntityId; private Snackbar snackbar; private boolean duplicateCheckPassed = false; - - private boolean haveCheckedForOtherImages = false; private boolean isNearbyUpload = false; + private Animator CurrentAnimator; + private long ShortAnimationDuration; + private boolean isFABOpen = false; + private float startScaleFinal; + private boolean isZoom = false; + /** * Called when user taps the submit button. + * Requests Storage permission, if needed. */ @Override public void uploadActionInitiated(String title, String description) { @@ -130,8 +151,6 @@ public class ShareActivity this.description = description; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // Check for Storage permission that is required for upload. - // Do not allow user to proceed without permission, otherwise will crash if (needsToRequestStoragePermission()) { requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_PERM_ON_SUBMIT_STORAGE); @@ -143,34 +162,44 @@ public class ShareActivity } } + /** + * Checks whether storage permissions need to be requested. + * Permissions are needed if the file is not owned by this application, (e.g. shared from the Gallery) + * + * @return true if file is not owned by this application and permission hasn't been granted beforehand + */ @RequiresApi(16) private boolean needsToRequestStoragePermission() { - // We need to ask storage permission when - // the file is not owned by this application, (e.g. shared from the Gallery) - // and permission is not obtained. return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri) && (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED); } + /** + * Called after permission checks are done. + * Gets file metadata for category suggestions, displays toast, caches categories found, calls uploadController + */ private void uploadBegins() { - getFileMetadata(locationPermitted); + fileObj.processFileCoordinates(locationPermitted); Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG); startingToast.show(); - if (!cacheFound) { + if (!fileObj.isCacheFound()) { //Has to be called after apiCall.request() cacheController.cacheCategory(); Timber.d("Cache the categories found"); } - uploadController.startUpload(title, mediaUri, description, mimeType, source, decimalCoords, c -> { + uploadController.startUpload(title, mediaUri, description, mimeType, source, decimalCoords, wikiDataEntityId, c -> { ShareActivity.this.contribution = c; showPostUpload(); }); } + /** + * Starts CategorizationFragment after uploadBegins. + */ private void showPostUpload() { if (categorizationFragment == null) { categorizationFragment = new CategorizationFragment(); @@ -180,6 +209,11 @@ public class ShareActivity .commit(); } + /** + * Send categories to modifications queue after they are selected + * + * @param categories categories selected + */ @Override public void onCategoriesSave(List categories) { if (categories.size() > 0) { @@ -217,9 +251,6 @@ public class ShareActivity finish(); } - protected boolean isNearbyUpload() { - return isNearbyUpload; - } @Override public void onCreate(Bundle savedInstanceState) { @@ -228,7 +259,6 @@ public class ShareActivity setContentView(R.layout.activity_share); ButterKnife.bind(this); initBack(); - backgroundImageView = (SimpleDraweeView) findViewById(R.id.backgroundImage); backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder .newInstance(getResources()) .setPlaceholderImage(VectorDrawableCompat.create(getResources(), @@ -237,7 +267,54 @@ public class ShareActivity R.drawable.ic_error_outline_black_24dp, getTheme())) .build()); - //Receive intent from ContributionController.java when user selects picture to upload + receiveImageIntent(); + + if (savedInstanceState != null) { + contribution = savedInstanceState.getParcelable("contribution"); + } + + requestAuthToken(); + Timber.d("Uri: %s", mediaUri.toString()); + Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory()); + + useNewPermissions = false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + useNewPermissions = true; + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + locationPermitted = true; + } + } + + // Check location permissions if M or newer for category suggestions, request via snackbar if not present + if (!locationPermitted) { + requestPermissionUsingSnackBar( + getString(R.string.location_permission_rationale), + new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, + REQUEST_PERM_ON_CREATE_LOCATION); + } + + SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView"); + categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization"); + if (shareView == null && categorizationFragment == null) { + shareView = new SingleUploadFragment(); + getSupportFragmentManager() + .beginTransaction() + .add(R.id.single_upload_fragment_container, shareView, "shareView") + .commitAllowingStateLoss(); + } + uploadController.prepareService(); + + ContentResolver contentResolver = this.getContentResolver(); + fileObj = new FileProcessor(mediaUri, contentResolver, this); + checkIfFileExists(); + gpsObj = fileObj.processFileCoordinates(locationPermitted); + decimalCoords = fileObj.getDecimalCoords(); + } + + /** + * Receive intent from ContributionController.java when user selects picture to upload + */ + private void receiveImageIntent() { Intent intent = getIntent(); if (Intent.ACTION_SEND.equals(intent.getAction())) { @@ -257,174 +334,100 @@ public class ShareActivity if (mediaUri != null) { backgroundImageView.setImageURI(mediaUri); } - - if (savedInstanceState != null) { - contribution = savedInstanceState.getParcelable("contribution"); - } - - requestAuthToken(); - - Timber.d("Uri: %s", mediaUri.toString()); - Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory()); - - useNewPermissions = false; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - useNewPermissions = true; - - if (!needsToRequestStoragePermission()) { - storagePermitted = true; - } - if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { - locationPermitted = true; - } - } - - // Check storage permissions if marshmallow or newer - if (useNewPermissions && (!storagePermitted || !locationPermitted)) { - if (!storagePermitted && !locationPermitted) { - String permissionRationales = - getResources().getString(R.string.read_storage_permission_rationale) + "\n" - + getResources().getString(R.string.location_permission_rationale); - snackbar = requestPermissionUsingSnackBar( - permissionRationales, - new String[]{ - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.ACCESS_FINE_LOCATION}, - REQUEST_PERM_ON_CREATE_STORAGE_AND_LOCATION); - View snackbarView = snackbar.getView(); - TextView textView = (TextView) snackbarView.findViewById(android.support.design.R.id.snackbar_text); - textView.setMaxLines(3); - } else if (!storagePermitted) { - requestPermissionUsingSnackBar( - getString(R.string.read_storage_permission_rationale), - new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_PERM_ON_CREATE_STORAGE); - } else if (!locationPermitted) { - requestPermissionUsingSnackBar( - getString(R.string.location_permission_rationale), - new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, - REQUEST_PERM_ON_CREATE_LOCATION); - } - } - performPreUploadProcessingOfFile(); - - - SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView"); - categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization"); - if (shareView == null && categorizationFragment == null) { - shareView = new SingleUploadFragment(); - getSupportFragmentManager() - .beginTransaction() - .add(R.id.single_upload_fragment_container, shareView, "shareView") - .commitAllowingStateLoss(); - } - uploadController.prepareService(); } + /** + * Function to display the zoom and map FAB + */ + private void showFABMenu() { + isFABOpen = true; + + if (gpsObj != null && gpsObj.imageCoordsExists) + mapButton.setVisibility(View.VISIBLE); + zoomInButton.setVisibility(View.VISIBLE); + + mainFab.animate().rotationBy(180); + mapButton.animate().translationY(-getResources().getDimension(R.dimen.second_fab)); + zoomInButton.animate().translationY(-getResources().getDimension(R.dimen.first_fab)); + } + + /** + * Function to close the zoom and map FAB + */ + private void closeFABMenu() { + isFABOpen = false; + mainFab.animate().rotationBy(-180); + mapButton.animate().translationY(0); + zoomInButton.animate().translationY(0).setListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animator) { + } + + @Override + public void onAnimationEnd(Animator animator) { + if (!isFABOpen) { + mapButton.setVisibility(View.GONE); + zoomInButton.setVisibility(View.GONE); + } + } + + @Override + public void onAnimationCancel(Animator animator) { + } + + @Override + public void onAnimationRepeat(Animator animator) { + } + }); + } + + /** + * Checks if upload was initiated via Nearby + * + * @return true if upload was initiated via Nearby + */ + protected boolean isNearbyUpload() { + return isNearbyUpload; + } + + /** + * Handles BOTH snackbar permission request (for location) and submit button permission request (for storage) + * + * @param requestCode type of request + * @param permissions permissions requested + * @param grantResults grant results + */ @Override - public void onRequestPermissionsResult(int requestCode, - @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { - case REQUEST_PERM_ON_CREATE_STORAGE: { - if (grantResults.length >= 1 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - backgroundImageView.setImageURI(mediaUri); - storagePermitted = true; - performPreUploadProcessingOfFile(); - } - return; - } case REQUEST_PERM_ON_CREATE_LOCATION: { - if (grantResults.length >= 1 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { locationPermitted = true; - performPreUploadProcessingOfFile(); - } - return; - } - case REQUEST_PERM_ON_CREATE_STORAGE_AND_LOCATION: { - if (grantResults.length >= 2 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - backgroundImageView.setImageURI(mediaUri); - storagePermitted = true; - performPreUploadProcessingOfFile(); - } - if (grantResults.length >= 2 - && grantResults[1] == PackageManager.PERMISSION_GRANTED) { - locationPermitted = true; - performPreUploadProcessingOfFile(); + checkIfFileExists(); } return; } + // Storage (from submit button) - this needs to be separate from (1) because only the // submit button should bring user to next screen case REQUEST_PERM_ON_SUBMIT_STORAGE: { - if (grantResults.length >= 1 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { //It is OK to call this at both (1) and (4) because if perm had been granted at //snackbar, user should not be prompted at submit button - performPreUploadProcessingOfFile(); + checkIfFileExists(); //Uploading only begins if storage permission granted from arrow icon uploadBegins(); snackbar.dismiss(); } - return; } } } - private void performPreUploadProcessingOfFile() { - if (!useNewPermissions || storagePermitted) { - if (!duplicateCheckPassed) { - //Test SHA1 of image to see if it matches SHA1 of a file on Commons - try { - InputStream inputStream = getContentResolver().openInputStream(mediaUri); - Timber.d("Input stream created from %s", mediaUri.toString()); - String fileSHA1 = getSHA1(inputStream); - Timber.d("File SHA1 is: %s", fileSHA1); - - ExistingFileAsync fileAsyncTask = - new ExistingFileAsync(new WeakReference(this), fileSHA1, new WeakReference(this), result -> { - Timber.d("%s duplicate check: %s", mediaUri.toString(), result); - duplicateCheckPassed = (result == DUPLICATE_PROCEED - || result == NO_DUPLICATE); - /* - TODO: 16/9/17 should we run DetectUnwantedPicturesAsync if DUPLICATE_PROCEED is returned? Since that means - we are processing images that are already on server???... - */ - - if (duplicateCheckPassed) { - //image can be uploaded, so now check if its a useless picture or not - performUnwantedPictureDetectionProcess(); - } - - },mwApi); - fileAsyncTask.execute(); - } catch (IOException e) { - Timber.d(e, "IO Exception: "); - } - } - - getFileMetadata(locationPermitted); - } else { - Timber.w("not ready for preprocessing: useNewPermissions=%s storage=%s location=%s", - useNewPermissions, storagePermitted, locationPermitted); - } - } - - private void performUnwantedPictureDetectionProcess() { - String imageMediaFilePath = FileUtils.getPath(this,mediaUri); - DetectUnwantedPicturesAsync detectUnwantedPicturesAsync - = new DetectUnwantedPicturesAsync(new WeakReference(this) - , imageMediaFilePath); - - detectUnwantedPicturesAsync.execute(); - } - - private Snackbar requestPermissionUsingSnackBar(String rationale, - final String[] perms, - final int code) { + /** + * Displays Snackbar to ask for location permissions + */ + private Snackbar requestPermissionUsingSnackBar(String rationale, final String[] perms, final int code) { Snackbar snackbar = Snackbar.make(findViewById(android.R.id.content), rationale, Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok, view -> ActivityCompat.requestPermissions(ShareActivity.this, perms, code)); @@ -432,202 +435,44 @@ public class ShareActivity return snackbar; } - @Nullable - private String getPathOfMediaOrCopy() { - String filePath = FileUtils.getPath(getApplicationContext(), mediaUri); - Timber.d("Filepath: " + filePath); - if (filePath == null) { - // in older devices getPath() may fail depending on the source URI - // creating and using a copy of the file seems to work instead. - // TODO: there might be a more proper solution than this - String copyPath = null; - try { - ParcelFileDescriptor descriptor - = getContentResolver().openFileDescriptor(mediaUri, "r"); - if (descriptor != null) { - boolean useExtStorage = prefs.getBoolean("useExternalStorage", true); - if (useExtStorage) { - copyPath = Environment.getExternalStorageDirectory().toString() - + "/CommonsApp/" + new Date().getTime() + ".jpg"; - File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp"); - newFile.mkdir(); - FileUtils.copy( - descriptor.getFileDescriptor(), - copyPath); - Timber.d("Filepath (copied): %s", copyPath); - return copyPath; - } - copyPath = getApplicationContext().getCacheDir().getAbsolutePath() - + "/" + new Date().getTime() + ".jpg"; - FileUtils.copy( - descriptor.getFileDescriptor(), - copyPath); - Timber.d("Filepath (copied): %s", copyPath); - return copyPath; - } - } catch (IOException e) { - Timber.w(e, "Error in file " + copyPath); - return null; - } - } - return filePath; - } - /** - * Gets coordinates for category suggestions, either from EXIF data or user location - * - * @param gpsEnabled if true use GPS + * Check if file user wants to upload already exists on Commons */ - private void getFileMetadata(boolean gpsEnabled) { - Timber.d("Calling GPSExtractor"); - try { - if (imageObj == null) { - ParcelFileDescriptor descriptor - = getContentResolver().openFileDescriptor(mediaUri, "r"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (descriptor != null) { - imageObj = new GPSExtractor(descriptor.getFileDescriptor(), this, prefs); - } - } else { - String filePath = getPathOfMediaOrCopy(); - if (filePath != null) { - imageObj = new GPSExtractor(filePath, this, prefs); - } - } - } - - if (imageObj != null) { - // Gets image coords from exif data or user location - decimalCoords = imageObj.getCoords(gpsEnabled); - if(decimalCoords==null || !imageObj.imageCoordsExists){ -// Check if the location is from GPS or EXIF -// Find other photos taken around the same time which has gps coordinates - Timber.d("EXIF:false"); - Timber.d("EXIF call"+(imageObj==tempImageObj)); - if(!haveCheckedForOtherImages) - findOtherImages(gpsEnabled);// Do not do repeat the process - } - else { -// As the selected image has GPS data in EXIF go ahead with the same. - useImageCoords(); - } - } - } catch (FileNotFoundException e) { - Timber.w("File not found: " + mediaUri, e); - } - } - - private void findOtherImages(boolean gpsEnabled) { - Timber.d("filePath"+getPathOfMediaOrCopy()); - String filePath = getPathOfMediaOrCopy(); - long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created - File folder = new File(filePath.substring(0,filePath.lastIndexOf('/'))); - File[] files = folder.listFiles(); - Timber.d("folderTime Number:"+files.length); - - for(File file : files){ - if(file.lastModified()-timeOfCreation<=(120*1000) && file.lastModified()-timeOfCreation>=-(120*1000)){ - //Make sure the photos were taken within 20seconds - Timber.d("fild date:"+file.lastModified()+ " time of creation"+timeOfCreation); - tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos - ParcelFileDescriptor descriptor - = null; + private void checkIfFileExists() { + if (!useNewPermissions || storagePermitted) { + if (!duplicateCheckPassed) { + //Test SHA1 of image to see if it matches SHA1 of a file on Commons try { - descriptor = getContentResolver().openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r"); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (descriptor != null) { - tempImageObj = new GPSExtractor(descriptor.getFileDescriptor(),this, prefs); - } - } else { - if (filePath != null) { - tempImageObj = new GPSExtractor(file.getAbsolutePath(), this, prefs); - } - } - - if(tempImageObj!=null){ - Timber.d("not null fild EXIF"+tempImageObj.imageCoordsExists +" coords"+tempImageObj.getCoords(gpsEnabled)); - if(tempImageObj.getCoords(gpsEnabled)!=null && tempImageObj.imageCoordsExists){ -// Current image has gps coordinates and it's not current gps locaiton - Timber.d("This fild has image coords:"+ file.getAbsolutePath()); -// Create a dialog fragment for the suggestion - FragmentManager fragmentManager = getSupportFragmentManager(); - SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment(); - Bundle args = new Bundle(); - args.putString("originalImagePath",filePath); - args.putString("possibleImagePath",file.getAbsolutePath()); - newFragment.setArguments(args); - newFragment.show(fragmentManager, "dialog"); - break; - } + InputStream inputStream = getContentResolver().openInputStream(mediaUri); + String fileSHA1 = getSHA1(inputStream); + Timber.d("Input stream created from %s", mediaUri.toString()); + Timber.d("File SHA1 is: %s", fileSHA1); + ExistingFileAsync fileAsyncTask = + new ExistingFileAsync(new WeakReference(this), fileSHA1, new WeakReference(this), result -> { + Timber.d("%s duplicate check: %s", mediaUri.toString(), result); + duplicateCheckPassed = (result == DUPLICATE_PROCEED || result == NO_DUPLICATE); + if (duplicateCheckPassed) { + //image is not a duplicate, so now check if its a unwanted picture or not + fileObj.detectUnwantedPictures(); + } + }, mwApi); + fileAsyncTask.execute(); + } catch (IOException e) { + Timber.e(e, "IO Exception: "); } - } + } else { + Timber.w("not ready for preprocessing: useNewPermissions=%s storage=%s location=%s", + useNewPermissions, storagePermitted, locationPermitted); } - haveCheckedForOtherImages = true; //Finished checking for other images - return; - } - - @Override - public void onPostiveResponse() { - imageObj = tempImageObj; - decimalCoords = imageObj.getCoords(false);// Not necessary to use gps as image already ha EXIF data - Timber.d("EXIF from tempImageObj"); - useImageCoords(); - } - - @Override - public void onNegativeResponse() { - Timber.d("EXIF from imageObj"); - useImageCoords(); - - } - - /** - * Initiates retrieval of image coordinates or user coordinates, and caching of coordinates. - * Then initiates the calls to MediaWiki API through an instance of MwVolleyApi. - */ - public void useImageCoords() { - if (decimalCoords != null) { - Timber.d("Decimal coords of image: %s", decimalCoords); - Timber.d("is EXIF data present:"+imageObj.imageCoordsExists+" from findOther image:"+(imageObj==tempImageObj)); - - // Only set cache for this point if image has coords - if (imageObj.imageCoordsExists) { - double decLongitude = imageObj.getDecLongitude(); - double decLatitude = imageObj.getDecLatitude(); - cacheController.setQtPoint(decLongitude, decLatitude); - } - - MwVolleyApi apiCall = new MwVolleyApi(this); - - List displayCatList = cacheController.findCategory(); - boolean catListEmpty = displayCatList.isEmpty(); - - // If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories - if (catListEmpty) { - cacheFound = false; - apiCall.request(decimalCoords); - Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList); - } else { - cacheFound = true; - Timber.d("Cache found, setting categoryList in MwVolleyApi to %s", displayCatList); - MwVolleyApi.setGpsCat(displayCatList); - } - }else{ - Timber.d("EXIF: no coords"); - } - } @Override public void onPause() { super.onPause(); try { - imageObj.unregisterLocationManager(); + gpsObj.unregisterLocationManager(); Timber.d("Unregistered locationManager"); } catch (NullPointerException e) { Timber.d("locationManager does not exist, not unregistered"); @@ -654,40 +499,157 @@ public class ShareActivity return super.onOptionsItemSelected(item); } - // Get SHA1 of file from input stream - private String getSHA1(InputStream is) { + /** + * Allows zooming in to the image about to be uploaded. Called when zoom FAB is tapped + */ + private void zoomImageFromThumb(final View thumbView, Uri imageuri) { + // If there's an animation in progress, cancel it immediately and proceed with this one. + if (CurrentAnimator != null) { + CurrentAnimator.cancel(); + } + isZoom = true; + ViewUtil.hideKeyboard(ShareActivity.this.findViewById(R.id.titleEdit | R.id.descEdit)); + closeFABMenu(); + mainFab.setVisibility(View.GONE); - MessageDigest digest; + InputStream input = null; try { - digest = MessageDigest.getInstance("SHA1"); - } catch (NoSuchAlgorithmException e) { - Timber.e(e, "Exception while getting Digest"); - return ""; + input = this.getContentResolver().openInputStream(imageuri); + } catch (FileNotFoundException e) { + e.printStackTrace(); } - byte[] buffer = new byte[8192]; - int read; - try { - while ((read = is.read(buffer)) > 0) { - digest.update(buffer, 0, read); - } - byte[] md5sum = digest.digest(); - BigInteger bigInt = new BigInteger(1, md5sum); - String output = bigInt.toString(16); - // Fill to 40 chars - output = String.format("%40s", output).replace(' ', '0'); - Timber.i("File SHA1: %s", output); + Zoom zoomObj = new Zoom(thumbView, flContainer, this.getContentResolver()); + Bitmap scaledImage = zoomObj.createScaledImage(input, imageuri); - return output; - } catch (IOException e) { - Timber.e(e, "IO Exception"); - return ""; - } finally { - try { - is.close(); - } catch (IOException e) { - Timber.e(e, "Exception on closing MD5 input stream"); + // Load the high-resolution "zoomed-in" image. + expandedImageView.setImageBitmap(scaledImage); + float startScale = zoomObj.adjustStartEndBounds(startBounds, finalBounds, globalOffset); + + // Hide the thumbnail and show the zoomed-in view. When the animation + // begins, it will position the zoomed-in view in the place of the + // thumbnail. + thumbView.setAlpha(0f); + expandedImageView.setVisibility(View.VISIBLE); + zoomOutButton.setVisibility(View.VISIBLE); + zoomInButton.setVisibility(View.GONE); + + // Set the pivot point for SCALE_X and SCALE_Y transformations + // to the top-left corner of the zoomed-in view (the default + // is the center of the view). + expandedImageView.setPivotX(0f); + expandedImageView.setPivotY(0f); + + // Construct and run the parallel animation of the four translation and + // scale properties (X, Y, SCALE_X, and SCALE_Y). + AnimatorSet set = new AnimatorSet(); + set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left, finalBounds.left)) + .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top, finalBounds.top)) + .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f)) + .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f)); + set.setDuration(ShortAnimationDuration); + set.setInterpolator(new DecelerateInterpolator()); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + CurrentAnimator = null; } + + @Override + public void onAnimationCancel(Animator animation) { + CurrentAnimator = null; + } + }); + set.start(); + CurrentAnimator = set; + + // Upon clicking the zoomed-in image, it should zoom back down + // to the original bounds and show the thumbnail instead of + // the expanded image. + startScaleFinal = startScale; + } + + /** + * Called when user taps the ^ FAB button, expands to show Zoom and Map + */ + @OnClick(R.id.main_fab) + public void onMainFabClicked() { + if (!isFABOpen) { + showFABMenu(); + } else { + closeFABMenu(); } } + + @OnClick(R.id.media_upload_zoom_in) + public void onZoomInFabClicked() { + try { + zoomImageFromThumb(backgroundImageView, mediaUri); + } catch (Exception e) { + Timber.e(e); + } + } + + @OnClick(R.id.media_upload_zoom_out) + public void onZoomOutFabClicked() { + if (CurrentAnimator != null) { + CurrentAnimator.cancel(); + } + isZoom = false; + zoomOutButton.setVisibility(View.GONE); + mainFab.setVisibility(View.VISIBLE); + + // Animate the four positioning/sizing properties in parallel, + // back to their original values. + AnimatorSet set = new AnimatorSet(); + set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left)) + .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top)) + .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScaleFinal)) + .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScaleFinal)); + + set.setDuration(ShortAnimationDuration); + set.setInterpolator(new DecelerateInterpolator()); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + //background image view is thumbView + backgroundImageView.setAlpha(1f); + expandedImageView.setVisibility(View.GONE); + CurrentAnimator = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + //background image view is thumbView + backgroundImageView.setAlpha(1f); + expandedImageView.setVisibility(View.GONE); + CurrentAnimator = null; + } + }); + set.start(); + CurrentAnimator = set; + } + + @OnClick(R.id.media_map) + public void onFabShowMapsClicked() { + if (gpsObj != null && gpsObj.imageCoordsExists) { + Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude()); + Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); + mapIntent.setPackage("com.google.android.apps.maps"); + startActivity(mapIntent); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + if(isZoom) { + onZoomOutFabClicked(); + return true; + } + } + return super.onKeyDown(keyCode,event); + + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java index a8f336927..59b8a1223 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java @@ -13,6 +13,9 @@ import android.view.ViewGroup; import android.view.Window; import android.widget.Button; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.view.SimpleDraweeView; import com.facebook.imagepipeline.listener.RequestListener; @@ -29,29 +32,33 @@ import fr.free.nrw.commons.R; */ public class SimilarImageDialogFragment extends DialogFragment { + + @BindView(R.id.orginalImage) SimpleDraweeView originalImage; + @BindView(R.id.possibleImage) SimpleDraweeView possibleImage; + @BindView(R.id.postive_button) Button positiveButton; + @BindView(R.id.negative_button) Button negativeButton; onResponse mOnResponse;//Implemented interface from shareActivity Boolean gotResponse = false; + public SimilarImageDialogFragment() { } public interface onResponse{ - public void onPostiveResponse(); + public void onPositiveResponse(); + public void onNegativeResponse(); } + @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_similar_image_dialog, container, false); + ButterKnife.bind(this,view); Set requestListeners = new HashSet<>(); requestListeners.add(new RequestLoggingListener()); - originalImage =(SimpleDraweeView) view.findViewById(R.id.orginalImage); - possibleImage =(SimpleDraweeView) view.findViewById(R.id.possibleImage); - positiveButton = (Button) view.findViewById(R.id.postive_button); - negativeButton = (Button) view.findViewById(R.id.negative_button); - originalImage.setHierarchy(GenericDraweeHierarchyBuilder .newInstance(getResources()) .setPlaceholderImage(VectorDrawableCompat.create(getResources(), @@ -70,22 +77,6 @@ public class SimilarImageDialogFragment extends DialogFragment { originalImage.setImageURI(Uri.fromFile(new File(getArguments().getString("originalImagePath")))); possibleImage.setImageURI(Uri.fromFile(new File(getArguments().getString("possibleImagePath")))); - negativeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - mOnResponse.onNegativeResponse(); - gotResponse = true; - dismiss(); - } - }); - positiveButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - mOnResponse.onPostiveResponse(); - gotResponse = true; - dismiss(); - } - }); return view; } @@ -105,8 +96,23 @@ public class SimilarImageDialogFragment extends DialogFragment { @Override public void onDismiss(DialogInterface dialog) { // I user dismisses dialog by pressing outside the dialog. - if(!gotResponse) + if (!gotResponse) { mOnResponse.onNegativeResponse(); + } super.onDismiss(dialog); } + + @OnClick(R.id.negative_button) + public void onNegativeButtonClicked() { + mOnResponse.onNegativeResponse(); + gotResponse = true; + dismiss(); + } + + @OnClick(R.id.postive_button) + public void onPositiveButtonClicked() { + mOnResponse.onPositiveResponse(); + gotResponse = true; + dismiss(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java index 0fa98e530..a32fb7b42 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java @@ -1,21 +1,18 @@ package fr.free.nrw.commons.upload; import android.annotation.SuppressLint; -import android.app.Activity; - -import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Color; -import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.view.ViewCompat; import android.support.v7.app.AlertDialog; import android.text.Editable; +import android.text.Html; import android.text.TextWatcher; -import android.util.Log; +import android.text.method.LinkMovementMethod; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -23,7 +20,6 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; @@ -46,9 +42,9 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.settings.Prefs; +import fr.free.nrw.commons.utils.ViewUtil; import timber.log.Timber; -import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; public class SingleUploadFragment extends CommonsDaggerSupportFragment { @@ -59,6 +55,7 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { @BindView(R.id.share_license_summary) TextView licenseSummaryView; @BindView(R.id.licenseSpinner) Spinner licenseSpinner; + @Inject @Named("default_preferences") SharedPreferences prefs; @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; @@ -166,13 +163,13 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { titleEdit.setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus) { - hideKeyboard(v); + ViewUtil.hideKeyboard(v); } }); descEdit.setOnFocusChangeListener((v, hasFocus) -> { if(!hasFocus){ - hideKeyboard(v); + ViewUtil.hideKeyboard(v); } }); @@ -181,12 +178,6 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { return rootView; } - public void hideKeyboard(View view) { - Log.i("hide", "hideKeyboard: "); - InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); - } - @Override public void onDestroyView() { titleEdit.removeTextChangedListener(textWatcher); @@ -222,21 +213,9 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { setLicenseSummary(license); prefs.edit() .putString(Prefs.DEFAULT_LICENSE, license) - .commit(); + .apply(); } - @OnTouch(R.id.share_license_summary) - boolean showLicence(View view, MotionEvent motionEvent) { - if (motionEvent.getActionMasked() == ACTION_DOWN) { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(licenseUrlFor(license))); - startActivity(intent); - return true; - } else { - return false; - } - } @OnClick(R.id.titleDescButton) void setTitleDescButton() { @@ -294,8 +273,10 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { @SuppressLint("StringFormatInvalid") private void setLicenseSummary(String license) { - licenseSummaryView.setText(getString(R.string.share_license_summary, getString(Utils.licenseNameFor(license)))); - } + String licenseHyperLink = ""+ getString(Utils.licenseNameFor(license)) + "
"; + licenseSummaryView.setMovementMethod(LinkMovementMethod.getInstance()); + licenseSummaryView.setText(Html.fromHtml(getString(R.string.share_license_summary, licenseHyperLink))); + } @Override public void onActivityCreated(Bundle savedInstanceState) { @@ -309,11 +290,8 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { super.onStop(); // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next - View target = getView().findFocus(); - if (target != null) { - InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(target.getWindowToken(), 0); - } + View target = getActivity().getCurrentFocus(); + ViewUtil.hideKeyboard(target); } @NonNull @@ -354,6 +332,7 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { } } + private void showInfoAlert (int titleStringID, int messageStringID){ new AlertDialog.Builder(getContext()) .setTitle(titleStringID) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index 32554da0f..5fd6d909b 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -91,7 +91,7 @@ public class UploadController { * @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615") * @param onComplete the progress tracker */ - public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, ContributionUploadProgress onComplete) { + public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, String wikiDataEntityId, ContributionUploadProgress onComplete) { Contribution contribution; //TODO: Modify this to include coords @@ -101,6 +101,7 @@ public class UploadController { contribution.setTag("mimeType", mimeType); contribution.setSource(source); + contribution.setWikiDataEntityId(wikiDataEntityId); //Calls the next overloaded method startUpload(contribution, onComplete); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 94c005256..d5ab1d65a 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -7,7 +7,6 @@ import android.app.PendingIntent; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Intent; -import android.content.SharedPreferences; import android.graphics.BitmapFactory; import android.os.Bundle; import android.support.v4.app.NotificationCompat; @@ -23,7 +22,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; -import javax.inject.Named; import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.R; @@ -36,6 +34,7 @@ import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.UploadResult; +import fr.free.nrw.commons.wikidata.WikidataEditService; import timber.log.Timber; public class UploadService extends HandlerService { @@ -49,8 +48,8 @@ public class UploadService extends HandlerService { public static final String EXTRA_CAMPAIGN = EXTRA_PREFIX + ".campaign"; @Inject MediaWikiApi mwApi; + @Inject WikidataEditService wikidataEditService; @Inject SessionManager sessionManager; - @Inject @Named("default_preferences") SharedPreferences prefs; @Inject ContributionDao contributionDao; private NotificationManager notificationManager; @@ -137,6 +136,7 @@ public class UploadService extends HandlerService { @Override public void queue(int what, Contribution contribution) { + Timber.d("Upload service queue has contribution with wiki data entity id as %s", contribution.getWikiDataEntityId()); switch (what) { case ACTION_UPLOAD_FILE: @@ -231,10 +231,10 @@ public class UploadService extends HandlerService { Timber.d("Successfully revalidated token!"); } else { Timber.d("Unable to revalidate :("); - // TODO: Put up a new notification, ask them to re-login stopForeground(true); Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG); failureToast.show(); + sessionManager.forceLogin(this); return; } } @@ -253,6 +253,7 @@ public class UploadService extends HandlerService { if (!resultStatus.equals("Success")) { showFailedNotification(contribution); } else { + wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), filename); contribution.setFilename(uploadResult.getCanonicalFilename()); contribution.setImageUrl(uploadResult.getImageUrl()); contribution.setState(Contribution.STATE_COMPLETED); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Zoom.java b/app/src/main/java/fr/free/nrw/commons/upload/Zoom.java new file mode 100644 index 000000000..438c7f77b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/Zoom.java @@ -0,0 +1,115 @@ +package fr.free.nrw.commons.upload; + +import android.content.ContentResolver; +import android.graphics.Bitmap; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Point; +import android.graphics.Rect; +import android.net.Uri; +import android.provider.MediaStore; +import android.support.v4.graphics.BitmapCompat; +import android.view.View; +import android.widget.FrameLayout; + +import java.io.IOException; +import java.io.InputStream; + +import timber.log.Timber; + +/** + * Contains utility methods for the Zoom function in ShareActivity. + */ +public class Zoom { + + private View thumbView; + private ContentResolver contentResolver; + private FrameLayout flContainer; + + Zoom(View thumbView, FrameLayout flContainer, ContentResolver contentResolver) { + this.thumbView = thumbView; + this.contentResolver = contentResolver; + this.flContainer = flContainer; + } + + /** + * Create a scaled bitmap to display the zoomed-in image + * @param input the input stream corresponding to the uploaded image + * @param imageUri the uploaded image's URI + * @return a zoomable bitmap + */ + Bitmap createScaledImage(InputStream input, Uri imageUri) { + + Bitmap scaled = null; + BitmapRegionDecoder decoder = null; + Bitmap bitmap = null; + + try { + decoder = BitmapRegionDecoder.newInstance(input, false); + bitmap = decoder.decodeRegion(new Rect(10, 10, 50, 50), null); + } catch (IOException e) { + Timber.e(e); + } catch (NullPointerException e) { + Timber.e(e); + } + try { + //Compress the Image + System.gc(); + Runtime rt = Runtime.getRuntime(); + long maxMemory = rt.freeMemory(); + bitmap = MediaStore.Images.Media.getBitmap(contentResolver, imageUri); + int bitmapByteCount = BitmapCompat.getAllocationByteCount(bitmap); + long height = bitmap.getHeight(); + long width = bitmap.getWidth(); + long calHeight = (long) ((height * maxMemory) / (bitmapByteCount * 1.1)); + long calWidth = (long) ((width * maxMemory) / (bitmapByteCount * 1.1)); + scaled = Bitmap.createScaledBitmap(bitmap, (int) Math.min(width, calWidth), (int) Math.min(height, calHeight), true); + } catch (IOException e) { + Timber.e(e); + } catch (NullPointerException e) { + Timber.e(e); + scaled = bitmap; + } + return scaled; + } + + /** + * Calculate the starting and ending bounds for the zoomed-in image. + * Also set the container view's offset as the origin for the + * bounds, since that's the origin for the positioning animation + * properties (X, Y). + * @param startBounds the global visible rectangle of the thumbnail + * @param finalBounds the global visible rectangle of the container view + * @param globalOffset the container view's offset + * @return scaled start bounds + */ + float adjustStartEndBounds(Rect startBounds, Rect finalBounds, Point globalOffset) { + + thumbView.getGlobalVisibleRect(startBounds); + flContainer.getGlobalVisibleRect(finalBounds, globalOffset); + startBounds.offset(-globalOffset.x, -globalOffset.y); + finalBounds.offset(-globalOffset.x, -globalOffset.y); + + // Adjust the start bounds to be the same aspect ratio as the final + // bounds using the "center crop" technique. This prevents undesirable + // stretching during the animation. Also calculate the start scaling + // factor (the end scaling factor is always 1.0). + float startScale; + if ((float) finalBounds.width() / finalBounds.height() + > (float) startBounds.width() / startBounds.height()) { + // Extend start bounds horizontally + startScale = (float) startBounds.height() / finalBounds.height(); + float startWidth = startScale * finalBounds.width(); + float deltaWidth = (startWidth - startBounds.width()) / 2; + startBounds.left -= deltaWidth; + startBounds.right += deltaWidth; + } else { + // Extend start bounds vertically + startScale = (float) startBounds.width() / finalBounds.width(); + float startHeight = startScale * finalBounds.height(); + float deltaHeight = (startHeight - startBounds.height()) / 2; + startBounds.top -= deltaHeight; + startBounds.bottom += deltaHeight; + } + return startScale; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ContinueUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ContinueUtils.java new file mode 100644 index 000000000..b05c8bc45 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ContinueUtils.java @@ -0,0 +1,15 @@ +package fr.free.nrw.commons.utils; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import fr.free.nrw.commons.category.QueryContinue; + +public class ContinueUtils { + + public static QueryContinue getQueryContinue(Node document) { + Element continueElement = (Element) document; + return new QueryContinue(continueElement.getAttribute("continue"), + continueElement.getAttribute("gcmcontinue")); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java deleted file mode 100644 index d56a7b608..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java +++ /dev/null @@ -1,92 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.os.Environment; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; - -import timber.log.Timber; - -public class FileUtils { - /** - * Read and return the content of a resource file as string. - * - * @param fileName asset file's path (e.g. "/queries/nearby_query.rq") - * @return the content of the file - */ - public static String readFromResource(String fileName) throws IOException { - StringBuilder buffer = new StringBuilder(); - BufferedReader reader = null; - try { - InputStream inputStream = FileUtils.class.getResourceAsStream(fileName); - if (inputStream == null) { - throw new FileNotFoundException(fileName); - } - reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); - String line; - while ((line = reader.readLine()) != null) { - buffer.append(line).append("\n"); - } - } finally { - if (reader != null) { - reader.close(); - } - } - return buffer.toString(); - } - - /** - * Deletes files. - * @param file context - */ - public static boolean deleteFile(File file) { - boolean deletedAll = true; - if (file != null) { - if (file.isDirectory()) { - String[] children = file.list(); - for (String child : children) { - deletedAll = deleteFile(new File(file, child)) && deletedAll; - } - } else { - deletedAll = file.delete(); - } - } - - return deletedAll; - } - - public static File createAndGetAppLogsFile(String logs) { - try { - File commonsAppDirectory = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp"); - if (!commonsAppDirectory.exists()) { - commonsAppDirectory.mkdir(); - } - - File logsFile = new File(commonsAppDirectory,"logs.txt"); - if (logsFile.exists()) { - //old logs file is useless - logsFile.delete(); - } - - logsFile.createNewFile(); - - FileOutputStream outputStream = new FileOutputStream(logsFile); - OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); - outputStreamWriter.append(logs); - outputStreamWriter.close(); - outputStream.flush(); - outputStream.close(); - - return logsFile; - } catch (IOException ioe) { - Timber.e(ioe); - return null; - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java index 4f6a6d456..79dad33e5 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java @@ -1,10 +1,27 @@ package fr.free.nrw.commons.utils; +import android.app.WallpaperManager; +import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapRegionDecoder; import android.graphics.Color; import android.graphics.Rect; +import android.net.Uri; +import android.support.annotation.Nullable; +import com.facebook.common.executors.CallerThreadExecutor; +import com.facebook.common.references.CloseableReference; +import com.facebook.datasource.DataSource; +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.imagepipeline.core.ImagePipeline; +import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; +import com.facebook.imagepipeline.image.CloseableImage; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.imagepipeline.request.ImageRequestBuilder; + +import java.io.IOException; + +import fr.free.nrw.commons.R; import timber.log.Timber; /** @@ -132,4 +149,52 @@ public class ImageUtils { return isImageDark; } + + /** + * Downloads the image from the URL and sets it as the phone's wallpaper + * Fails silently if download or setting wallpaper fails. + * @param context + * @param imageUrl + */ + public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) { + Timber.d("Trying to set wallpaper from url %s", imageUrl.toString()); + ImageRequest imageRequest = ImageRequestBuilder + .newBuilderWithSource(imageUrl) + .setAutoRotateEnabled(true) + .build(); + + ImagePipeline imagePipeline = Fresco.getImagePipeline(); + final DataSource> + dataSource = imagePipeline.fetchDecodedImage(imageRequest, context); + + dataSource.subscribe(new BaseBitmapDataSubscriber() { + + @Override + public void onNewResultImpl(@Nullable Bitmap bitmap) { + if (dataSource.isFinished() && bitmap != null){ + Timber.d("Bitmap loaded from url %s", imageUrl.toString()); + setWallpaper(context, Bitmap.createBitmap(bitmap)); + dataSource.close(); + } + } + + @Override + public void onFailureImpl(DataSource dataSource) { + Timber.d("Error getting bitmap from image url %s", imageUrl.toString()); + if (dataSource != null) { + dataSource.close(); + } + } + }, CallerThreadExecutor.getInstance()); + } + + private static void setWallpaper(Context context, Bitmap bitmap) { + WallpaperManager wallpaperManager = WallpaperManager.getInstance(context); + try { + wallpaperManager.setBitmap(bitmap); + ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_successfully)); + } catch (IOException e) { + Timber.e(e,"Error setting wallpaper"); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java index e934e53e5..b9da22e6e 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java @@ -9,7 +9,7 @@ public class NetworkUtils { public static boolean isInternetConnectionEstablished(Context context) { ConnectivityManager cm = - (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); + (ConnectivityManager)context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); return activeNetwork != null && diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java index 03b1469e0..e409b856d 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.utils; import java.util.Comparator; +import java.util.Locale; import info.debatty.java.stringsimilarity.Levenshtein; @@ -28,8 +29,8 @@ public class StringSortingUtils { } private static double calculateSimilarity(String firstString, String secondString) { - String longer = firstString.toLowerCase(); - String shorter = secondString.toLowerCase(); + String longer = firstString.toLowerCase(Locale.getDefault()); + String shorter = secondString.toLowerCase(Locale.getDefault()); if (firstString.length() < secondString.length()) { longer = secondString; diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java index b4b26746b..0c22a40a2 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java @@ -5,10 +5,15 @@ import android.content.Context; import android.support.design.widget.Snackbar; import android.view.Display; import android.view.View; +import android.view.inputmethod.InputMethodManager; import android.widget.Toast; public class ViewUtil { + public static final String SHOWCASE_VIEW_ID_1 = "SHOWCASE_VIEW_ID_1"; + public static final String SHOWCASE_VIEW_ID_2 = "SHOWCASE_VIEW_ID_2"; + public static final String SHOWCASE_VIEW_ID_3 = "SHOWCASE_VIEW_ID_3"; + public static void showSnackbar(View view, int messageResourceId) { Snackbar.make(view, messageResourceId, Snackbar.LENGTH_SHORT).show(); } @@ -27,4 +32,14 @@ public class ViewUtil { } } + public static void hideKeyboard(View view){ + if (view != null) { + InputMethodManager manager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + view.clearFocus(); + if (manager != null) { + manager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + } + } diff --git a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java new file mode 100644 index 000000000..82bad3f09 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java @@ -0,0 +1,80 @@ +package fr.free.nrw.commons.widget; + +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.widget.RemoteViews; + +import com.prof.rssparser.Article; +import com.prof.rssparser.Parser; +import com.squareup.picasso.Picasso; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.util.ArrayList; + +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.R; + +/** + * Implementation of App Widget functionality. + */ +public class PicOfDayAppWidget extends AppWidgetProvider { + + static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, + int appWidgetId) { + + // Construct the RemoteViews object + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.pic_of_day_app_widget); + + String urlString = BuildConfig.WIKIMEDIA_API_POTD; + Parser parser = new Parser(); + parser.execute(urlString); + parser.onFinish(new Parser.OnTaskCompleted() { + @Override + public void onTaskCompleted(ArrayList
list) { + String desc = list.get(list.size() - 1).getDescription(); + if (desc != null) { + Document document = Jsoup.parse(desc); + Elements elements = document.select("img"); + String imageUrl = elements.get(0).attr("src"); + if (imageUrl != null && imageUrl.length() > 0) { + Picasso.get().load(imageUrl).into(views, R.id.appwidget_image, new int[]{appWidgetId}); + } + } + + } + + @Override + public void onError() { + } + }); + + // Instruct the widget manager to update the widget + appWidgetManager.updateAppWidget(appWidgetId, views); + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // There may be multiple widgets active, so update all of them + for (int appWidgetId : appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId); + } + } + + @Override + public void onEnabled(Context context) { + // Enter relevant functionality for when the first widget is created + } + + @Override + public void onDisabled(Context context) { + // Enter relevant functionality for when the last widget is disabled + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java new file mode 100644 index 000000000..30fb26ddc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java @@ -0,0 +1,16 @@ +package fr.free.nrw.commons.wikidata; + +public abstract class WikidataEditListener { + + protected WikidataP18EditListener wikidataP18EditListener; + + public abstract void onSuccessfulWikidataEdit(); + + public void setAuthenticationStateListener(WikidataP18EditListener wikidataP18EditListener) { + this.wikidataP18EditListener = wikidataP18EditListener; + } + + public interface WikidataP18EditListener { + void onWikidataEditSuccessful(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java new file mode 100644 index 000000000..407c24711 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.wikidata; + +/** + * Listener for wikidata edits + */ +public class WikidataEditListenerImpl extends WikidataEditListener { + + public WikidataEditListenerImpl() { + } + + /** + * Fired when wikidata P18 edit is successful. If there's an active listener, then it is fired + */ + @Override + public void onSuccessfulWikidataEdit() { + if (wikidataP18EditListener != null) { + wikidataP18EditListener.onWikidataEditSuccessful(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java new file mode 100644 index 000000000..8bff40b89 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java @@ -0,0 +1,134 @@ +package fr.free.nrw.commons.wikidata; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; + +import java.util.Locale; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +/** + * This class is meant to handle the Wikidata edits made through the app + * It will talk with MediaWikiApi to make necessary API calls, log the edits and fire listeners + * on successful edits + */ +@Singleton +public class WikidataEditService { + + private final Context context; + private final MediaWikiApi mediaWikiApi; + private final WikidataEditListener wikidataEditListener; + private final SharedPreferences directPrefs; + + @Inject + public WikidataEditService(Context context, + MediaWikiApi mediaWikiApi, + WikidataEditListener wikidataEditListener, + @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs) { + this.context = context; + this.mediaWikiApi = mediaWikiApi; + this.wikidataEditListener = wikidataEditListener; + this.directPrefs = directPrefs; + } + + /** + * Create a P18 claim and log the edit with custom tag + * @param wikidataEntityId + * @param fileName + */ + public void createClaimWithLogging(String wikidataEntityId, String fileName) { + if(wikidataEntityId == null + || fileName == null) { + return; + } + editWikidataProperty(wikidataEntityId, fileName); + } + + /** + * Edits the wikidata entity by adding the P18 property to it. + * Adding the P18 edit requires calling the wikidata API to create a claim against the entity + * + * @param wikidataEntityId + * @param fileName + */ + @SuppressLint("CheckResult") + private void editWikidataProperty(String wikidataEntityId, String fileName) { + Timber.d("Upload successful with wiki data entity id as %s", wikidataEntityId); + Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId); + Observable.fromCallable(() -> { + String propertyValue = getFileName(fileName); + return mediaWikiApi.wikidatCreateClaim(wikidataEntityId, "P18", "value", propertyValue); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(revisionId -> handleClaimResult(wikidataEntityId, revisionId), throwable -> { + Timber.e(throwable, "Error occurred while making claim"); + ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); + }); + } + + private void handleClaimResult(String wikidataEntityId, String revisionId) { + if (revisionId != null) { + wikidataEditListener.onSuccessfulWikidataEdit(); + showSuccessToast(); + logEdit(revisionId); + } else { + Timber.d("Unable to make wiki data edit for entity %s", wikidataEntityId); + ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); + } + } + + /** + * Log the Wikidata edit by adding Wikimedia Commons App tag to the edit + * @param revisionId + */ + @SuppressLint("CheckResult") + private void logEdit(String revisionId) { + Observable.fromCallable(() -> mediaWikiApi.addWikidataEditTag(revisionId)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + if (result) { + Timber.d("Wikidata edit was tagged successfully"); + } else { + Timber.d("Wikidata edit couldn't be tagged"); + } + }, throwable -> { + Timber.e(throwable, "Error occurred while adding tag to the edit"); + }); + } + + /** + * Show a success toast when the edit is made successfully + */ + private void showSuccessToast() { + String title = directPrefs.getString("Title", ""); + String successStringTemplate = context.getString(R.string.successful_wikidata_edit); + String successMessage = String.format(Locale.getDefault(), successStringTemplate, title); + ViewUtil.showLongToast(context, successMessage); + } + + /** + * Formats and returns the filename as accepted by the wiki base API + * https://www.mediawiki.org/wiki/Wikibase/API#wbcreateclaim + * + * @param fileName + * @return + */ + private String getFileName(String fileName) { + fileName = String.format("\"%s\"", fileName.replace("File:", "")); + Timber.d("Wikidata property name is %s", fileName); + return fileName; + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_zoom_in_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_zoom_in_white_24dp.png new file mode 100644 index 000000000..637329408 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_zoom_in_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_zoom_out_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_zoom_out_white_24dp.png new file mode 100644 index 000000000..7772ceea7 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_zoom_out_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_zoom_in_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_zoom_in_white_24dp.png new file mode 100644 index 000000000..36a659dd2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_zoom_in_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_zoom_out_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_zoom_out_white_24dp.png new file mode 100644 index 000000000..e69da0ba0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_zoom_out_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_zoom_in_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_zoom_in_white_24dp.png new file mode 100644 index 000000000..ce88ebdd7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_zoom_in_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_zoom_out_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_zoom_out_white_24dp.png new file mode 100644 index 000000000..8c6ccea7b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_zoom_out_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_zoom_in_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_zoom_in_white_24dp.png new file mode 100644 index 000000000..30b847067 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_zoom_in_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_zoom_out_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_zoom_out_white_24dp.png new file mode 100644 index 000000000..68aaec6d8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_zoom_out_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_zoom_in_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_zoom_in_white_24dp.png new file mode 100644 index 000000000..b636f6e3c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_zoom_in_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_zoom_out_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_zoom_out_white_24dp.png new file mode 100644 index 000000000..d85e242b9 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_zoom_out_white_24dp.png differ diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_up_black_24dp.xml b/app/src/main/res/drawable/ic_keyboard_arrow_up_black_24dp.xml new file mode 100644 index 000000000..bc010396b --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_up_black_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_share_black_24dp.xml b/app/src/main/res/drawable/ic_share_black_24dp.xml index 01c81322d..203b1d84c 100644 --- a/app/src/main/res/drawable/ic_share_black_24dp.xml +++ b/app/src/main/res/drawable/ic_share_black_24dp.xml @@ -1,5 +1,5 @@ - + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/app/src/main/res/drawable/ic_star_black_24dp.xml b/app/src/main/res/drawable/ic_star_black_24dp.xml new file mode 100644 index 000000000..a87ca098d --- /dev/null +++ b/app/src/main/res/drawable/ic_star_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_zoom_in_white_24dp.png b/app/src/main/res/drawable/ic_zoom_in_white_24dp.png new file mode 100644 index 000000000..637329408 Binary files /dev/null and b/app/src/main/res/drawable/ic_zoom_in_white_24dp.png differ diff --git a/app/src/main/res/drawable/ic_zoom_out_white_24dp.png b/app/src/main/res/drawable/ic_zoom_out_white_24dp.png new file mode 100644 index 000000000..7772ceea7 Binary files /dev/null and b/app/src/main/res/drawable/ic_zoom_out_white_24dp.png differ diff --git a/app/src/main/res/layout-land/welcome_final.xml b/app/src/main/res/layout-land/welcome_final.xml index 0b99b481f..d44339647 100644 --- a/app/src/main/res/layout-land/welcome_final.xml +++ b/app/src/main/res/layout-land/welcome_final.xml @@ -1,13 +1,33 @@ - + > + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_category_images.xml b/app/src/main/res/layout/activity_category_images.xml new file mode 100644 index 000000000..c329e4458 --- /dev/null +++ b/app/src/main/res/layout/activity_category_images.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_share.xml b/app/src/main/res/layout/activity_share.xml index e915517b5..b6e523239 100644 --- a/app/src/main/res/layout/activity_share.xml +++ b/app/src/main/res/layout/activity_share.xml @@ -19,7 +19,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/toolbar" - android:background="?attr/mainBackground"> + android:background="?attr/mainBackground" + android:id="@+id/container"> + + + + + + + + + + + + + android:layout_gravity="bottom" + android:padding="5dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_category_images.xml b/app/src/main/res/layout/fragment_category_images.xml new file mode 100644 index 000000000..001f0a780 --- /dev/null +++ b/app/src/main/res/layout/fragment_category_images.xml @@ -0,0 +1,41 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_media_detail.xml b/app/src/main/res/layout/fragment_media_detail.xml index 190eb011d..f265bc3ea 100644 --- a/app/src/main/res/layout/fragment_media_detail.xml +++ b/app/src/main/res/layout/fragment_media_detail.xml @@ -80,6 +80,39 @@ android:textSize="@dimen/description_text_size" /> + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_single_upload.xml b/app/src/main/res/layout/fragment_single_upload.xml index a340e0213..37e43ee81 100644 --- a/app/src/main/res/layout/fragment_single_upload.xml +++ b/app/src/main/res/layout/fragment_single_upload.xml @@ -74,6 +74,8 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/standard_gap" android:gravity="center" + android:clickable="true" + android:textColorLink="@color/button_blue" android:text="@string/share_license_summary" /> + diff --git a/app/src/main/res/layout/layout_category_images.xml b/app/src/main/res/layout/layout_category_images.xml new file mode 100644 index 000000000..9b3ffee0f --- /dev/null +++ b/app/src/main/res/layout/layout_category_images.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_upload_item.xml b/app/src/main/res/layout/layout_upload_item.xml index fb552560a..155664d91 100644 --- a/app/src/main/res/layout/layout_upload_item.xml +++ b/app/src/main/res/layout/layout_upload_item.xml @@ -34,6 +34,7 @@ android:textColor="#FFFFFFFF" style="?android:textAppearanceSmall" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/light_simple_spinner_dropdown_item.xml b/app/src/main/res/layout/light_simple_spinner_dropdown_item.xml index be4e086e4..11f95bf2e 100644 --- a/app/src/main/res/layout/light_simple_spinner_dropdown_item.xml +++ b/app/src/main/res/layout/light_simple_spinner_dropdown_item.xml @@ -2,7 +2,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/welcome_final.xml b/app/src/main/res/layout/welcome_final.xml index 7f323253b..561e660aa 100644 --- a/app/src/main/res/layout/welcome_final.xml +++ b/app/src/main/res/layout/welcome_final.xml @@ -1,14 +1,18 @@ - + - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/menu/drawer.xml b/app/src/main/res/menu/drawer.xml index 61c0739fe..ae6e0cce2 100644 --- a/app/src/main/res/menu/drawer.xml +++ b/app/src/main/res/menu/drawer.xml @@ -9,6 +9,12 @@ android:id="@+id/action_nearby" android:icon="@drawable/ic_location_black_24dp" android:title="@string/navigation_item_nearby" /> + + + + Аҭалара қәҿиарала имҩаҧысит! Асистемахь аҭалараан агха! Афаил ҧшаам. Даҽа фаилк шәахәаҧш. - Аутентификациа агха! + Аутентификациа агха! Аҭагалара иалагоуп! %1$s иҭагалоуп! Шәақәыӷәӷәа иҭагалоу афаил ахәаҧшраз diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 6d9c94c1d..fb18f34f2 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -27,7 +27,7 @@ تم الدخول بشكل صحيح! فشل تسجيل الدخول الملف غير موجود. فضلا اختر ملفا آخر. - فشل الاستيقان! + فشل الاستيقان! بدأ الرفع! رُفع %1$s! انقر لعرض ملفك المرفوع @@ -54,8 +54,6 @@ العنوان الوصف لا يمكن تسجيل الدخول - فشل في شبكة الاتصال - لا يمكن تسجيل الدخول - فضلا تحقق من اسم المستخدم - لا يمكن تسجيل الدخول - فضلا تحقق من كلمة السر الكثير من المحاولات غير الناجحة. الرجاء المحاولة مرة أخرى في بضع دقائق. عذراً، لقد تم منع هذا المستخدم على كومنز يجب توفير رمز التحقق المزدوج. @@ -145,4 +143,5 @@ مرحبا بكم في ويكيمديا كومنز، %1$s! نحن سعداء لأنك هنا. %1$s رسالة على صفحة الحديث %1$s ذكر لك على %2$s. + شارك التطبيق diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index 1c486a0b8..b4012047f 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -21,7 +21,7 @@ ¡Identificación correuta! ¡Falló l\'aniciu de sesión! Nun s\'alcontró\'l ficheru. Tenta con otru. - ¡Falló la identificación! + Falló la identificación, anicia sesión nuevamente Principió la xuba ¡%1$s xubíu! Toque pa ver la xuba @@ -49,8 +49,7 @@ Apurre un títulu pa esti ficheru Descripción Nun se pudo aniciar sesión – error de rede - Nun se pudo aniciar sesión – por favor compruebe\'l so nome d\'usuariu - Nun se pudo aniciar sesión – por favor compruebe la so contraseña + Nun pudo aniciase sesión. Revisa\'l nome d\'usuariu y la contraseña Demasiaos intentos incorreutos. Téntalo otra vuelta n\'unos minutos. Sentímoslo, esti usuariu ta bloquiáu en Commons Tienes de dar el códigu d\'identificación de dos factores. @@ -84,6 +83,7 @@ Categoríes Configuración Date d\'alta + Imáxenes destacaes Tocante a La app de Wikimedia Commons ye software de códigu abiertu, creáu y calteníu por becaos y voluntarios de la comunidá de Wikimedia. La Fundación Wikimedia nun participa na creación, desendolcu nin caltenimientu de la app. Crea una nueva <a href=\"https://github.com/commons-app/apps-android-commons/issues\">incidencia en GitHub</a> pa informar de problemes y suxerencies. @@ -103,7 +103,7 @@ Llicencia predeterminada Usar un títulu/descripción anterior Llograr automáticamente l\'allugamientu actual - Recuperar l\'allugamientu actual pa ufiertar suxerencies de categoríes si la imaxe nun tien etiquetes xeográfiques + Recupera la posición actual si la imaxe nun tien etiquetes xeográfiques, y marca la imaxe con ella. Atención: Esto revelará\'l to allugamientu actual. Mou nocherniegu Usar tema escuru Reconocimientu-CompartirIgual 4.0 @@ -155,8 +155,8 @@ Ensin descripción Llicencia desconocida Refrescar - Permisu riquíu: llectura d\'almacenamientu esternu. L\'aplicación nun puede funcionar ensin él. - Permisu riquíu: escritura d\'almacenamientu esternu. L\'aplicación nun puede funcionar ensin él. + Permisu riquíu: llectura d\'almacenamientu esternu. L\'aplicación nun puede entrar na to galería ensin él. + Permisu necesariu: Escritura n\'almacenamientu esternu. L\'aplicación nun puede aportar a la cámara ensin él. Permisu opcional: llograr l\'allugamientu actual pa suxerir categoríes Aceutar Llugares cercanos @@ -169,6 +169,8 @@ Títulu del mediu Descripción Equí va la descripción del mediu. Esto pué ser llargo enforma, y necesitará espardese per delles llinies. Sicasí, esperamos que se vea bien. + Autor + El nome d\'usuariu del autor de la imaxe destacada va equí. Data d\'unviu Llicencia Coordenaes @@ -211,6 +213,7 @@ Salir Tutorial Avisos + Destacada Los sitios cercanos nun pueden amosase ensin los permisos d\'allugamientu nun s\'atoparon descripciones Páxina del ficheru en Commons @@ -259,4 +262,20 @@ Siguir Encaboxar Retentar + Entendílo + Estos son sitios cercanos a ti que precisen imaxes para ilustrar los sos artículos de Wikipedia + Tocando esti botón amuésase la llista d\'esos llugares + Puedes xubir una imaxe pa cualquier sitiu dende la galería o la cámara + Nun s\'alcontró nenguna imaxe + Asocedió un error al cargar les imáxenes. + Xubida por: %1$s + Compartir app + Nun s\'especificaron les coordenaes al escoyer la imaxe + Error al llograr los llugares cercanos. + Semeya del día + Semeya del día + Añadióse correutamente la imaxe a %1$s en Wikidata. + Nun pudo anovase la entidá de Wikidata correspondiente. + Definir fondu + Fondu definíu correutamente diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml index d361ef334..557be7714 100644 --- a/app/src/main/res/values-b+sr+Latn/strings.xml +++ b/app/src/main/res/values-b+sr+Latn/strings.xml @@ -16,7 +16,7 @@ Uspešno ste prijavljeni. Prijavljivanje nije uspelo. Datoteka nije pronađena. Pokušajte sa drugom datotekom. - Provera identiteta nije uspela. + Provera identiteta nije uspela. Otpremanje je započeto. Datoteka „%1$s“ je otpremljena. Tapnite da biste videli otpremanje @@ -43,8 +43,6 @@ Naslov Opis Ne mogu da vas prijavim – mreža ne radi - Ne mogu da vas prijavim – proverite svoje korisničko ime - Ne mogu da vas prijavim – proverite svoju lozinku Previše neuspešnih pokušaja. Probajte ponovo za nekoliko minuta. Nažalost, korisnik je blokiran na Ostavi Morate uneti svoj dvofaktorski kod za autentifikaciju. @@ -96,7 +94,7 @@ Licenca Koristi prethodan naslov/opis Automatski detektuj trenutnu lokaciju - Primi trenutnu lokaciju da bi predložili kategoriju ako slika nije geografski označena + Primi trenutnu lokaciju da bi predložili kategoriju ako slika nije geografski označena Noćni režim Koristiti tamnu temu Autorstvo-Deliti pod istim uslovima 4.0 @@ -139,8 +137,8 @@ Nema opisa Nepoznata licenca Osveži - Potrebna dozvola: Provera spoljašnje memorije. Aplikacija bez ovoga ne može da funkcioniše. - Neophodna dozvola: Pisanje spoljašnjeg skladišta. Aplikacija ne može da funkcioniše bez ovoga. + Potrebna dozvola: Provera spoljašnje memorije. Aplikacija bez ovoga ne može da funkcioniše. + Neophodna dozvola: Pisanje spoljašnjeg skladišta. Aplikacija ne može da funkcioniše bez ovoga. Opciona dozvola: Preuzmi trenutnu lokaciju za predloge kategorija U redu Mesta u blizini diff --git a/app/src/main/res/values-ba/strings.xml b/app/src/main/res/values-ba/strings.xml index 03729cfac..f91a69803 100644 --- a/app/src/main/res/values-ba/strings.xml +++ b/app/src/main/res/values-ba/strings.xml @@ -23,7 +23,7 @@ Танышыу уңышлы үтте Танылыу хатаһы Файл табылманы. Башҡа файлды эҙлә. - Кем икәнегеҙ танылманы! + Кем икәнегеҙ танылманы! Тейәү башланды! %1$s тейәлде! Ошонда баҫып тейәлгән файлды ҡара @@ -50,8 +50,6 @@ Был файлдың атамаһын күрһәт Тасуирлама Инеп булмай - интернет хатаһы - Инмәнең - ҡулланыусы исемеңде тикшер - Инмәнең - серһуҙеңде тикшер Күп тапҡыр яңылыштың. Зинһар, бер-нисә минуттан тағы ла инеп ҡара Ғәфү итегеҙ, әммә был исемдәге ҡатнашыусыға Викискладҡа инеү тыйылған Ике тапҡыр раҫлай торған шәхси кодты яҙырға кәрәк @@ -102,7 +100,7 @@ Нығытылған рөхсәтнамә Алдағы атама/һәрәтләмәне ҡулланыу Автомат рәүешендә сираттағы урынды алыу - Әгәр рәсемдең геотегтары булмаһа, категориялар үҙенән-үҙе тәҡдим ителһен өсөн сираттағы урынды алырға + Әгәр рәсемдең геотегтары булмаһа, категориялар үҙенән-үҙе тәҡдим ителһен өсөн сираттағы урынды алырға Төнгө режим Ҡараңғы теманы ҡулланыу Attribution-ShareAlike 4.0 @@ -154,8 +152,8 @@ Һүрәтләүе юҡ Билдәһеҙ лицензия Яңыртып алыу - Кәрәкле рөхсәт: тышҡы һаҡлағыстан алып уҡыу. Ҡушымта шунһыҙ эшләмәйәсәк. - Кәрәкле рөхсәт: тышҡы һаҡлағысҡа яҙыу. Ҡушымта шунһыҙ эшләмәйәсәк. + Кәрәкле рөхсәт: тышҡы һаҡлағыстан алып уҡыу. Ҡушымта шунһыҙ эшләмәйәсәк. + Кәрәкле рөхсәт: тышҡы һаҡлағысҡа яҙыу. Ҡушымта шунһыҙ эшләмәйәсәк. Мотлаҡ булмаған рөхсәт: категория тәҡдиме өсөн ошо урынды алыу Яҡындағы урындар Яҡындағы урындар табылманы diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index cfaf021f9..d6a675bf2 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -18,7 +18,7 @@ Успешно влизане. Неуспешно влизане! Файлът не е намерен. Моля, опитайте с друг файл. - Неуспешен опит за удостоверяване! + Неуспешен опит за удостоверяване! Качването започна! Файл %1$s е качен! Докоснете, за да видите качения файл @@ -32,8 +32,6 @@ Заглавие Описание Неуспешно влизане – проблем в мрежата - Неуспешно влизане – моля проверете потребителското си име - Неуспешно влизане – моля проверете паролата си Качване Изменения Качване diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index fe404d495..d9aa9e657 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -29,7 +29,7 @@ প্রবেশ সফল! প্রবেশ ব্যর্থ :( ফাইল পাওয়া যায়নি। আরেকটি ফাইল চেষ্টা করুন। - প্রমাণীকরণ ব্যর্থ হয়েছে! + প্রমাণীকরণ ব্যর্থ হয়েছে, আবার প্রবেশ করুন আপলোড আরম্ভ হয়েছে! %1$s আপলোড হয়েছে! আপনার আপলোড দেখতে টোকা দিন @@ -57,8 +57,6 @@ এই ফাইলটির জন্য একটি শিরোনাম প্রদান করুন বিবরণ প্রবেশ করা যাচ্ছে না - নেটওয়ার্ক ব্যর্থতা - প্রবেশ করা যাচ্ছে না - অনুগ্রহ করে আপনার ব্যবহারকারী নাম পরীক্ষা করুন। - প্রবেশ করা যাচ্ছে না - অনুগ্রহ করে আপনার পাসওয়ার্ড পরীক্ষা করুন খুব বেশি অসফল প্রচেষ্টা। অনুগ্রহ করে কয়েক মিনিট পরে আবারও চেষ্টা করুন। দুঃখিত, এই ব্যবহারকারীকে কমন্সে বাধা দেয়া হয়েছে অাপনাকে অবশ্যই অাপনার দু\'স্তরের সত্যায়নকরণ কোড দিতে হবে। @@ -92,6 +90,7 @@ বিষয়শ্রেণীসমূহ সেটিং নিবন্ধন করুন + নির্বাচিত ছবি পরিচিতি উইকিমিডিয়া কমন্স অ্যাপ হচ্ছে একটি উন্মুক্ত উৎস সম্বলিত অ্যাপ যা উইকিমিডিয়া সম্প্রদায়ের ব্যবহারকারী ও সেচ্ছাসেবকবৃন্দ কর্তৃক তৈরিকৃত এবং পরিচালিত। উইকিমিডিয়া ফাউন্ডেশন এই অ্যাপ তৈরি, উন্নয়ন বা রক্ষণাবেক্ষণে জড়িত নয়। কোন সমস্যা ও পরামর্শের জন্য <a href=\"https://github.com/commons-app/apps-android-commons/issues\">গিটহাব ইস্যু</a> তৈরি করুন। @@ -111,7 +110,7 @@ পূর্বনির্ধারিত লাইসেন্স পূর্ববর্তী শিরোনাম/বিবরণ ব্যবহার করুন স্বয়ংক্রিয়ভাবে বর্তমান অবস্থান পান - বিষয়শ্রেণীর পরামর্শ দিতে বর্তমান অবস্থান পান যদি ছবিতে ভূ-ট্যাগ না থেকে থাকে + বিষয়শ্রেণীর পরামর্শ দিতে বর্তমান অবস্থান পান যদি ছবিতে ভূ-ট্যাগ না থেকে থাকে রাত্রি মোড কালো থিম ব্যবহার করুন অ্যাট্রিবিউশন-শেয়ারঅ্যালাইক ৪.০ @@ -162,8 +161,8 @@ বিবরণ নেই অজানা লাইসেন্স পুনঃসতেজ - প্রয়োজনীয় অনুমতি: বহিঃস্ত সঞ্চয়স্থান পড়া। এটি ছাড়া অ্যাপ কাজ করবে না। - অনুমতি প্রয়োজন: অালাদাভাবে সংযুক্ত স্টোরেজ লিখুন। এটি ছাড়া অ্যাপটি চলতে পারেনা। + প্রয়োজনীয় অনুমতি: বহিঃস্ত সঞ্চয়স্থান পড়া। এটি ছাড়া অ্যাপ কাজ করবে না। + অনুমতি প্রয়োজন: অালাদাভাবে সংযুক্ত স্টোরেজ লিখুন। এটি ছাড়া অ্যাপটি চলতে পারেনা। ঐচ্ছিক অনুমতি: বিষয়শ্রেণী পরামর্শের জন্য বর্তমান অবস্থান নেয় ঠিক আছে কাছাকাছি স্থান @@ -176,6 +175,7 @@ মিডিয়ার শিরোনাম বিবরণ মিডিয়ার বিবরণ এখানে যাবে। এই মোটামুটি দীর্ঘ হতে পারে এবং একাধিক লাইনে লিখতে হতে পারে। আমরা আশা করি এটি দেখতে সুন্দর হবে। + স্রষ্টা আপলোডের তারিখ লাইসেন্স স্থানাঙ্কসমূহ @@ -218,6 +218,7 @@ প্রস্থান ভূমিকা বিজ্ঞপ্তি + নির্বাচিত অবস্থানের অনুমতি ছাড়া কাছাকাছি জায়গাগুলি প্রদর্শন করা যাবে না কোন বিবরণ পাওয়া যায়নি কমন্সে ফাইলের পাতা @@ -254,4 +255,10 @@ টিউটোরিয়াল এড়ান ইন্টারনেট অনুপলব্ধ ইন্টারনেট উপলব্ধ + কোন বিজ্ঞপ্তি পাওয়া যায়নি + পুনঃচেষ্টা করুন + বুঝেছি! + কোন চিত্র পাওয়া যায়নি! + আপলোড করেছেন: %1$s + কাছাকাছি স্থানগুলি আনতে ত্রুটি। diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 673d9ee7b..90ede0b76 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -3,6 +3,7 @@ * Dishual * Fohanno * Fulup +* Gwendal * Gwenn-Ael * Y-M D --> @@ -24,7 +25,7 @@ Kevreet oc\'h ! Kudenn gevreañ ! N\'eo ket bet kavet ar restr. Klask gant unan all. - Dilesadur c\'hwitet! + Dilesadur c\'hwitet! Kroget da enporzhiañ! %1$s bet enporzhiet ! Pouezit evit gwelet hoc\'h enporzhiadenn @@ -52,8 +53,6 @@ Roit un titl d\'ar restr-mañ, mar plij Deskrivadur Ne c\'haller ket kevreañ - rouedad sac\'het - Ne c\'haller ket kevreañ - Gwiriit hoc\'h anv implijer, mar plij - Ne c\'haller ket kevreañ - Gwiriit ho ker tremen, mar plij Re a daolioù-esae. Klaskit en-dro a-benn ur pennadig amzer. Hon digarezit, prennet eo bet an implijer-mañ e Commons Rankout a rit reiñ ho kod dilesa gant daou faktor. @@ -104,7 +103,7 @@ Aotre-implijout dre ziouer Ober gant an titl/deskrivadur kent Tapout al lec\'hiadur red ent emgefre - Kavout al lec\'hiadur red evit pourchas kinnigoù rummadoù ma n\'eo ket douarlec\'hiet ar skeudenn. + Kavout al lec\'hiadur red evit pourchas kinnigoù rummadoù ma n\'eo ket douarlec\'hiet ar skeudenn. Mod noz Ober gant an tem teñval Deroadur-RannañHeñvel 4.0 @@ -153,8 +152,8 @@ Deskrivadur ebet Aotre-implijout dizanv Freskaat - Aotre rekis : lenn ur stokañ diavaez. Hep se, n\'hall ket an arload mont en-dro. - Aotre ret ; skrivañ war al lec\'h stokañ diavaez. Ne c\'hall ket an arload mont en-dro hep an dra-se. + Aotre rekis : lenn ur stokañ diavaez. Hep se, n\'hall ket an arload mont en-dro. + Aotre ret ; skrivañ war al lec\'h stokañ diavaez. Ne c\'hall ket an arload mont en-dro hep an dra-se. Aotre diret : kaout al lec\'hiadur red evit kinnig rummadoù Mat eo Lec\'hioù nes @@ -219,4 +218,5 @@ N\'eo ket cheñchet al lec\'hiadur. Kaout urzhioù Lenn ar pennad + Skeudenn an deiz diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index 0a753c037..1a8253fa3 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -16,7 +16,7 @@ Prijavljivanje uspješno! Prijavljivanje nije uspjelo! Datoteka nije pronađena. Pokušajte drugu. - Provjera identiteta nije uspjela! + Provjera identiteta nije uspjela! Postavljanje je započelo! Datoteka %1$s je postavljena! Dodirnite da biste vidjeli datoteku @@ -43,8 +43,6 @@ Naslov Opis Ne mogu Vas prijaviti – mreža ne radi - Ne mogu Vas prijaviti – provjerite svoje korisničko ime - Ne mogu Vas prijaviti – provjerite svoje lozinku Napravili ste previše grešaka u prijavi. Pokušajte ponovo za nekoliko minuta. Žao nam je, korisnik je blokiran na Commonsu Morate upisati kôd za potvrdu u 2 koraka. @@ -95,7 +93,7 @@ Licenca Koristi prethodni naziv/opis Automatski dobavi trenutnu lokaciju - Dobavi trenutnu lokaciju za davanje prijedloga o kategorijama ako nema geooznaku + Dobavi trenutnu lokaciju za davanje prijedloga o kategorijama ako nema geooznaku Noćni režim Koristi tamnu temu Autorstvo-Dijeliti pod istim uslovima 4.0 @@ -138,7 +136,7 @@ Nema opisa Nepoznata licenca Osvježi - Potrebna dozvola: Čitanje vanjske memorije. Aplikacija ne može raditi bez ovog. + Potrebna dozvola: Čitanje vanjske memorije. Aplikacija ne može raditi bez ovog. Neobavezna dozvola: Dobavljanje trenutne lokacije za predlaganje kategorija U redu Mjesta u blizini diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index f7f40f427..98a120f00 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -23,7 +23,7 @@ S\'ha iniciat sessió correctament! Error en iniciar la sessió! No s\'ha trobat el fitxer. Proveu-ho amb un altre fitxer. - L\'autenticació ha fallat! + L’autenticació ha fallat. Torneu a provar d’iniciar una sessió. Ha començat la càrrega! S’ha pujat %1$s. Prem per veure la teva càrrega @@ -50,8 +50,6 @@ Títol Descripció No s\'ha pogut iniciar la sessió – error de xarxa - No s\'ha pogut iniciar la sessió – si et plau comprova el teu nom d\'usuari - No s’ha pogut iniciar la sessió. Comproveu la vostra contrasenya Massa intents erronis – Proveu-ho de nou d\'aquí uns minuts. Ho sentim, aquest usuari ha estat blocat a Commons Heu de proporcionar el vostre codi d\'autenticació de dos factors. @@ -192,4 +190,5 @@ Gràcies per fer una modificació %1$s us ha mencionat a %2$s. Preguntes freqüents + No s’ha trobat cap imatge. diff --git a/app/src/main/res/values-cs/error.xml b/app/src/main/res/values-cs/error.xml index c9ff5927c..e6677dedc 100644 --- a/app/src/main/res/values-cs/error.xml +++ b/app/src/main/res/values-cs/error.xml @@ -2,9 +2,10 @@ - Commons spadly + Aplikace Commons spadla Něco se pokazilo! Řekněte nám, co jste dělali a dejte nám to vědět e-mailem. Pomůže sjednat nápravu! Děkujeme vám! diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index b22c86fb7..192656956 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -9,6 +9,7 @@ * Leanes * Matěj Suchánek * Mormegil +* Patriccck * Spotter * Vojtěch Dostál * Walter Klosse @@ -32,7 +33,7 @@ Přihlášení uspělo! Přihlášení se nezdařilo! Soubor nebyl nalezen. Prosím, zkuste jiný soubor. - Ověření se nezdařilo! + Ověření se nezdařilo, prosím přihlaste se znovu Nahrávání začalo! %1$s nahráno! Klepnutím zobrazíte upload @@ -60,8 +61,6 @@ Vložte prosím název tohoto souboru Popis Nelze se přihlásit - selhání sítě - Nelze se přihlásit - prosím zkontrolujte své uživatelské jméno - Nelze se přihlásit - zkontrolujte prosím své heslo Příliš mnoho neúspěšných pokusů. Zkuste to prosím znovu za několik minut. Omlouváme se, tento uživatel byl na Commons zablokován Prosím vložte kód pro své dvoufázové ověření. @@ -73,6 +72,7 @@ Hledání kategorií Uložit Obnovit + Seznam GPS ve vašem zařízení není povoleno. Chtěli byste ho spustit? Spustit GPS Žádné nahrané soubory @@ -113,7 +113,7 @@ Výchozí licence Použít předchozí název a popis Automaticky získat aktuální polohu - Nabídnout kategorie na základě aktuální polohy (pokud není obrázek opatřen souřadnicemi) + Nabídnout kategorie na základě aktuální polohy (pokud není obrázek opatřen souřadnicemi) Noční režim Použít tmavý režim Uveďte autora-Zachovejte licenci 4.0 @@ -165,8 +165,8 @@ Bez popisu Neznámá licence Obnovit - Požadováno oprávnění ke čtení externího úložiště. Aplikace bez toho nemůže pracovat. - Požadováno oprávnění k zápisu do externího úložiště. Aplikace bez toho nemůže pracovat. + Požadované oprávnění: Čtení externího úložiště. Bez něj nemůže aplikace číst vaši galerii. + Požadované oprávnění: Zapisování do externího úložiště. Bez něj nemůže aplikace používat vaši kameru. Volitelně: Umožněte aplikaci, aby získávala aktuální polohu a nabízela na jejím základě kategorie OK Místa v okolí @@ -225,6 +225,7 @@ nebyl nalezen žádný popisek Stránka souboru na Commons Položka Wikidat + Článek na Wikipedii Chyba při meziukládání obrázků Unikátní a popisný název pro daný soubor, který bude sloužit jako název souboru. Můžete použít běžný psaný jazyk s mezerami; nezahrnujte koncovku souboru. Popište prosím obrázek, jak jen to je možné: Kde byl pořízen? Co znázorňuje? Jaký je kontext obrázku? Popisujte prosím významné předměty nebo osoby na obrázku a nezapomeňte na informace, které není možné snadno odhadnout ze samotného obrázku, jako je například denní doba, pokud jde o krajinu. Pokud je na obrázku něco neobvyklého, popište, co to dělá neobvyklým. @@ -240,6 +241,7 @@ Chyba! URL nenalezeno Navrhnout na smazání Tento obrázek byl nominován na smazání. + Zobrazit v prohlížeči Vaše umístění se nezměnilo. Umístění není dostupné. @@ -251,11 +253,21 @@ Děkujeme za vaši editaci %1$s vás zmínil na %2$s. Přepnout pohled + POKYNY + WIKIDATA + WIKIPEDIE + COMMONS <u>Ohodnoť nás</u> <u>Často kladené otázky</u> Přeskočit úvod Internet je nedostupný Internet je dostupný + Při načítání oznámení došlo k chybě + Nebyly nalezeny žádné oznámení <u>Přeložit</u> + Jazyky + Vyberte jazyk, pro který chcete odeslat překlady + Pokračovat + Zrušit Zkusit znovu diff --git a/app/src/main/res/values-csb/strings.xml b/app/src/main/res/values-csb/strings.xml index 2d7b99904..3fab9c6fb 100644 --- a/app/src/main/res/values-csb/strings.xml +++ b/app/src/main/res/values-csb/strings.xml @@ -14,7 +14,7 @@ Ùdałi logòwanié! Logòwanié nie darzëło sã! Felënk lopka. Proszã spróbòwac znowa. - Fela ùdowierzaniô! + Fela ùdowierzaniô! Wladënk zrëszony! %1$s wladowóné! Tkni, abë òbôczëc ladowóny lopk @@ -41,8 +41,6 @@ Titel Òpisënk Ni mòże sã wlogòwac - fela sécë - Ni mòże sã wlogòwac - sprôwdzë miono brëkòwnika - Ni mòże sã wlogòwac - sprôwdzë parolã Za wiele nieùdałich prób wlogòwaniô. Spróbùjë znowa za czile minut. Nen brëkòwnik òstôł zablokòwóny na Commons Mùszisz wpisac swój kòd dlô dwafaktorowi aùtorizacëji. diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index ef56bc3b0..68ffef8a0 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -16,7 +16,7 @@ Llwyddodd y mewngofnodi! Methodd y mewngofnodi! Ni chafwyd hyd i\'r ffeil. Ceisiwch un arall. - Methodd y dilysu! + Methodd y dilysu! Dechreuodd yr uwchlwytho! Uwchlwythwyd %1$s! Tapiwch i weld eich uwchlwythiad @@ -43,8 +43,6 @@ Teitl Disgrifiad Yn methu mewngofnodi - methodd y rhwydwaith - Yn methu mewngofnodi - gwirwch eich enw defnyddiwr - Yn methu mewngofnodi - gwirwch eich cyfrinair Cafwyd gormod o ymgeision aflwyddiannus. Oedwch ennyd cyn ceisio eto. Ymddiheurwn. Mae\'r defnyddiwr hwn wedi ei flocio ar Gomin Wikimedia Mae\'n rhaid i chi roi eich cod adnabod 2 ffactor. @@ -96,7 +94,7 @@ Trwydded Defnydiwch y teitl/disgrifiad blaenorol Defnyddiwch y lleoliad cyfredol - Canfyddwch eich lleoliad, er mwyn i ni gynnig categori (os nad ydych wedi nodi\'r cyfesurynnau). + Canfyddwch eich lleoliad, er mwyn i ni gynnig categori (os nad ydych wedi nodi\'r cyfesurynnau). Modd fin nos Defnyddiwch thema tywyll Attribution-ShareAlike 4.0 diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index a0b50111d..4a724e640 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -26,7 +26,7 @@ Du er nu logget på! Det mislykkedes at logge på! Filen blev ikke fundet. Forsøg med en anden fil. - Godkendelse mislykkedes! + Godkendelse mislykkedes! Overførsel begyndt! %1$s overført! Tryk for at få vist din upload @@ -54,8 +54,6 @@ Angiv venligt en titel for denne fil Beskrivelse Kan ikke logge på - netværksfejl - Ude af stand til at logge på - tjek venligst dit brugernavn - Ude af stand til at logge på - tjek venligst din adgangskode Alt for mange mislykkede forsøg. Prøv igen om et par minutter. Beklager, denne bruger er blevet blokeret på Commons Du skal angive din tofaktorgodkendelseskode. @@ -107,7 +105,7 @@ Standardlicens Brug forrige titel/beskrivelse Hent automatisk nuværende placering - Hent nuværende placering for at tilbyde kategoriforslag hvis billedet ikke er geografisk mærket + Hent nuværende placering for at tilbyde kategoriforslag hvis billedet ikke er geografisk mærket Nat-tilstand Brug mørkt tema Attribution-ShareAlike 4.0 @@ -159,8 +157,8 @@ Ingen beskrivelse Ukendt licens Opdater - Krævet tilladelse: Læs eksternt lager. Programmet kan ikke fungere uden denne tilladelse. - Krævet tilladelse: Skriv til eksternt lager. Program kan ikke fungere uden denne funktion. + Krævet tilladelse: Læs eksternt lager. Programmet kan ikke fungere uden denne tilladelse. + Krævet tilladelse: Skriv til eksternt lager. Program kan ikke fungere uden denne funktion. Valgfri tilladelse: Hent nuværende position for kategoriforslag O.k. Steder i nærheden diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6eeba5b1f..f6fddb820 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -25,7 +25,7 @@ Anmeldung erfolgreich! Anmeldung fehlgeschlagen! Datei nicht gefunden. Bitte versuche es mit einer anderen. - Authentifizierung fehlgeschlagen! + Authentifizierung fehlgeschlagen. Bitte erneut anmelden. Hochladen gestartet! „%1$s“ hochgeladen! Tippe, um deinen Upload anzusehen @@ -53,8 +53,7 @@ Bitte gib einen Titel für diese Datei an Beschreibung Anmeldung fehlgeschlagen – Netzwerkfehler - Anmeldung fehlgeschlagen – Bitte Benutzernamen überprüfen - Anmeldung fehlgeschlagen – Bitte Passwort überprüfen + Anmeldung fehlgeschlagen. Bitte Benutzernamen und Passwort überprüfen. Zu viele erfolglose Versuche. Bitte in einigen Minuten erneut versuchen. Dieser Benutzer wurde leider auf Commons gesperrt Du musst deinen Code zur Zwei-Faktor-Authentifizierung angeben. @@ -88,6 +87,7 @@ Kategorien Einstellungen Registrieren + Vorgestellte Bilder Über Die Wikimedia-Commons-App ist eine Open-Source-App, entwickelt und gewartet von Freiwilligen der Wikimedia-Gemeinschaft. Die Wikimedia Foundation ist nicht bei der Erstellung, Entwicklung oder Wartung der App beteiligt. Einen neuen <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub-Eintrag</a> für Fehlerberichte und Vorschläge erstellen. @@ -107,7 +107,7 @@ Standardlizenz Vorherige(n) Titel/Beschreibung verwenden Aktuellen Standort automatisch abrufen - Ruft den aktuellen Standort ab, um Kategorievorschläge anzubieten, falls das Bild keine Geotags hat. + Ruft den aktuellen Standort ab, falls das Bild nicht georeferenziert ist und markiert es. Warnung: Diese Aktion verrät deinen aktuellen Standort. Nachtmodus Dunkles Thema verwenden Attribution-ShareAlike 4.0 @@ -153,14 +153,15 @@ Vermeide urheberrechtlich geschütztes Material, das du im Internet gefunden hast wie Bilder von Postern, Buchcovern etc. Verstanden? Ja! + Kategorien Lade … Keine ausgewählt Keine Beschreibung Unbekannte Lizenz Aktualisieren - Erforderliche Berechtigung: Externen Speicher lesen. Die App funktioniert ohne diese Berechtigung nicht. - Erforderliche Berechtigung: Externen Speicher beschreiben. Die App kann ohne dies nicht funktionieren. + Erforderliche Berechtigung: Externen Speicher lesen. Die App kann ohne diese Berechtigung nicht auf deine Galerie zugreifen. + Erforderliche Berechtigung: Externen Speicher beschreiben. Die App kann ohne diese Berechtigung nicht auf deine Kamera zugreifen. Optionale Berechtigung: Ruft den aktuellen Standort für Kategorievorschläge ab Okay Orte in der Nähe @@ -173,6 +174,8 @@ Titel des Mediums Beschreibung Hier folgt die Beschreibung des Mediums. Diese kann möglicherweise ziemlich lang sein und erfordert dann einen Umbruch auf mehreren Zeilen. Wir hoffen, dass sie dennoch gut aussieht. + Autor + Hier steht der Benutzername des Autors des vorgestellten Bildes. Hochgeladen am Lizenz Koordinaten @@ -215,6 +218,7 @@ Abmelden Anleitung Benachrichtigungen + Vorgestellt Orte in der Nähe können ohne Berechtigung zur Standortbestimmung nicht ermittelt werden Keine Beschreibung gefunden Commons-Dateiseite @@ -264,4 +268,20 @@ Fortfahren Abbrechen Erneut versuchen + Verstanden! + Dies sind die Orte in deiner Nähe, die Bilder zur Illustration ihrer Wikipedia-Artikel benötigen. + Das Antippen dieser Schaltfläche zeigt eine Liste mit diesen Orten + Du kannst ein Bild für einen beliebigen Ort von deiner Galerie oder Kamera hochladen + Keine Bilder gefunden! + Beim Laden der Bilder ist ein Fehler aufgetreten. + Hochgeladen von: %1$s + App teilen + Während der Bildauswahl wurden keine Koordinaten angegeben + Fehler beim Abrufen der Orte in der Nähe. + Bild des Tages + Bild des Tages + Bild erfolgreich nach %1$s auf Wikidata hinzugefügt! + Fehler bei der Aktualisierung des dazugehörigen Wikidata-Objekts! + Hintergrundbild festlegen + Hintergrundbild erfolgreich festgelegt! diff --git a/app/src/main/res/values-diq/error.xml b/app/src/main/res/values-diq/error.xml index 9776fc6b2..a9cbfb85a 100644 --- a/app/src/main/res/values-diq/error.xml +++ b/app/src/main/res/values-diq/error.xml @@ -3,10 +3,11 @@ * 1917 Ekim Devrimi * Gorizon * Marmase +* Mirzali --> Commons lığiya Oops. Thebayo nigurweyino! Şıma se kerd bı, marê vacê. Dıma e-posta ra bırışê marê. Ney timar kerdışi de marê beno desteg! - Teşekur kena + Teşekur kenan! diff --git a/app/src/main/res/values-diq/strings.xml b/app/src/main/res/values-diq/strings.xml index b88e412e7..a312fcc9d 100644 --- a/app/src/main/res/values-diq/strings.xml +++ b/app/src/main/res/values-diq/strings.xml @@ -9,18 +9,23 @@ * Mirzali --> + Asayış + Bıngeh + Lokasyon Commons + Eyari Namey karberi Parola Cı kewe + Parola, xo vira kerde? Qeyd be Cıkewtış Kerem kerên, bıpawên... Cıkewtış hewl bi. Nidekeweya de Dosya nêvineya. Dosyê da bine bıcerebnê. - Tesdiq kerdış nebı + Tesdiq kerdış nebı Barkerdış sertera! %1$s bıbar! Barkerdışê xo pıro bıde. @@ -47,8 +52,6 @@ Sername Şınasnayış Xırabiya kewten-network xeta - Ronıştışo abeno - Namey karberi ye xo kontrol kerë - Ronıştışo nêabeno - Parolay xo kontrol kerë Şıma xeylê rayi kerd ke cı kewê, a ser nêvıst. Şıma rê zehmet 2-3 deqey ra tepeya reyna bıcerrebnên. Qısur mewni rê, Karber commons dı bloqe biyo. Nidekeweya de @@ -58,6 +61,8 @@ Bar ke Kategoriyan dı cı geyr Star ke + Newe ke + Liste \@string/contributions_subtitle_zero Yew barbiyayış @@ -78,15 +83,16 @@ Qeyd be Heq te cı Qandê yew <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub-cıkewtış</a>ê neweyi rê rapor û teklifan bıaferne. - <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">Politikay nımıtışi</a> + <u>Politikaya nımıtışi</u> + <u>İştırakkerdoği</u> Heq te cı Peyd rışten bırış (E-posta ra) E-posta eyar nêbi Karıyaye Kategoriyê peyêni - Fına + Anciya bıcerrebne Bıtexelne Ron - Lisans + Lisanso hesebiyaye Attribution-ShareAlike 3.0 Attribution 3.0 CC0 @@ -121,6 +127,8 @@ E Sername + Şınasnayış + Nuştekar Lisans Koordinati Korbıze @@ -129,4 +137,7 @@ Keye Bar ke Veciyayış + Bıtexelne + Anciya bıcerrebne + Mı fehm kerd! diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 320d7a72f..02ba64754 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -29,7 +29,7 @@ Επιτυχής σύνδεση! Η είσοδος απέτυχε! Το αρχείο δεν βρέθηκε. Παρακαλώ δοκιμάστε ένα άλλο αρχείο. - Απέτυχε ο έλεγχος ταυτότητας! + Απέτυχε ο έλεγχος ταυτότητας, παρακαλώ συνδεθείτε ξανά Η αποστολή ξεκίνησε! %1$s επιφορτώθηκε! Πατήστε για να προβάλλετε την αποστολή @@ -57,8 +57,7 @@ Παρακαλώ παρέχετε ένα τίτλο για αυτό το αρχείο Περιγραφή Δεν είναι δυνατή η σύνδεση - αποτυχία του δικτύου - Δεν είναι δυνατή η σύνδεση - ελέγξτε το όνομα χρήστη σας - Δεν είναι δυνατή η σύνδεση - παρακαλούμε ελέγξτε τον κωδικό σας + Αποτυχία σύνδεσης - παρακαλώ ελέγξτε το όνομα χρήστη και τον κωδικό σας Πάρα πολλές ανεπιτυχείς προσπάθειες. Παρακαλώ δοκιμάστε ξανά σε λίγα λεπτά. Συγνώμη, αυτός ο χρήστης έχει αποκλειστεί από τα Commons Πρέπει να δώσετε τον κωδικό πιστοποίησης με δύο παράγοντες @@ -92,6 +91,7 @@ Κατηγορίες Ρυθμίσεις Εγγραφή + Προβεβλημμένες εικόνες Σχετικά Λογισμικό ανοικτού κωδικού που κυκλοφορεί υπό την <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Άδεια Apache v2</a>. Το Wikimedia Commons και το λογότυπο είναι εμπορικά σήματα του Ιδρύματος Wikimedia και χρησιμοποιούνται με άδεια από το Ίδρυμα Wikimedia. Δεν συμμετέχουμε στην δημιουργία, ανάπτυξη ή συντήρηση του Ιδρύματος Wikimedia. Δημιουργήστε ένα νέο <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub θέμα</a> για αναφορές σφαλμάτων και προτάσεις. @@ -111,7 +111,7 @@ Προεπιλεγμένη άδεια Χρήση προηγούμενου τίτλου/περιγραφής Αυτόματη ανάκτηση τρέχουσας θέσης - Ανάκτηση τρέχουσας τοποθεσία για να σας προσφέρουμε προτάσεις κατηγοριών αν η εικόνα δεν είναι γεωσεσημασμένη. + Ανακτά την τρέχουσα τοποθεσία εάν η εικόνα δεν είναι γεωσεσημασμένη, και τις γεωσημάνσεις με αυτή. Προειδοποίηση: Αυτό θα αποκαλύψει την τρέχουσα τοποθεσία σας. Νυχτερινή λειτουργία Χρήση σκοτεινού θέματος Attribution-ShareAlike 4.0 @@ -157,14 +157,15 @@ Αποφύγετε προστατευμένο υλικό που βρήκατε από το Internet, καθώς και εικόνες, αφίσες, εξώφυλλα βιβλίων, κλπ. Τι λες, μπορείς; Ναι! + Κατηγορίες Φόρτωση… Καμία επιλεγμένη Καμία περιγραφή Άγνωστη άδεια Ανανέωση - Απαιτούμενη άδεια: Ανάγνωση εξωτερικής αποθήκευσης. Η εφαρμογή δεν μπορεί να λειτουργήσει χωρίς αυτή. - Απαιτούμενη άδεια: Με εξωτερική αποθήκευση.Το πρόγραμμα δεν μπορεί να λειτουργήσει με αυτήν. + Απαιτούμενη άδεια: Ανάγνωση εξωτερικής αποθήκευσης. Η εφαρμογή δεν μπορεί να έχει πρόσβαση στην συλλογή σας χωρίς αυτή. + Απαιτούμενη άδεια: Με εξωτερική αποθήκευση. Το πρόγραμμα δεν μπορεί να έχει πρόσβαση στην κάμερα σας χωρίς αυτήν. Προαιρετική άδεια: Ανάκτηση τρέχουσας θέσης σας για προτάσεις κατηγοριών Εντάξει Κοντινοί Τόποι @@ -177,6 +178,8 @@ Τίτλος πολυμέσου Περιγραφή Η περιγραφή του πολυμέσου μπαίνει εδώ. Αυτή μπορεί να είναι σχετικά μεγάλη, και θα χρειαστεί να αναδιπλωθεί σε πολλές γραμμές. Ελπίζουμε ωστόσο ότι θα φαίνεται όμορφα. + Συγγραφέας + Το όνομα χρήστη του συγγραφέα της επιλεγμένης εικόνας πάει εδώ. Ημερομηνία φόρτωσης Άδεια Συντεταγμένες @@ -219,6 +222,7 @@ Αποσύνδεση Σεμινάριο Ενημερώσεις + Επιλεγμένο Οι κοντινές τοποθεσίες δεν μπορούν να προβληθούν δίχως τις άδειες τοποθεσίας δεν βρέθηκε περιγραφή Σελίδα φακέλλου κοινής χρήσης @@ -255,17 +259,33 @@ Βικιδεδομένα Βικιπαίδεια Κοινά - + <u>Βαθμολογήστε μας</u> <u>Συχνές ερωτήσεις</u> Παράβλεψη εισαγωγής Το διαδίκτυο δεν είναι διαθέσιμο Το διαδίκτυο είναι διαθέσιμο Σφάλμα κατά την συγκέντρωση ειδοποιήσεων Δεν βρέθηκαν ειδοποιήσεις - + <u>Μεταφράστε</u> Γλώσσες Επιλέξτε την γλώσσα που θα θέλατε να υποβάλετε μεταφράσεις για αυτή Συνέχεια Ακύρωση Ξαναπροσπαθήστε + Κατάλαβα! + Αυτά είναι τα μέρη κοντά σας που χρειάζονται φωτογραφίες για να εικονογραφηθούν τα λήμματά τους στη Βικιπαίδεια + Πατώντας αυτό το κουμπί φέρνει μια λίστα αυτών των μερών + Μπορείτε να ανεβάσετε μια εικόνα για οποιοδήποτε μέρος από την γκαλερί ή την κάμερά σας + Δεν βρέθηκαν εικόνες! + Συνέβη σφάλμα κατά το ανέβασμα των εικόνων. + Ανέβηκε από: %1$s + Κοινοποίηση εφαρμογής + Οι συντεταγμένες δεν ορίστηκαν κατά την διάρκεια της επιλογής εικόνας + Σφάλμα κατά την εύρεση κοντινών μερών. + Φωτογραφία της Ημέρας + Φωτογραφία της Ημέρας + Η εικόνα προστέθηκε επιτυχώς στο %1$s στο Wikidata! + Αποτυχία ενημέρωσης της αντιστοιχούσας οντότητας του Wikidata! + Ρύθμιση ταπετσαρίας + Η ταπετσαρία ρυθμίστηκε επιτυχώς! diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ef67e2024..4c065f214 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,6 +1,7 @@ + Itxura + Orokorra + Feedback + Kokapena Commons + Hobespenak Erabiltzaile izena Pasahitza Saioa hasi + Pasahitza ahaztu duzu? Eman izena Saioa hasten Mesedez itxaron… Saio hasiera egina Saio hasieran akatsa! Fitxategia ez da aurkitu. Mesedez saiatu beste batekin. - Autentifikazioan akatsa! + Autentifikazioan akatsa! Igoera hasi da! %1$s igotzen! Ukitu igotakoa ikusteko @@ -42,8 +48,6 @@ Izenburua Deskribapena Ezin izan da sartu - sarean akatsa - Ezin izan da sartu - ziurtatu ezazu zure erabiltzaile izena - Ezin izan da sartu - ziurta ezazu zure pasahitza Sartzeko saiakera txar gehiegi. Mesedez saiatu zaitez minutu batzuk barru. Barka, baina erabiltzaile hau blokeatuta dago Commonsen Saio hasieran akatsa @@ -54,22 +58,23 @@ Kategoriak bilatu Gorde Eguneratu + Zerrenda GPSa gaitu Oraindik ez da ezer igo - - Oraindik igoerarik ez + + \@string/contributions_subtitle_zero igoera 1 %1$d igoera Ez da kategoriak aukritu %1$s izenarekin - Gehitu kategoriak zure argazkiak Wikimedia Commonsen aurkitzen errazagoak izan daitezen. + Gehitu kategoriak zure argazkiak Wikimedia Commonsen aurkitzen errazagoak izan daitezen.\nHasi idazten kategoriak gehitzeko. Kategoriak Hobespenak Eman izena Honi buruz Open Source softwarea <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache v2 Lizentziaren</a> pean egina. Wikimedia Commons eta bere logoa Wikimedia Fundazioaren marka erregistratuak dira eta Wikimedia Fundazioaren baimenarekin erabiltzen dira. Ez gaude Wikimedia Fundaziora afiliatuta. GitHub-eko <a href=\"https://github.com/commons-app/apps-android-commons\">Iturria</a> eta <a href=\"https://commons-app.github.io/\">webgunea</a>. <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub-eko gai</a> berria sortu erroreen berri emateko. - <a href=\"https://wikimediafoundation.org/wiki/Privacy_policy\">Pribatutasun politika</a> + <u>Pribatutasun politika</u> Honi buruz Bidali zure iritzia (e-posta bidez) Posta bezerorik ez da instalatu @@ -81,7 +86,7 @@ Irudi hau %1$s lizentziapean egongo da Irudi hau bidaltzen, nire lan propioa dela aitortzen dut, copyrighta duten materiala edo selfiak ez duela, eta beste motatakoak <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">Wikimedia Ohikoaren arauak</a> Jaitsi - Lizentzia + Berezko lizentzia Aurreko izenburu/deskribapena erabili Oraingo kokapena automatikoki lortu Gau modua diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 3ec113535..fe87d5736 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -28,7 +28,7 @@ ورود موفق! ورود ناموفق! پرونده یافت نشد لطفاً پرونده دیگری را امتحان کنید. - تأیید اعتبار انجام نشد! + تأیید اعتبار انجام نشد! بارگذاری آغاز شد! %1$s بارگذاری شد! برای دیدن بارگذاریتان بر روی صفحه انگشت بزنید @@ -56,8 +56,6 @@ لطفاً نامی را برای این پرونده انتخاب کنید توضیحات قادر به ورود نیست - شکست شبکه‌ای - ناتوانی در ورود - لطفاً نام کاربریتان را بررسی کنید - ناتوانی در ورود - لطفاً گذرواژه‌یتان را بررسی کنید تلاش ناموفق بیش از حد. لطفاً چند دقیقهٔ دیگر دوباره تلاش کنید پوزش، کاربر در ویکی‌انبار بسته شده‌است باید تأیید دومرحله‌ای را فعال کنید. @@ -106,7 +104,7 @@ مجوز پیش‌فرض از عنوان/توضیحات پیشین استفاده کنید دریافت خودکار موقعیت کنونی - درحال دریافت موقعیت برای پیشنهاد رده در صورتی که برچسب جغرافیایی وجود نداشته باشد. + درحال دریافت موقعیت برای پیشنهاد رده در صورتی که برچسب جغرافیایی وجود نداشته باشد. حالت شبانه استفاده از حالت تیره CC Attribution-ShareAlike 4.0 @@ -149,8 +147,8 @@ بدون توضیحات مجوز ناشناخته تازه‌کردن - اجازه‌های مورد نیاز: مطالعهٔ حافظهٔ خارجی. اپلیکیشن بدون آن نمی‌تواند کار کند. - اجازه‌های مورد نیاز: نوشتن حافظهٔ خارجی. اپلیکیشن بدون آن نمی‌تواند کار کند. + اجازه‌های مورد نیاز: مطالعهٔ حافظهٔ خارجی. اپلیکیشن بدون آن نمی‌تواند کار کند. + اجازه‌های مورد نیاز: نوشتن حافظهٔ خارجی. اپلیکیشن بدون آن نمی‌تواند کار کند. اجازه‌های اختیاری: دریافت موقعیت برای پیشنهاد رده تأیید مکان‌‌های اطراف diff --git a/app/src/main/res/values-fi/error.xml b/app/src/main/res/values-fi/error.xml index bc47fa3fb..546752de6 100644 --- a/app/src/main/res/values-fi/error.xml +++ b/app/src/main/res/values-fi/error.xml @@ -2,10 +2,11 @@ - Commons app on kaatunut + Commons on kaatunut Pahoittelemme, virhe tapahtui. Kerro meille mitä teit äsken, sähköpostitse. Se auttaa meitä korjaamaan ongelman! Kiitos! diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 544c8bf11..7ecba53a5 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -30,7 +30,7 @@ Kirjautuminen onnistui! Kirjautuminen epäonnistui! Tiedostoa ei löytynyt. Yritä toista tiedostoa. - Tunnistautuminen epäonnistui! + Tunnistautuminen epäonnistui! Tallentaminen aloitettiin! %1$s tallennettiin! Napauta katsoaksesi tallennusta @@ -58,8 +58,6 @@ Anna tälle tiedostolle otsikko Kuvaus Kirjautuminen epäonnistui - verkkovirhe - Kirjautuminen epäonnistui - tarkista käyttäjätunnus - Kirjautuminen epäonnistui - tarkista salasanasi Liikaa epäonnistuneita yrityksiä. Yritä uudelleen parin minuutin kuluttua. Pahoittelut, tämä käyttäjä on estetty Commonsissa Anna kaksivaiheisen tunnistuksen koodi. @@ -71,6 +69,7 @@ Etsi luokkia Tallenna Päivitä + Lista GPS ei ole käytössä. Haluatko ottaa sen käyttöön? Ota GPS käyttöön Ei tallennuksia vielä @@ -111,7 +110,7 @@ Oletuslisenssi Käytä edellistä otsikkoa/kuvausta Hae tämänhetkinen sijainti automaattisesti - Nouda nykyinen sijainti asettaaksesi käyttöön luokkaehdotuksia jos kuva ei ole paikkamerkitty + Nouda nykyinen sijainti asettaaksesi käyttöön luokkaehdotuksia jos kuva ei ole paikkamerkitty Yötila Käytä tummaa teemaa Nimeä-JaaSamoin 4.0 @@ -161,8 +160,8 @@ Ei kuvausta Tuntematon lisenssi Päivitä - Vaadittu oikeus: Ulkoisen tallennustilan luku. Appi ei toimi ilman tätä oikeutta. - Vaadittava lupa: Kirjoita ulkoiseen tallennustilaan. Sovellus ei voi toimia ilman tätä. + Vaadittu oikeus: Ulkoisen tallennustilan luku. Sovellus ei voi päästä galleriaasi ilman tätä oikeutta. + Vaadittu oikeus: Kirjoita ulkoiseen tallennustilaan. Sovellus ei voi päästä kameraasi ilman tätä oikeutta. Valinnainen lupa: Saada tämänhetkinen sijainti loukkasuosituksia varten. OK Lähellä olevat paikat @@ -175,6 +174,7 @@ Median otsikko Kuvaus Median kuvaus menee tänne. Tämä voi mahdollisesti olla melko pitkä, ja sen täytyy kääriä poikittain useita rivejä. Toivomme, että se näyttää silti hyvältä. + Tekijä Tallennuspäivämäärä Lisenssi Koordinaatit @@ -221,6 +221,7 @@ kuvausta ei löytynyt Commons-tiedostosivu Wikidata-kohde + Wikipedia-artikkeli Virhe varastoidessa kuvia Tiedoston yksilöllinen ja kuvaava otsikko, jota käytetään tiedostonimenä. Voit käyttää tavallista kieltä välilyönnein. Älä sisällytä tiedoston päätettä. Kuvaile mediaa niin paljon kuin mahdollista: Missä se otettiin? Mitä se esittää? Mikä on asiayhteys? Kuvaile esineitä tai henkilöitä. Tuo ilmi tietoja, joita ei ole helppo arvailla, esimerkiksi vuorokaudenaika, jos se on maisema. Jos media esittää jotain epätavallista, selitä, mikä tekee siitä epätavallisen. @@ -233,6 +234,9 @@ Lähetä lokitiedosto Lähetä lokitiedosto kehittäjille sähköpostin kautta Virhe! URL-osoitetta ei löytynyt + Ehdotettu poistettavaksi + Tätä kuvaa on ehdotettu poistettavaksi. + Näytä selaimessa Sijainti ei ole muuttunut. Sijainti ei käytettävissä. Lupa vaaditaan läheisten paikkojen luettelon näyttämiseen @@ -243,6 +247,21 @@ Kiitos muokkaamisestasi %1$s mainitsi sinut %2$s. Vaihda näkymä - Usein Kysytyt Kysymykset + WIKIDATA + WIKIPEDIA + COMMONS + <u>Arvostele meidät</u> + <u>UKK</u> Ohita opetus + Internet ei saatavissa + Internet saatavana + <u>Käännä</u> + Kielet + Jatka + Peruuta + Yritä uudelleen + Selvä! + Kuvia ei löytynyt! + Tallentanut: %1$s + Jaa sovellus diff --git a/app/src/main/res/values-fo/strings.xml b/app/src/main/res/values-fo/strings.xml index c4649407a..369c59d5d 100644 --- a/app/src/main/res/values-fo/strings.xml +++ b/app/src/main/res/values-fo/strings.xml @@ -12,7 +12,7 @@ Vinarliga bíða… Innritan væleydnað! Innritan miseydnaðist - Góðkenning miseydnaðist! + Góðkenning miseydnaðist! Upplóting er byrjað! %1$s er lagt út! Trýst fyri at síggja tað sum tú legði út @@ -34,8 +34,6 @@ Heiti Frágreiðing Ómøguligt at rita inn - feilur í netsambandinum - Ómøguligt at rita inn - vinarliga eftirkanna títt brúkaranavn - Ómøguligt at rita inn - vinarliga kanna eftir, um títt loyniorð er rætt Ov nógv miseydnaðar royndir. Vinarliga royn aftur um fáir minuttir Haldið okkum tilgóðar, hesin brúkari er blivin sperraður á Commons Login miseydnaðist diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8897574a9..63eb7eefb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,9 +1,12 @@ @@ -26,7 +27,7 @@ Accedeu correctamente! Erro durante o inición de sesión! Ficheiro non atopado. Por favor, probe con outro. - Erro durante a autenticación! + Erro durante a autenticación! A carga comezou! Cargouse \"%1$s\"! Prema para ollar a súa carga @@ -54,8 +55,7 @@ Por favor, proporcione un título para este ficheiro Descrición Erro ao acceder ao sistema: Fallou a rede - Erro ao acceder ao sistema: Comprobe o seu nome de usuario - Erro ao acceder ao sistema: Comprobe o seu contrasinal + Non se pode acceder. Revise o nome de usuario e o contrasinal Demasiados intentos incorrectos. Inténteo de novo nuns minutos. Sentímolo, este usuario está bloqueado en Commons Debe proporcionar o seu código de autenticación de dous factores. @@ -89,10 +89,11 @@ Categorías Configuracións Rexistrarse + Imaxes destacadas Acerca de A aplicación Wikimedia Commons é unha aplicación de código aberto creada e mantida polos cesionarios e voluntarios da comunidade de Wikimedia. A Fundación Wikimedia non está involucrada na creación, desenvolvemento ou mantemento da aplicación. Crear unha nova <a href=\"https://github.com/commons-app/apps-android-commons/issues\">incidencia</a> para informar de problemas e suxestións. - <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">Política de privacidade</a> + <u>Política de privacidade</u> <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\">Créditos</a> Acerca de Enviar comentarios (por correo electrónico) @@ -108,7 +109,7 @@ Licenza por defecto Usar o título ou a descrición anterior Obter automaticamente a localización actual - Obter a localización actual para ofrecer suxestións de categoría se a imaxe non está xeolocalizada + Obter a localización actual para ofrecer suxestións de categoría se a imaxe non está xeolocalizada Modo nocturno Usar tema escuro Recoñecemento-CompartirIgual 4.0 @@ -160,8 +161,8 @@ Sen descrición Licenza descoñecida Refrescar - Permiso necesarioː ler un almacenamento externo. A aplicación non pode funcionar sen isto. - Permiso necesario: Escribir en almacenamento externo. A aplicación non pode funcionar sen el. + Permiso necesarioː ler un almacenamento externo. A aplicación non pode acceder á súa galería sen isto. + Permiso necesario: Escribir en almacenamento externo. A aplicación non pode acceder á súa cámara sen el. Permiso opcionalː obter a localización actual para suxerir categorías Aceptar Lugares próximos @@ -174,6 +175,8 @@ Título do ficheiro multimedia Descrición Aquí vai a descrición do ficheiro multimedia. Potencialmente, pode ser bastante longo, e necesitará agruparse en múltiples liñas. De tódolos xeitos esperamos que se vexa ben. + Autor + O nome de usuario do autor da imaxe destacada vai aquí. Data de suba Licenza Coordenadas @@ -216,6 +219,7 @@ Saír Titorial Notificacións + Destacados Os sitios situados preto non poden visualizarse sen permisos de localización non se atopou descrición Páxina do ficheiro en Commons @@ -251,14 +255,29 @@ WIKIDATA WIKIPEDIA COMMONS - FAQ + <u>Avalíenos</u> + <u>FAQ</u> Saltar titorial Internet non dispoñible Internet dispoñible Erro ó recuperar as notificacións Non se atopou ningunha notificación + <u>Traducir</u> Linguas Seleccione a lingua para a que quere enviar as traducións Proceder Cancelar + Reintentar + Entendido! + Hai sitios preto de vostede que precisan fotos para ilustrar os seus artigos de Wikipedia + Premendo neste botón aparecerá unha lista destes lugares + Pode cargar unha imaxe de calquera lugar dende a galería ou a cámara + Non se atopou ningunha imaxeǃ + Houbo un erro ó subir as imaxes. + Subida porː %1$s + Compartir a aplicación + Non se indicaron as coordenadas ó seleccionar a imaxe + Erro ó procurar os lugares próximos. + Definir imaxe de fondo + A imaxe de fondo configurouse correctamenteǃ diff --git a/app/src/main/res/values-haw/strings.xml b/app/src/main/res/values-haw/strings.xml index a81ab120d..7693bc628 100644 --- a/app/src/main/res/values-haw/strings.xml +++ b/app/src/main/res/values-haw/strings.xml @@ -12,7 +12,7 @@ E kali ke ʻoluʻolu… ʻEʻe kūleʻa Hāʻule ka ʻeʻena - Hāʻule ka hōʻoiaʻiʻo ʻike + Hāʻule ka hōʻoiaʻiʻo ʻike Ua hoʻomaka ka hoʻouka! Ua hoʻouka ʻia ʻo %1$s Kīkē no ka ʻike ʻana o kāu hoʻouka @@ -34,8 +34,6 @@ Poʻo Inoa Hōʻike ʻAno Hiki ʻole ke ʻeʻe - hāʻule pūnaewele - Hiki ʻole ke ʻeʻe - hōʻoiaʻiʻo i kāu inoa mea hoʻohana ke ʻoluʻolu - Hiki ʻole ke ʻeʻe - hōʻoiaʻiʻo i kāu ʻōlelo hūnā ke ʻoluʻolu Hoʻāʻo ʻeʻe ʻole he nui kā. E ʻoluʻolu, e hana hou i ka wā hou aku E kala mai, ua pale ʻia kēia mea hoʻohana ma ke Kahilehulehu Hāʻule ka ʻeʻena diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 6bf7385be..4858c991a 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -26,7 +26,7 @@ प्रवेश में सफल हुआ! प्रवेश विफल हुआ! फ़ाइल नहीं मिली, कृपया अन्य फ़ाइल से प्रयास करें। - प्रमाणीकरण विफल! + प्रमाणीकरण विफल! अपलोड आरंभ! %1$s अपलोड हुआ! अपना अपलोड देखने के लिए टैप करें @@ -53,8 +53,6 @@ शीर्षक विवरण प्रवेश नहीं हो रहा - नेटवर्क विफल - प्रवेश नहीं हो रहा - कृपया अपना सदस्य नाम जाँचें - प्रवेश नहीं हो रहा - कृपया अपना पासवर्ड जाँचें ढेर सारे असफल प्रयास होने के कारण कुछ मिनटों के बाद प्रयास करें। क्षमा करें, यह सदस्य कॉमन्स में अवरोधित है आपको अपना दो कारक प्रमाणन कोड प्रदान करना होगा। @@ -106,7 +104,7 @@ लाइसेन्स पिछले शीर्षक/विवरण का उपयोग करें वर्तमान स्थान स्वतः ज्ञात करें - यदि छवि पर जियोटैग नहीं है तो श्रेणियों के सुझाव हेतु वर्तमान स्थान ज्ञात करें। + यदि छवि पर जियोटैग नहीं है तो श्रेणियों के सुझाव हेतु वर्तमान स्थान ज्ञात करें। रात्रि मोड डार्क थीम का प्रयोग करें एट्रीब्यूशन-शेयरअलाइक 4.0 @@ -149,8 +147,8 @@ कोई विवरण नहीं अज्ञात लाइसेन्स ताजा करें - अनिवार्य अनुमति: बाहरी स्मृति पढ़ें। एप इसके बिना कार्य नहीं करेगा। - अनिवार्य अनुमति:बाहरी कंप्यूटर स्टोरेज लिखना|इसके बिना एप कार्य नहीं करेगा। + अनिवार्य अनुमति: बाहरी स्मृति पढ़ें। एप इसके बिना कार्य नहीं करेगा। + अनिवार्य अनुमति:बाहरी कंप्यूटर स्टोरेज लिखना|इसके बिना एप कार्य नहीं करेगा। वैकल्पिक अनुमति: श्रेणी सुझाव हेतु वर्तमान स्थान ज्ञात करें ठीक है आसपास के स्थान diff --git a/app/src/main/res/values-hr/error.xml b/app/src/main/res/values-hr/error.xml new file mode 100644 index 000000000..4bc77982c --- /dev/null +++ b/app/src/main/res/values-hr/error.xml @@ -0,0 +1,10 @@ + + + + Aplikacija Zajednički poslužitelj je prestala s radom + Nešto je krenulo po krivu! + Napišite nam što radite i podijelite s nama putem elektroničke pošte. Pomoći će nam da to popravimo! + Hvala Vam! + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml new file mode 100644 index 000000000..34be966b1 --- /dev/null +++ b/app/src/main/res/values-hr/strings.xml @@ -0,0 +1,275 @@ + + + + Izgled + Opće + Povratna informacija + Lokacija + Zajednički poslužitelj + + Postavke + Suradničko ime + Zaporka + Prijavite se na Commons beta račun + Prijavi se + Zaboravljena zaporka? + Otvori račun + Prijava + Molimo pričekajte ... + Prijava uspješna! + Prijava neuspješna! + Datoteka nije pronađena. Molimo probajte drugu. + Autentifikacija neuspješna! + Postavljanje započeto! + %1$s postavljeno! + Dodirnite da biste vidjeli datoteku + Počinje postavljanje %1$s + Postavljanje %1$s + Završeno postavljanje %1$s + Postavljanje %1$s neuspješno + Dodirnite da biste vidjeli + + Postavlja se %1$d datoteka + Postavljaju se %1$d datoteke + + Moja nedavja postavljanja + U redu čekanja + Neuspješno + %1$d%% postavljeno + Postavljanje + Iz galerije + Napravi sliku + U blizini + Moja postavljanja + Podijeli + Pogledaj u pregledniku + Naziv + Molimo imenujte ovu datoteku + Opis + Prijava nije moguća - mrežna pogrješka + Prijava nije moguća - molimo provjerite suradničko ime i zaportku + Previše neuspješnih pokušaja, molimo probajte opet za par minuta. + Ispričavamo se, ovaj je suradnik blokiran na Zajendičkom poslužitelju + Morate upisati autetifikacijski kôd od dva faktora + Prijava neuspješna + Postavljanje + Imenujte ovaj set + Promjene + Postavljanje + Pretraži kategorije + Spremi + Osvježi + Popis + GPS je onemogućen na Vašem uređaju. Želite li ga omogućiti? + Omogući GPS + Nemate još postavljenih datoteka + + \@string/contributions_subtitle_zero + %1$d postavljena datoteka + %1$d postavljene datoteke + + + Započeto %1$d postavljanje + Započeta %1$d postavljanja + + + %1$d postavljanje + %1$d postavljanja + + Nema kategorija koje odgovoraju upitu %1$s + Dodajte slikama kategorije kako bi se lakše pronašle. Da biste ih dodali, započnite s upisivanjem. + Kategorije + Postavke + Otvori račun + Izabrane slike + O + Aplikacija The Wikimedia Commons je aplikacija otvorenog kôda koju razvijaju i održavaju volonteri Wikimedijine zajednice. Zaklada Wikimedija nije uključena u stvaranje, razvoj ili održavanje ove aplikacije. + Da biste prijavili poteškoću ili dali prijedlog, stvorite <a href=\"https://github.com/commons-app/apps-android-commons/issues\">novi zahtjev na GitHubu</a>. + <u>Politika privatnosti</u> + <u>Zasluge</u> + O + Pošaljite povratnu informaciju (putem elektroničke pošte) + Klijent za elektroničku poštu nije instaliran + Nedavno rabljene kategorije + Pričekajte za prvu sinkronizaciju... + Nemate još postavljenih slika. + Pokušaj ponovo + Odustani + Ova će slika biti licencirana pod %1$s + Slanjem ove slike izjavljujem da je ona moje djelo i ne sadrži materijale zaštićene autorskim pravom ili selfije, te da je u skladu sa <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">smjernicama Zajedničkog poslužitelja</a>. + Preuzmi + Podrazumijevana licencija + Rabite prethodni naziv/opis + Automatski pribavi trenutačnu lokaciju + Ako slika nema oznaku lokacije, pribavi trenutačnu lokaciju kako bi se mogle ponuditi kategorije + Noćni način + Rabi tamnu temu + Imenovanje-Dijeli pod istim uvjetima 4.0 + Imenovanje 4.0 + Imenovanje-Dijeli pod istim uvjetima 3.0 + Imenovanje 3.0 + CC0 + CC BY-SA 3.0 + CC BY-SA 3.0 (Austrija) + CC BY-SA 3.0 (Njemačka) + CC BY-SA 3.0 (Estonija) + CC BY-SA 3.0 (Španjolska) + CC BY-SA 3.0 (Hrvatska) + CC BY-SA 3.0 (Luksemburg) + CC BY-SA 3.0 (Nizozemska) + CC BY-SA 3.0 (Norveška) + CC BY-SA 3.0 (Poljska) + CC BY-SA 3.0 (Rumunjska) + CC BY 3.0 + CC BY-SA 4.0 + CC BY 4.0 + CC Zero + Na Zajedničkom poslužitelju se nalazi većina slika rabljena na Wikipediji. + Vaše slike pomažu u edukaciji ljudi diljem svijeta! + Molimo postavite slike koje su u cijelosti Vaše djelo: + Objekti iz prirode (cvijeće, životinje, planine)\n• Korisni objekti (bicikla, željezničke postaje)\n• Poznate osobe (Vaš gradonačelnik, olimpijski sportaš kojeg ste sreli) + Objekti iz prirode (cvijeće, životinje, planine) + Korisni objekti (bicikla, željezničke postaje) + Poznate osobe (Vaš gradonačelnik, olimpijski sportaš kojeg ste sreli) + Molimo NE postavljajte: + - selfije ili slike Vaših prijatelja\n- slike koje ste preuzeli s interneta\n- snimke ekrana zaštićenih aplikacija + Selfije ili slike Vaših prijatelja + Slike koje ste preuzeli s interneta + Snimke ekrana zaštićenih aplikacija + Primjer postavljanja: + - Naziv: Sydneyska opera\n- Opis: Sydneyska opera viđena iz zaljeva\n- Kategorije: Sydney Opera House from the west, Sydney Opera House remote views + Naziv: Sydneyska opera + Opis: Sydneyska opera viđena iz zaljeva + Kategorije: Sydney Opera House from the west, Sydney Opera House remote views + Dijelite Vaše slike. Pomozite da članci na Wikipediji zažive! + Slike na wikipediji su sa Zajedničkog poslužitelja. + Vaše slike pomažu u edukaciji ljudi diljem svijeta. + Izbjegavajte materijale s autorskim pravima koje ste pronašli na internetu (slike plakata, naslovnice knjiga, i slično). + Jeste li razumjeli? + Da! + Kategorije + Učitavanje... + Ništa nije odabrano + Nema opisa + Nepoznata licencija + Osvježi + Potrebno dopuštenje čitanja vanjske pohrane. Bez toga aplikacija ne može pristupiti Vašoj galeriji. + Potrebno dopuštenje spremanja na vanjsku pohranu. Bez toga aplikacija ne može pristupiti Vašoj kameri. + Potrebno dopuštenje za određivanje trenutačne lokacije za prijedloge kategorija (nije obvezno) + U redu + Mjesta u blizini + Nisu pronađena mjesta u blizini + Upozorenje + Ova datoteka već postoji na Zajedničkom poslužitelju. Jeste li sigurni da želite nastaviti? + Da + Ne + Naslov + Naslov medija + Opis + Ovdje ide opis datoteke. Mogao bi biti poprilično dug i trebat će se prelomiti u nekoliko redova. Nadamo se da će lijepo izgledati. + Autor + Ovdje ide suradničko ime autora izabrane slike. + Datum postavljanja + Licencija + Koordinate + Ništa nije navedeno + Postani beta tester + Prijavite se na naš beta-kanal na Google Playu i dobijte raniji pristup novim mogućnostima i ispravkama pogrješaka + Kôd za provjeru u 2 koraka + Moje ograničenje nedavnih postavljanja + Najviše moguće + Nije moguće prikazati više od 500 + Postavi ograničenje nedavnih postavljanja + Kôd za provjeru u 2 koraka nije podržan. + Zaista se želite odjaviti? + Logotip Zajedničkog poslužitelja + Mrežno mjesto Zajedničkog poslužitelja + Stranica Zajedničkog poslužitelja na Facebooku + Izvorni kôd Zajedničkog poslužitelja na Githubu + Pozadinska slika + Slika nije uspjela + Slika nije pronađena + Postavi sliku + Planina Zao + Ljame + Dugin most + Tulipan + Bez selfija + Vlasnička slika + Welcome (Wikipedija) + Dobro došli (autorska prava) + Sidnejska opera + Odustani + Otvori + Zatvori + Početna stranica + Postavljanje + U blizini + O + Postavke + Povratna informacija + Odjava + Upute + Obavijesti + Izabrano + Mjesta u blizini ne mogu biti prikazana bez dopuštenja određivanja lokacije + nema opisa + Stranica datoteke na Zajedničkom poslužitelju + Stavka na Wikidati + Članak na Wikipediji + Pogrješka predmemoriranja slika + Jedinstveni naziv datoteke koji će služiti kao njeno ime. Možete koristiti uobičajeni jezik s razmacima. Ne uključuje datotečni nastavak. + Opišite medij što je više moguće: gdje je napravljen, što prikazuje,... Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično. + Slika je pretamna, želite ili je ipak ostaviti? Zajednički poslužitelj je namijenjen slikama od enciklopedijske vrijednosti. + Slika je mutna, želite ili je ipak ostaviti? Zajednički poslužitelj je namijenjen slikama od enciklopedijske vrijednosti. + Daj dopuštenje + Rabi vanjsku pohranu + Spremite slike načinjene kamerom Vašeg uređaja + Prijavite se na Vaš račun + Pošalji zapisnik + Pošalji zapisnik elektroničkom poštom razvijateljima + Preglednik nije pronađen + Pogrješka! URL nije pronađen + Predloži za brisanje + Slika je predložena za brisanje + Pogledaj u pregledniku + Lokacija nepromijenjena. + Lokacija nedostupna. + Potrebno je dopuštenje za popis mjesta u blizini + PRIBAVI UPUTE + PROČITAJ ČLANAK + %1$s, dobro došli na Zajednički poslužitelj! Drago nam je da ste tu. + %1$s Vam je ostavio poruku na Vašoj razgovornoj stranici + Hvala Vam na uređivanju + %1$s Vas je spomenuo na %2$s. + Prebaci prikaz + UPUTE + WIKIDATA + WIKIPEDIJA + ZAJEDNIČKI POSLUŽITELJ + <u>Ocijenite nas</u> + <u>ČPP</u> + Preskoči upute + Internet nije dostupan + Internet je dostupan + Pogrješka dohvaćanja obavijesti + Nema obavijesti + <u>Prevedi</u> + Jezici + Odaberite jezik na koji bi željeli prevoditi + Nastavi + Odustani + Pokušaj ponovo + U redu! + Ovo su mjesta u blizini koja trebaju slike za ilustriranje članaka o njima na Wikipediji + Dodirnite da biste dobili popis ovih mjesta + Možete postaviti sliku s bilo kojeg mjesta u Vašoj galeriji ili kameri + Slike nisu pronađene! + Pogrješka prilikom učitavanja slika. + Postavio: %1$s + Aplikacija za dijeljenje + Prilikom označavanja slike koordinate nisu navedene + Pogrješka prilikom dohvaćanja mjesta u blizini. + diff --git a/app/src/main/res/values-hrx/strings.xml b/app/src/main/res/values-hrx/strings.xml index 7905a92d7..b2b57412a 100644 --- a/app/src/main/res/values-hrx/strings.xml +++ b/app/src/main/res/values-hrx/strings.xml @@ -13,7 +13,7 @@ Bittschön woorte … Oonmeldung erfollichreich! Oonmeldung fehlgeschlooht! - Authentifizierung fehlgeschlooht! + Authentifizierung fehlgeschlooht! Hochloode oongefang! „%1$s“ hochgelood! Tippe mit dein Fingerspitze, um dein Upload (Ufflloodung) oonzusiehn @@ -35,8 +35,6 @@ Titel Beschreibung Oonmeldung fehlgeschlooht – Netzwerrekfehler - Oonmeldung fehlgeschlooht – Bittschön Benutzernoome üwerprüfe - Oonmeldung fehlgeschlooht – Bittschön Passwort üwerprüfe Zu viele erfollichlose Versuche. Bittschön in en poor Minute wieder erneit versuche. Der Benutzer woard leider uff Commons gesperrt Oonmeldung fehlgeschlooht diff --git a/app/src/main/res/values-hsb/strings.xml b/app/src/main/res/values-hsb/strings.xml index 97b0d1af5..a1a9a6578 100644 --- a/app/src/main/res/values-hsb/strings.xml +++ b/app/src/main/res/values-hsb/strings.xml @@ -12,7 +12,7 @@ Prošu čakaj… Přizjewjenje wuspěšne! Přizjewjenje je so njeporadźiło! - Awtentifikacija je so njeporadźiła! + Awtentifikacija je so njeporadźiła! Nahraće je so započało! %1$s nahraty! Dótkń so, zo by swoje nahraće widźał @@ -34,8 +34,6 @@ Titul Wopisanje Přizjewjenje je so njeporadźiło - syćowy zmylk - Přizjewjenje je njemóžno - prošu přepruwuj swoje wužiwarske mjeno - Přizjewjenje njeje móžno - prošu přepruwuj swoje hesło Přewjele njewuspěšnych pospytow. Prošu spytaj za něšto mjeńšin hišće raz. Tutoho wužiwarja su bohužel na Commons zablokowali Přizjewjenje je so njeporadźiło diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 3b163f9a2..30bf6a193 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -13,19 +13,25 @@ * ViDam --> + Megjelenés + Általános + Visszajelzés + Helyszín Commons + Beállítások Felhasználónév Jelszó Jelentkezz be a Commons Béta fiókoddal Bejelentkezés + Elfelejtett jelszó: Regisztráció Belépés… Kérlek várj… Sikeres bejelentkezés A bejelentkezés nem sikerült. A fájl nem található. Próbálkozz másik fájllal. - Sikertelen hitelesítés. + Sikertelen hitelesítés. Feltöltés elindult. %1$s feltöltve. Feltöltés megtekintése @@ -50,10 +56,10 @@ Megosztás Megtekintés böngészőben Cím + Kérlek, adj címet a fájlnak Leírás Nem lehet bejelentkezni - hálózati hiba - Nem lehet bejelentkezni - ellenőrizd a felhasználóneved - Nem lehet bejelentkezni - ellenőrizd a jelszavad + Nem sikerült bejelentkezni – kérlek, ellenőrizd a felhasználónevedet és a jelszavadat Túl sok sikertelen próbálkozás. Próbálkozz újra pár perc múlva. Sajnáljuk, ezt a felhasználót blokkolták a Commonson Meg kell adnia a kétlépcsős hitelesítő kódját. @@ -65,6 +71,7 @@ Keresés a kategóriák között Mentés Frissítés + Lista A GPS le van tiltva az eszközén. Szeretné engedélyezni? GPS engedélyezése Még nincsenek feltöltések @@ -86,11 +93,12 @@ Kategóriák Beállítások Regisztráció + Kiemelt képek Névjegy A Wikimedia Commons applikáció egy nyílt forráskódú szoftver, amit a Wikimedia-közösség önkéntesei készítettek és tartanak karban. A Wikimédia Alapítvány nem vesz részt az applikáció megalkotásában, fejlesztésében és üzemeltetésében. Nyiss egy új <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub-problémát</a> hibabejelentéssel vagy fejlesztési javaslattal. - <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">Adatvédelmi irányelvek</a> - <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\">Köszönetnyilvánítás</a> + <u>Adatvédelmi irányelvek</u> + <u>Köszönetnyilvánítás</u> Névjegy Visszajelzés küldése (e-mailben) Nincs telepített levelezőprogram @@ -102,10 +110,10 @@ Ez a kép %1$s licenc alatt kerül feltöltésre A kép feltöltésével kijelentem, hogy ez a saját munkám és nem tartalmaz jogvédett anyagot, nem szelfi és megfelel a<a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">Wikimedia Commons irányelveinek</a>. Letöltés - Licenc + Alapértelmezett licenc Előző cím/leírás használata Automatikusan megkapja a jelenlegi helyet - Lekéri a jelenlegi helyet, hogy lehetőség legyen kategóriajavaslatokra a nem földrajzi címkézett képeknél. + Lekéri a jelenlegi helyet, hogy lehetőség legyen kategóriajavaslatokra a nem földrajzi címkézett képeknél. Éjszakai mód Sötét téma használata Nevezd meg! – Így add tovább! 4.0 @@ -131,11 +139,16 @@ A Wikimédia Commons tárolja a Wikipédián használt képek többségét. A képeid segítik az ismeretterjesztést az egész világon! Kérjük, tölts fel képeket, amelyeket teljes mértékben te fotóztál vagy készítettél: - - Természeti tárgyak (virágok, állatok, hegyek)\n- Hasznos tárgyak (kerékpárok, vasútállomások)\n- Híres emberek (a polgármestered, olimpikonok, akikkel találkoztál) + Természeti tárgyak (virágok, állatok, hegyek)\n• Hasznos tárgyak (kerékpárok, vasútállomások)\n• Híres emberek (a polgármestered, olimpikonok, akikkel találkoztál) + Természeti tárgyak (virágok, állatok, hegyek) + Hasznos tárgyak (kerékpárok, vasútállomások) + Híres emberek (a polgármestered, olimpikonok, akikkel találkoztál) Kérjük, NE tölts fel: - Szelfiket vagy képeket a barátaidról\n- Internetröl letöltött képeket\n- Kereskedelmi alkalmazások képernyőképeit + Az Internetről letöltött képek Példa feltöltés: - Cím: Sydney-i Operaház\n- Leírás: A Sydney-i Operaház az öböl túlpartjáról\n- Kategóriák: Sydney Opera House from the west, Sydney Opera House remote views + Cím: Sydney-i Operaház Tedd közzé a képeidet! Segíts életre kelteni a Wikipédia-szócikkeket! A Wikipédián található képek a Wikimédia Commonsből származnak. A képeid segítenek a világ minden táján élő emberek oktatásában. @@ -149,7 +162,7 @@ Ismeretlen licenc Frissítés Szükséges engedély: Külső tárhely olvasása. Az alkalmazás nem működik enélkül. - Szükséges engedély: Külső tárhely írása. Az alkalmazás nem működik enélkül. + Szükséges engedély: Külső tárhely írása. Az alkalmazás nem tudja használni a kamerát enélkül. Lehetséges engedély: Jelenlegi hely megszerzése, a kategóriajavaslatok lehetőségéért. OK Közeli helyek @@ -162,6 +175,7 @@ Média címe Leírás A média leírása kerül ide. Ez akár egészen hosszú is lehet, és több sorba fog kerülni. Azért reméljük, jól néz majd ki. + Szerző Feltöltési dátum Licenc Koordináták @@ -178,6 +192,7 @@ Commons Logo Commons weboldal Commons Facebook-oldal + Commons Github forráskód Háttérkép Nem található kép Kép feltöltése @@ -201,21 +216,57 @@ Kijelentkezés Bevezető Értesítések + Kiemelt Közeli helyek nem megjeleníthetőek a helyadatokhoz való hozzáférés engedélyezése nélkül nincs leírás Commons leírólap Wikidata-elem + Wikipédia-cikk Hiba a képek gyorsítótárazásakor Egy egyedi, leíró cím a fájlnak, ami fájlnévként fog szolgálni. Egyszerű nyelvezetet használhatsz szóközökkel. Ne tedd bele a kiterjesztést. Kérlek a lehető legteljesebb módon írd le a fájlt: hol készült, mit ábrázol, mi a kontextus? Kérlek add meg az objektumokat vagy személyeket a képen, valamint a nehezen kitalálható információkat (például a kép készítésének dátumát, ha az egy tájkép). Amennyiben a média valami szokatlant ábrázol, kérlek fejtsd ki, hogy mi teszi szokatlanná. + Ez a fénykép túl sötét, biztos fel akarod tölteni? A Wikimédia Commons csak enciklopédikus értékkel bíró képeket tart meg. + Ez a fénykép homályos, biztos fel akarod tölteni? A Wikimédia Commons csak enciklopédikus értékkel bíró képeket tart meg. Engedély adása Külső tárhely használata Az alkalmazáson belüli kamerával készült képek mentése az eszközre Bejelentkezés a fiókodba Naplófájlok küldése Naplófájlok küldése e-mailben a fejlesztőknek + Nem található böngésző az URL megnyitásához + Hiba! URL nem található. + Törlésre jelölés + Ezt a képet törlésre jelölték. + Megtekintés böngészőben A hely nem változott. A hely nem érhető el. + Közeli helyek listájának megtekintéséhez engedély szükséges + SZÓCIKK OLVASÁSA Üdvözlünk a Wikimedia Commonson, %1$s! Örülünk, hogy itt vagy. + %1$s üzenetet hagyott a vitalapodon Köszönjük a szerkesztésedet! + WIKIDATA + WIKIPÉDIA + COMMONS + <u>Értékelj minket</u> + <u>GYIK</u> + Útmutató átugrása + Internet nem elérhető + Internet elérhető + Nincs értesítés + <u>Fordítás</u> + Nyelvek + Folytatás + Mégse + Újra + Értettem! + Ezek a helyek vannak a közeledben, amikről van Wikipédia szócikk és nincs bennük kép. + A gombra koppintva bejön egy lista, ami ezeket a helyeket mutatja. + Bármelyik helyhez feltölthetsz képet a galériádból vagy készíthetsz újat a kamerával. + Nem található kép! + Képbetöltés közben hiba történt + Feltöltötte: %1$s + Alkalmazás megosztása + A koordináták nem lettek megadva a kép kiválasztásakor. + Hiba a közeli helyek elérésekor. diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 16185f35d..70a0f556a 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -19,7 +19,7 @@ Berhasil masuk log! Masuk log gagal! Berkas tidak ditemukan, silakan coba berkas lain - Autentikasi gagal! + Autentikasi gagal! Mulai unggah! %1$s terunggah! Tekan untuk melihat unggahan Anda @@ -46,8 +46,6 @@ Judul Deskripsi Tidak dapat login - kesalahan pada jaringan - Tidak dapat masuk log - harap periksa nama pengguna Anda - Tidak dapat masuk log - harap periksa kata sandi Anda Terlalu banyak usaha yang gagal. Harap coba lagi dalam beberapa menit Maaf, pengguna ini telah diblokir di Commons Gagal masuk log diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 15afc41f2..6eff4d9f3 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -21,7 +21,7 @@ Innskráning tókst! Innskráning mistókst! Skráin fannst ekki. Prófaðu aðra skrá. - Auðkenning tókst ekki! + Auðkenning tókst ekki! Innsending í gangi! %1$s var sent inn! Bankaðu til að skoða sem þú ert að senda inn @@ -49,8 +49,6 @@ Gefðu þessari skrá einhvern titil Lýsing Innskráning mistókst - bilun í neti - Innskráning mistókst. Athugaðu notandanafnið þitt - Innskráning mistókst. Athugaðu lykilorðið þitt Of margar misteknar tilraunir. Reyndu aftur eftir nokkrar mínútur. Því miður, þessi notandi hefur verið bannaður á Commons Þú verður að setja inn tveggja-þrepa auðkenningarkóðann þinn. @@ -62,6 +60,7 @@ Leita í flokkum Vista Endurlesa + Listi GPS er óvirkt í tækinu þínu. Viltu virkja það? Virkja GPS Engar innsendingar ennþá @@ -83,6 +82,7 @@ Flokkar Stillingar Nýskrá + Áberandi myndir Um Wikimedia Commons forritið er opinn og frjáls hugbúnaður sem gerður er og viðhaldið af stuðningsaðilum og sjálfboðaliðum Wikimedia samfélagsins. Wikimedia Foundation sjálfseignarstofnunin kemur ekki að gerð, forritun eða viðhaldi forritsins. \ Útbúðu nýjar <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub tilkynningar (issue)</a> til að koma villum og uppástungum á framfæri. @@ -102,7 +102,7 @@ Sjálfgefið notkunarleyfi Nota fyrri titil/lýsingu Ná sjálfvirkt í núverandi staðsetningu - Lesa núverandi staðsetningu til að geta stungið upp á flokkum ef myndin er ekki með hnattstaðsetningarhnitum + Lesa núverandi staðsetningu til að geta stungið upp á flokkum ef myndin er ekki með hnattstaðsetningarhnitum Næturhamur Nota dökkt þema Attribution-ShareAlike 4.0 @@ -154,8 +154,8 @@ Engin lýsing Óþekkt notkunarleyfi Endurlesa - Nauðsynlegar heimildir: Lesa ytri gagnageymslu. Forritið virkar ekki án þess. - Nauðsynlegar heimildir: Skrifa í ytri gagnageymslu. Forritið virkar ekki án þess. + Nauðsynlegar heimildir: Lesa ytri gagnageymslu. Forritið fær ekki aðgang að myndasafni ekki án þessa. + Nauðsynlegar heimildir: Skrifa í ytri gagnageymslu. Forritið nær ekki sambandi við myndavél ekki án þessa. Nauðsynlegar heimildir: Lesa núverandi staðsetningu til að geta stungið upp á flokkum Í lagi Staðir í nágrenninu @@ -168,6 +168,8 @@ Titill þessa gagnamiðils Lýsing Lýsing á gagnamiðlinum kemur hér. Þetta má vera nokkuð langt og mun þurfa að skríða yfir nokkrar línur. Við vonum að þetta líti samt þokkalega út. + Höfundur + Nafn höfundar myndarinnar kemur hér. Sent inn þann Notkunarleyfi Hnit @@ -210,10 +212,12 @@ Útskráning Kennsla Tilkynningar + Efst á baugi Ekki er hægt að birta nálæga staði an heimildar til að ná í hnattstaðsetningu engin lýsing fannst Síða Commons-skrár Wikidata-atriði + Wikipedia-grein Villa kom upp í skyndiminni mynda Einstakur og lýsandi titill, sem mun verða skráarheiti. Þú mátt nota einfaldan texta með bilum. Ekki hafa með neina skráarendingu Lýstu gögnunum eins vel og auðið er: Hvar er myndin tekin? Hvað sýnir hún? Hvert er samhengið? Lýstu fólki og fyrirbærum. Gefðu upp þær upplýsingar sem ekki er auðvelt að giska á, til dæmis á hvaða tíma dags myndin er tekin ef hún sýnir landslag. Ef gögnin sýna eitthvað óvenjulegt, útskýrðu þá hvað það er sem sé sérstakt. @@ -227,6 +231,9 @@ Senda atvikaskrá til forritaranna með tölvupósti Gat ekki ræst vefvafra til að opna slóð Villa: Slóð fannst ekki + Tilnefna til eyðingar + Þessi mynd hefur verið valin til eyðingar. + Skoða í vafra Staðsetning hefur ekki breyst. Staðsetning ekki tiltæk. Heimild þarf til að birta lista yfir staði í nágrenninu @@ -237,6 +244,31 @@ Takk fyrir að hafa gert breytingar %1$s minntist á þig á %2$s. Víxla sýn - Algengar spurningar + STEFNUR + WIKIDATA + WIKIPEDIA + COMMONS + <u>Gefðu okkur einkunn</u> + <u>Algengar spurningar</u> Sleppa kennslu + Nettenging ekki tiltæk + Nettenging í boði + Villa við að sækja tilkynningar + Engar tilkynningar fundust + <u>Þýða</u> + Tungumál + Veldu tungumálið sem þú vill senda inn þýðingar fyrir + Halda áfram + Hætta við + Reyna aftur + Náði því! + Þetta eru þeir staðir í næsta nágrenni við þig sem vantar myndir til að skýra með Wikipedia-greinar + Ef ýtt er á þennan hnapp birtist listi yfir þessa staði + Þú getur sent inn mynd úr myndasafninu þínu eða myndavélinni + Engir myndir fundust! + Villa kom upp við að hlaða inn myndum. + Sent inn af: %1$s + Deila forriti + Hnit voru ekki tilgreind við val myndar + Villa við að sækja nálæga staði. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 379e20449..bf121244d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -5,6 +5,7 @@ * Davio * Nemo bis * S4b1nuz E.656 +* Sarah Bernabei * Una giornata uggiosa '94 * Wim b --> @@ -25,7 +26,7 @@ Accesso effettuato! Accesso non riuscito! File non trovato. Prova con un altro file. - Autenticazione non riuscita! + Autenticazione non riuscita, si prega di riprovare Caricamento iniziato! %1$s caricato! Premi per vedere i tuoi caricamenti @@ -52,8 +53,6 @@ Titolo Descrizione Impossibile effettuare l\'accesso - errore di rete - Impossibile effettuare l\'accesso - controlla il nome utente - Impossibile effettuare l\'accesso - controlla la password Troppi tentativi falliti. Riprova tra alcuni minuti. Spiacente, questo utente è stato bloccato su Commons Devi fornire il tuo codice di autenticazione a due fattori. @@ -87,6 +86,7 @@ Categorie Impostazioni Registrati + Immagini in evidenza Informazioni L\'app di Wikimedia Commons è un\'applicazione open source creata e mantenuta da beneficiari e volontari della comunità Wikimedia. La Fondazione Wikimedia non è coinvolta nella creazione, sviluppo o manutenzione dell\'applicazione. Crea una nuova <a href=\"https://github.com/commons-app/apps-android-commons/issues\">segnalazione GitHub</a> per riportare errori e suggerimenti. @@ -160,6 +160,8 @@ Titolo Titolo del file multimediale Descrizione + Autore + Il nome dell\'autore dell\'immagine in evidenza va scritto qui. Data di caricamento Licenza Coordinate @@ -193,6 +195,7 @@ Esci Tutorial Notifiche + In evidenza nessuna descrizione trovata Pagina di Commons del file Elemento Wikidata @@ -218,4 +221,13 @@ Lingue Annulla Riprova + Capito! + Questi sono i luoghi vicino a te che necessitano di immagini per illustrare le loro voci di Wikipedia + Puoi caricare un\'immagine per ogni luogo dalla tua galleria o fotocamera + Nessuna immagine trovata! + Si è verificato un errore durante il caricamento delle immagini. + Caricato da: %1$s + Condividi applicazione + Foto del giorno + Foto del giorno diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 533c6d950..66c7c1cff 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -17,6 +17,7 @@ משוב מיקום ויקישיתוף + הגדרות שם משתמש ססמה @@ -29,7 +30,7 @@ הכניסה הצליחה הכניסה נכשלה! הקובץ לא נמצא. נא לנסות קובץ אחר. - אימות הפרטים נכשל + אימות הפרטים נכשל, נא להיכנס מחדש ההעלאה התחילה! הקובץ %1$s הועלה! ללחוץ כאן כדי לצפות בהעלאה שלך @@ -57,8 +58,7 @@ נא לתת כותרת לקובץ הזה תיאור לא ניתן להיכנס – כשל בתקשורת - לא ניתן להיכנס – נא לבדוק את שם המשתמש שלך - לא ניתן להיכנס – נא לבדוק את הססמה שלך + לא ניתן להיכנס לחשבון – נא לבדוק את שם המשתמש ואת הסיסמה יותר מדי ניסיונות כושלים להיכנס. נא לנסות שוב בעוד מספר דקות. סליחה, החשבון הזה חסום בוויקישיתוף יש לספק את קוד האימות הדו־שלבי שלך. @@ -70,6 +70,7 @@ חיפוש קטגוריות שמירה רענון + רשימה ה־GPS במכשיר שלך אינו מופעל. האם להפעיל אותו? הפעלת GPS לא הועלה עדיין שום דבר @@ -91,6 +92,7 @@ קטגוריות הגדרות רישום + תמונות מומלצות אודות יישום ויקישיתוף (Wikimedia Commons app) הוא יישום קוד פתוח שמפותח ומתוחזק על־ידי מקבלי מלגות ומתנדבים של קהילת ויקימדיה. קרן ויקימדיה אינה מעורבת ביצירה, פיתוח, או תחזוקה של היישום. נא ליצור <a href=\"https://github.com/commons-app/apps-android-commons/issues\">דיווח בגיטהאב</a> בשביל באגים והצעות. @@ -110,7 +112,7 @@ רישיון ברירת מחדל להשתמש בכותרת ובתיאור קודמים לקבל אוטומטית את המיקום הנוכחי - לאחזר את המיקום הנוכחי כדי להציע קטגוריות אם בתמונה אין תגי מיקום + אחזור המיקום הנוכחי אם אין בתמונה תגי מיקום, וכן הוספת תגי מיקום לתמונה. אזהרה: פעולה זו תחשוף את המיקום הנוכחי שלך. מצב לילה שימוש במצב לילה ייחוס–שיתוף זהה 4.0 @@ -162,8 +164,8 @@ אין תיאור רישיון לא ידוע רענון - הרשאה מחייבת: אחסון. היישום לא יכול לעבוד בלי זה. - נדרשת הרשאה: כתיבה לאחסון חיצוני. היישום לא יכול לעבוד בלי זה. + נדרשת הרשאה: קריאת אחסון חיצוני. היישום לא יכול לגשת לגלריה שלך בלי זה. + נדרשת הרשאה: כתיבה לאחסון חיצוני. היישום לא יכול לגשת למצלמה שלך בלי זה. הרשאה לא מחייבת: קבלת מיקום נוכחי בשביל הצעות קטגוריות אישור מקומות בסביבה @@ -176,6 +178,8 @@ כותרת המדיה תיאור תיאור המדיה יהיה כאן. זה יכול להיות ארוך למדי, ולהתפרס על מספר שורות. אנחנו מקווים שזה נראה טוב. + יוצר + שם המשתמש של יוצר התמונה המומלצת. תאריך העלאה רישיון נקודות ציון @@ -204,6 +208,7 @@ בלי תמונות סלפי תמונה קניינית ברוך בואך ויקיפדיה + הודעה לגבי זכויות יוצרים בית האופרה של סידני ביטול פתיחה @@ -217,10 +222,12 @@ יציאה מדריך הודעות + מומלץ אי־אפשר להציג מקומות בסביבה ללא הרשאות מיקום לא נמצא תיאור דף קובץ בוויקישיתוף פריט ויקינתונים + ערך בוויקיפדיה שגיאה במשירת תמונות במטמון כותרת מתארת ייחודית לקובץ, שתשמש שם קובץ. אפשר להשתמש בשפה פשוטה עם רווחים. אין לכלול סיומת קובץ נא לתאר את המדיה כמה שיותר: איפה היא נוצרה? מה היא מראה? מה ההקשר? נא לתאר את העצמים או את האנשים. נא לחשוף מידע שאי־אפשר לנחש בקלות, למשל, הזמן ביום אם זאת תמונת נוף. אם המדיה מציגה משהו בלתי־רגיל, נא להסביר מה מיוחד בה. @@ -233,6 +240,11 @@ שליחת קובץ יומן שליחת קובץ יומן למפתחים בדואר אלקטרוני לא נמצא דפדפן שיוכל לפתוח את הכתובת + שגיאה! כתובת ה־URL לא נמצאה + להציע מחיקה + התמונה הזאת מועמדת למחיקה + + הצגה בדפדפן המיקום לא השתנה. המיקום אינו זמין. נדרשת הרשאה כדי להציג רשימה של מקומות בסביבה @@ -243,6 +255,37 @@ תודה לך על העריכה אוזכרת על ידי %2$s ב{{GRAMMAR:תחילית|%1$s}}. החלפת מצב תצוגה - שאלות נפוצות + כיוונים + ויקינתונים + ויקיפדיה + ויקישיתוף + <u>תנו לנו ציון</u> + <u>שאלות נפוצות</u> לדלג על ההדרכה + האינטרנט אינו זמין + האינטרנט זמין + שגיאה באחזור התראות + לא נמצאו התראות + <u>תרגום</u> + שפות + נא לבחור את השפה שבה תשלחו את התרגומים + המשך + ביטול + לנסות שוב + הבנתי! + אלה המקומות בסביבתך שזקוקים לתמונות כדי להמחיש את הערכים שלהם בוויקיפדיה + ניתן ללחוץ על כפתור זה כדי להציג רשימה של המקומות האלה + באפשרותך להעלות תמונה של כל מקום מהגלריה או מהמצלמה שלך + לא נמצאו תמונות! + אירעה שגיאה בטעינת התמונות. + הועלתה על־ידי: %1$s + שיתוף היישום + לא צוינו קואורדינטות בעת בחירת התמונה + שגיאה באחזור המקומות בסביבתך. + תמונת היום + תמונת היום + התמונה נוספה בהצלחה ל־%1$s בוויקינתונים! + לא ניתן היה לעדכן הישות המתאימה בוויקינתונים! + הגדרת רקע + הרקע הוגדר בהצלחה! diff --git a/app/src/main/res/values-ja/error.xml b/app/src/main/res/values-ja/error.xml index 9d95a7d2d..bbc722ee1 100644 --- a/app/src/main/res/values-ja/error.xml +++ b/app/src/main/res/values-ja/error.xml @@ -1,10 +1,11 @@ - コモンズがクラッシュしました - エラーが発生しました! + コモンズアプリがクラッシュしました + おおっと、何かおかしいようです! 何をしていたかを記入してメールでお送りください。それをもとに問題点を解決します。 ありがとうございます! diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 09e65ba94..c2e243770 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -12,20 +12,25 @@ * Yusuke1109 --> + 表示 + 全般 フィードバック 場所 コモンズ + 設定 利用者名 パスワード + コモンズのベータ版アカウントにログイン ログイン + パスワードを忘れた場合 利用者登録 ログイン中 お待ちください… ログインしました! ログインに失敗しました! ファイルが見つかりません。別のファイルでお試しください。 - 認証に失敗しました! + 認証に失敗しました! アップロードを開始しました! %1$s をアップロードしました! アップロードしたものを表示するにはタップしてください @@ -35,7 +40,8 @@ %1$s のアップロードに失敗しました 閲覧するにはタップしてください - %1$d 件のファイルをアップロード中 + %1$d file uploading + %1$d件のファイルをアップロード中 自分の最近のアップロードファイル 順番待ち中 @@ -49,13 +55,13 @@ 共有 ブラウザーで表示 タイトル + ファイル名をつけてください 説明 ログインできません - ネットワークのエラーです - ログインできません - 利用者名を確認してください - ログインできません - パスワードを確認してください - 失敗した回数が多すぎます。数分でもう一度お試しください。 + ログインできません - 利用者名とパスワードを確認してください + 失敗した回数が多すぎます。数分待ってからもう一度お試しください。 申し訳ありませんが、この利用者はコモンズでブロックされています。 - 2 要素認証コードを提供する必要があります。 + 2要素認証コードを提供する必要があります。 ログイン失敗 アップロード このセットに名前をつけてください @@ -64,26 +70,30 @@ カテゴリを検索 保存 更新 + 一覧 お使いのデバイスではGPSが無効になっています。有効にしますか? GPSを有効にする まだ何もアップロードされていません。 \@string/contributions_subtitle_zero - %1$d 件のアップロード + pne=%1$d upload - %1$d 件のアップロードを開始中 + Starting %1$d upload + %1$d件のアップロードを開始中 + %1$d upload %1$d 件のアップロード %1$s に一致するカテゴリが見つかりません - あなたの画像をウィキメディア・コモンズで見つけやすくするためにカテゴリを追加してください。\n\nカテゴリ名の入力を開始してください。\nこの手順をスキップするにはこのメッセージをタップしてください(または戻るボタン)。 + あなたの画像をウィキメディア・コモンズで見つけやすくするためにカテゴリを追加してください。\nカテゴリ名の入力を開始してください。 カテゴリ 設定 利用者登録 + 秀逸な画像 このアプリについて - ウィキメディア・コモンズ・アプリはウィキメディア・コミュニティの助成金受給者とボランティアによって製作・メンテナンスされているオープンソースソフトウェアです。ウィキメディア財団はこのアプリの製作・開発・メンテナンスに関与していません。 + ウィキメディア・コモンズ・アプリはウィキメディア・コミュニティの助成金受給者とボランティアが製作・管理しているオープンソースソフトウェアです。ウィキメディア財団はこのアプリの製作・開発・メンテナンスに関与していません。 バグとアイディアは <a href=\"https://github.com/commons-app/apps-android-commons/issues\">Github</a> へ。 <u>プライバシー・ポリシー</u> <u>クレジット</u> @@ -95,12 +105,13 @@ まだ写真をアップロードしていません。 再試行 キャンセル - この画像が %1$s ライセンスでアップロードされます。 + この画像は%1$sライセンスのもとにアップロードされます。 + この画像の投稿に当たり、私はこれが自分自身の作品であり、著作権のあるコンテンツや自撮りは含まれていないと宣言します。 ダウンロード 既定のライセンス 前回のタイトルと記述を使用 現在の位置を自動的に取得 - 画像にジオタグが付いていない場合、現在の位置を取得してカテゴリを提案 + Retrieves current location if image is not geotagged, \n画像にジオタグが付いていない場合、現在の位置を取得して画像に添付。ご注意: 自分の現在地が明示されます。 夜モード 暗いテーマを使う 表示-継承 4.0 @@ -126,11 +137,20 @@ ウィキメディア・コモンズにはウィキペディアで使用する画像のほぼすべてが保管されています。 あなたの画像は世界中の人々が学習する助けになります! アップロードする画像はあなたご本人が撮影したものかあなたが単独で制作したものに限定します。 - - 自然物 (動植物、山)\n- 道具 (自転車、駅)\n- 著名人 (市区村長・都道府県知事、自分が会ったオリンピック選手) + 自然 (動植物、山)\n• 道具 (自転車、駅)\n• 著名人 (市区村長・都道府県知事、自分が会ったオリンピック選手) + 自然物 (動植物、山) + 道具 (自転車、駅) + 著名人 (市区村長・都道府県知事、自分が会ったオリンピック選手) アップロードが《禁止》のもの: - あなたの友人の自撮り写真や画像\n- インターネットからダウンロードした画像\n- 著作権のあるアプリのスクリーンショット + 自撮りもしくは友達を撮影した写真 + ウェブからダウンロードした画像 + 独自のアプリケーションのスクリーンショット アップロードの例: - 題名: シドニー・オペラハウス\n- 説明: 湾の向こうから見たシドニー・オペラハウス\n- カテゴリ: 西側から見たシドニー・オペラハウス、遠くから見たシドニー・オペラハウス + 題名: シドニーのオペラハウス + 説明: シドニーのオペラハウス。湾を挟んで撮影。 + カテゴリ: シドニーオペラハウスの西面、シドニーオペラ遠景 画像を投稿してください。ウィキペディアの記事に彩りを! ウィキペディアの画像はウィキメディア・コモンズに保管されています。 あなたの画像は世界中の人々が学習する助けになります @@ -143,20 +163,22 @@ 説明はありません。 不明なライセンス 更新 - 必要な権限:外部ストレージを読み込みます。これがなければアプリは機能しません。 - 必要な権限:外部ストレージを作成します。これがなければアプリは機能しません。 - オプションの権限:カテゴリ候補の現在の位置を取得する + 必要な権限:外部ストレージを読み込みます。これがなければアプリはギャラリーを開けません。 + 必要な権限:外部ストレージに入力します。これがないとアプリはカメラにアクセスできません。 + オプションの権限:カテゴリ候補のため現在の位置を取得する 承認 - 周りの場所 + 近くの場所 付近の場所が見つかりません 警告 - このファイルが既にコモンズにあります。本当にアップロードしますか? + このファイルは既にコモンズにあります。本当にアップロードしますか? はい いいえ タイトル メディアのタイトル 記述 ここにメディアの説明が入ります。かなり長文になる場合には数行にわたることがあります。それでも見栄えがよいと願っています。 + 作者 + 秀逸な画像の作者名を記入します。 アップロード日時 ライセンス 緯度経度 @@ -166,21 +188,26 @@ 2FA コード 最近のアップロードファイルに表示する最大件数 最大限 - 500 以上の項目を表示できません + 表示できるのは500件以下です 最近のアップロードファイルに表示する最大件数 2段階認証は現在サポートされていません。 ログアウトしてもよろしいですか? コモンズの商標 コモンズのウェブサイト コモンズのフェイスブックページ + コモンズのGithubソースコード 背景画像 + メディアイメージが失敗しました 画像がありません 画像をアップロード 蔵王連峰 リャマ レインボーブリッジ チューリップ + 自撮りはアップロードできません + 独自の著作権がある画像 ウィキペディアへようこそ + 著作権について シドニーオペラハウス キャンセル 開く @@ -194,28 +221,67 @@ ログアウト チュートリアル 通知 - 場所の権限がないと、近くの場所を表示できません + 秀逸 + 場所の権限がないため、近くの場所を表示できません 説明がありません + コモンズのファイルページ ウィキデータ項目 ウィキペディアの記事 画像をキャッシュする際のエラー ファイル固有の説明的な表題。ファイル名として使われます。平易な言葉を使い、空白を入れることができます。拡張子は含めないでください。 - 可能な限りメディアを説明してください:どこで撮られましたか?それは何を示していますか?文脈とは何ですか?物や人を説明してください。容易に推測できない情報、例えば風景の場合の時刻を明らかにする。メディアに珍しいことがある場合は、何が珍しいのかを説明してください。 - 権限を取得 + 可能な限りメディアを説明してください: 撮影地はどこですか? それは何を示していますか? どんな文脈がありますか? 被写体の物や人を説明してください。容易に推測できない情報、例えば風景であれば時刻を明示します。特筆すべき物事が映っている場合は、何が珍しいのかを説明してください。 + この画像は暗すぎますがアップロードしますか? ウィキメディア・コモンズは百科事典に適した画像のみ受け付けます。 + ピントが合っていませんが、アップロードしますか? ウィキメディア・コモンズは百科事典に適した画像のみ受け付けます。 + 権限を付与 外部ストレージを使用 アプリ内のカメラで撮影した写真を端末に保存する 自分のアカウントにログイン ログファイルを送信する メールで開発者にログファイルを送信する + URLを開くブラウザーが見つかりません + エラーが発生しました。URL が見つかりません + 削除の提案 + この画像の削除が提案されています。 ブラウザーで表示 場所は変更されていません。 位置が無効です。 + 近くの場所を表示するには権限が必要です + 道順を調べる 記事を読む + ウィキメディアコモンズにようこそ、%1$さん! このサイトへ来てくれてありがとうございます。 + %1$さんからアナタのとーくぺ^字にメッセージが届いています + 編集をしてくれてありがとうございます + %1$さんが%2$であなたに言及しています。 + 表示の切り替え + 道順 + ウィキデータ + ウィキペディア + コモンズ <u>評価する</u> - <u>FAQ</u> + <u>よくある質問</u> チュートリアルをスキップする + インターネットに接続していません + インターネットに接続しました + 通知の取得に失敗しました + 通知はありません <u>翻訳</u> 言語 + どの言語に編集するか選択 + 次へ キャンセル - 再試行 + やり直す + 了解 + 近くでウィキペディアの記事に使う写真がない場所はこちら + このボタンをタップするとリストを表示します + 場所の写真をアップロードするには、ギャラリーから選ぶことも撮影することもできます + 画像がありません + 画像の読み込み中にエラーが発生しました + アップロードした人: %1$ + アプリをシェアする + 画像の選択中に位置情報を特定できませんでした + 付近の場所を取得しようとしてエラーが発生しました。 + ウィキデータの%1$sに画像を追加しました ! + 対応するウィキデータの引数の更新に失敗しました。 + 壁紙の設定 + 壁紙の設定ができました! diff --git a/app/src/main/res/values-ji/strings.xml b/app/src/main/res/values-ji/strings.xml index bc3af8a30..9fa4b1d68 100644 --- a/app/src/main/res/values-ji/strings.xml +++ b/app/src/main/res/values-ji/strings.xml @@ -1,8 +1,12 @@ + אַלגעמיין + פֿידבעק + לאקאציע קאמאנס איינשטעלונגען באַניצער־נאָמען @@ -14,7 +18,7 @@ אריינלאגירט מיט הצלחה! ארײַנלאגירן אדורכגעפאלן! טעקע נישט געראפן. פרובירט אפשר אן אנדער טעקע. - אויטענטיפֿיצירן דורכגעפֿאלן! + אויטענטיפֿיצירן דורכגעפֿאלן! ארויפלאדן אנגעהויבן! %1$s ארויפגעלאדן!! דרוקט צו זען אײַער ארויפֿלאד @@ -48,6 +52,7 @@ זוכן קאטעגאריעס אויפֿהיטן דערפֿרישן + ליסטע \@string/contributions_subtitle_zero איין ארויפֿלאד @@ -59,13 +64,14 @@ קאַטעגאריעס איינשטעלונגען + רעקאמענדירטע בילדער וועגן וועגן שיקן פֿידבעק (דורך בליצפאסט) לעצטיק־געניצטע קאטעגאריעס אַנולירן אראָפלאָדן - ליצענץ + סטאנדארט־ליצענץ Attribution-ShareAlike 3.0 Attribution 3.0 CC0 @@ -95,6 +101,7 @@ ווארענונג יא ניין + מחבר ליצענץ קאארדינאטן טולפאן @@ -108,4 +115,8 @@ איינשטעלונגען פֿידבעק אַרויסלאָגירן + רעקאמנדירט + בילד פונעם טאָג + בילד פונעם טאָג + ס׳איז ניט געלונגען צו דערהיינטיקן דעם אַנטשפּרעכנדיקן בלאַט אין וויקידאַטן. diff --git a/app/src/main/res/values-jv/strings.xml b/app/src/main/res/values-jv/strings.xml index 43d7929e7..c02290b4c 100644 --- a/app/src/main/res/values-jv/strings.xml +++ b/app/src/main/res/values-jv/strings.xml @@ -14,7 +14,7 @@ Kasil mlebu log! Wurung mlebu log! Barkas ora katemu. Jajalana barkas liyané. - Wurung otèntifikasi! + Wurung otèntifikasi! Wiwit ngunggah! %1$s kaunggah! Dudul saperlu ndeleng unggahané panjenengan @@ -40,8 +40,6 @@ Sesirah Wedharan Ora bisa mlebu log - jaringané gagal - Ora bisa mlebu log - tiliki jeneng panganggoné panjenengan - Ora bisa mlebu log - tiliki tembung wadiné panjenengan Kakèhan upaya sing gagal. Jajalana manèh mengko. Ngapunten, panganggo iki wis diblokir ing Commons Panjenengan kudu ngisi kodhe otèntifikasi rong faktoré panjenengan @@ -122,7 +120,7 @@ Tanpa katerangan Lisènsi ora kaweruhan Anyarana - Butuh palilah: Maca panyimpenan njaba. Aplikasi mokal mlaku yèn tanpa iki. + Butuh palilah: Maca panyimpenan njaba. Aplikasi mokal mlaku yèn tanpa iki. Palilah manasuka: Njupuk pernah saiki kanggo saran ing kategori Oké Papan Cedhak Kéné diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index 2bb1c5191..31103028e 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -15,7 +15,7 @@ სისტემაში შესვლა წარმატებით განხორციელდა! სისტემაში შესვლა ვერ განხორციელდა! ფაილი არ მოიძებნა. გთხოვთ, სცადოთ სხვა ფაილი. - ავთენტიფიკაცია ვერ მოხერხდა! + ავთენტიფიკაცია ვერ მოხერხდა! ატვირთვა დაიწყო! %1$ ატვირთულია! დააჭირეთ თქვენი ატვირთვის სანახავად @@ -42,8 +42,6 @@ სათაური აღწერა შესვლა ვერ ხერხდება - ქსელის შეცდომა - შესვლა ვერ ხერხდება - გთხოვთ შეამოწმოთ სახელი - შესვლა ვერ ხერხდება - გთხოვთ შეამოწმოთ პაროლი ძალიან ბევრი წარუმატებელი მცდელობა. გთხოვთ, რამდენიმე წუთში სცადეთ კვლავ. უკაცრავად, ეს მომხმარებელი დაბლოკილია ვიკისაწყობში თქვენ უნდა შეიყვანოთ ორფაქტორიანი ავტორიზაციის კოდი. diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index c3cc422c8..f465ab913 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -15,7 +15,7 @@ Tuqqna tedda! Tqqna ur teddi ara! Ulac afaylu. Ɛreḍ wayeḍ ma ulac aɣilif. - Asesteb yecceḍ! + Asesteb yecceḍ! Asali yebda! %1$s yuli! Senned akken ad twaliḍ asali-ik @@ -42,8 +42,6 @@ Azwel Aglam Ur izmir ara ad yeqqen - tuccḍa n uẓeṭṭa - Ur izmir ara ad yeqqen - wali isem-ik n useqdac - Ur izmir ara ad yeqqen - wali awal-ik uffir Ddeq n uɛraḍ ur yeddin ara. Ɛreḍ akka di kra n tisdatin Suref-aɣ, aseqdac-agi yewḥel di Commons Yessefk ad d-muddeḍ tangalt n n usesbteb s snat n tarrayin. @@ -95,7 +93,7 @@ Turagt Seqdec azwel neɣ aglam yezrin Awi s wudem awurman adig amiran - Awi adig amiran akken ad tsumreḍ taggayt ma yella tugna ur tettwacreḍ ara di tirakalt + Awi adig amiran akken ad tsumreḍ taggayt ma yella tugna ur tettwacreḍ ara di tirakalt Askar n yiḍ Seqdec asentel aberkan Attribution-ShareAlike 4.0 @@ -138,8 +136,8 @@ Ulac aglam Turagt tarussint Smiren - Yesra tasiregt: Ɣeṛ asekles azɣaray. Asnas ur yezmir ara ad yeddu s war aya. - Ysera tasiregt: Aru deg usekles azɣaray. Asnas ur yezmir ara ad yeddu s war aya. + Yesra tasiregt: Ɣeṛ asekles azɣaray. Asnas ur yezmir ara ad yeddu s war aya. + Ysera tasiregt: Aru deg usekles azɣaray. Asnas ur yezmir ara ad yeddu s war aya. Tasiregt tafrayant: Awi adig amiran i yisumar n taggayt IH Idigen iqeṛben diff --git a/app/src/main/res/values-km/strings.xml b/app/src/main/res/values-km/strings.xml index c057e04c7..ba7a57655 100644 --- a/app/src/main/res/values-km/strings.xml +++ b/app/src/main/res/values-km/strings.xml @@ -4,58 +4,75 @@ * វ័ណថារិទ្ធ --> - Wikimedia Commons - ការកំណត់​នានា - ឈ្មោះអ្នកប្រើប្រាស់ - លេខ​សម្ងាត់​ - ឡុកអ៊ីន​ - កំពុងឡុកអ៊ីន​ - សូមរង់ចាំ… - កត់ឈ្មោះចូលបានសំរេច - កត់ឈ្មោះចូលបរាជ័យ - Authentication បានបរាជ័យ! - បានចាប់ផ្តើមការផ្ទុកឡើង! - %1$s បានផ្ទូកឡើងហើយ! - Tap ដើម្បីមើលការផ្ទុកឡើងរបស់អ្នក - កំពុងចាប់ផ្តើម ផ្ទុកឡើង %1$s - %1$s កំពុងផ្ទូកឡើង + ការ​រចនា + ទូទៅ​ + មតិយោបល់ + ទីកន្លែង + វិគីមេឌារួម + ការកំណត់​ + អត្តនាម + ពាក្យ​សម្ងាត់ + កត់ឈ្មោះចូលទៅក្នុងគណនីវិគីមេឌារួមបេតា + កត់ឈ្មោះចូល + ពេលភ្លេច​ពាក្យ​សម្ងាត់ + ចុះ​ឈ្មោះ + កំពុងកត់ឈ្មោះចូល + សូមរង់ចាំបន្តិច… + កត់ឈ្មោះចូលបានសម្រេច! + កត់ឈ្មោះចូលមិនបានសម្រេច! + រកមិនឃើញឯកសារទេ។ សូមសាកល្បងជាមួយឯកសារផ្សេងមួយទៀត។ + ការបញ្ជាក់ទទួលស្គាល់មិនបានសម្រេច។ សូមកត់ឈ្មោះចូលម្ដងទៀត។ + ការផ្ទុកឡើងបានចាប់ផ្តើមហើយ! + បានផ្ទុកឡើង %1$s ហើយ! + ចុចដើម្បីមើលអ្វីដែលអ្នកផ្ទុកឡើង + កំពុងចាប់ផ្តើមផ្ទុកឡើង %1$s + កំពុងផ្ទុកឡើង %1$s កំពុងបញ្ចប់ការផ្ទុកឡើង %1$s - ការផ្ទុកឡើង %1$s បានបរាជ័យ - Tap ដើម្បីមើល - ការផ្ទុកឡើងរបស់ខ្ញុំ - បានដាក់ក្នុងជួររង់ចាំ - បានបរាជ័យ - %1$d%% រួចរាល់ + ការផ្ទុកឡើង %1$s មិនបានសម្រេច + ចុចដើម្បីមើល + អ្វីដែលខ្ញុំផ្ទុកឡើងថ្មីៗ + ក្នុងជួររង់ចាំ + មិនបានសម្រេច + រួចរាល់​បាន %1$d%% កំពុង​ផ្ទុកឡើង​ ពីវិចិត្រសាល ថតរូប - ការផ្ទុកឡើងរបស់ខ្ញុំ - ចែករំលែក - មើលក្នុង browser + ជិតខាង + អ្វីដែលខ្ញុំផ្ទុកឡើង + ចែកចាយ + មើលក្នុងឧបករណ៍រាយរក ចំណងជើង - បរិយាយ - មិនអាចកត់ឈ្មោះចូល - បណ្តាញ network បរាជ័យ - មិនអាចកត់ឈ្មោះចូល - សូមពិនិត្យឈ្មោះអ្នកប្រើប្រាស់របស់អ្នក - មិនអាចកត់ឈ្មោះចូល - សូមពិនិត្យលេខសម្ងាត់របស់អ្នក - ការព្យាយាមមិនបានសម្រេចមានចំនួនច្រើនដងពេក។ សូមព្យាយាមម្តងទៀតនៅប៉ុន្មាននាទីក្រោយ។ - សូមអភ័យទោស អ្នកប្រើប្រាស់រូបនេះត្រូវបានហាមឃាត់នៅ Commons - កត់ឈ្មោះចូលបរាជ័យ + សូមដាក់ចំណងជើងអោយឯកសារនេះ + ការពិពណ៌នា + មិនអាចកត់ឈ្មោះចូលបានទេ ព្រោះបណ្ដាញកំពុងមានបញ្ហា។ + មិនអាចកត់ឈ្មោះចូលបានទេ។ សូមពិនិត្យអត្តនាមនិងពាក្យសម្ងាត់របស់អ្នកឡើងវិញ។ + ការព្យាយាមមិនបានសម្រេចច្រើនដងពេក។ សូមព្យាយាមម្តងទៀតនៅប៉ុន្មាននាទីក្រោយ។ + សូមអភ័យទោស អ្នកប្រើប្រាស់រូបនេះត្រូវបានហាមឃាត់នៅវិគីមេឌារួម + អ្នកចាំបាច់ត្រូវតែផ្ដល់លេខកូដសម្រាប់បញ្ជាក់ទទួលស្គាល់ពីរតង់។ + កត់ឈ្មោះចូលមិនបានសម្រេច ផ្ទុកឡើង ដាក់ឈ្មោះឲ្យសំនុំនេះ បម្រែបម្រួល ផ្ទុកឡើង ស្វែងរកចំណាត់ថ្នាក់ក្រុម រក្សាទុក + ផ្ទុកឡើងវិញ + បញ្ជី + ឧបករណ៍របស់អ្នកកំពុងបិទមិនប្រើGPS។ តើអ្នកចង់បើកវាប្រើទេ? + បើកប្រើGPS + គ្មានអ្វីដែលបានផ្ទុកឡើងទេ រកមិនឃើញចំណាត់ថ្នាក់ក្រុមដែលត្រូវនឹង %1$s ទេ - បន្ថែមចំណាត់ថ្នាក់ក្រុមអោយរូបភាពរបស់អ្នកដើម្បីអោយងាយស្រួលស្វែងរក្នុង Wikimedia Commons។\nចាប់ផ្ដើមវាយបញ្ចូលឈ្មោះចំណាត់ថ្នាក់ក្រុម។\nចុចលើសារនេះ (ឬចុចប៊ូតុងត្រលប់ក្រោយ)ដើម្បីរំលងជំហ៊ាននេះ។ + បន្ថែមចំណាត់ថ្នាក់ក្រុមអោយរូបភាពរបស់អ្នកដើម្បីអោយងាយស្រួលស្វែងរកក្នុងវិគីមេឌារួម។ ចាប់ផ្ដើមវាយបញ្ចូលឈ្មោះចំណាត់ថ្នាក់ក្រុម។ ចំណាត់ថ្នាក់ក្រុម ការកំណត់ + ចុះឈ្មោះ​ + រូបភាពឆ្នើម អំពី សូហ្វវែរប្រភពបើកទូលាយត្រូវបានចេញផ្សាយក្រោមអាជ្ញាបណ្ណ <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache License v2</a> ប្រភពកូដមាននៅ <a href=\"https://github.com/commons-app/apps-android-commons\">GitHub</a>. Bugs មាននៅ <a href=\" https://github.com/commons-app/apps-android-commons/issues\">Github</a>. - <a href=\"https://wikimediafoundation.org/wiki/Privacy_policy\">គោលការភាពជាឯកជន</a> + <a href=\"https://wikimediafoundation.org/wiki/Privacy_policy\">គោលការភាពឯកជន</a> អំពី - ផ្ញើមតិកែលម្អ (តាមអ៊ីមែល) + ផ្ញើមតិយោបល់ (តាមអ៊ីមែល) ចំណាត់ថ្នាក់ក្រុមដែលត្រូវបានប្រើថ្មីៗ កំពុងរង់ចាំ ធ្វើការ sync ជាលើកដំបូង… អ្នកមិនទាន់បានផ្ទុករូបថតណាមួយឡើងនៅឡើយទេ។ @@ -92,4 +109,25 @@ គ្មាន​ការ​ពណ៌នា Unknown license ធ្វើឱ្យស្រស់ + រូបភាពផ្ទៃខាងក្រោយ + គ្មានរូបភាព + ផ្ទុករូបភាពឡើង + ភ្នំហ្សាអូ + ស្ពានឥន្ទធនូ + បោះបង់ + បើក + បិទ + ទំព័រដើម + ផ្ទុកឡើង​ + ជិតខាង + អំពី + ការកំណត់​ + មតិយោបល់ + កត់ឈ្មោះចេញ + រៀនប្រើ + សារជូនដំណឹង + ឆ្នើម + គ្មានការពិពណ៌នា + វត្ថុក្នុងវិគីទិន្នន័យ + អត្ថបទវិគីភីឌា diff --git a/app/src/main/res/values-ko-rKP/strings.xml b/app/src/main/res/values-ko-rKP/strings.xml index 670479f75..4a12d8efb 100644 --- a/app/src/main/res/values-ko-rKP/strings.xml +++ b/app/src/main/res/values-ko-rKP/strings.xml @@ -15,7 +15,7 @@ 가입 성공! 가입 실패! 서류를 찾을수 없습니다. 다른 서류를 사용해주십시오. - 인증 실패! + 인증 실패! 올리적재를 시작했습니다! %1$s 서류를 올리적재하였습니다! 당신의 올리적재를 보려면 두드리세요 @@ -42,8 +42,6 @@ 제목 설명 가입할수 없습니다 - 망 오유입니다 - 가입할수 없습니다 - 사용자이름을 확인하세요 - 가입할수 없습니다 - 통행암호를 확인하세요 실패한 시도가 너무 많습니다. 몇분후에 다시 시도하세요. 죄송합니다, 이 사용자는 공용에서 차단되였습니다 두인자검증부호를 제공해야 합니다. @@ -91,7 +89,7 @@ 허가권 이전의 제목/설명을 사용하기 자동으로 현재 위치 얻기 - 화상에 지리정보꼬리표가 달려 있지 않다면, 현재 위치를 검색하여 분류를 제안해주십시오 + 화상에 지리정보꼬리표가 달려 있지 않다면, 현재 위치를 검색하여 분류를 제안해주십시오 야간방식 어두운 주제 쓰기 저작자표시-동일조건변경허락 4.0 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 5156b7e3a..05f001e24 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1,6 +1,8 @@ Xuyabûn @@ -69,6 +70,7 @@ Hişyarî Erê Na + Xwedî Koordînat Betal bike Veke @@ -78,4 +80,6 @@ Destûr bide <u>Pirsên ku pir têne pirsîn</u> Rênîşandanê derbas bike + Wêneya rojê + Wêneya rojê diff --git a/app/src/main/res/values-kum/strings.xml b/app/src/main/res/values-kum/strings.xml new file mode 100644 index 000000000..34db9a395 --- /dev/null +++ b/app/src/main/res/values-kum/strings.xml @@ -0,0 +1,97 @@ + + + + Гёрюнюш + Ортакъ + + Кюйлевлер + Къоллавчу аты + Чечил + Гирмек + Чечилни унутдунгму? + Къайытланмакъ + %1$s юкленген! + %1$s юкленип тура + Къарамакъ учун бас + Гезикде + Юклев + Сурат алмакъ + Ювукъда + Юклевлерим + Пайлашмакъ + Ат + Тасвир + Юклев + Юклев + Сакъламакъ + Янгыртмакъ + Тизме + GPS ишлетмек + Юклевлер ёкъ гьали де + Категориялар + Кюйлевлер + Къайытланмакъ + Тасвири + <u>Баракаллалар</u> + Тасвири + Такрарламакъ + Гери алмакъ + Эндирмек + CC0 + CC BY-SA 3.0 + CC BY-SA 3.0 (Алмания) + CC BY-SA 3.0 (Эстония) + CC BY 3.0 + CC BY-SA 4.0 + CC BY 4.0 + CC Zero + Интернетден эндирген суратларынг + Юклев уьлгю: + Дюр! + Категориялар + Юклев... + Бир зат сайланмагъан + Тасвири ёкъ + Янгыртмакъ + OK + Буварыв + Дюр + Ёкъ + Ат + Тасвир + Ясавчу + Юклев тархы + 2FA код + Лале + Оьзсуратсыз + Хош гел Википедиягъа + Гери алмакъ + Ачмакъ + Япмакъ + Баш + Юклемек + Ювукъда + Тасвир + Кюйлевлер + Чыкъмакъ + Билдиривлер + Сайламлы + тасвири табылмады + Википедия макъала + Янгылыш! Байланыв табылмады + ЯКЪЛАР + ВИКИПЕДИЯ + <u>Къыйматла бизин</u> + Интернет гиришсиз + Интернет гиришли + Билдиривлер ёкъ + <u>Таржума этмек</u> + Тиллер + Гери алыв + Такрарламакъ + Тюшюндюм! + Гьеч сурат табылмады! + Уьстевню пайлашмакъ + diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index 6d78e91fe..24e1fb1d7 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -13,7 +13,7 @@ Сураныч, күтө туруңуз… Сиз ийгиликтүү кирдиңиз Системага кирүүдө катачылык бар! - Таану катачылыгы! + Таану катачылыгы! Жүктөө башталды! %1$s жүктөлүүдө Жүктөлгөн файлды көрүү үчүн басыңыз @@ -39,8 +39,6 @@ Аталышы Баяндамасы Кирүүгө болбой жатат - тармакта үзгүлтүк бар - Кирүүгө мүмкүн эмес - сураныч, колдонуучу ысымыңызды текшериңиз - Кирүүгө мүмкүн эмес - сураныч, сыр сөзүңүздү текшериңиз Өтө көп натыйжасыз иш аракет. Суранабыз, бир нече мүнөттөн кийин кайталаңыз Кечириңиз, бул кодонуучу Уикиказынада блокко алынган. Системага кирүүдө катачылык бар! diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index a43f2ea89..ffed21e4f 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -23,7 +23,7 @@ Umeldung huet geklappt! D\'Aloggen huet net funktionéiert Fichier net fonnt. probéiert w.e.g. en anere Fichier. - Authentifizéierung huet net funktionéiert! + Authentifizéierung huet net funktionéiert, loggt Iech w.e.g. nach eng Kéier an. D\'Eroplueden huet ugefaang! %1$s eropgelueden! Dréckt fir de Fichier ze gesinn deen Dir eropgelueden hutt @@ -50,8 +50,6 @@ Titel Beschreiwung Aloggen huet net funktionéiert - Feeler mam Reseau - Login net méiglech - kuckt w.e.g. Äre Benotzernumm no - Login net méiglech - kuckt w.e.g. Äert Passwuert no Ze dacks ouni Succès probéiert. Probéiert w.e.g. an e puer Minutten nach eng Kéier. Pardon, dëse Benotzer ass op Commons gespaart Aloggen huet net funktionéiert @@ -84,11 +82,12 @@ Kategorien Astellungen Mellt Iech un + Bemierkenswäert Biller Iwwer D\'App Wikimedia Commons ass eng \'Open-Source-App\' déi vu Fräiwëllege vun der Wikimedia Foundation entwéckelt gouf an och vun hinnen ënnerhal gëtt. D\'Wikimedia Foundation ass net an d\'Entwécklung oder den Ënnerhalt vun der App implizéiert. Leet w.e.g. <a href=\"https://github.com/commons-app/apps-android-commons/issues\"> e GitHub Problem</a> fir Problemer ze mellen a Proposen ze maachen. <u>Dateschutzrichtlinn</u> - <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\">Merci</a> + <u>Merci</u> Iwwer Feedback schécken (per E-Mail) Keen E-Mail-Client installéiert @@ -102,7 +101,7 @@ Standardlizenz Viregen Titel/Beschreiwung benotzen Automatesch déi aktuell Plaz kréien - Aktuell Plaz ofruffe fir Propose fir Kategorien ze erméigleche wann d\'Bild keng Geotaggen huet + Aktuell Plaz ofruffe fir Propose fir Kategorien ze erméigleche wann d\'Bild keng Geotaggen huet Nuetsmodus Donkele Layout benotzen Attribution-ShareAlike 4.0 @@ -152,7 +151,7 @@ Keng Beschreiwung Onbekannt Lizenz Aktualiséieren - Obligatoresch Autorisatioun: Externe Späicher liesen. D\'App kann net ouni dat funktionéieren. + Obligatoresch Autorisatioun: Externe Späicher liesen. D\'App kann ouni dat net op d\'Galerie zougräifen. Fakultativ Autorisatioun: Déi aktuell Plaz kréie fir Propose fir Kategorien OK Plazen nobäi @@ -165,6 +164,7 @@ Titel vum Medium Beschreiwung D\' Beschreiwung vum Medium kënnt hei. Dëst ka méiglecherweis laang sinn a gëtt eventuell op méi Zeile verdeelt. Mir hoffen et gesäit trotzdeem gutt aus. + Auteur Datum vum Eroplueden Lizenz Koordinaten @@ -200,6 +200,7 @@ Ausloggen Uleedung Notifikatiounen + Bemierkenswäert Plazen nobäi kënnen net gewise ginn ouni Rechter fir d\'Lokalisatioun keng Beschreiwung fonnt Commons-Fichierssäit @@ -237,4 +238,13 @@ Virufueren Ofbriechen Nach eng Kéier probéieren + Verstanen! + Keng Biller fonnt! + Feeler beim Eropluede vu Biller. + Eropgeluede vum: %1$s + Bild vum Dag + Bild vum Dag + Déi entspriechend Wikidata-Entitéit konnt net aktualiséiert ginn! + Hannergrondbild festleeën + Hannergrondbild festgeluecht diff --git a/app/src/main/res/values-li/strings.xml b/app/src/main/res/values-li/strings.xml new file mode 100644 index 000000000..48733bd97 --- /dev/null +++ b/app/src/main/res/values-li/strings.xml @@ -0,0 +1,168 @@ + + + + Uterlik + Algemein + Feedback + Locatie + Commons + + Instèllinge + Gebroekersnaam + Wachwaord + Meld dich aan mit diene Commons-Bètakonto + Melj dich aan + Wachwaord vergaete? + Teiken dich in + Aan \'nt melje... + Wach estebleef... + Aanmelje gelök! + Aanmelje mislök! + Bestandj neet gevónje. Perbeer \'n anger bestandj. + Verificatie mislök! + Upload begós! + %1$s upgeloadj! + Wies aan veur dienen upload te betrachte + Upload van %1$s aan \'nt vange + %1$s up \'nt loade + d\'n Upload van %1$s is vaerdig + d\'n Upload van %1$s is mislök + Wies aan veur te betrachte + + %1$d bestandj up \'nt loade + %1$d bestenj up \'nt loade + + Mien recènte uploads + Inne wachrie + Mislök + %1$d%% vaerdig + Up \'nt loade + Oete gallerie + Trèk foto aaf + Kórtbie + Mien uploads + Deil + Tuin in browser + Titel + Gaef estebleef \'ne naam veur dit bestandj + Besjrieving + Kan zich neet aanmelde - netwirkfout + Te väöl mislökde kieëre geperbeerd. Perbeer estebleef oppernuuj euver e paar menuut. + Deze gebroeker is geblokkeerd op Commons + Doe mós diene twieëfaktorische bevestigingscode opgaeve. + Aanmelje mislök + Upload + Gaef dees verzameling \'ne naam + Aanpassinge + Upload + Zeuk categorieje + Slaon op + Vernuuj + Lies + GPS steit oet op die toestèl. Wils se \'t aanzètte? + Zèt GPS aan + Nag gein uploads + + \@string/contributions_subtitle_zero + %1$d upload + %1$d uploads + + + Beginnendj mit %1$d upload + Beginnendj mit %1$d uploads + + + %1$d upload + %1$d uploads + + Gein categorieje die euvereinkómme mit %1$s gevónje + Veug categorieje toe veur dien plaetjes mekkeliker te kinne vinje op Wikimedia Commons.\nBegin mit tikke veur categorieje toe te veuge. + Categorieje + Instèllinge + Registreer + Oetgeleechde plaetjes + Euver + De Wikimedia Commons-app is \'nen app op aope brónne, gemaak en óngerhaje door gerechtigde en vriewilligers vanne Wikimedia-gemeinsjap. De Wikimedia Foundation is neet betróch inne maak, óntwikkeling of \'t óngerhawd vannen app. + Maak e nuuj <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub-perbleem</a> veur fouteverslaag en veurstèlle. + <u>Privaatbeleid</u> + <u>Toesjrif</u> + Euver + Sjik feedback (mitten e-mail) + Geine e-mailcliënt geïnstalleerd + Recèntelik gebroekde categorieje + Oppe ieëste synchronisatie \'nt wachte... + Doe höbs nag gein plaetjes geüpload. + Perbeer oppernuuj + Braek aaf + Dit plaetje weurt gelicenseerd ónger %1$s + Door dit plaetje toe te veuge verklaor ich det dit mien eige werk is en det \'t gein auteursrechtelik besjurmp matterjaal of selfies bevatj en angeszins instump mit \'t <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">beleid op Wikimedia Commons</a>. + Download + Standerdlicentie + Gebroek veurige naam/besjrieving + Haol autematis de hujige locatie op + Haol de hujige locatie op veur categorieveurstèlle te make wen \'t bild gein geotags haet + Nachmodus + Gebroeker duuster thema + Naamsvermeljing-GeliekDeile 4.0 + Naamsvermeljing 4.0 + Naamsvermeljing-GeliekDeile 3.0 + Naamsvermeljing 3.0 + CC0 + CC BY-SA 3.0 + CC BY-SA 3.0 (Oeësteriek) + CC BY-SA 3.0 (Duutsjlandj) + CC BY-SA 3.0 (Eslandj) + CC BY-SA 3.0 (Spaanje) + CC BY-SA 3.0 (Kroatië) + CC BY-SA 3.0 (Luxemburg) + CC BY-SA 3.0 (Nederlandj) + CC BY-SA 3.0 (Noorwaeg) + CC BY-SA 3.0 (Pole) + CC BY-SA 3.0 (Roemenië) + CC BY 3.0 + CC BY-SA 4.0 + CC BY-4.0 + CC Zero + Op Wikimedia Commons staon de meiste plaetjes die waere gebroek op Wikipedia. + Dien plaetjes helpe luuj oppe ganse werreld mit \'t opdoon van kènnis! + Upload estebleef allein plaetjes die gans door dichzelf zint gemaak: + Netuurobjekte (blome, bieëster, berg)\n• Henjige veurwerpe (fietsje, treinstaasjes)\n• Bekandje luuj (de börgermeisters, Olumpische atlete die se kins) + Netuurobjekte (blome, bieëster, berg) + Henjige veurwerpe (fietsje, treinstaasjes) + Bekandje luuj (de börgermeisters, Olumpische atlete die se kins) + Upload estebleef NEET: + - Selfies of foto\'s van dien vrunj\n- Foto\'s die se höbs downgeload van \'t internet\n- Sjermplaetjes van eige apps + Selfies of foto\'s van dien vrunj + Foto\'s die se höbs downgeload van \'t internet + Sjermplaetjes van eige apps + Uploadveurbild: + - Titel: Operahoes in Sydney\n- Besjrieving: Operahoes in Sydney gezeen vanaaf d\'n euverkantj vanne bej\n- Categorieje: Operahoes in Sydney vanoet \'t wèste, Operahoes in Sydney van wied aaf + Titel: Operahoes in Sydney + Besjrieving: Operahoes in Sydney gezeen vanaaf d\'n euverkantj vanne bej + Categorieje: Operahoes in Sydney vanoet \'t wèste, Operahoes in Sydney van wied aaf + Draag dien plaetjes bie. Help Wikipedia-artikel toet laeve te kómme! + Plaetjes op Wikipedia kómme van Wikimedia Commons. + Dien plaetjes helpe luuj van euver de ganse werreld mit lieëre. + Verkóm auteursrechtelik besjurmp matterjaal det se op internet höbs gevónje wie plaetjes van poeasters, beuk en zo wiejer. + Versteis se \'t? + Jao! + Categorieje + \'nt laje... + Geine gekaoze + Gein besjrieving + Ónbekande licentie + Vernuuj + Klaor + Plaatse in de buurt + Gein plaatse in de buurt gevónje + Waorsjoewing + Dit bestandj besteit al op Commons. Wèts se zeker det se door wils gaon? + Jao + Nae + Titel + Bestandjstitel + Besjrieving + diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 2011f748b..08397cb0b 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -7,18 +7,25 @@ * Zygimantus --> + Išvaizda + Bendra + Atsiliepimai + Vietovė Vikiteka + Nustatymai Naudotojo vardas Slaptažodis + Prisijunkite prie savo Commons Beta paskyros Prisijungti + Pamiršote Slaptažodį? Užsiregistruoti Jungiamasi Prašome palaukti… Sėkmingai prisijungėte! Prisijungti nepavyko! Failas nerastas. Prašome pabandyti kitą failą. - Autentifikavimas nepavyko! + Autentifikavimas nepavyko, prašome prisijungti dar kartą Įkėlimas prasidėjo! %1$s įkelta! Bakstelėkite norėdami peržiūrėti jūsų įkėlimą @@ -43,12 +50,13 @@ Dalintis Atidaryti naršyklėje Pavadinimas + Prašome pateikti šiam failui pavadinimą Aprašymas Negalima prisijungti - tinklo klaida - Negalima prisijungti - prašome patikrinti savo vartotojo vardą - Negalima prisijungti - prašome patikrinti savo slaptažodį + Nepavyko prisijungti - prašome patikrinti savo naudotojo vardą ir slaptažodį Per daug nesėkmingų bandymų. Pabandykite dar kartą po keleto minučių. Atsiprašome, šis vartotojas buvo užblokuotas Commons + Turite pateikti savo dviejų žingsnių patvirtinimo kodą. Prisijungti nepavyko Įkelti Pavadinkite šį rinkinį @@ -57,6 +65,7 @@ Ieškoti kategorijas Išsaugoti Atnaujinti + Sąrašas GPS išjungta jūsų įrenginyje. Ar norite įjungti? Išjungti GPS Nėra įkėlimų kol kas @@ -78,8 +87,10 @@ Kategorijos Nustatymai Užsiregistruoti + Rinktiniai Paveikslėliai Apie - <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">Privatumo politika</a> + <u>Privatumo politika</u> + <u>Kūrėjai</u> Apie Siųsti Atsiliepimą (El. paštu) Nėra įdiegtos el. pašto tvarkyklės @@ -90,10 +101,10 @@ Atšaukti Šio paveikslėlio licencija bus %1$s Parsisiųsti - Licencija + Numatytoji Licencija Naudoti ankstesnį pavadinimą/aprašymą Automatiškai gauti dabartinę vietą - Gauti dabartinę vietove, kad būtų pasiūlytos kategorijos, jei paveikslėlis neturi geografinės žymės + Gauti dabartinę vietove, kad būtų pasiūlytos kategorijos, jei paveikslėlis neturi geografinės žymės Naktinis režimas Naudoti tamsią temą CC BY-SA 4.0 @@ -101,8 +112,15 @@ Vikimedija Commons talpina daugumą paveikslėlių, kurie yra naudojami Vikipedijoje. Jūsų paveikslėliai padeda šviesti žmones visame pasaulyje! Prašome kelti nuotraukos, kurios yra padarytos ar sukurtos tik jūsų: - - Gamtos objektai (gėlės, gyvūnai, kalnai)\n- Naudingi objektai (dviračiai, traukinių stotys)\n- Įžymūs žmonės (merai, Olimpiniai atletai) + Gamtos objektai (gėlės, gyvūnai, kalnai)\n• Naudingi objektai (dviračiai, traukinių stotys)\n• Įžymūs žmonės (jūsų meras, Olimpiniai atletai, kurios sutikote) + Gamtos objektai (gėlės, gyvūnai, kalnai) + Naudingi objektai (dviračiai, traukinių stotys) + Įžymūs žmonės (jūsų meras, Olimpiniai atletai, kurios sutikote) Prašome NEkelti: + - Asmenukės ar jūsų draugų nuotraukos\n- Nuotraukos, kurias atsisiuntėte iš interneto\n- Patentuotų programėlių nuotraukos + Asmenukės ar jūsų draugų nuotraukos + Nuotraukos, kurias atsisiuntėte iš interneto + Patentuotų programėlių nuotraukos Pavyzdinis įkėlimas: Įkelkite savo paveikslėlius. Padėkite Vikipedijos straipsniams būti spalvingesniems! Paveikslėliai Vikipedijoje yra iš Vikimedija Commons. @@ -116,7 +134,7 @@ Nėra aprašymo Nežinoma licencija Atnaujinti - Reikalinga teisė: Skaityti išorinę talpyklą. Programėle be to negali funkcionuoti. + Reikalinga teisė: Skaityti išorinę talpyklą. Programėle be to negali funkcionuoti. Neprivaloma teisė: Gauti dabartinę vietovę, kad būtų pasiūlomos kategorijos Gerai Netoliese Esančios Vietos diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index d73312361..5f1d28ce2 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -16,7 +16,7 @@ Lūdzu, uzgaidiet… Ieiešana veiksmīga Pieteikšanās neizdevās. - Autentifikācija neizdevās! + Autentifikācija neizdevās! Augšupielāde sākās! %1$s augšupielādēti! Uzsāk %1$s augšupielādi @@ -79,6 +79,7 @@ Nosaukums Apraksts + Autors Augšupielādēšanas datums Licence Koordinātas @@ -108,4 +109,5 @@ Valodas Turpināt Atcelt + Sapratu! diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 81e831769..c00a6e97e 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -21,7 +21,7 @@ Најавата е успешна! Најавата не успеа! Не ја пронајдов податотеката. Пробајте со друга. - Заверката не успеа! + Заверката не успеа. Најавете се повторно. Подигањето започна Податотеката „%1$s“ е подигната! Допрете за да го погледате подигањето @@ -49,8 +49,7 @@ Ставете ѝ наслов на податотеката Опис Не можам да Ве најавам — мрежата не работи - Не можам да Ве најавам — проверете си го корисничкото име - Не можам да Ве најавам — проверете си ја лозинката + Не можев да ве најавам. Проверете ги корисничкото име и лозинката. Направени се премногу неуспешни обиди. Обидете се пак за некоја минута. Нажалост, корисникот е блокиран на Ризницата Мора да го укажете вашиот код за двочинителска заверка. @@ -84,6 +83,7 @@ Категории Нагодувања Регистрација + Избрани слики За извршникот Прилогот на Ризницата има отворен код. Негови творци и одржувачи се примателите на наменските средства од Викимедиината заедница како и членовите на заедницата. Фондацијата Викимедија нема учество во нејзиното создавање, разработка и одржување. Создајте нов <a href=\"https://github.com/commons-app/apps-android-commons/issues\">проблем на GitHub</a> за пријавување на грешки и давање предлози. @@ -103,7 +103,7 @@ Стандардна лиценца Користи претходен наслов/опис Автоматски давај тековна местоположба - Добивање на тековната местоположба за да се дадат предлози за категории, доколку сликата нема геоознаки + Става геоознака од тековната местоположба во слика (ако ја нема). Предупредување: ова ви го разоткрива наоѓалиштето. Ноќен режим Користи темен изглед Наведи извор-Сподели под исти услови 4.0 @@ -155,8 +155,8 @@ Нема опис Непозната лиценца Превчитај - Потребна дозвола: Треба да се прочита од надворешен склад. Прилогот не може да работи без ова. - Потребна дозвола: Треба да се запише на надворешен склад. Прилогот не може да работи без ова. + Потребна дозвола: Треба да се прочита од надворешен склад. Прилогот без ова нема пристап до вашата галерија. + Потребна дозвола: Треба да се запише на надворешен склад. Прилогот без ова нема пристап до вашата камера. Дозвола по желба: Утврдување на тековната местоположба за предлагање категории ОК Околни места @@ -169,6 +169,8 @@ Наслов на податотеката Опис Тука оди описот на податотеката. Ова потенцијално може да биде прилично долго, и ќе треба да се преломи во неколку реда. Се надеваме дека ќе изгледа добро. + Автор + Тука оди корисничкото име на авторот на избраната слика. Датум на подигање Лиценца Координати @@ -211,6 +213,7 @@ Одјава Упатства Известувања + Избрана Местата во близина не можат да се прикажат без дозволи за местоположба. не најдов описи Податотечна страница @@ -259,4 +262,20 @@ Продолжи Откажи Пробај пак + Јасно! + Ова се места во ваша близинана кои им требаат слики за илустрирање на нивните статии на Википедија + Ако допрете на копчево ќе добиете список на тие места + Можете да подигнете слика за било кое од местата од вашата галерија или камера + Не пронајдов ниедна слика! + Се појави грешка при вчитувањето на сликите. + Подигач: %1$s + Сподели прилог + Не беа укажани координати при изборот на сликата + Грешка при добивањето на околните места. + Слика на денот + Слика на денот + Сликата е успешно додадена кон %1$s на Википодатоците! + Не успеав да ја изменам соодветната единица на Википодатоците! + Задај позадина + Позадината е успешно зададена! diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 485e66acc..f5ca39042 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -5,16 +5,24 @@ * Santhosh.thottingal --> - വിക്കിമീഡിയ കോമൺസ് + ദൃശ്യരൂപം + സാർവത്രികം + പ്രതികരണം + സ്ഥലം + കോമൺസ് സജ്ജീകരണങ്ങൾ ഉപയോക്തൃനാമം രഹസ്യവാക്ക് + താങ്കളുടെ കോമൺസ് ബീറ്റ അംഗത്വത്തിൽ പ്രവേശിക്കുക പ്രവേശിക്കുക + രഹസ്യവാക്ക് മറന്നോ? + അംഗത്വമെടുക്കുക പ്രവേശിക്കുന്നു ദയവായി കാത്തിരിക്കുക… പ്രവേശനം വിജയകരം! പ്രവേശനം പരാജയപ്പെട്ടു! - സാധുതാനിർണ്ണയം പരാജയപ്പെട്ടു! + പ്രമാണം കണ്ടെത്താനായില്ല. ദയവായി മറ്റൊരു പ്രമാണം നോക്കുക. + സാധുതാനിർണ്ണയം പരാജയപ്പെട്ടു! അപ്‌ലോഡ് തുടങ്ങി! %1$s അപ്‌ലോഡ് ചെയ്തിരിക്കുന്നു! താങ്കളുടെ അപ്‌ലോഡ് കാണാനായി ടാപ് ചെയ്യുക @@ -23,27 +31,28 @@ %1$s അപ്‌ലോഡിങ് പൂർത്തിയാക്കുന്നു %1$s അപ്‌ലോഡിങ് പരാജയപ്പെട്ടു കാണാനായി ടാപ് ചെയ്യുക - - 1 പ്രമാണം അപ്‌ലോഡ് ചെയ്യുന്നു + + ഒരു പ്രമാണം അപ്‌ലോഡ് ചെയ്യുന്നു %1$d പ്രമാണങ്ങൾ അപ്‌ലോഡ് ചെയ്യുന്നു - എന്റെ അപ്‌ലോഡുകൾ + എന്റെ സമീപകാല അപ്‌ലോഡുകൾ നിരയായി വെച്ചു പരാജയപ്പെട്ടു %1$d%% പൂർണ്ണം അപ്‌ലോഡ് ചെയ്തുകൊണ്ടിരിക്കുന്നു ചിത്രശാല ചിത്രം എടുക്കുക + സമീപസ്ഥം എന്റെ അപ്‌ലോഡുകൾ പങ്ക് വെയ്ക്കുക ബ്രൗസറിൽ കാണുക തലക്കെട്ട് + ഈ പ്രമാണത്തിന് ഒരു തലക്കെട്ട് നൽകുക. വിവരണം പ്രവേശിക്കാനായില്ല - നെറ്റ്‌വർക്ക് പരാജയപ്പെട്ടു - പ്രവേശിക്കാനായില്ല - ദയവായി താങ്കളുടെ ഉപയോക്തൃനാമം പരിശോധിക്കുക - പ്രവേശിക്കാനായില്ല - ദയവായി താങ്കളുടെ രഹസ്യവാക്ക് പരിശോധിക്കുക - നിരവധി വിജയകരമല്ലാത്ത ശ്രമങ്ങൾ നടന്നിരിക്കുന്നു. വീണ്ടും ശ്രമിക്കുന്നതിനു മുമ്പ് ഏതാനം മിനിറ്റുകൾ വിശ്രമിക്കുക + നിരവധി വിജയകരമല്ലാത്ത ശ്രമങ്ങൾ നടന്നിരിക്കുന്നു. വീണ്ടും ശ്രമിക്കുന്നതിനു മുമ്പ് ഏതാനം മിനിറ്റുകൾ വിശ്രമിക്കുക. ക്ഷമിക്കുക, ഈ ഉപയോക്താവ് കോമൺസിൽ നിന്ന് തടയപ്പെട്ടിരിക്കുകയാണ് + താങ്കളുടെ ദ്വി-ഘടക സാധൂകരണ കോഡ് നൽകുക. പ്രവേശനം പരാജയപ്പെട്ടു അപ്‌ലോഡ് ഈ ഗണത്തിന് പേരിടുക @@ -51,6 +60,11 @@ അപ്‌ലോഡ് വർഗ്ഗങ്ങളിൽ തിരയുക സേവ് ചെയ്യുക + പുതുക്കുക + പട്ടിക + താങ്കളുടെ ഉപകരണത്തിൽ ജി.പി.എസ്. പ്രവർത്തനരഹിതമാണ്. അത് പ്രവർത്തനസജ്ജമാക്കണോ? + ജി.പി.എസ്. സജ്ജമാക്കുക + ഇതുവരെ അപ്‌ലോഡുകൾ ഒന്നുമില്ല ഒരു അപ്‌ലോഡും ചെയ്തില്ല ഒരു അപ്‌ലോഡ് @@ -68,6 +82,8 @@ താങ്കളുടെ ചിത്രങ്ങൾ വിക്കിമീഡിയ കോമൺസിൽ കൂടുതൽ എളുപ്പത്തിൽ കണ്ടെത്തപ്പെടാനായി വർഗ്ഗങ്ങൾ ചേർക്കുക.\n\nവർഗ്ഗങ്ങൾ ചേർക്കാനായി ടൈപ്പ് ചെയ്ത് തുടങ്ങുക.\nഈ ഘട്ടം ഒഴിവാക്കാൻ ടാപ് ചെയ്യുക (അല്ലെങ്കിൽ പിന്നോട്ട് പോവുക). വർഗ്ഗങ്ങൾ സജ്ജീകരണങ്ങൾ + അംഗത്വമെടുക്കുക + തിരഞ്ഞെടുത്ത ചിത്രങ്ങൾ വിവരണം <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">അപാച്ചേ അനുമതിപത്രം പതിപ്പ് 2</a> പ്രകാരം പുറത്തിറക്കപ്പെട്ട ഓപ്പൺ സോഴ്സ് സോഫ്റ്റ്‌വേർ സ്രോതസ്സ് രൂപം <a href=\"https://github.com/commons-app/apps-android-commons\">ജിറ്റ്ഹബിൽ</a> ലഭ്യമാണ്.\nപ്രശ്നങ്ങൾ <a href=\" https://github.com/commons-app/apps-android-commons/issues\">ബഗ്സില്ലയിൽ</a> അറിയിക്കുക. @@ -81,9 +97,11 @@ റദ്ദാക്കുക ചിത്രം %1$s പ്രകാരം അനുമതി നൽകപ്പെടുന്നതാണ് ഡൗൺലോഡ് - അനുമതി - സി.സി. ആട്രിബ്യൂഷൻ-ഷെയർ‌എലൈക് 3.0 - സി.സി. ആട്രിബ്യൂഷൻ 3.0 + സ്വതേയുള്ള ഉപയോഗാനുമതി + ആട്രിബ്യൂഷൻ-ഷെയർ‌എലൈക് 4.0 + ആട്രിബ്യൂഷൻ 4.0 + ആട്രിബ്യൂഷൻ-ഷെയർ‌എലൈക് 3.0 + ആട്രിബ്യൂഷൻ 3.0 സി.സി.0 സി.സി. ബൈ-എസ്.എ. 3.0 സി.സി. ബൈ-എസ്.എ. 3.0 (ഓസ്ട്രിയ) @@ -97,6 +115,8 @@ സി.സി. ബൈ-എസ്.എ. 3.0 (പോളണ്ട്) സി.സി. ബൈ-എസ്.എ. 3.0 (റൊമേനിയ) സി.സി. ബൈ 3.0 + സി.സി. ബൈ-എസ്.എ. 4.0 + സി.സി. ബൈ 4.0 സി.സി. സീറോ താങ്കളെടുക്കുന്ന ചിത്രങ്ങൾ സംഭാവന ചെയ്യുക. വിക്കിപീഡിയ ലേഖനങ്ങൾ ജീവസ്സുറ്റതാക്കിത്തീർക്കുക! വിക്കിപീഡിയയിലുള്ള ചിത്രങ്ങൾ വിക്കിമീഡിയ കോമൺസിൽ നിന്നാണ് @@ -105,9 +125,63 @@ മനസ്സിലായോ? ശരി! വർഗ്ഗങ്ങൾ - ശേഖരിക്കുന്നു… + ശേഖരിക്കുന്നു… ഒന്നും തിരഞ്ഞെടുത്തിട്ടില്ല വിവരണമൊന്നുമില്ല അജ്ഞാതമായ അനുമതി പുതുക്കുക + ശരി + സമീപ സ്ഥലങ്ങൾ + മുന്നറിയിപ്പ് + ഈ പ്രമാണം കോമൺസിൽ നിലവിലുണ്ട്. തുടരണം എന്ന് താങ്കൾക്കുറപ്പാണോ? + അതെ + അല്ല + ശീർഷകം + മീഡിയയുടെ തലക്കെട്ട് + വിവരണം + സ്രഷ്ടാവ് + അപ്‌ലോഡ് ചെയ്ത തീയതി + ഉപയോഗാനുമതി + നിർദ്ദേശാങ്കങ്ങൾ + ഒന്നും നൽകിയിട്ടില്ല + കോമൺസ് ലോഗോ + കോമൺസ് വെബ്‌സൈറ്റ് + പശ്ചാത്തല ചിത്രം + ചിത്രം അപ്‌ലോഡ് ചെയ്യുക + സാവോ പർവ്വതം + ലാമകൾ + മഴവിൽ പാലം + തുലിപ് + സെൽഫികൾ വേണ്ട + പകർപ്പവകാശ സംരക്ഷിത ചിത്രം + സിഡ്നി ഓപെറാ ഹൗസ് + റദ്ദാക്കുക + തുറക്കുക + അടയ്ക്കുക + പ്രധാനം + അപ്‌ലോഡ് + സമീപസ്ഥം + വിവരണം + സജ്ജീകരണങ്ങൾ + പ്രതികരണം + ലോഗൗട്ട് + സഹായം + അറിയിപ്പുകൾ + തിരഞ്ഞെടുക്കപ്പെട്ടത് + വിവരണങ്ങൾ ഒന്നും കണ്ടെത്തിയില്ല + കോമൺസ് പ്രമാണ താൾ + വിക്കിഡേറ്റാ ഇനം + വിക്കിപീഡിയ ലേഖനം + അനുമതി നൽകുക + താങ്കളുടെ അംഗത്വത്തിൽ പ്രവേശിക്കുക + ബ്രൗസറിൽ കാണുക + വിക്കിഡേറ്റാ + വിക്കിപീഡിയ + കോമൺസ് + <u>പതിവുചോദ്യങ്ങൾ</u> + <u>പരിഭാഷപ്പെടുത്തുക</u> + ഭാഷകൾ + തുടരുക + റദ്ദാക്കുക + വീണ്ടും ശ്രമിക്കുക diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index ec68c9d3e..b5edf2370 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -1,5 +1,6 @@ + حليو + عام + پذيرائي + جڳهه العام + ترتيبون واپرائيندڙ-نانءُ ڳجھولفظ + وڪي ڪامنز جي آزمائشي کاتي ۾ داخل ٿيو داخل ٿيو + ڳجھو لفظ وساري ويٺا آهيو؟ کاتو کوليو داخل ٿيندي براءِ مھرباني انتظار ڪريو… داخل ٿيڻ ڪامياب! داخل ٿيڻ ناڪام! فائيل نہ لڌو. براءِ مھرباني ٻيو ڪو فائيل آزمايو. - اٿينٽيڪيشن ناڪام! + تصديق ناڪام! ٻيهر داخل ٿيو چاڙھ شروع! %1$s چڙھي چڪا! پنھنجو چاڙھ ڏسڻ لاءِ ٺونگو ھڻو @@ -40,12 +48,13 @@ ونڊيو جھانگوءَ ۾ ڏسو عنوان + هن فائيل لاءِ ڪا سُرخي ڏيو تشريح ناقابلِ داخل ٿيڻ - باھمڄار ناڪامي - ناقابلِ داخل ٿيڻ - براءِ مھرباني پنھنجو واپرائيندڙ-نانءُ چڪاسيو - ناقابل داخل ٿيڻ - براءِ مھرباني پنھنجو ڳجھولفظ چڪاسيو + داخل نه ٿيا آهيو - مهرباني ڪري ڳجهو لفظ ۽ کاتي جو نالو چيڪ ڪيو ھيڪانديون ناڪام ڪوششون. براءِ مھرباني ڪجھ منٽن کانپوءِ ٻيھر ڪوشش ڪريو. افسوس، ھي واپرائيندڙ العام تي بندشيل آھي + اوهان هر صورت ۾ ٻن عنصرن واري تصديق جو ڪوڊ ڏيو. داخل ٿيڻ ناڪام چاڙھيو ھن سيٽ کي نالو ڏيو @@ -54,14 +63,16 @@ زمرا ڳوليو سانڍيو تازو ڪيو + فهرست + اوهان جي ڊوائيس ۾ جي پي ايس بند آهي. اوهان کولڻ چاهيو ٿا؟ جي پي ايس چالو ڪيو (اين ايبل جي پي ايس) اڃان تائين ڪو به ڄاڙهه (اَپلوڊ) نه ٿيو آهي - + \@string/contributions_subtitle_zero %1$d چاڙھ %1$d چاڙھَ - + چاڙھ %1$d شروع ڪندي چاڙھَ %d$1 شروع ڪندي @@ -74,8 +85,9 @@ زمرا ترتيبون کاتو کوليو + چونڊ تصويرون بابت - <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">ذاتيات پاليسي</a> + <u>ذاتيات پاليسي</u> بابت پذيرائي موڪليو (برقٽپال ذريعي) ڪوبہ برقٽپال ڪلائينٽ تنصيبيل ناھي @@ -86,10 +98,10 @@ رد ھن عڪس کي %1$s جي تحت لائسنس ٿيندو لاھيو - لائسنس + رٿيل لائسنس گذريل عنوان/تشريح استعمال ڪريو خوبخود ھاڻوڪي مڪانيت وٺو - ھاڻوڪي مڪانيت لھو زمرن جون تجويزون پيش ڪرڻ لاءِ جيڪڏھن عڪس تي جيوٽيگ ناھي لڳل + ھاڻوڪي مڪانيت ڏئي ٿو جيڪڏھن عڪس جيوٽيگ ناھي ٿيل، ۽ عڪس کي ان سان جيوٽيگ ڪري ٿو. چتاءُ: ھي توھان جي ھاڻوڪي مڪانيت ظاھر ڪندو. رات جو ڏيک گھرو نظارو استعمال ڪريو انتساب-ھجھڙي ڀاڱيداري 4.0 @@ -115,11 +127,17 @@ وڪيپيڊيا تي استعمال ٿيندڙ گھڻن عڪسن جي وڪيميڊيا العام ميزباني ڪري ٿو. توھان جا عڪس سڄي دنيا جي ماڻھن کي تعليم يافتا ڪرڻ ۾ مدد ڪن ٿا براءِ مھرباني اھي تصويرون چاڙھيو مڪمل طور تي توھان پاران ڪڍيل يا تخليقيل آھن: - u2022 قدرتي شيون (گل، جانور، جبل) \nu2022 استعمال جوڳيون شيون (سائيڪلون، ٽرين اسٽيشنون) \nu2022 مشھور شخصيتون (توھان جو ناظم، اولمپڪ رانديگر جنھن سان توھان مليئو) + قدرتي شيون (گل، جانور، جبل) \nاستعمال جوڳيون شيون (سائيڪلون، ريل اسٽيشنون) \nمشھور شخصيتون (توھان جو ناظم، اولمپڪ رانديگر جنھن سان توھان مليو) + قدرتي شيون (گل، جانور، جبل) + استعمال جوڳيون شيون (سائيڪلون، ريل اسٽيشنون) + مشھور شخصيتون (توھان جو ناظم، اولمپڪ رانديگر جنھن سان توھان مليو) براءِ مھرباني نہ چاڙھيو: u2022 سيلفيون يا پنھنجي دوستن جو تصويرون \nu2022 اھي تصويرون جيڪي توھان انٽرنيٽ تان ڊائونلوڊ ڪيون \nu2022 پروپرائيٽري ايپس جا اسڪرين شاٽ + سيلفي يا اوهان جي دوست جي تصوير + انٽرنيٽ تان کنيل تصويرون مثال چاڙھ: - عنوان: سڊني اوپيرا گھر \n- تشريح: سڊني اوپيرا گھر نھر جي پاسي کان ڏيک \n- زمرا: سڊني اوپيرا گھر، سڊني اوپيرا گھر اولھ کان، سڊني اوپيرا گھر ڏورانھان ڏيک + سُرخي: سڊني اوپيرا گھر پنھنجي عڪسن جي ڀاڱيداري ڪريو. وڪيپيڊيا ڪي مضمونن ۾ زندگي آڻيو! وڪيپيڊيا تي عڪس وڪيميڊيا العام تان اچن ٿا. توھان جا عڪس سڄي دنيا ۾ ماڻھن کي تعليم يافتا ڪرڻ ۾ مدد ڪن ٿا. @@ -132,7 +150,7 @@ ڪا تشريح ناھي اڻڄاتل لائسنس تازو ڪريو - گھربل اجازت: خارجي اسٽوريج پڙھڻ. ايپ ھن کانسواءِ فنڪشن نٿي ڪري سگھي. + گھربل اجازت: خارجي اسٽوريج پڙھڻ. ايپ ھن کانسواءِ فنڪشن نٿي ڪري سگھي. چونڊ اجازت: زمرن جي تجويزن لاءِ ھاڻوڪي مڪانيت وٺو ٺيڪ ويجھڙائيءَ ۾ جڳھون @@ -182,9 +200,30 @@ پنھنجي کاتي ۾ داخل ٿيو لاگ فائيل موڪليو لاگ فائيل سرجڻھارن کي برقٽپال ذريعي موڪليو + تصوير مٽائڻ لاءِ نامزد ڪئي وئي آهي. + برائوزر ۾ ڏسو مڪانيت تبديلي ناھي ٿي. مڪانيت موجود ناھي. ويجھين جڳھن جي فھرست ڏيکارڻ لاءِ اجازت گھربل آھي ھدايتون وٺو مضمون پڙھو + وڪيپيڊيا ڪامنز ۾ ڀليڪار، %1$s! اسان کي خوشي آهي ته اوهان هتي آهيو + %1$s اوهان جي بحث صفحي تي پيغام ڇڏيو آهي. + سنوارڻ لاءِ مهرباني + %1$s اوهان جو %2$s تي ذڪر ڪيو آهي. + طرف + وڪيپيڊيا + <u>پذيرائي ڏيو</u> + <u>عام سوال</u> + سبق کي ڇڏيو + انٽرنيٽ ناهي + انٽرنيٽ آهي + <u>ترجمو</u> + ٻوليون + رد + سمجھي ويس! + ڪوبہ عڪس نہ لڌو! + ايپ ونڊيو + اڄ جي تصوير + اڄ جي تصوير diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 4ae0db17c..6fb06002a 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -17,7 +17,7 @@ පිවිසුම සාර්ථකයි! පිවිසීම අසාර්ථකයි! ගොනුව හමු නොවිණි. තවත් ගොනුවක් උත්සාහ කරන්න. - සහතික කිරීම අසාර්ථකයි + සහතික කිරීම අසාර්ථකයි උඩුගතකිරීම ආරම්හවූවා! %1$s උඩුගතකලා! ඔබගේ උඩුගතකිරීම පෙන්වන්න තට්ටු කරන්න @@ -44,8 +44,6 @@ මාතෘකාව විස්තරය පිවිසීමට නොහැකිය-ජාලය ඇනහිටීමක් - පිවිසීමට නොහැකිය-කරුණාකර ඔබගේ පිවිසුම්-නාමය පරික්ෂා කරන්න - පිවිසීමට නොහැකිය-කරුණාකර ඔබගේ මුරපදය පරික්ෂා කරන්න. බොහෝ අසාර්ථක උත්සාහයන් කර ඇත. කරුණාකර මිනිත්තු කිහිපයකට පසුව උත්සාහ කරන්න කණගාටුයි,මෙම පරිශීලක වාරණයට ලක්කර ඇත. ඔබ ඔබගේ ද්විත්ව සහතික කිරීමේ කේතය ඇතුලත් කළ යුතුය. diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 20fe02f20..ae91a8d25 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -5,18 +5,25 @@ * Sudo77(new) --> + Vzhľad + Všeobecné + Spätná väzba + Poloha Commons + Nastavenia Používateľské meno Heslo + Prihláste sa do svojho účtu Commons Beta Prihlásiť sa + Zabudli ste heslo? Zaregistrovať sa Prihlasovanie Čakajte prosím… Prihlásenie úspešné Prihlásenie zlyhalo! Súbor nebol nájdený. Skúste, prosím, iný súbor. - Overenie zlyhalo! + Overenie zlyhalo! Nahrávanie začalo! %1$s je nahraný! Kliknutím zobrazíte váš upload @@ -41,10 +48,9 @@ Zdieľať Otvoriť v prehliadači Názov + Prosím, dajte tomuto súboru názov Opis prihlásenie zlyhalo - zlyhanie siete - Prihlásenie zlyhalo - skontrolujte vaše používateľské meno - Prihlásenie zlyhalo - skontrolujte vaše heslo Príliš veľa neúspešných pokusov. Skúste to znova o niekoľko minút. Ospravedlňujeme sa, tento užívateľ bol na Commons zablokovaný Prihlásenie zlyhalo @@ -64,7 +70,7 @@ O aplikácii Open Source softvér dostupný za podmienok <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache License v2</a> Zdroj na <a href=\"https://github.com/commons-app/apps-android-commons\">GitHub</a>. Bugy na <a href=\" https://github.com/commons-app/apps-android-commons/issues\">Github</a>. - <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">Zásady ochrany súkromia</a> + <u>Zásady ochrany súkromia</u> O aplikácii Odoslať spätnú väzbu (emailom) Nemáte nainštalovaného žiadneho e-mailového klienta @@ -75,7 +81,7 @@ Zrušiť Tento obrázok bude licencovaný podľa %1$s Stiahnuť - Licencia + Predvolená licencia Použiť predchádzajúci názov/popis Automaticky získať súčasnú polohu Nočný režim @@ -103,11 +109,20 @@ Na Wikimedia Commons sa nachádza väčšina obrázkov, ktoré sa používajú na Wikipédii. Váš obrázok pomáha vzdelávať ľudí po celom svete! Prosím, nahrávajte obrázky, ktoré ste odfotili alebo vytvorili vy: - - Prírodu (kvety, zvieratá, hory)\n- Užitočné objekty (bicykle, železničné stanice)\n- Slávni ľudia (starosta, olympionici, s ktorými ste sa stretli) + Prírodu (kvety, zvieratá, hory)\n• Užitočné objekty (bicykle, železničné stanice)\n• Slávnych ľudí (starosta, olympionici, s ktorými ste sa stretli) + Prírodu (kvety, zvieratá, hory) + Užitočné objekty (bicykle, železničné stanice) + Slávnych ľudí (starosta, olympionici, s ktorými ste sa stretli) Prosím NENAHRÁVAJTE: - Selfies alebo fotky vašich priateľov\n- Obrázky prevzaté z internetu\n- Snímky obrazovky proprietárnych aplikácii + Selfies alebo fotky vašich priateľov + Obrázky prevzaté z internetu + Snímky obrazovky proprietárnych aplikácii Príklad nahratia: - Názov: Opera v Sydney\n- Opis: Opera v Sydney - pohľad spoza zátoky\n- Kategórie: Sydney Opera House from the west, Sydney Opera House remote views + Názov: Opera v Sydney + Popis: Opera v Sydney - pohľad spoza zátoky + Kategórie: Sydney Opera House from the west, Sydney Opera House remote views Prispejte svojimi obrázkami. Pomôžte oživiť články na Wikipédií. Obrázky na Wikipédií pochádzajú z Wikimedia Commons. Vaše obrázky pomáhajú vzdelávať ľudí po celom svete. @@ -130,14 +145,20 @@ Názov Názov média Popis + Autor + Dátum nahrania Licencia Súradnice neposkytnuté Staňte sa beta testerom Naozaj sa chcete odhlásiť? Logo Commons + Webová stránka Commons + Facebooková stránka Commons + Zdrojový kód Commons na Githube Obrázok pozadia Nenašiel sa žiaden obrázok + Nahrať obrázok Hora Zaó Lamy Dúhový most @@ -157,4 +178,15 @@ Návod Upozornenia nenašiel sa žiaden popis + Otvoriť v prehliadači + WIKIÚDAJE + WIKIPÉDIA + COMMONS + <u>Ohodnoťte nás</u> + <u>FAQ</u> + <u>Preložiť</u> + Jazyky + Pokračovať + Zrušiť + Obnoviť diff --git a/app/src/main/res/values-skr/strings.xml b/app/src/main/res/values-skr/strings.xml index dc9de3fc5..7a4dffb41 100644 --- a/app/src/main/res/values-skr/strings.xml +++ b/app/src/main/res/values-skr/strings.xml @@ -20,7 +20,7 @@ لاگ ان کامیاب! لاگ ان ناکام! فائل کائنی لبھی،ٻئی فائل کیتے کوشش کرو۔ - تصدیق ناکام! + تصدیق ناکام! اپ لوڈ شروع! %1$s اپ لوڈ تھی ڳیا! آپݨی اپلوڈ ݙیکھݨ کیتے ٹیپ کرو @@ -89,6 +89,7 @@ عنوان میڈیا دا عنوان تفصیل + مصنف اپ لوڈ تھیوݨ دی تاریخ لائیسنس کوآرڈینیٹ @@ -113,6 +114,7 @@ تہاڈی رائے لاگ آؤٹ ٹیٹوریل + خاص وکی ڈیٹا آئٹم وکی پیڈیا دا مضمون اجازت ݙیوو @@ -122,8 +124,10 @@ وکی پیڈیا عام <u>عام طور تے پچھے ونڄݨ آلے سوال</u> + <u>ترجمہ کرو</u> زباناں اڳوں تے تھیوو منسوخ ولدا کوشش کرو + گھن گھندا diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 8511187ae..29a025bd8 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -1,6 +1,8 @@ + การแสดงผล + ทั่วไป + คำติชม + ที่ตั้ง คอมมอนส์ + การตั้งค่า ชื่อผู้ใช้ รหัสผ่าน - เข้าสู่ระบบ + ลงชื่อเข้าระบบบัญชีคอมมอนส์บีตาของคุณ + ลงชื่อเข้า + ลืมรหัสผ่านหรือ? สมัครสมาชิก กำลังเข้าสู่ระบบ กรุณารอสักครู่… การเข้าสู่ระบบสำเร็จแล้ว! การเข้าสู่ระบบล้มเหลว! ไม่พบไฟล์ กรุณาลองใช้ไฟล์อื่น - การตรวจสอบความถูกต้องล้มเหลว! + การตรวจสอบความถูกต้องล้มเหลว! เริ่มการอัปโหลดแล้ว! อัปโหลด %1$s แล้ว! แตะเพื่อดูการอัปโหลดของคุณ @@ -38,10 +45,9 @@ แชร์ ดูในเบราว์เซอร์ ชื่อเรื่อง + กรุณาระบุชืิ่อเรื่องของไฟล์นี้ คำอธิบาย ไม่สามารถเข้าสู่ระบบได้ - ความล้มเหลวของเครือข่าย - ไม่สามารถเข้าสู่ระบบได้ - กรุณาตรวจสอบชื่อผู้ใช้ของคุณ - ไม่สามารถเข้าสู่ระบบได้ - กรุณาตรวจสอบรหัสผ่านของคุณ จำนวนครั้งที่พยายามไม่สำเร็จมากเกินไป กรุณาลองอีกครั้งในอีกสักครู่ ขออภัย ผู้ใช้นี้ถูกบล็อกบนคอมมอนส์อยู่ คุณต้องระบุโค้ดการตรวจสอบความถูกต้องสองปัจจัยของคุณ @@ -53,6 +59,7 @@ ค้นหาหมวดหมู่ บันทึก รีเฟรช + รายการ GPS ถูกปิดใช้งานในอุปกรณ์ของคุณอยู่ คุณต้องการเปิดใช้งานหรือไม่? เปิดใช้งาน GPS ยังไม่มีการอัปโหลด @@ -73,8 +80,8 @@ เกี่ยวกับ แอปวิกิมีเดียคอมมอนส์เป็นแอปโอเพนซอร์สที่สร้างขึ้นและดูแลโดยผู้มีสิทธิและอาสาสมัครของชุมชนวิกิมีเดีย มูลนิธิวิกิมีเดียไม่มีส่วนเกี่ยวข้องในการสร้าง พัฒนา หรือการบำรุงรักษาแอปใดๆ ทั้งสิ้น สร้าง <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub issue</a> ใหม่เพื่อรายงานบั๊กและส่งข้อเสนอแนะ - <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">นโยบายความเป็นส่วนตัว</a> - <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\">เครดิต</a> + <u>นโยบายความเป็นส่วนตัว</u> + <u>เครดิต</u> เกี่ยวกับ ส่งคำติชม (ผ่านทางอีเมล) ไม่ได้ติดตั้งไคลเอนต์อีเมล @@ -86,20 +93,49 @@ รูปภาพนี้จะอนุญาตให้ใช้ได้ภายใต้สัญญาอนุญาต %1$s โดยการส่งรูปภาพนี้ ฉันยืนยันว่านี่เป็นงานของฉันเอง ซึ่งไม่ประกอบด้วยเนื้อหาที่ละเมิดลิขสิทธิ์หรือภาพเซลฟี หรืออื่นๆ ตามที่ระบุใน<a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">นโยบายของ Wikimedia Commons</a> ดาวน์โหลด - สัญญาอนุญาต + สัญญาอนุญาตปริยาย ใช้ชื่อเรื่อง/คำอธิบายก่อนหน้านี้ รับข้อมูลตำแหน่งที่ตั้งปัจจุบันโดยอัตโนมัติ - ดึงข้อมูลตำแหน่งที่ตั้งปัจจุบันเพื่อรับข้อเสนอแนะเกี่ยวกับหมวดหมู่ถ้ารูปภาพไม่ได้ติดแท็กตำแหน่งที่ตั้งเอาไว้ + ดึงข้อมูลตำแหน่งที่ตั้งปัจจุบันเพื่อรับข้อเสนอแนะเกี่ยวกับหมวดหมู่ถ้ารูปภาพไม่ได้ติดแท็กตำแหน่งที่ตั้งเอาไว้ โหมดกลางคืน ใช้ธีมสีเข้ม + Attribution-ShareAlike 4.0 + Attribution 4.0 + Attribution-ShareAlike 3.0 + Attribution 3.0 + CC0 + CC BY-SA 3.0 + CC BY-SA 3.0 (ออสเตรีย) + CC BY-SA 3.0 (เยอรมนี) + CC BY-SA 3.0 (เอสโตเนีย) + CC BY-SA 3.0 (สเปน) + CC BY-SA 3.0 (โครเอเชีย) + CC BY-SA 3.0 (ลักเซมเบิร์ก) + CC BY-SA 3.0 (เนเธอร์แลนด์) + CC BY-SA 3.0 (นอร์เวย์) + CC BY-SA 3.0 (โปแลนด์) + CC BY-SA 3.0 (โรมาเนีย) + CC BY 3.0 + CC BY-SA 4.0 + CC BY 4.0 + CC Zero วิกิมีเดียคอมมอนส์เก็บรูปภาพทั้งหมดที่ถูกใช้ในวิกิพีเดีย รูปภาพของคุณช่วยให้ความรู้แก่ผู้คนทั่วโลก! โปรดอัปโหลดรูปภาพที่ถ่ายหรือสร้างด้วยตัวคุณเองทั้งหมด: - - วัตถุธรรมชาติ (ดอกไม้ สัตว์ ภูเขา)\n- วัตถุที่สามารถใช้งานได้ (จักรยาน สถานีรถไฟ)\n- บุคคลที่มีชื่อเสียง (นายกเทศมนตรีของคุณ นักกีฬาโอลิมปิกที่คุณรู้จัก) + วัตถุธรรมชาติ (ดอกไม้ สัตว์ ภูเขา)\n• วัตถุที่สามารถใช้งานได้ (จักรยาน สถานีรถไฟ)\n• บุคคลที่มีชื่อเสียง (นายกเทศมนตรีของคุณ นักกีฬาโอลิมปิกที่คุณรู้จัก) + วัตถุธรรมชาติ (ดอกไม้ สัตว์ ภูเขา) + วัตถุที่สามารถใช้งานได้ (จักรยาน สถานีรถไฟ) + บุคคลที่มีชื่อเสียง (นายกเทศมนตรีของคุณ นักกีฬาโอลิมปิกที่คุณรู้จัก) โปรดอย่าอัปโหลด: - ภาพเซลฟีหรือภาพที่มีเพื่อนของคุณ\n- ภาพที่คุณดาวน์โหลดจากอินเทอร์เน็ต\n- ภาพหน้าจอแอปที่เป็นซอฟต์แวร์กรรมสิทธิ์ + ภาพเซลฟีหรือภาพที่มีเพื่อนของคุณ + ภาพที่คุณดาวน์โหลดจากอินเทอร์เน็ต + ภาพหน้าจอแอปที่เป็นซอฟต์แวร์กรรมสิทธิ์ ตัวอย่างการอัปโหลด: - ชื่อเรื่อง: โรงอุปรากรซิดนีย์\n- คำอธิบาย: โรงอุปรากรซิดนีย์เมื่อมองจากบริเวณอ่าวซิดนีย์\n- หมวดหมู่: โรงอุปรากรเมื่อมองจากทิศตะวันตก, ทิวทัศน์โรงอุปรากรซิดนีย์จากระยะไกล + ชื่อเรื่อง: โรงอุปรากรซิดนีย์ + คำอธิบาย: โรงอุปรากรซิดนีย์เมื่อมองจากบริเวณอ่าวซิดนีย์ + หมวดหมู่: โรงอุปรากรเมื่อมองจากทิศตะวันตก, ทิวทัศน์โรงอุปรากรซิดนีย์จากระยะไกล ลงรูปของคุณ ช่วยให้บทความวิกิพีเดียมีชีวิตชีวา! รูปภาพบนวิกิพีเดียมาจากวิกิพีเดียคอมมอนส์ รูปภาพของคุณช่วยให้การศึกษาแก่ผู้คนทั่วโลก @@ -112,8 +148,8 @@ ไม่มีคำอธิบาย สัญญาอนุญาตที่ไม่รู้จัก รีเฟรช - สิทธิที่ต้องการ: อ่านที่เก็บข้อมูลภายนอก แอปไม่สามารถทำงานได้โดยไม่มีสิทธินี้ - สิทธิที่ต้องการ: เขียนที่เก็บข้อมูลภายนอก แอปไม่สามารถทำงานได้โดยไม่มีสิทธินี้ + สิทธิที่ต้องการ: อ่านที่เก็บข้อมูลภายนอก แอปไม่สามารถเข้าถึงคลังภาพได้โดยไม่มีสิทธินี้ + สิทธิที่ต้องการ: เขียนที่เก็บข้อมูลภายนอก แอปไม่สามารถเข้าถึงกล้องของคุณได้โดยไม่มีสิทธินี้ สิทธิทางเลือก: รับข้อมูลตำแหน่งที่ตั้งปัจจุบันสำหรับข้อเสนอแนะหมวดหมู่ ตกลง สถานที่ใกล้เคียง @@ -126,6 +162,7 @@ ชื่อเรื่องสื่อ คำอธิบาย ป้อนคำอธิบายสื่อที่นี่ คำอธิบายที่มีความยาวมากอาจมีหลายบรรทัด เราหวังว่ามันจะดูดี + ผู้สร้างสรรค์ วันที่อัปโหลด สัญญาอนุญาต พิกัด @@ -142,14 +179,19 @@ โลโก้คอมมอนส์ เว็บไซต์คอมมอนส์ หน้าเฟซบุ๊กคอมมอนส์ + ซอร์สโค้ดคอมมอนส์บน GitHub ภาพพื้นหลัง + ภาพสื่อล้มเหลว ไม่พบรูปภาพ อัปโหลดรูปภาพ ภูเขาซะโอ ยามา สะพานสายรุ้ง ทิวลิป + ไม่มีภาพเซลฟี + ภาพกรรมสิทธิ์ ยินดีต้อนรับสู่วิกิพีเดีย + ลิขสิทธิ์ต้อนรับ โรงอุปรากรซิดนีย์ ยกเลิก เปิด @@ -165,16 +207,50 @@ การแจ้งเตือน ไม่สามารถแสดงสถานที่ใกล้เคียงได้โดยขาดสิทธิ์การเข้าถึงตำแหน่ง ไม่พบคำอธิบาย + หน้าไฟล์คอมมอนส์ รายการวิกิสนเทศ + บทความวิกิพีเดีย ข้อผิดพลาดขณะแคชภาพ ชื่อเรื่องที่อธิบายลักษณะเฉพาะของไฟล์ ซึ่งจะใช้เป็นชื่อไฟล์ คุณอาจใช้ภาษาธรรมดาที่มีเว้นวรรคก็ได้ อย่ารวมนามสกุลไฟล์ โปรดอธิบายสื่อดังกล่าวให้มากที่สุดเท่าที่จะได้: สื่อนี้ถูกถ่ายที่ไหน? สื่อนี้แสดงถึงอะไร? บริบทคืออะไร? โปรดอธิบายถึงวัตถุหรือบุคคล เปิดเผยข้อมูลที่ไม่อาจคาดเดาได้อย่างง่ายดาย เช่น เวลาที่ถ่าย หากเป็นภาพทิวทัศน์ หากสื่อแสดงถึงสิ่งที่ไม่ธรรมดา โปรดอธิบายว่าอะไรทำให้สื่อดังกล่าวไม่ธรรมดา + ภาพนี้มืดเกินไป คุณแน่ใจหรือว่าคุณต้องการอัปโหลดภาพนี้? วิกิมีเดียคอมมอนส์นั้นมีไว้สำหรับรูปภาพที่มีคุณค่าในทางสารานุกรมเท่านั้น + ภาพนี้มัว คุณแน่ใจหรือว่าคุณต้องการอัปโหลดภาพนี้? วิกิมีเดียคอมมอนส์นั้นมีไว้สำหรับรูปภาพที่มีคุณค่าในทางสารานุกรมเท่านั้น ให้สิทธิ์ ใช้ที่จัดเก็บข้อมูลภายนอก บันทึกรูปภาพที่ถ่ายด้วยกล้องในแอปบนอุปกรณ์ของคุณ ลงชื่อเข้าใช้บัญชีของคุณ ส่งไฟล์ปูม ส่งไฟล์ปูมไปยังนักพัฒนาผ่านทางอีเมล + ไม่พบเว็บเบราว์เซอร์ที่จะเปิด URL + ข้อผิดพลาด! ไม่พบ URL + เสนอการลบ + ภาพนี้ถูกเสนอการลบแล้ว + ดูในเบราว์เซอร์ ไม่ได้เปลี่ยนตำแหน่งที่ตั้ง ตำแหน่งที่ตั้งไม่พร้อมใช้งาน + ต้องการสิทธิเพื่อแสดงรายการสถานที่ใกล้เคียง + สอบถามเส้นทาง + อ่านบทความ + ยินดีต้อนรับสู่วิกิมีเดียคอมมอนส์ %1$s! เราดีใจที่คุณอยู่ที่นี่ + %1$s ส่งสารบนหน้าคุยของคุณ + ขอบคุณที่ร่วมแก้ไข + %1$s ได้กล่าวถึงคุณบน %2$s + สลับมุมมอง + เส้นทาง + วิกิสนเทศ + วิกิพีเดีย + คอมมอนส์ + <u>ให้คะแนนเรา</u> + <u>FAQ</u> + ข้ามบทช่วยสอน + อินเทอร์เน็ตไม่พร้อมใช้งาน + อินเทอร์เน็ตพร้อมใช้งาน + เกิดข้อผิดพลาดในการดึงข้อมูลการแจ้งความ + ไม่พบการแจ้งความ + <u>แปล</u> + ภาษา + เลือกภาษาที่คุณต้องการส่งการแปล + ดำเนินการต่อ + ยกเลิก + ลองใหม่ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index d373c859e..96e7cd96d 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -6,6 +6,7 @@ * Incelemeelemani * McAang * Neslihan Turan +* Rapsar * Sayginer * Trockya * VikipediBilgini @@ -16,6 +17,7 @@ Geri bildirim Konum Commons + Ayarlar Kullanıcı adı Parola @@ -28,7 +30,7 @@ Oturum açma başarılı! Oturum açma başarısız oldu! Dosya bulunamadı. Lütfen başka bir dosya deneyin. - Kimlik doğrulama başarısız oldu! + Kimlik doğrulama başarısız oldu! Yükleme başladı! %1$s yüklendi! Yüklemelerinizi görüntülemek için dokunun @@ -56,8 +58,7 @@ Lütfen bu dosya için bir başlık ekleyin Açıklama Oturum açılamıyor - ağ hatası - Oturum açılamıyor - lütfen kullanıcı adınızı kontrol edin - Oturum açılamıyor - lütfen parolanızı kontrol edin + Giriş yapılamıyor - lütfen kullanıcı adınızı ve şifrenizi kontrol edin Çok sayıda başarısız girişimde bulundunuz. Birkaç dakika sonra tekrar deneyin. Üzgünüz, bu kullanıcı Commons\'ta engellendi İki faktörlü kimlik doğrulama kodunu sağlamalısınız. @@ -91,6 +92,7 @@ Kategoriler Ayarlar Kaydol + Seçkin Resimler Hakkında Wikimedia Commons uygulaması, Wikimedia topluluğunun imtiyaz sahibi ve gönüllüleri tarafından oluşturulmuş ve sürdürülmüş açık kaynak kodlu bir uygulamadır. Vikipedi Vakfı, uygulamanın oluşturulması, geliştirilmesi veya bakımına dahil değildir. GitHub üzerinde <a href=\"https://github.com/commons-app/apps-android-commons\">Kaynak</a> ve <a href=\"https://commons-app.github.io/\">website</a>. Hata raporları ve önerileri için yeni bir <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub sorunu</a> oluştur. @@ -110,7 +112,7 @@ Varsayılan lisans Önceki başlığı/açıklamayı kullan Otomatik olarak mevcut konumu al - Resim koordinat olarak etiketlendirilmemişse kategori önerileri için mevcut konum bulun + Resim koordinat olarak etiketlendirilmemişse kategori önerileri için mevcut konum bulun Gece modu Koyu temayı kullanın Attribution-ShareAlike 4.0 @@ -149,20 +151,22 @@ - Başlık: Sydney Opera Binası\n- Tanım: Körfezin genelinden bakıldığında Sydney Opera Binası\n- Kategoriler: Sydney Opera Binası, batıdan Sydney Opera Binası Başlık: Sidney Opera Binası Açıklama: Sidney Opera Binası\'nın körfezin karşısından görünümü + Kategoriler: Batıdan Sidney Opera Binası, Sidney Opera Binası uzaktan manzaraları Resimleriniz ile Vikipedi maddelerinin canlandırılmasına katkıda bulunabilirsiniz! Vikipedi\'ye Wikimedia Commons\'tan gelen görüntüler. Görüntüler dünya insanlarının eğitiminde yardımcı olur. Telif hakkı olan ve internette bulunan film afişi, kitap kapağı gibi malzemelerin kullanımından kaçının. Bunu anladınız mı? Evet! + * Kategoriler Yükleniyor... Hiçbir şey seçilmedi Açıklama yok Bilinmeyen lisans Yenile - Gerekli izinler: Harici depolama biriminin okunması. Uygulama buna izin verilmeden çalışmaz. - Gerekli izin: Harici depolama birimi üzerine yazma. Uygulama buna izin verilmeden çalışmaz. + Gerekli izin: Harici belleği oku. Uygulama, galerinize bu olmadan erişemez. + Gerekli izin: Harici depolama yazın. Uygulama kameranıza bu olmadan erişemez. İsteğe bağlı izin: Kategori önerileri için geçerli konum alma Tamam Yakındaki yerler @@ -175,6 +179,8 @@ Medyanın başlığı Açıklama Medya için yapılan tanımı/açıklamayı buraya yazınız. Açıklamanız uzun olabilir ve birden fazla satıra sığabilir. Umuyoruz ki güzel ve bilgilendirici olacaktır. + Yazar + Seçkin resim yazarının kullanıcı adı buraya eklenir. Yükleme tarihi Lisans Koordinatlar @@ -217,6 +223,7 @@ Çıkış Eğitim Bildirimler + Seçkin Yakındaki yerler, konum izinleri olmadan görüntülenemez hiçbir açıklama bulunamadı Commons dosya sayfası @@ -235,7 +242,7 @@ Kayıt dosyasını, e-posta aracılığıyla geliştiricilere gönderin URL\'yi açabilecek bir tarayıcı bulunamadı Hata! URL bulunamadı - Silinmesi için aday göster + Silinmeye aday göster Bu görsel silinmesi için aday gösterildi. Tarayıcıda görüntüle Konum değiştirilmedi @@ -244,7 +251,39 @@ TALİMATLAR MADDE OKU Wikimedia Commons\'a hoşgedin %1$s! Burada olduğun için mutluyuz. + %1$s mesaj sayfasınıza bir mesaj bıraktı. Düzenlemeniz için teşekkürler %1$s , %2$s \'de senden bahsetti + Görünümü değiştir YOL TARİFİ + VİKİVERİ + VİKİPEDİ + COMMONS + <u>Bizi oyla</u> + <u>SSS</u> + Eğiticiyi Atla + İnternet kullanılamıyor + İnternet kullanılabilir + Bildirim alınırken hata oluştu + Bildirim bulunamadı + <u>Çevir</u> + Diller + Çevirileri göndermek istediğiniz dili seçin + İlerle + Vazgeç + Tekrar Deneyin + Anladım! + Vikipedi maddelerine eklemek için fotoğrafa ihtiyaç duyan size yakın yerler + Bu tuşa dokunmak bu yerlerin bir listesini getirir + Galerinizden veya kameranızla herhangi bir yer için resim yükleyebilirsiniz. + Resim bulunamadı! + Resimler yüklenirken hata oluştu. + Yükleyen: %1$s + Uygulamayı Paylaş + Koordinatlar görüntü seçimi sırasında belirlenmedi + Yakındaki yerler alınırken hata oluştu. + Resim, Vikiveri\'de %1$s içine başarıyla eklendi. + Karşılık gelen viki veri varlığı güncellenemedi! + Duvar kağıdı ayarla + Duvar kağıdı başarıyla ayarlandı! diff --git a/app/src/main/res/values-ug/strings.xml b/app/src/main/res/values-ug/strings.xml index 3c180fbf2..40342d1d1 100644 --- a/app/src/main/res/values-ug/strings.xml +++ b/app/src/main/res/values-ug/strings.xml @@ -16,7 +16,7 @@ تىزىمغا كىرىش مۇۋەپپەقىيەتلىك! تىزىمغا كىرەلمىدى! ھۆججەت تېپىلمىدى . سىناپ بېقىڭ ، باشقا ھۆججەتلەر . - سالاھىيەتنى ئىسپاتىنى تەكشۈرۈش مەغلۇپ بولدى ! + سالاھىيەتنى ئىسپاتىنى تەكشۈرۈش مەغلۇپ بولدى ! يۈكلەش باشلاندى! %1$s يۈكلەندى! چېكىپ كۆرۈپ بېقىڭ ، سىزنىڭ يۇقىرىغا يوللاش @@ -39,8 +39,6 @@ ماۋزۇ چۈشەندۈرۈش تىزىملىتىش - تور كاشىلىسى - تىزىملاشقا ئامالسىز - سىزنىڭ ئابونت نامىڭىزنى تەكشۈرۈپ بېقىڭ - تىزىملاشقا ئامالسىز مەخپىي نومۇرىڭىزنى تەكشۈرۈپ بېقىڭ مەغلۇپ بولغان قېتىم سانى بەك كۆپ . نەچچە مىنۇتتىن كېيىن قايتا سىناڭ . كەچۈرۈڭ، بۇ ئابونت ئاللىقاچان ئورتاق بەھرىمەن بولىدىغان بايلىق مەنبەسى دائىرىلىك سىز چوقۇم سىزنىڭ قوش ئامىل تەكشۈرۈش كودىنى تاپشۇرىسىز . diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8f39e04c5..8bace5fb9 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -28,7 +28,7 @@ Ви успішно увійшли! Не вдалося увійти Файл не знайдено. Будь ласка, спробуйте інший файл. - Помилка автентифікації + Помилка автентифікації. Будь ласка, увійдіть у свій обліковий запис знову Завантаження розпочато! Завантажено %1$s! Торкніться, щоб переглянути Ваше завантаження @@ -58,8 +58,7 @@ Будь ласка, вкажіть назву цього файлу Опис Неможливо увійти — збій у мережі - Неможливо увійти — будь ласка, перевірте своє ім\'я користувача - Неможливо увійти — будь ласка, перевірте свій пароль + Неможливо увійти — будь ласка перевірте ім\'я користувача та пароль Надто багато невдалих спроб. Будь ласка, спробуйте знову через кілька хвилин. Вибачте, цього користувача було заблоковано на Вікісховищі Ви повинні надати код двофакторної автентифікації. @@ -99,6 +98,7 @@ Категорії Налаштування Зареєструватися + Вибрані зображення Про програму Додаток «Вікісховище» — це програма з відкритим кодом, яку створили отримувачі грантів та волонтери спільноти Вікімедіа. Фонд Вікімедіа не брав участі у створенні, розробці чи обслуговуванні цього додатка. Ви можете створити новий <a href=\"https://github.com/commons-app/apps-android-commons/issues\">запит на GitHub</a>, щоб повідомити про помилки, або висловити пропозиції. @@ -118,7 +118,7 @@ Стандартна ліцензія Використати попередню назву/опис Автоматично отримати поточне розташування - Отримати поточне розташування, щоб з\'явилися підказки категорій, якщо зображення не має геотегів + Якщо зображення не містить координат, то буде отримано і поставлено ваше поточне розташування. Будьте уважні, якщо ви не хочете розкривати ваше розташування. Нічний режим Використати темну тему Attribution-ShareAlike 4.0 @@ -149,7 +149,7 @@ Корисні об\'єкти (велосипеди, залізничні станції) Відомі люди (ваш мер, спортсмен-олімпієць, якого ви зустріли) Будь ласка, НЕ завантажуйте: - u2022 Селфі або фото своїх друзів \nu2022 Зображення, які Ви завантажили з інтернету \nu2022 Скріншоти патентованих програм + - Селфі або фото своїх друзів \n- Зображення, які Ви завантажили з інтернету \n- Знімки екрану пропрієтарних програм Селфі чи фото ваших друзів Зображення, які ви завантажили з інтернету Знімки екрану пропрієтарних програм @@ -161,17 +161,18 @@ Надсилайте Ваші зображення. Допоможіть оживити статті Вікіпедії! Зображення у Вікіпедії надходять з Вікісховища. Ваші зображення допомагають освіті людей у всьому світі. - Уникайте захищених авторським правом матеріалів, знайдених в Інтернеті, а також зображень плакатів, обкладинок книг і т. п. + Уникайте захищених авторським правом матеріалів, знайдених в Інтернеті, а також зображень плакатів, обкладинок книг, тощо. Ви це зрозуміли? Так! + Категорії Завантаження… Нічого не обрано Немає опису Невідома ліцензія Оновити - Обов\'язковий дозвіл: читання зовнішньої пам\'яті. Програма не може працювати без цього. - Обов\'язковий дозвіл: записування на зовнішнє сховище. Програма не може працювати без цього. + Обов\'язковий дозвіл: читання зовнішньої пам\'яті. Без цього дозволу програма не зможе отримати доступ до вашої галереї. + Обов\'язковий дозвіл: записування на зовнішнє сховище. Програма не зможе отримати доступ до камери без цього дозволу. Додатковий дозвіл: отримувати поточне розташування для підказок категорій Гаразд Місця поблизу @@ -184,6 +185,8 @@ Назва медіафайлу Опис Сюди потрапляє опис медіафайлу. Він потенційно може бути досить довгим і розтягнутися на декілька рядків. Однак ми сподіваємось, що він виглядатиме гарно. + Автор + Тут вказується ім\'я автора вибраного зображення Дата завантаження Ліцензія Координати @@ -199,7 +202,7 @@ Ви справді хочете вийти із системи? Логотип Вікісховища Веб-сайт Commons - Фейсбук сторінка Commons + Facebook-сторінка Commons Програмний код Commons на GitHub Фонове зображення Помилка медіазображення @@ -226,6 +229,7 @@ Вийти Посібник Сповіщення + Вибране Місця поблизу неможливо показати без дозволу на визначення місця розташування. опис не знайдено Сторінка файлу у Вікісховищі @@ -233,33 +237,62 @@ Стаття Вікіпедії Помилка кешування зображень Унікальна описова назва файлу. Ви можете використовувати простий текст з пробілами. Не вказуйте розширення файлу - Будь ласка, докладно опишіть файл: де його було зроблено? що на ньому зображено? який контекст? Будь ласка, опишіть об\'єкти чи осіб. Додайте інформацію, яку не можна легко здогадатися, наприклад, пору доби для фотографії пейзажу. Якщо зображено щось незвичайне, постарайтеся пояснити, що робить його незвичайним. + Будь ласка, докладно опишіть файл: де його було зроблено? що на ньому зображено? який контекст? Будь ласка, опишіть об\'єкти чи осіб. Додайте інформацію, яку не можна легко здогадатися, наприклад, пору доби для фотографії пейзажу. Якщо зображено щось незвичайне, спробуйте пояснити, що робить його незвичайним. Це зображення надто темне. Ви упевнені, що хочете його завантажити? Вікісховище призначене лише для зображень, що мають енциклопедичну цінність. Це зображення розмите. Ви упевнені, що хочете його завантажити? Вікісховище призначене лише для зображень, що мають енциклопедичну цінність. Надати дозвіл Використовувати зовнішнє сховище Зберігати зображення, виконані вбудованою камерою Вашого пристрою Увійдіть у свій обліковий запис - Надіслати лог-файл - Надіслати лог-файл розробникам електронною поштою + Надіслати файл журналу + Надіслати файл журналу розробникам електронною поштою Не знайдено браузера, щоб відкрити посилання Помилка! Посилання не знайдено Номінувати на вилучення + Цей файл номіновано на вилучення. + Переглянути в браузері Розташування не змінено Місцезнаходження недоступне Потрібний дозвіл для показу списку місць поблизу Показати на мапі у зовнішній програмі ЧИТАТИ СТАТТЮ - Вітаємо у Wikimedia Commons, %1$s! Раді вас бачити. + Вітаємо у Вікісховищі, %1$s! Раді вас бачити. %1$s залишив повідомлення на вашій сторінці обговорення - Дякуємо за правку + Дякуємо за редагування %1$s згадав вас на %2$s. Перемкнути режим перегляду НАПРЯМКИ ВІКІДАНІ ВІКІПЕДІЯ ВІКІСХОВИЩЕ - Часті запитання + <u>Оцініть нас</u> + Часті запитання Пропустити інструкцію + Інтернет недоступний + Інтернет доступний + Помилка при отриманні сповіщення + Сповіщень немає + <u>Перекласти</u> + Мови + Виберіть мову, переклади якою ви хочете відправити + Виконується + Скасувати + Повторити + Зрозуміло + Це місця поблизу, про які є статті Вікіпедії, але які потребують ілюстрацій + Натискання цієї кнопки згенерує список таких місць + Ви можете завантажити зображення для любого з цих місць, зробивши знімок камерою або вибравши зображення з галереї + Зображень не знайдено! + Сталася помилка при завантаженні зображень. + Завантажено: %1$s + Поділитися програмою + Під час вибору зображення не були вказані координати + Помилка отримання місць поблизу. + Зображення дня + Зображення дня + Зображення успішно додано до сторінки %1$s у Вікіданих! + Не вдалось оновити відповідну сторінку Вікіданих! + Поставити шпалерами екрану + Шпалери екрану виставлено успішно! diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 4fdd67514..cc4246e94 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -18,7 +18,7 @@ لاگ ان کامیاب۔ داخل ہونے میں ناکامی ہوئی! فائل نہیں ملی، براہ کرم دوسری فائل آزمائیں۔ - تصدیق ناکام! + تصدیق ناکام! اپلوڈ شروع! %1$s اپلوڈ شد! اپنی اپلوڈ دیکھنے کے لیے ٹیپ کریں۔ @@ -45,8 +45,6 @@ عنوان وضاحت لاگ ان ہونے میں ناکام - نیٹ ورک ناکامی - لاگ ان ہونے میں ناکام - براہ مہربانی اپنا صارف نام کی جانچ کریں - لاگ ان ہونے میں ناکام - براہ مہربانی - اپنے پاس ورڈ کی جانچ کریں بے شمار ناکام کوششیں کچھ منٹوں میں دوبارہ کوشش کریں۔ معذرت، یہ صارف کومنز پر بلاک کردیا گیا ہے آپ کو اپنے دو عامل کے تصدیق کوڈ فراہم کرنا چاہیے۔ @@ -99,7 +97,7 @@ اجازت نامہ گزشتہ عنوان/وضاحت استعمال کریں خودکارانہ طریقے سے حالیہ جگہ حاصل کریں - اگر تصویر جغرافیائی نہیں ہے تو قسم کے تجاویز پیش کرنے کیلئے موجودہ مقام کو دوبارہ حاصل کریں + اگر تصویر جغرافیائی نہیں ہے تو قسم کے تجاویز پیش کرنے کیلئے موجودہ مقام کو دوبارہ حاصل کریں نائٹ موڈ کالا تھیم استعمال کریں انتباہ-شراکت 4.0 diff --git a/app/src/main/res/values-v14/dimens.xml b/app/src/main/res/values-v14/dimens.xml new file mode 100644 index 000000000..4db8c5906 --- /dev/null +++ b/app/src/main/res/values-v14/dimens.xml @@ -0,0 +1,10 @@ + + + + + 0dp + + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 915376b3f..a45a6d7c1 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,23 +1,31 @@ + Giao diện + Tổng quát + Phản hồi + Vị trí Commons + Thiết lập Tên người dùng Mật khẩu + Đăng nhập vào tài khoản Commons Beta của bạn Đăng nhập + Quên mật khẩu? Mở tài khoản Đang đăng nhập Vui lòng chờ… Đã đăng nhập thành công! Đăng nhập thất bại! Không tìm thấy tập tin. Xin vui lòng thử tập tin khác. - Xác thực thất bại! + Xác thực thất bại! Đã bắt đầu tải lên! Đã tải lên %1$s! Chạm để xem những tập tin tải lên của bạn @@ -26,7 +34,10 @@ Đang hoàn thành việc tải lên tập tin %1$s Tải lên tập tin %1$s thất bại Chạm để xem - %d tập tin đang được tải lên + + %1$d tập tin đang được tải lên + %1$d tập tin đang được tải lên + Tập tin Tôi đã Tải lên Gần đây Đang chờ Thất bại @@ -39,10 +50,10 @@ Chia sẻ Mở trong Trình duyệt Tên + Xin hãy đặt tiêu đề cho tập tin này Miêu tả Không thể đăng nhập – có lỗi mạng - Không thể đăng nhập – xin vui lòng kiểm tra tên người dùng - Không thể đăng nhập – xin vui lòng kiểm tra mật khẩu + Không thể đăng nhập – vui lòng kiểm tra tên người dùng và mật khẩu của bạn Đã đăng nhập thất bại quá nhiều lần. Xin vui lòng thử lại trong vòng vài phút. Rất tiếc, người dùng này đã bị cấm tại Commons Bạn phải cung cấp mã xác thực dùng hai nhân tố. @@ -54,25 +65,31 @@ Tìm thể loại Lưu Làm mới + Danh sách Chức năng GPS đang tắt trên thiết bị của bạn. Bạn có muốn bật nó lên? Bật GPS Chưa có tập tin tải lên - + \@string/contributions_subtitle_zero - %1$d tập tin tải lên + %1$d tập tin đã tải lên + %1$d tập tin đã tải lên Đang bắt đầu tải lên %1$d tập tin - %1$d tập tin tải lên + + %1$d tập tin đã tải lên + %1$d tập tin đã tải lên + Không tìm thấy thể loại khớp với %1$s - Xếp các hình ảnh vào thể loại để cho chúng dễ tìm kiếm hơn trên Wikimedia Commons.\n\nHãy bắt đầu nhập tên thể loại để tìm kiếm.\nChạm vào thông điệp này (hoặc bấm Quay lại) để bỏ qua bước này. + Xếp các hình ảnh vào thể loại để giúp chúng dễ tìm kiếm hơn trên Wikimedia Commons.\nHãy bắt đầu nhập để thêm thể loại. Thể loại Cài đặt Mở tài khoản + Hình ảnh chọn lọc Giới thiệu - Phần mềm mã nguồn mở được phát hành theo <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Giấy phép Apache v2</a>. %1$s và biểu trưng của nó là nhãn hiệu của Quỹ Wikimedia và được sử dụng do Quỹ Wikimedia cho phép. Chúng tôi không được Quỹ Wikimedia ủng hộ hoặc nhận làm chi nhánh. - <a href=\"https://github.com/commons-app/apps-android-commons\">Mã nguồn</a> và <a href=\"https://commons-app.github.io/\">trang chủ</a> tại GitHub. <a href=\"https://github.com/commons-app/apps-android-commons/issues\">Tạo vấn đề GitHub mới</a> để báo cáo lỗi hoặc gợi ý thay đổi. - - <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\">Công trạng</a> + Ứng dụng Wikimedia Commons là ứng dụng mã nguồn mở được sáng tạo và quản lý bởi các tình nguyện viên và những người được tin tưởng của cộng đồng Wikipedia. Wikimedia Foundation không tham gia vào sự tạo lập, phát triển cũng như quản lý của ứng dụng. + Tạo một <a href=\"https://github.com/commons-app/apps-android-commons/issues\">thảo luận (issue) mới trên GitHub</a> để báo cáo lỗi cũng như đưa ra các ý tưởng. + <u>Chính sách riêng tư</u> + <u>Công trạng</u> Giới thiệu Gửi Phản hồi (qua Thư điện tử) Không có chương trình thư điện tử nào được cài đặt @@ -84,10 +101,10 @@ Hình này sẽ được phát hành theo giấy phép %1$s Với việc đăng hình này, tôi tuyên bố rằng nó là tác phẩm của chính mình, rằng nó không chứa nội dung có bản quyền hoặc ảnh tự chụp, và về mặt khác thì hoàn toàn tuân theo <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines?uselang=vi\">các quy định Wikimedia Commons</a>. Tải về - Giấy phép + Giấy phép mặc định Sử dụng tiêu đề/miêu tả trước Lấy vị trí hiện tại tự động - Định vị hiện tại để nhận gợi ý thể loại trong trường hợp hình ảnh chưa được gắn thẻ địa lý + Định vị hiện tại để nhận gợi ý thể loại trong trường hợp hình ảnh chưa được gắn thẻ địa lý Chế độ ban đêm Dùng chủ đề tối Ghi công–Chia sẻ tương tự 4.0 @@ -113,11 +130,20 @@ Wikimedia Commons là nơi lưu giữ phần nhiều hình ảnh xuất hiện trong Wikipedia. Các hình ảnh của bạn giúp giáo dục người dân trên khắp thế giới! Xin hãy tải lên các hình ảnh do chính bạn chụp hoặc vẽ: - - Thiên nhiên (bông hoa, thú vật, cảnh núi)\n- Vật nhân tạo (xe đạp, nhà ga, món ăn)\n- Nhân vật nổi tiếng (thị trưởng của bạn, cầu thủ đội tuyển mà bạn gặp) + Thiên nhiên (bông hoa, thú vật, cảnh núi)\n* Vật nhân tạo (xe đạp, nhà ga, món ăn)\n* Nhân vật nổi tiếng (thị trưởng của bạn, cầu thủ đội tuyển mà bạn gặp) + Thiên nhiên (bông hoa, thú vật, cảnh núi) + Vật nhân tạo (xe đạp, nhà ga, món ăn) + Nhân vật nổi tiếng (thị trưởng của bạn, cầu thủ đội tuyển mà bạn gặp) Xin DỪNG tải lên: - Hình tự sướng hoặc hình bạn bè\n- Hình ảnh tải về từ Internet\n- Ảnh chụp màn hình của ứng dụng thương mại + Ảnh tự chụp hoặc hình bạn bè + Hình ảnh trên Internet tải về + Ảnh chụp màn hình của ứng dụng có bản quyền Tập tin tải lên ví dụ: - - Tiêu đề: Nhà hát Opera Sydney\n- Miêu tả: Nhà hát Opera Sydney nhìn qua cảng\n- Thể loại: Sydney Opera House, Sydney Opera House from the west, Sydney Opera House remote views + - Tiêu đề: Nhà hát Opera Sydney\n- Miêu tả: Nhà hát Opera Sydney nhìn qua cảng\n- Thể loại: Nhà hát Opera Sydney nhìn từ phía tây, Nhà hát Opera Sydney nhìn từ xa + Tiêu đề: Nhà hát Opera Sydney + Miêu tả: Nhà hát Opera Sydney nhìn qua cảng + Thể loại: Nhà hát Opera Sydney nhìn từ phía tây, Nhà hát Opera Sydney nhìn từ xa Đóng góp hình ảnh của bạn. Làm sinh động các bài viết Wikipedia! Các hình ảnh trên Wikipedia được cung cấp bởi Wikimedia Commons. Các hình ảnh của bạn giúp giáo dục người dân trên khắp thế giới. @@ -130,8 +156,8 @@ Không miêu tả Giấy phép không rõ Làm tươi - Yêu cầu cấp phép: Đọc thiết bị lưu trữ bên ngoài. Ứng dụng cần được phép đọc thiết bị lưu trữ bên ngoài để hoạt động. - Yêu cầu cấp phép: Ghi vào thiết bị lưu trữ bên ngoài. Ứng dụng cần được phép ghi vào thiết bị lưu trữ bên ngoài để hoạt động. + Yêu cầu cấp phép: Đọc thiết bị lưu trữ bên ngoài. Ứng dụng cần được phép đọc thiết bị lưu trữ bên ngoài để truy cập kho ảnh của bạn. + Yêu cầu cấp phép: Ghi vào thiết bị lưu trữ bên ngoài. Ứng dụng cần được phép ghi vào thiết bị lưu trữ bên ngoài để truy cập máy chụp hình của bạn. Tùy chọn cấp phép: Định vị hiện tại để nhận gợi ý thể loại OK Nơi Lân cận @@ -144,6 +170,7 @@ Tiêu đề phương tiện Mô tả Mô tả phương tiện xuất hiện tại đây. Nó có thể khá dài và sẽ cần phải ngắt thành nhiều dòng. Nhưng chúng tôi hy vọng sẽ trông ưa nhìn. + Tác giả Ngày tải lên Giấy phép Tọa độ @@ -158,6 +185,9 @@ Hiện chưa hỗ trợ xác thực dùng hai nhân tố. Bạn có chắc chắn muốn đăng xuất? Biểu trưng Commons + Trang Web Commons + Trang Commons tại Facebook + Mã nguồn Commons tại GitHub Hình nền Hình ảnh bị Thất bại Không tìm thấy Hình ảnh @@ -182,15 +212,58 @@ Phản hồi Đăng xuất Hướng dẫn + Thông báo + Chọn lọc Cần có quyền truy cập vị trí của bạn để hiển thị các địa điểm lân cận không tìm thấy miêu tả Trang tập tin Commons Khoản mục Wikidata + Bài Wikipedia Xuất hiện lỗi khi đưa hình ảnh vào vùng nhớ đệm Tên ngắn và duy nhất cho tập tin sẽ được dùng làm tên tập tin. Có thể dùng thuật ngữ bình thường với khoảng cách. Đừng bao gồm phần mở rộng tập tin. Xin vui lòng miêu tả phương tiện càng đầy đủ càng tốt: Chụp ở đâu? Trong hình có gì? Bối cảnh làm sao? Xin vui lòng miêu tả các đối tượng và người trong hình. Cho biết những thông tin khó đoán ra, chẳng hạn giờ trong ngày nếu là phong cảnh. Nếu phương tiện có gì kỳ lạ, xin vui lòng giải thích tại sao nó kỳ lạ. + Cho phép Sử dụng thiết bị lưu trữ bên ngoài Lưu các hình ảnh được chụp bằng máy chụp hình trong ứng dụng vào thiết bị của bạn + Đăng nhập vào tài khoản của bạn + Gửi tập tin nhật trình + Gửi tập tin nhật trình cho nhà phát triển qua thư điện tử + Không tìm thấy trình duyệt để mở URL + Lỗi! Không tìm thấy URL Bầu chọn để xóa + Có đề nghị xóa hình này. Hiện lên ở trang xem browse + Vị trí không thay đổi. + Vị trí không có sẵn. + Bạn cần cho phép để hiển thị danh sách nơi lân cận + HƯỚNG DẪN + ĐỌC BÀI + Hoan nghênh %1$s đã đến Wikimedia Commons! Chúng tôi rất mừng bạn đến đây. + %1$s đã nhắn tin tại trang thảo luận của bạn + Cảm ơn vì bạn đã thực hiện sửa đổi + %1$s đã nhắc đến bạn tại %2$s. + HƯỚNG DẪN + WIKIDATA + WIKIPEDIA + COMMONS + <u>Đánh giá chúng tôi</u> + <u>Câu thường hỏi</u> + Bỏ qua Hướng dẫn + Internet không có sẵn + Internet có sẵn + Lỗi khi lấy thông báo + Không tìm thấy thông báo + <u>Biên dịch</u> + Ngôn ngữ + Chọn ngôn ngữ để gửi bản dịch + Tiến hành + Hủy bỏ + Thử lại + Được rồi! + Không tìm thấy hình ảnh! + Đã xuất hiện lỗi khi tải hình ảnh. + Tải lên bởi: %1$s + Chia sẻ Ứng dụng + Tọa độ không được chỉ định khi chọn hình ảnh + Lỗi khi lấy các nơi lân cận. diff --git a/app/src/main/res/values-xmf/strings.xml b/app/src/main/res/values-xmf/strings.xml index b9689f9e2..3ab830fb9 100644 --- a/app/src/main/res/values-xmf/strings.xml +++ b/app/src/main/res/values-xmf/strings.xml @@ -14,7 +14,7 @@ სისტემაშა მიშულაქ წჷმოძინელო გეთუ! სისტემაშა მიშულაქ ვემიხუჯინუ! ფაილქ ვეგორუ. ქორთხინთ, ქოცადით შხვა ფაილი. - აუთენტიფიკაციაქ ვემიხუჯინუ! + აუთენტიფიკაციაქ ვემიხუჯინუ! ეხარგუაქ ქჷდიჭყუ! %1$ ეხარგილი რე! ქეგუწკანტეთ თქვანი ეხარგუაშ ოძირაფალო @@ -41,8 +41,6 @@ დუდჯოხო ეჭარუა მიშულაქ ვემიხუჯინუ - რშვილიშ ჩილათა - მიშულაქ ვემიხუჯინუ - ქორთხინთ გეგნაჯინით ჯოხოს - მიშულაქ ვემიხუჯინუ - ქორთხინთ გეგნაჯინით პაროლს ძალამ მიარე უმწუძინუ ცადება. ქორთხინ, მუხირენ წუთშა ხოლო ქოცადით. მორდება, თე მახვარებუ ბლოკირი რე ვიკიოწკარუეს თქვა გემშიონათ ოკო ჟირფაქტორიანი ავტორიზაციაშ კოდი. diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index c715f9375..d2871c3b7 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -4,6 +4,7 @@ * Kly * LNDDYL * Liuxinyu970226 +* S099001 * Simon Shek * StephDC * Wwycheuk @@ -28,11 +29,11 @@ 請稍候… 登入成功! 登入失敗! - 找不到檔案。請嘗試其它檔案看看。 - 未能核對身分! + 找不到檔案。請試試看其它檔案。 + 身份驗證失敗,請重新登入 開始上傳! 已上傳%1$s! - 點選檢視您上傳的項目 + 輕觸來檢視您上傳的項目 開始上傳%1$s 正在上傳%1$s 即將完成上傳 %1$s @@ -42,7 +43,7 @@ 正在上載 %1$d 個檔案 正在上載 %1$d 個檔案 - 我的最近上傳 + 我最近的上傳 已佇列 失敗 %1$d%%完成 @@ -57,8 +58,7 @@ 請提供此檔案的標題 說明 無法登入-網路故障 - 無法登入-請檢查您的使用者名稱 - 無法登入-請檢查您的密碼 + 無法登入 - 請檢查您的使用者名稱與密碼 失敗次數過多。請於幾分鐘後重試。 很抱歉,該使用者已被維基共享資源封禁 必須提供您的雙重因素身分核對代碼。 @@ -92,6 +92,7 @@ 分類 設定 註冊 + 特色圖片 關於 維基共享資源應用程式是透過維基媒體社群上的受讓人,與志願者們所建立及維護的開放原始碼應用程式。維基媒體基金會並不涉及此應用程式的建立、開發,與維護方面。 建立新的<a href=\"https://github.com/commons-app/apps-android-commons/issues\"> GitHub 問題</a>來回報程式錯誤和提出建議。 @@ -109,9 +110,9 @@ 透過提交此圖片,我宣佈這是我個人創作的成品,且不包含受版權保護或自拍內容,並除此之外遵守<a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">維基媒體共享資源方針</a>。 下載 預設授權條款 - 使用先前標題/說明 + 使用先前標題、說明 自動獲取目前位置 - 若圖片未有地理標記,就以目前位置來作為分類建議。 + 如果圖片沒有地理標記,就索取目前位置的地理資訊來標記在圖片上。注意:這會透露出您目前所在的位置。 夜間模式 使用暗黑佈景主題 姓名標示-相同方式分享4.0 @@ -154,17 +155,18 @@ 貢獻您的圖片,使維基百科的文章更加生動! 維基百科的圖片,來自維基共享資源。 您的圖片可以幫助教育世界各地的人。 - 避免使用受版權保護的材料,例如從互聯網找來的圖片、海報、書籍封面等 - 明白了嗎? - 是! + 避免使用受版權保護的材料,例如從網際網路找來的圖片、海報、書籍封面等 + 以上您明白了嗎? + 是的! + 此提示為空,可能無效。請見錯誤報告: https://github.com/commons-app/apps-android-commons/issues/1333 。 分類 載入中… 未選擇 無說明 不明授權 重新整理 - 必要權限:讀取外部存儲裝置。應用程式必須此功能,才能進行正確運作。 - 必要權限:寫入外部存儲裝置。應用程式必須此功能,才能進行正確運作。 + 必要權限:讀取外部存儲裝置。否則應用程式無法存取您的圖庫。 + 必要權限:寫入外部存儲裝置。否則應用程式無法取用您的相機。 可有可無的權限:獲取目前的地理位置,以用於分類建議 附近地點 @@ -177,6 +179,8 @@ 媒體標題 說明 本媒體的說明。若內容很長,請換行,會好看一些。 + 作者 + 特色圖片作者使用者名稱於此。 上傳日期 授權協議 座標 @@ -219,6 +223,7 @@ 登出 教程 通知 + 特色 附近地點需要位置權限才可顯示 找不到說明 共享資源檔案頁面 @@ -239,6 +244,7 @@ 錯誤!查無 URL 提名刪除 此圖片已被提名刪除。 + 此提示為空,可能無效。請見錯誤報告: https://github.com/commons-app/apps-android-commons/issues/1333 。 於瀏覽器檢視 位置無法更改。 位置無效。 @@ -267,4 +273,20 @@ 已進行 取消 重試 + 了解! + 這些是在您的附近,並且需要圖片來圖解有關它們的維基百科條目之地點 + 輕觸此按鈕來帶出這些地點的清單 + 您可從您的圖庫或相機,來上傳任何地點的圖片 + 找不到圖片! + 當載入圖片時發生錯誤。 + 由:%1$s 上傳 + 分享應用程式 + 當選擇圖片時未指定座標 + 索取附近地點時出錯。 + 每日圖片 + 每日圖片 + 圖片已成功添加到維基數據上的 %1$s! + 更新所對應的維基數據項目失敗! + 設定桌布 + 桌布設定成功! diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 274ec92c8..5bcf095db 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -27,7 +27,7 @@ 登录成功! 登录失败! 找不到文件。请尝试其他文件。 - 身份验证失败! + 身份验证失败,请重新登录 上传开始! %1$s已上传! 点击查看您的上传 @@ -55,8 +55,7 @@ 请提供此文件的标题 说明 无法登录 - 网络故障 - 无法登录 - 请检查您的用户名 - 无法登录 - 请检查您的密码 + 无法登录——请检查您的用户名和密码 失败次数过多。请在几分钟后重试。 对不起,该用户已经被共享资源封禁 您必须提供您的双因素验证代码。 @@ -90,6 +89,7 @@ 分类 设置 注册 + 特色图片 关于 维基共享资源应用程序是由维基媒体社区的受助者和志愿者创建和维护的开源应用程序。维基媒体基金会不参与该应用程序的创立,开发或维护。 创建新的<a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub问题</a>以发送错误报告和建议。 @@ -109,7 +109,7 @@ 默认许可协议 使用之前的标题/描述 自动获取当前位置 - 如果图片没有地理标记的话,就取得当前位置以提供分类建议 + 如果图片没有地理标记,以及地理标签图片的话,就取得当前位置。警告:这将暴露您的当前位置。 夜间模式 使用黑暗主题 署名-相同方式共享4.0 @@ -161,8 +161,8 @@ 没有说明 未知许可协议 刷新 - 需要权限:读取外部存储。应用不能在没有它的情况下运行。 - 需要权限:写入外部存储。应用不能在没有它的情况下运行。 + 需要权限:读取外部存储。应用不能在没有它的情况下访问您的图册。 + 需要权限:写入外部存储。应用不能在没有它的情况下访问您的图册。 可选权限:获取当前位置以提供分类建议 确定 附近地点 @@ -175,6 +175,8 @@ 媒体的标题 说明 在此填写媒体的说明。这可能会相当长,并将需要包裹在多行中。我们希望它看起来很好。 + 作者 + 这里有特色图片作者的用户名。 上传日期 许可协议 坐标 @@ -217,6 +219,7 @@ 退出 教程 通知 + 特色 附近地点不能在没有位置权限的情况下显示 找不到描述 共享资源文件页面 @@ -252,17 +255,33 @@ 维基数据 维基百科 共享资源 - + <u>评价我们</u> <u>常见问题</u> 跳过指导 互联网不可用 互联网可用 检索通知时出错 找不到通知 - + <u>翻译</u> 语言 选择您希望提交翻译的语言 已处理 取消 重试 + 明白了! + 这些是您附近需要图片以阐明维基百科条目的地方 + 点按此按钮会出现这些地点的列表 + 您可以从您的图库或照相机中上传任意地点的图片 + 找不到图片! + 加载图片时出错。 + 由%1$s上传 + 分享应用 + 图片选择时,坐标并未指定 + 检索附近地点时出错。 + 每日图片 + 每日图片 + 图片已成功添加到维基数据上的%1$s! + 更新对应维基数据实体失败! + 设置墙纸 + 墙纸已成功设置! diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ef5000d60..1db51fd8b 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -52,4 +52,6 @@ #424242 #757575 + #FFFFFF + #000000 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 1e98a1e04..1697853e8 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -23,4 +23,12 @@ 20sp 16sp 14sp + 15dp + 25dp + + + 8dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 810ddc65a..1372d5866 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,7 +18,7 @@ Login success! Login failed! File not found. Please try another file. - Authentication failed! + Authentication failed, please login again Upload started! %1$s uploaded! Tap to view your upload @@ -46,8 +46,7 @@ Please provide a title for this file Description Unable to login - network failure - Unable to login - please check your username - Unable to login - please check your password + Unable to login - please check your username and password Too many unsuccessful attempts. Please try again in a few minutes. Sorry, this user has been blocked on Commons You must provide your two factor authentication code. @@ -82,6 +81,7 @@ Categories Settings Sign Up + Featured Images About The Wikimedia Commons app is an open-source app created and maintained by grantees and volunteers of the Wikimedia community. The Wikimedia Foundation is not involved in the creation, development, or maintenance of the app. Wikimedia Commons @@ -102,7 +102,7 @@ Default License Use previous title/description Automatically get current location - Retrieve current location to offer category suggestions if image is not geotagged + Retrieves current location if image is not geotagged, and geotags image with it. Warning: This will reveal your current location. Night mode Use dark theme Attribution-ShareAlike 4.0 @@ -148,6 +148,7 @@ Avoid copyrighted materials you found from the Internet as well as images of posters, book covers, etc. You think you got it? Yes! + More Information Categories Loading… None selected @@ -168,6 +169,8 @@ Title of the media Description Description of the media goes here. This can potentially be fairly long, and will need to wrap across multiple lines. We hope it looks nice though. + Author + Featured image author user name goes here. Uploaded date License Coordinates @@ -214,6 +217,7 @@ Logout Tutorial Notifications + Featured Nearby places cannot be displayed without location permissions no description found Commons file page @@ -237,32 +241,52 @@ See webpage for details View in Browser - Location has not changed. - Location not available. - Permission required to display a list of nearby places - GET DIRECTIONS - READ ARTICLE + Location has not changed. + Location not available. + Permission required to display a list of nearby places + GET DIRECTIONS + READ ARTICLE - Welcome to Wikimedia Commons, %1$s! We\'re glad you\'re here. - %1$s left a message on your talk page - Thank you for making an edit - %1$s mentioned you on %2$s. - Toggle view - DIRECTIONS - WIKIDATA - WIKIPEDIA - COMMONS - Rate us]]> - FAQ]]> - Skip Tutorial - Internet unavailable - Internet available - Error fetching notifications - No notifications found - Translate]]> - Languages - Select the language that you would like to submit translations for - Proceed - Cancel - Retry + Welcome to Wikimedia Commons, %1$s! We\'re glad you\'re here. + %1$s left a message on your talk page + Thank you for making an edit + %1$s mentioned you on %2$s. + Toggle view + DIRECTIONS + WIKIDATA + WIKIPEDIA + COMMONS + Rate us]]> + FAQ]]> + Skip Tutorial + Internet unavailable + Internet available + Error fetching notifications + No notifications found + Translate]]> + Languages + Select the language that you would like to submit translations for + Proceed + Cancel + Retry + + Got it! + These are the places near you that need pictures to illustrate their Wikipedia articles + Tapping this button brings up a list of these places + You can upload a picture for any place from your gallery or camera + + No images found! + Error occurred while loading images. + Uploaded by: %1$s + + Share App + Coordinates were not specified during image selection + Error fetching nearby places. + Pic of the Day + Pic of the Day + + Image successfully added to %1$s on Wikidata! + Failed to update corresponding Wikidata entity! + Set wallpaper + Wallpaper set successfully! diff --git a/app/src/main/res/xml/pic_of_day_app_widget_info.xml b/app/src/main/res/xml/pic_of_day_app_widget_info.xml new file mode 100644 index 000000000..5b370e9a1 --- /dev/null +++ b/app/src/main/res/xml/pic_of_day_app_widget_info.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index baf1a760e..49720b247 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -52,10 +52,10 @@ - - - + \ No newline at end of file diff --git a/app/src/test/java/fr/free/nrw/commons/FileUtilsTest.java b/app/src/test/java/fr/free/nrw/commons/FileUtilsTest.java deleted file mode 100644 index c6febc1e4..000000000 --- a/app/src/test/java/fr/free/nrw/commons/FileUtilsTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package fr.free.nrw.commons; - -import org.junit.Assert; -import org.junit.Test; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; - -import fr.free.nrw.commons.upload.FileUtils; - -import static org.hamcrest.CoreMatchers.is; - -public class FileUtilsTest { - @Test public void copiedFileIsIdenticalToSource() throws IOException { - File source = File.createTempFile("temp", ""); - File dest = File.createTempFile("temp", ""); - writeToFile(source, "Hello, World"); - FileUtils.copy(new FileInputStream(source), new FileOutputStream(dest)); - Assert.assertThat(getString(dest), is(getString(source))); - } - - private static void writeToFile(File file, String s) throws IOException { - BufferedOutputStream buf = new BufferedOutputStream(new FileOutputStream(file)); - buf.write(s.getBytes()); - buf.close(); - } - - private static String getString(File file) throws IOException { - int size = (int) file.length(); - byte[] bytes = new byte[size]; - BufferedInputStream buf = new BufferedInputStream(new FileInputStream(file)); - buf.read(bytes, 0, bytes.length); - buf.close(); - return new String(bytes); - } -} diff --git a/app/src/test/java/fr/free/nrw/commons/LatLngTests.java b/app/src/test/java/fr/free/nrw/commons/LatLngTests.java deleted file mode 100644 index c2fb1b159..000000000 --- a/app/src/test/java/fr/free/nrw/commons/LatLngTests.java +++ /dev/null @@ -1,64 +0,0 @@ -package fr.free.nrw.commons; - -import org.junit.Assert; -import org.junit.Test; - -import fr.free.nrw.commons.location.LatLng; - -import static org.hamcrest.CoreMatchers.is; - -public class LatLngTests { - @Test public void testZeroZero() { - LatLng place = new LatLng(0, 0, 0); - String prettyString = place.getPrettyCoordinateString(); - Assert.assertThat(prettyString, is("0.0 N, 0.0 E")); - } - - @Test public void testAntipode() { - LatLng place = new LatLng(0, 180, 0); - String prettyString = place.getPrettyCoordinateString(); - Assert.assertThat(prettyString, is("0.0 N, 180.0 W")); - } - - @Test public void testNorthPole() { - LatLng place = new LatLng(90, 0, 0); - String prettyString = place.getPrettyCoordinateString(); - Assert.assertThat(prettyString, is("90.0 N, 0.0 E")); - } - - @Test public void testSouthPole() { - LatLng place = new LatLng(-90, 0, 0); - String prettyString = place.getPrettyCoordinateString(); - Assert.assertThat(prettyString, is("90.0 S, 0.0 E")); - } - - @Test public void testLargerNumbers() { - LatLng place = new LatLng(120, 380, 0); - String prettyString = place.getPrettyCoordinateString(); - Assert.assertThat(prettyString, is("90.0 N, 20.0 E")); - } - - @Test public void testNegativeNumbers() { - LatLng place = new LatLng(-120, -30, 0); - String prettyString = place.getPrettyCoordinateString(); - Assert.assertThat(prettyString, is("90.0 S, 30.0 W")); - } - - @Test public void testTooBigWestValue() { - LatLng place = new LatLng(20, -190, 0); - String prettyString = place.getPrettyCoordinateString(); - Assert.assertThat(prettyString, is("20.0 N, 170.0 E")); - } - - @Test public void testRounding() { - LatLng place = new LatLng(0.1234567, -0.33333333, 0); - String prettyString = place.getPrettyCoordinateString(); - Assert.assertThat(prettyString, is("0.1235 N, 0.3333 W")); - } - - @Test public void testRoundingAgain() { - LatLng place = new LatLng(-0.000001, -0.999999, 0); - String prettyString = place.getPrettyCoordinateString(); - Assert.assertThat(prettyString, is("0.0 S, 1.0 W")); - } -} diff --git a/app/src/test/java/fr/free/nrw/commons/LengthUtilsTest.java b/app/src/test/java/fr/free/nrw/commons/LengthUtilsTest.java deleted file mode 100644 index 561f3f0e6..000000000 --- a/app/src/test/java/fr/free/nrw/commons/LengthUtilsTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package fr.free.nrw.commons; - -import org.junit.Assert; -import org.junit.Test; - -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.utils.LengthUtils; - -import static org.hamcrest.CoreMatchers.is; - -public class LengthUtilsTest { - @Test public void testZeroDistance() { - LatLng pointA = new LatLng(0, 0, 0); - LatLng pointB = new LatLng(0, 0, 0); - String distance = LengthUtils.formatDistanceBetween(pointA, pointB); - Assert.assertThat(distance, is("0m")); - } - - @Test public void testOneDegreeOnEquator() { - LatLng pointA = new LatLng(0, 0, 0); - LatLng pointB = new LatLng(0, 1, 0); - String distance = LengthUtils.formatDistanceBetween(pointA, pointB); - Assert.assertThat(distance, is("111.2km")); - } - - @Test public void testOneDegreeFortyFiveDegrees() { - LatLng pointA = new LatLng(45, 0, 0); - LatLng pointB = new LatLng(45, 1, 0); - String distance = LengthUtils.formatDistanceBetween(pointA, pointB); - Assert.assertThat(distance, is("78.6km")); - } - - @Test public void testOneDegreeSouthPole() { - LatLng pointA = new LatLng(-90, 0, 0); - LatLng pointB = new LatLng(-90, 1, 0); - String distance = LengthUtils.formatDistanceBetween(pointA, pointB); - Assert.assertThat(distance, is("0m")); - } - - @Test public void testPoleToPole() { - LatLng pointA = new LatLng(90, 0, 0); - LatLng pointB = new LatLng(-90, 0, 0); - String distance = LengthUtils.formatDistanceBetween(pointA, pointB); - Assert.assertThat(distance, is("20,015.1km")); - } -} diff --git a/app/src/test/java/fr/free/nrw/commons/MediaTest.java b/app/src/test/java/fr/free/nrw/commons/MediaTest.java deleted file mode 100644 index 4f53351ad..000000000 --- a/app/src/test/java/fr/free/nrw/commons/MediaTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package fr.free.nrw.commons; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -@RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class) -public class MediaTest { - @Test - public void displayTitleShouldStripExtension() { - Media m = new Media("File:Example.jpg"); - assertThat(m.getDisplayTitle(), is("Example")); - } - - @Test - public void displayTitleShouldUseSpaceForUnderscore() { - Media m = new Media("File:Example 1_2.jpg"); - assertThat(m.getDisplayTitle(), is("Example 1 2")); - } -} diff --git a/app/src/test/java/fr/free/nrw/commons/NearbyControllerTest.java b/app/src/test/java/fr/free/nrw/commons/NearbyControllerTest.java deleted file mode 100644 index 752b7404d..000000000 --- a/app/src/test/java/fr/free/nrw/commons/NearbyControllerTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package fr.free.nrw.commons; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; - -import java.util.ArrayList; -import java.util.List; - -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.nearby.NearbyBaseMarker; -import fr.free.nrw.commons.nearby.Place; - -import static fr.free.nrw.commons.nearby.NearbyController.loadAttractionsFromLocationToBaseMarkerOptions; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -@RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class) -public class NearbyControllerTest { - - @Test - public void testNullAttractions() { - LatLng location = new LatLng(0, 0, 0); - - List options = loadAttractionsFromLocationToBaseMarkerOptions( - location, null, RuntimeEnvironment.application); - - assertThat(options.size(), is(0)); - } - - @Test - public void testEmptyList() { - LatLng location = new LatLng(0, 0, 0); - List emptyList = new ArrayList<>(); - - List options = loadAttractionsFromLocationToBaseMarkerOptions( - location, emptyList, RuntimeEnvironment.application); - - assertThat(options.size(), is(0)); - } -} diff --git a/app/src/test/java/fr/free/nrw/commons/PageTitleTest.java b/app/src/test/java/fr/free/nrw/commons/PageTitleTest.java deleted file mode 100644 index 16336b1b0..000000000 --- a/app/src/test/java/fr/free/nrw/commons/PageTitleTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package fr.free.nrw.commons; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import java.net.URLEncoder; - -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -@RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class) -public class PageTitleTest { - @Test - public void displayTextShouldNotBeUnderscored() { - assertThat(new PageTitle("Ex_1 ").getDisplayText(), - is("Ex 1")); - } - - @Test - public void moreThanTwoColons() { - assertThat(new PageTitle("File:sample:a.jpg").getPrefixedText(), - is("File:Sample:a.jpg")); - } - - @Test - public void getTextShouldReturnWithoutNamespace() { - assertThat(new PageTitle("File:sample.jpg").getText(), - is("Sample.jpg")); - } - - - @Test - public void capitalizeNameAfterNamespace() { - assertThat(new PageTitle("File:sample.jpg").getPrefixedText(), - is("File:Sample.jpg")); - } - - @Test - public void prefixedTextShouldBeUnderscored() { - assertThat(new PageTitle("Ex 1 ").getPrefixedText(), - is("Ex_1")); - } - - @Test - public void getMobileUriForTest() { - assertThat(new PageTitle("Test").getMobileUri().toString(), - is(BuildConfig.MOBILE_HOME_URL + "Test")); - } - - @Test - public void spaceBecomesUnderscoreInUri() { - assertThat(new PageTitle("File:Ex 1.jpg").getCanonicalUri().toString(), - is(BuildConfig.HOME_URL + "File:Ex_1.jpg")); - } - - @Test - public void leaveSubpageNamesUncapitalizedInUri() { - assertThat(new PageTitle("User:Ex/subpage").getCanonicalUri().toString(), - is(BuildConfig.HOME_URL + "User:Ex/subpage")); - } - - @Test - public void unicodeUri() throws Throwable { - assertThat(new PageTitle("User:例").getCanonicalUri().toString(), - is(BuildConfig.HOME_URL + "User:" + URLEncoder.encode("例", "utf-8"))); - } -} diff --git a/app/src/test/java/fr/free/nrw/commons/TestCommonsApplication.java b/app/src/test/java/fr/free/nrw/commons/TestCommonsApplication.java deleted file mode 100644 index 9f7bc6184..000000000 --- a/app/src/test/java/fr/free/nrw/commons/TestCommonsApplication.java +++ /dev/null @@ -1,181 +0,0 @@ -package fr.free.nrw.commons; - -import android.content.Context; -import android.content.SharedPreferences; -import android.support.v4.util.LruCache; - -import com.squareup.leakcanary.RefWatcher; - -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import javax.inject.Named; - -import dagger.Provides; -import fr.free.nrw.commons.auth.AccountUtil; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.caching.CacheController; -import fr.free.nrw.commons.data.DBOpenHelper; -import fr.free.nrw.commons.di.CommonsApplicationComponent; -import fr.free.nrw.commons.di.CommonsApplicationModule; -import fr.free.nrw.commons.di.DaggerCommonsApplicationComponent; -import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.mwapi.MediaWikiApi; -import fr.free.nrw.commons.nearby.NearbyPlaces; -import fr.free.nrw.commons.upload.UploadController; - -public class TestCommonsApplication extends CommonsApplication { - - CommonsApplicationComponent mockApplicationComponent; - - @Mock - CommonsApplicationModule commonsApplicationModule; - @Mock - AccountUtil accountUtil; - @Mock - SharedPreferences appSharedPreferences; - @Mock - SharedPreferences defaultSharedPreferences; - @Mock - SharedPreferences otherSharedPreferences; - @Mock - UploadController uploadController; - @Mock - SessionManager sessionManager; - @Mock - MediaWikiApi mediaWikiApi; - @Mock - LocationServiceManager locationServiceManager; - @Mock - CacheController cacheController; - @Mock - DBOpenHelper dbOpenHelper; - @Mock - NearbyPlaces nearbyPlaces; - @Mock - LruCache lruCache; - - @Override - public void onCreate() { - MockitoAnnotations.initMocks(this); - if (mockApplicationComponent == null) { - mockApplicationComponent = DaggerCommonsApplicationComponent.builder() - .appModule(new CommonsApplicationModule(this) { - @Override - public AccountUtil providesAccountUtil(Context context) { - return accountUtil; - } - - @Override - public SharedPreferences providesApplicationSharedPreferences(Context context) { - return appSharedPreferences; - } - - @Override - public SharedPreferences providesDefaultSharedPreferences(Context context) { - return defaultSharedPreferences; - } - - @Override - public SharedPreferences providesOtherSharedPreferences(Context context) { - return otherSharedPreferences; - } - - @Override - public UploadController providesUploadController(SessionManager sessionManager, SharedPreferences sharedPreferences, Context context) { - return uploadController; - } - - @Override - public SessionManager providesSessionManager(Context context, MediaWikiApi mediaWikiApi, SharedPreferences sharedPreferences) { - return sessionManager; - } - - @Override - public MediaWikiApi provideMediaWikiApi(Context context, SharedPreferences sharedPreferences) { - return mediaWikiApi; - } - - @Override - public LocationServiceManager provideLocationServiceManager(Context context) { - return locationServiceManager; - } - - @Override - public CacheController provideCacheController() { - return cacheController; - } - - @Override - public DBOpenHelper provideDBOpenHelper(Context context) { - return dbOpenHelper; - } - - @Override - public NearbyPlaces provideNearbyPlaces() { - return nearbyPlaces; - } - - @Override - public LruCache provideLruCache() { - return lruCache; - } - }).build(); - } - super.onCreate(); - } - - @Override - protected RefWatcher setupLeakCanary() { - // No leakcanary in unit tests. - return RefWatcher.DISABLED; - } - - public AccountUtil getAccountUtil() { - return accountUtil; - } - - public SharedPreferences getAppSharedPreferences() { - return appSharedPreferences; - } - - public SharedPreferences getDefaultSharedPreferences() { - return defaultSharedPreferences; - } - - public SharedPreferences getOtherSharedPreferences() { - return otherSharedPreferences; - } - - public UploadController getUploadController() { - return uploadController; - } - - public SessionManager getSessionManager() { - return sessionManager; - } - - public MediaWikiApi getMediaWikiApi() { - return mediaWikiApi; - } - - public LocationServiceManager getLocationServiceManager() { - return locationServiceManager; - } - - public CacheController getCacheController() { - return cacheController; - } - - public DBOpenHelper getDbOpenHelper() { - return dbOpenHelper; - } - - public NearbyPlaces getNearbyPlaces() { - return nearbyPlaces; - } - - public LruCache getLruCache() { - return lruCache; - } -} diff --git a/app/src/test/java/fr/free/nrw/commons/UtilsFixExtensionTest.java b/app/src/test/java/fr/free/nrw/commons/UtilsFixExtensionTest.java deleted file mode 100644 index 186f30df9..000000000 --- a/app/src/test/java/fr/free/nrw/commons/UtilsFixExtensionTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package fr.free.nrw.commons; - -import org.junit.Assert; -import org.junit.Test; - -import static org.hamcrest.CoreMatchers.is; - -public class UtilsFixExtensionTest { - - @Test public void jpegResultsInJpg() { - Assert.assertThat(Utils.fixExtension("SampleFile.jpeg", "jpeg"), is("SampleFile.jpg")); - } - - @Test public void capitalJpegWithNoHintResultsInJpg() { - Assert.assertThat(Utils.fixExtension("SampleFile.JPEG", null), is("SampleFile.jpg")); - } - - @Test public void jpegWithBogusHintResultsInJpg() { - Assert.assertThat(Utils.fixExtension("SampleFile.jpeg", null), is("SampleFile.jpg")); - } - - @Test public void jpegToCapitalJpegResultsInJpg() { - Assert.assertThat(Utils.fixExtension("SampleFile.jpeg", "JPEG"), is("SampleFile.jpg")); - } - - @Test public void jpgToJpegResultsInJpg() { - Assert.assertThat(Utils.fixExtension("SampleFile.jpg", "jpeg"), is("SampleFile.jpg")); - } - - @Test public void jpegToJpgResultsInJpg() { - Assert.assertThat(Utils.fixExtension("SampleFile.jpeg", "jpg"), is("SampleFile.jpg")); - } - - @Test public void jpgRemainsJpg() { - Assert.assertThat(Utils.fixExtension("SampleFile.jpg", "jpg"), is("SampleFile.jpg")); - } - - @Test public void pngRemainsPng() { - Assert.assertThat(Utils.fixExtension("SampleFile.png", "png"), is("SampleFile.png")); - } - - @Test public void jpgHintResultsInJpg() { - Assert.assertThat(Utils.fixExtension("SampleFile", "jpg"), is("SampleFile.jpg")); - } - - @Test public void jpegHintResultsInJpg() { - Assert.assertThat(Utils.fixExtension("SampleFile", "jpeg"), is("SampleFile.jpg")); - } - - @Test public void dotLessJpgToJpgResultsInJpg() { - Assert.assertThat(Utils.fixExtension("SAMPLEjpg", "jpg"), is("SAMPLEjpg.jpg")); - } - - @Test public void inWordJpegToJpgResultsInJpg() { - Assert.assertThat(Utils.fixExtension("X.jpeg.SAMPLE", "jpg"),is("X.jpeg.SAMPLE.jpg")); - } -} diff --git a/app/src/test/java/fr/free/nrw/commons/category/CategoryDaoTest.java b/app/src/test/java/fr/free/nrw/commons/category/CategoryDaoTest.java deleted file mode 100644 index 1cf0c338b..000000000 --- a/app/src/test/java/fr/free/nrw/commons/category/CategoryDaoTest.java +++ /dev/null @@ -1,294 +0,0 @@ -package fr.free.nrw.commons.category; - -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.RemoteException; -import android.support.annotation.NonNull; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InOrder; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import java.util.Arrays; -import java.util.Date; -import java.util.List; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.TestCommonsApplication; -import fr.free.nrw.commons.category.CategoryDao.Table; - -import static fr.free.nrw.commons.category.CategoryContentProvider.BASE_URI; -import static fr.free.nrw.commons.category.CategoryContentProvider.uriForId; -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNull; -import static junit.framework.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isA; -import static org.mockito.Matchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -@RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class) -public class CategoryDaoTest { - - @Mock - private ContentProviderClient client; - @Mock - private SQLiteDatabase database; - @Captor - private ArgumentCaptor captor; - @Captor - private ArgumentCaptor queryCaptor; - - private CategoryDao testObject; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - testObject = new CategoryDao(() -> client); - } - - @Test - public void createTable() { - Table.onCreate(database); - verify(database).execSQL(Table.CREATE_TABLE_STATEMENT); - } - - @Test - public void deleteTable() { - Table.onDelete(database); - InOrder inOrder = Mockito.inOrder(database); - inOrder.verify(database).execSQL(Table.DROP_TABLE_STATEMENT); - inOrder.verify(database).execSQL(Table.CREATE_TABLE_STATEMENT); - } - - @Test - public void migrateTableVersionFrom_v1_to_v2() { - Table.onUpdate(database, 1, 2); - // Table didnt exist before v5 - verifyZeroInteractions(database); - } - - @Test - public void migrateTableVersionFrom_v2_to_v3() { - Table.onUpdate(database, 2, 3); - // Table didnt exist before v5 - verifyZeroInteractions(database); - } - - @Test - public void migrateTableVersionFrom_v3_to_v4() { - Table.onUpdate(database, 3, 4); - // Table didnt exist before v5 - verifyZeroInteractions(database); - } - - @Test - public void migrateTableVersionFrom_v4_to_v5() { - Table.onUpdate(database, 4, 5); - verify(database).execSQL(Table.CREATE_TABLE_STATEMENT); - } - - @Test - public void migrateTableVersionFrom_v5_to_v6() { - Table.onUpdate(database, 5, 6); - // Table didnt change in version 6 - verifyZeroInteractions(database); - } - - @Test - public void createFromCursor() { - MatrixCursor cursor = createCursor(1); - cursor.moveToFirst(); - Category category = testObject.fromCursor(cursor); - - assertEquals(uriForId(1), category.getContentUri()); - assertEquals("foo", category.getName()); - assertEquals(123, category.getLastUsed().getTime()); - assertEquals(2, category.getTimesUsed()); - } - - @Test - public void saveExistingCategory() throws Exception { - MatrixCursor cursor = createCursor(1); - cursor.moveToFirst(); - Category category = testObject.fromCursor(cursor); - - testObject.save(category); - - verify(client).update(eq(category.getContentUri()), captor.capture(), isNull(String.class), isNull(String[].class)); - ContentValues cv = captor.getValue(); - assertEquals(3, cv.size()); - assertEquals(category.getName(), cv.getAsString(Table.COLUMN_NAME)); - assertEquals(category.getLastUsed().getTime(), cv.getAsLong(Table.COLUMN_LAST_USED).longValue()); - assertEquals(category.getTimesUsed(), cv.getAsInteger(Table.COLUMN_TIMES_USED).intValue()); - } - - @Test - public void saveNewCategory() throws Exception { - Uri contentUri = CategoryContentProvider.uriForId(111); - when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(contentUri); - Category category = new Category(null, "foo", new Date(234L), 1); - - testObject.save(category); - - verify(client).insert(eq(BASE_URI), captor.capture()); - ContentValues cv = captor.getValue(); - assertEquals(3, cv.size()); - assertEquals(category.getName(), cv.getAsString(Table.COLUMN_NAME)); - assertEquals(category.getLastUsed().getTime(), cv.getAsLong(Table.COLUMN_LAST_USED).longValue()); - assertEquals(category.getTimesUsed(), cv.getAsInteger(Table.COLUMN_TIMES_USED).intValue()); - assertEquals(contentUri, category.getContentUri()); - } - - @Test(expected = RuntimeException.class) - public void testSaveTranslatesRemoteExceptions() throws Exception { - when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenThrow(new RemoteException("")); - testObject.save(new Category()); - } - - @Test - public void whenTheresNoDataFindReturnsNull_nullCursor() throws Exception { - when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(null); - - assertNull(testObject.find("foo")); - } - - @Test - public void whenTheresNoDataFindReturnsNull_emptyCursor() throws Exception { - when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(createCursor(0)); - - assertNull(testObject.find("foo")); - } - - @Test - public void cursorsAreClosedAfterUse() throws Exception { - Cursor mockCursor = mock(Cursor.class); - when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(mockCursor); - when(mockCursor.moveToFirst()).thenReturn(false); - - testObject.find("foo"); - - verify(mockCursor).close(); - } - - @Test - public void findCategory() throws Exception { - when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(createCursor(1)); - - Category category = testObject.find("foo"); - - assertEquals(uriForId(1), category.getContentUri()); - assertEquals("foo", category.getName()); - assertEquals(123, category.getLastUsed().getTime()); - assertEquals(2, category.getTimesUsed()); - - verify(client).query( - eq(BASE_URI), - eq(Table.ALL_FIELDS), - eq(Table.COLUMN_NAME + "=?"), - queryCaptor.capture(), - isNull(String.class) - ); - assertEquals("foo", queryCaptor.getValue()[0]); - } - - @Test(expected = RuntimeException.class) - public void findCategoryTranslatesExceptions() throws Exception { - when(client.query(any(), any(), anyString(), any(), anyString())).thenThrow(new RemoteException("")); - testObject.find("foo"); - } - - @Test(expected = RuntimeException.class) - public void recentCategoriesTranslatesExceptions() throws Exception { - when(client.query(any(), any(), anyString(), any(), anyString())).thenThrow(new RemoteException("")); - testObject.recentCategories(1); - } - - @Test - public void recentCategoriesReturnsEmptyList_nullCursor() throws Exception { - when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(null); - - assertTrue(testObject.recentCategories(1).isEmpty()); - } - - @Test - public void recentCategoriesReturnsEmptyList_emptyCursor() throws Exception { - when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(createCursor(0)); - - assertTrue(testObject.recentCategories(1).isEmpty()); - } - - @Test - public void cursorsAreClosedAfterRecentCategoriesQuery() throws Exception { - Cursor mockCursor = mock(Cursor.class); - when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(mockCursor); - when(mockCursor.moveToFirst()).thenReturn(false); - - testObject.recentCategories(1); - - verify(mockCursor).close(); - } - - @Test - public void recentCategoriesReturnsLessThanLimit() throws Exception { - when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(createCursor(1)); - - List result = testObject.recentCategories(10); - - assertEquals(1, result.size()); - assertEquals("foo", result.get(0)); - - verify(client).query( - eq(BASE_URI), - eq(Table.ALL_FIELDS), - isNull(String.class), - queryCaptor.capture(), - eq(Table.COLUMN_LAST_USED + " DESC") - ); - assertEquals(0, queryCaptor.getValue().length); - } - - @Test - public void recentCategoriesHomorsLimit() throws Exception { - when(client.query(any(), any(), anyString(), any(), anyString())).thenReturn(createCursor(10)); - - List result = testObject.recentCategories(5); - - assertEquals(5, result.size()); - } - - @NonNull - private MatrixCursor createCursor(int rowCount) { - MatrixCursor cursor = new MatrixCursor(new String[]{ - Table.COLUMN_ID, - Table.COLUMN_NAME, - Table.COLUMN_LAST_USED, - Table.COLUMN_TIMES_USED - }, rowCount); - - for (int i = 0; i < rowCount; i++) { - cursor.addRow(Arrays.asList("1", "foo", "123", "2")); - } - - return cursor; - } - -} \ No newline at end of file diff --git a/app/src/test/java/fr/free/nrw/commons/contributions/ContributionDaoTest.java b/app/src/test/java/fr/free/nrw/commons/contributions/ContributionDaoTest.java deleted file mode 100644 index 15e37e640..000000000 --- a/app/src/test/java/fr/free/nrw/commons/contributions/ContributionDaoTest.java +++ /dev/null @@ -1,370 +0,0 @@ -package fr.free.nrw.commons.contributions; - -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.MatrixCursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.RemoteException; -import android.support.annotation.NonNull; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InOrder; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import java.util.Arrays; -import java.util.Date; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.TestCommonsApplication; -import fr.free.nrw.commons.Utils; - -import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA; -import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY; -import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED; -import static fr.free.nrw.commons.contributions.Contribution.STATE_QUEUED; -import static fr.free.nrw.commons.contributions.ContributionDao.Table; -import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; -import static fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isA; -import static org.mockito.Matchers.isNull; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -@RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class) -public class ContributionDaoTest { - - private static final String LOCAL_URI = "http://example.com/"; - @Mock - private ContentProviderClient client; - @Mock - private SQLiteDatabase database; - @Captor - private ArgumentCaptor captor; - - private Uri contentUri; - private ContributionDao testObject; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - contentUri = uriForId(111); - - testObject = new ContributionDao(() -> client); - } - - @Test - public void createTable() { - Table.onCreate(database); - verify(database).execSQL(Table.CREATE_TABLE_STATEMENT); - } - - @Test - public void deleteTable() { - Table.onDelete(database); - - InOrder inOrder = Mockito.inOrder(database); - inOrder.verify(database).execSQL(Table.DROP_TABLE_STATEMENT); - inOrder.verify(database).execSQL(Table.CREATE_TABLE_STATEMENT); - } - - @Test - public void upgradeDatabase_v1_to_v2() { - Table.onUpdate(database, 1, 2); - - InOrder inOrder = Mockito.inOrder(database); - inOrder.verify(database).execSQL(Table.ADD_DESCRIPTION_FIELD); - inOrder.verify(database).execSQL(Table.ADD_CREATOR_FIELD); - } - - @Test - public void upgradeDatabase_v2_to_v3() { - Table.onUpdate(database, 2, 3); - - InOrder inOrder = Mockito.inOrder(database); - inOrder.verify(database).execSQL(Table.ADD_MULTIPLE_FIELD); - inOrder.verify(database).execSQL(Table.SET_DEFAULT_MULTIPLE); - } - - @Test - public void upgradeDatabase_v3_to_v4() { - Table.onUpdate(database, 3, 4); - - // No changes - verifyZeroInteractions(database); - } - - @Test - public void upgradeDatabase_v4_to_v5() { - Table.onUpdate(database, 4, 5); - - // No changes - verifyZeroInteractions(database); - } - - @Test - public void upgradeDatabase_v5_to_v6() { - Table.onUpdate(database, 5, 6); - - InOrder inOrder = Mockito.inOrder(database); - inOrder.verify(database).execSQL(Table.ADD_WIDTH_FIELD); - inOrder.verify(database).execSQL(Table.SET_DEFAULT_WIDTH); - inOrder.verify(database).execSQL(Table.ADD_HEIGHT_FIELD); - inOrder.verify(database).execSQL(Table.SET_DEFAULT_HEIGHT); - inOrder.verify(database).execSQL(Table.ADD_LICENSE_FIELD); - inOrder.verify(database).execSQL(Table.SET_DEFAULT_LICENSE); - } - - @Test - public void saveNewContribution_nonNullFields() throws Exception { - when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(contentUri); - Contribution contribution = createContribution(true, null, null, null, null); - - testObject.save(contribution); - - assertEquals(contentUri, contribution.getContentUri()); - verify(client).insert(eq(BASE_URI), captor.capture()); - ContentValues cv = captor.getValue(); - - // Long fields - assertEquals(222L, cv.getAsLong(Table.COLUMN_LENGTH).longValue()); - assertEquals(321L, cv.getAsLong(Table.COLUMN_TIMESTAMP).longValue()); - assertEquals(333L, cv.getAsLong(Table.COLUMN_TRANSFERRED).longValue()); - - // Integer fields - assertEquals(STATE_COMPLETED, cv.getAsInteger(Table.COLUMN_STATE).intValue()); - assertEquals(640, cv.getAsInteger(Table.COLUMN_WIDTH).intValue()); - assertEquals(480, cv.getAsInteger(Table.COLUMN_HEIGHT).intValue()); - - // String fields - assertEquals(SOURCE_CAMERA, cv.getAsString(Table.COLUMN_SOURCE)); - assertEquals("desc", cv.getAsString(Table.COLUMN_DESCRIPTION)); - assertEquals("create", cv.getAsString(Table.COLUMN_CREATOR)); - assertEquals("007", cv.getAsString(Table.COLUMN_LICENSE)); - } - - @Test - public void saveNewContribution_nullableFieldsAreNull() throws Exception { - when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(contentUri); - Contribution contribution = createContribution(true, null, null, null, null); - - testObject.save(contribution); - - assertEquals(contentUri, contribution.getContentUri()); - verify(client).insert(eq(BASE_URI), captor.capture()); - ContentValues cv = captor.getValue(); - - // Nullable fields are absent if null - assertFalse(cv.containsKey(Table.COLUMN_LOCAL_URI)); - assertFalse(cv.containsKey(Table.COLUMN_IMAGE_URL)); - assertFalse(cv.containsKey(Table.COLUMN_UPLOADED)); - } - - @Test - public void saveNewContribution_nullableImageUrlUsesFileAsBackup() throws Exception { - when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(contentUri); - Contribution contribution = createContribution(true, null, null, null, "file"); - - testObject.save(contribution); - - assertEquals(contentUri, contribution.getContentUri()); - verify(client).insert(eq(BASE_URI), captor.capture()); - ContentValues cv = captor.getValue(); - - // Nullable fields are absent if null - assertFalse(cv.containsKey(Table.COLUMN_LOCAL_URI)); - assertFalse(cv.containsKey(Table.COLUMN_UPLOADED)); - - assertEquals(Utils.makeThumbBaseUrl("file"), cv.getAsString(Table.COLUMN_IMAGE_URL)); - } - - @Test - public void saveNewContribution_nullableFieldsAreNonNull() throws Exception { - when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(contentUri); - Contribution contribution = createContribution(true, Uri.parse(LOCAL_URI), - "image", new Date(456L), null); - - testObject.save(contribution); - - assertEquals(contentUri, contribution.getContentUri()); - verify(client).insert(eq(BASE_URI), captor.capture()); - ContentValues cv = captor.getValue(); - - assertEquals(LOCAL_URI, cv.getAsString(Table.COLUMN_LOCAL_URI)); - assertEquals("image", cv.getAsString(Table.COLUMN_IMAGE_URL)); - assertEquals(456L, cv.getAsLong(Table.COLUMN_UPLOADED).longValue()); - } - - @Test - public void saveNewContribution_booleanEncodesTrue() throws Exception { - when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(contentUri); - Contribution contribution = createContribution(true, null, null, null, null); - - testObject.save(contribution); - - assertEquals(contentUri, contribution.getContentUri()); - verify(client).insert(eq(BASE_URI), captor.capture()); - ContentValues cv = captor.getValue(); - - // Boolean true --> 1 for ths encoding scheme - assertEquals("Boolean true should be encoded as 1", 1, - cv.getAsInteger(Table.COLUMN_MULTIPLE).intValue()); - } - - @Test - public void saveNewContribution_booleanEncodesFalse() throws Exception { - when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(contentUri); - Contribution contribution = createContribution(false, null, null, null, null); - - testObject.save(contribution); - - assertEquals(contentUri, contribution.getContentUri()); - verify(client).insert(eq(BASE_URI), captor.capture()); - ContentValues cv = captor.getValue(); - - // Boolean true --> 1 for ths encoding scheme - assertEquals("Boolean false should be encoded as 0", 0, - cv.getAsInteger(Table.COLUMN_MULTIPLE).intValue()); - } - - @Test - public void saveExistingContribution() throws Exception { - Contribution contribution = createContribution(false, null, null, null, null); - contribution.setContentUri(contentUri); - - testObject.save(contribution); - - verify(client).update(eq(contentUri), isA(ContentValues.class), isNull(String.class), isNull(String[].class)); - } - - @Test(expected = RuntimeException.class) - public void saveTranslatesExceptions() throws Exception { - when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenThrow(new RemoteException("")); - - testObject.save(createContribution(false, null, null, null, null)); - } - - @Test(expected = RuntimeException.class) - public void deleteTranslatesExceptions() throws Exception { - when(client.delete(isA(Uri.class), any(), any())).thenThrow(new RemoteException("")); - - Contribution contribution = createContribution(false, null, null, null, null); - contribution.setContentUri(contentUri); - testObject.delete(contribution); - } - - @Test(expected = RuntimeException.class) - public void exceptionThrownWhenAttemptingToDeleteUnsavedContribution() { - testObject.delete(createContribution(false, null, null, null, null)); - } - - @Test - public void deleteExistingContribution() throws Exception { - Contribution contribution = createContribution(false, null, null, null, null); - contribution.setContentUri(contentUri); - - testObject.delete(contribution); - - verify(client).delete(eq(contentUri), isNull(String.class), isNull(String[].class)); - } - - @Test - public void createFromCursor() { - long created = 321L; - long uploaded = 456L; - MatrixCursor mc = createCursor(created, uploaded, false, LOCAL_URI); - - Contribution c = testObject.fromCursor(mc); - - assertEquals(uriForId(111), c.getContentUri()); - assertEquals("file", c.getFilename()); - assertEquals(LOCAL_URI, c.getLocalUri().toString()); - assertEquals("image", c.getImageUrl()); - assertEquals(created, c.getTimestamp().getTime()); - assertEquals(created, c.getDateCreated().getTime()); - assertEquals(STATE_QUEUED, c.getState()); - assertEquals(222L, c.getDataLength()); - assertEquals(uploaded, c.getDateUploaded().getTime()); - assertEquals(88L, c.getTransferred()); - assertEquals(SOURCE_GALLERY, c.getSource()); - assertEquals("desc", c.getDescription()); - assertEquals("create", c.getCreator()); - assertEquals(640, c.getWidth()); - assertEquals(480, c.getHeight()); - assertEquals("007", c.getLicense()); - } - - @Test - public void createFromCursor_nullableTimestamps() { - MatrixCursor mc = createCursor(0L, 0L, false, LOCAL_URI); - - Contribution c = testObject.fromCursor(mc); - - assertNull(c.getTimestamp()); - assertNull(c.getDateCreated()); - assertNull(c.getDateUploaded()); - } - - @Test - public void createFromCursor_nullableLocalUri() { - MatrixCursor mc = createCursor(0L, 0L, false, ""); - - Contribution c = testObject.fromCursor(mc); - - assertNull(c.getLocalUri()); - assertNull(c.getDateCreated()); - assertNull(c.getDateUploaded()); - } - - @Test - public void createFromCursor_booleanEncoding() { - MatrixCursor mcFalse = createCursor(0L, 0L, false, LOCAL_URI); - assertFalse(testObject.fromCursor(mcFalse).getMultiple()); - - MatrixCursor mcHammer = createCursor(0L, 0L, true, LOCAL_URI); - assertTrue(testObject.fromCursor(mcHammer).getMultiple()); - } - - @NonNull - private MatrixCursor createCursor(long created, long uploaded, boolean multiple, String localUri) { - MatrixCursor mc = new MatrixCursor(Table.ALL_FIELDS, 1); - mc.addRow(Arrays.asList("111", "file", localUri, "image", - created, STATE_QUEUED, 222L, uploaded, 88L, SOURCE_GALLERY, "desc", - "create", multiple ? 1 : 0, 640, 480, "007")); - mc.moveToFirst(); - return mc; - } - - @NonNull - private Contribution createContribution(boolean multiple, Uri localUri, - String imageUrl, Date dateUploaded, String filename) { - Contribution contribution = new Contribution(localUri, imageUrl, filename, "desc", 222L, - new Date(321L), dateUploaded, "create", "edit", "coords"); - contribution.setState(STATE_COMPLETED); - contribution.setTransferred(333L); - contribution.setSource(SOURCE_CAMERA); - contribution.setLicense("007"); - contribution.setMultiple(multiple); - contribution.setTimestamp(new Date(321L)); - contribution.setWidth(640); - contribution.setHeight(480); // VGA should be enough for anyone, right? - return contribution; - } -} \ No newline at end of file diff --git a/app/src/test/java/fr/free/nrw/commons/modifications/ModifierSequenceDaoTest.java b/app/src/test/java/fr/free/nrw/commons/modifications/ModifierSequenceDaoTest.java deleted file mode 100644 index ef290500d..000000000 --- a/app/src/test/java/fr/free/nrw/commons/modifications/ModifierSequenceDaoTest.java +++ /dev/null @@ -1,181 +0,0 @@ -package fr.free.nrw.commons.modifications; - -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.MatrixCursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.RemoteException; -import android.support.annotation.NonNull; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InOrder; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import java.util.Arrays; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.TestCommonsApplication; - -import static fr.free.nrw.commons.modifications.ModificationsContentProvider.BASE_URI; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isA; -import static org.mockito.Matchers.isNull; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class) -public class ModifierSequenceDaoTest { - - private static final String EXPECTED_MEDIA_URI = "http://example.com/"; - - @Mock - ContentProviderClient client; - @Mock - SQLiteDatabase database; - @Captor - ArgumentCaptor contentValuesCaptor; - - private ModifierSequenceDao testObject; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - testObject = new ModifierSequenceDao(() -> client); - } - - @Test - public void createFromCursorWithEmptyModifiers() { - MatrixCursor cursor = createCursor(""); - - ModifierSequence seq = testObject.fromCursor(cursor); - - assertEquals(EXPECTED_MEDIA_URI, seq.getMediaUri().toString()); - assertEquals(BASE_URI.buildUpon().appendPath("1").toString(), seq.getContentUri().toString()); - assertTrue(seq.getModifiers().isEmpty()); - } - - @Test - public void createFromCursorWtihCategoryModifier() { - MatrixCursor cursor = createCursor("{\"name\": \"CategoriesModifier\", \"data\": {}}"); - - ModifierSequence seq = testObject.fromCursor(cursor); - - assertEquals(1, seq.getModifiers().size()); - assertTrue(seq.getModifiers().get(0) instanceof CategoryModifier); - } - - @Test - public void createFromCursorWithRemoveModifier() { - MatrixCursor cursor = createCursor("{\"name\": \"TemplateRemoverModifier\", \"data\": {}}"); - - ModifierSequence seq = testObject.fromCursor(cursor); - - assertEquals(1, seq.getModifiers().size()); - assertTrue(seq.getModifiers().get(0) instanceof TemplateRemoveModifier); - } - - @Test - public void deleteSequence() throws Exception { - when(client.delete(isA(Uri.class), isNull(String.class), isNull(String[].class))).thenReturn(1); - ModifierSequence seq = testObject.fromCursor(createCursor("")); - - testObject.delete(seq); - - verify(client).delete(eq(seq.getContentUri()), isNull(String.class), isNull(String[].class)); - } - - @Test(expected = RuntimeException.class) - public void deleteTranslatesRemoteExceptions() throws Exception { - when(client.delete(isA(Uri.class), isNull(String.class), isNull(String[].class))).thenThrow(new RemoteException("")); - ModifierSequence seq = testObject.fromCursor(createCursor("")); - - testObject.delete(seq); - } - - @Test - public void saveExistingSequence() throws Exception { - String modifierJson = "{\"name\":\"CategoriesModifier\",\"data\":{}}"; - String expectedData = "{\"modifiers\":[" + modifierJson + "]}"; - MatrixCursor cursor = createCursor(modifierJson); - - testObject.save(testObject.fromCursor(cursor)); - - verify(client).update(eq(testObject.fromCursor(cursor).getContentUri()), contentValuesCaptor.capture(), isNull(String.class), isNull(String[].class)); - ContentValues cv = contentValuesCaptor.getValue(); - assertEquals(2, cv.size()); - assertEquals(EXPECTED_MEDIA_URI, cv.get(ModifierSequenceDao.Table.COLUMN_MEDIA_URI)); - assertEquals(expectedData, cv.get(ModifierSequenceDao.Table.COLUMN_DATA)); - } - - @Test - public void saveNewSequence() throws Exception { - Uri expectedContentUri = BASE_URI.buildUpon().appendPath("1").build(); - when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenReturn(expectedContentUri); - - ModifierSequence seq = new ModifierSequence(Uri.parse(EXPECTED_MEDIA_URI)); - testObject.save(seq); - - verify(client).insert(eq(ModificationsContentProvider.BASE_URI), contentValuesCaptor.capture()); - ContentValues cv = contentValuesCaptor.getValue(); - assertEquals(2, cv.size()); - assertEquals(EXPECTED_MEDIA_URI, cv.get(ModifierSequenceDao.Table.COLUMN_MEDIA_URI)); - assertEquals("{\"modifiers\":[]}", cv.get(ModifierSequenceDao.Table.COLUMN_DATA)); - assertEquals(expectedContentUri.toString(), seq.getContentUri().toString()); - } - - @Test(expected = RuntimeException.class) - public void saveTranslatesRemoteExceptions() throws Exception { - when(client.insert(isA(Uri.class), isA(ContentValues.class))).thenThrow(new RemoteException("")); - - testObject.save(new ModifierSequence(Uri.parse(EXPECTED_MEDIA_URI))); - } - - @Test - public void createTable() { - ModifierSequenceDao.Table.onCreate(database); - - verify(database).execSQL(ModifierSequenceDao.Table.CREATE_TABLE_STATEMENT); - } - - @Test - public void updateTable() { - ModifierSequenceDao.Table.onUpdate(database, 1, 2); - - InOrder inOrder = inOrder(database); - inOrder.verify(database).execSQL(ModifierSequenceDao.Table.DROP_TABLE_STATEMENT); - inOrder.verify(database).execSQL(ModifierSequenceDao.Table.CREATE_TABLE_STATEMENT); - } - - @Test - public void deleteTable() { - ModifierSequenceDao.Table.onDelete(database); - - InOrder inOrder = inOrder(database); - inOrder.verify(database).execSQL(ModifierSequenceDao.Table.DROP_TABLE_STATEMENT); - inOrder.verify(database).execSQL(ModifierSequenceDao.Table.CREATE_TABLE_STATEMENT); - } - - @NonNull - private MatrixCursor createCursor(String modifierJson) { - MatrixCursor cursor = new MatrixCursor(new String[]{ - ModifierSequenceDao.Table.COLUMN_ID, - ModifierSequenceDao.Table.COLUMN_MEDIA_URI, - ModifierSequenceDao.Table.COLUMN_DATA - }, 1); - cursor.addRow(Arrays.asList("1", EXPECTED_MEDIA_URI, "{\"modifiers\": [" + modifierJson + "]}")); - cursor.moveToFirst(); - return cursor; - } -} \ No newline at end of file diff --git a/app/src/test/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java b/app/src/test/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java deleted file mode 100644 index e2c8b82f6..000000000 --- a/app/src/test/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java +++ /dev/null @@ -1,248 +0,0 @@ -package fr.free.nrw.commons.mwapi; - -import android.content.SharedPreferences; -import android.os.Build; -import android.preference.PreferenceManager; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.TestCommonsApplication; -import io.reactivex.observers.TestObserver; -import okhttp3.HttpUrl; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -@RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class) -public class ApacheHttpClientMediaWikiApiTest { - - private ApacheHttpClientMediaWikiApi testObject; - private MockWebServer server; - private SharedPreferences sharedPreferences; - - @Before - public void setUp() throws Exception { - server = new MockWebServer(); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application); - testObject = new ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.getHostName() + ":" + server.getPort() + "/", sharedPreferences); - testObject.setWikiMediaToolforgeUrl("http://" + server.getHostName() + ":" + server.getPort() + "/"); - } - - @After - public void teardown() throws IOException { - server.shutdown(); - } - - @Test - public void authCookiesAreHandled() { - assertEquals("", testObject.getAuthCookie()); - - testObject.setAuthCookie("cookie=chocolate-chip"); - - assertEquals("cookie=chocolate-chip", testObject.getAuthCookie()); - } - - @Test - public void simpleLoginWithWrongPassword() throws Exception { - server.enqueue(new MockResponse().setBody("")); - server.enqueue(new MockResponse().setBody("")); - - String result = testObject.login("foo", "bar"); - - RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "POST"); - Map body = parseBody(loginTokenRequest.getBody().readUtf8()); - assertEquals("xml", body.get("format")); - assertEquals("query", body.get("action")); - assertEquals("login", body.get("type")); - assertEquals("tokens", body.get("meta")); - - RecordedRequest loginRequest = assertBasicRequestParameters(server, "POST"); - body = parseBody(loginRequest.getBody().readUtf8()); - assertEquals("1", body.get("rememberMe")); - assertEquals("foo", body.get("username")); - assertEquals("bar", body.get("password")); - assertEquals("baz", body.get("logintoken")); - assertEquals("https://commons.wikimedia.org", body.get("loginreturnurl")); - assertEquals("xml", body.get("format")); - - assertEquals("wrongpassword", result); - } - - @Test - public void simpleLogin() throws Exception { - server.enqueue(new MockResponse().setBody("")); - server.enqueue(new MockResponse().setBody("")); - - String result = testObject.login("foo", "bar"); - - RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "POST"); - Map body = parseBody(loginTokenRequest.getBody().readUtf8()); - assertEquals("xml", body.get("format")); - assertEquals("query", body.get("action")); - assertEquals("login", body.get("type")); - assertEquals("tokens", body.get("meta")); - - RecordedRequest loginRequest = assertBasicRequestParameters(server, "POST"); - body = parseBody(loginRequest.getBody().readUtf8()); - assertEquals("1", body.get("rememberMe")); - assertEquals("foo", body.get("username")); - assertEquals("bar", body.get("password")); - assertEquals("baz", body.get("logintoken")); - assertEquals("https://commons.wikimedia.org", body.get("loginreturnurl")); - assertEquals("xml", body.get("format")); - - assertEquals("PASS", result); - } - - @Test - public void twoFactorLogin() throws Exception { - server.enqueue(new MockResponse().setBody("")); - server.enqueue(new MockResponse().setBody("")); - - String result = testObject.login("foo", "bar", "2fa"); - - RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "POST"); - Map body = parseBody(loginTokenRequest.getBody().readUtf8()); - assertEquals("xml", body.get("format")); - assertEquals("query", body.get("action")); - assertEquals("login", body.get("type")); - assertEquals("tokens", body.get("meta")); - - RecordedRequest loginRequest = assertBasicRequestParameters(server, "POST"); - body = parseBody(loginRequest.getBody().readUtf8()); - assertEquals("true", body.get("rememberMe")); - assertEquals("foo", body.get("username")); - assertEquals("bar", body.get("password")); - assertEquals("baz", body.get("logintoken")); - assertEquals("true", body.get("logincontinue")); - assertEquals("2fa", body.get("OATHToken")); - assertEquals("xml", body.get("format")); - - assertEquals("PASS", result); - } - - @Test - public void validateLoginForLoggedInUser() throws Exception { - server.enqueue(new MockResponse().setBody("")); - - boolean result = testObject.validateLogin(); - - RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "GET"); - Map body = parseQueryParams(loginTokenRequest); - assertEquals("xml", body.get("format")); - assertEquals("query", body.get("action")); - assertEquals("userinfo", body.get("meta")); - - assertTrue(result); - } - - @Test - public void validateLoginForLoggedOutUser() throws Exception { - server.enqueue(new MockResponse().setBody("")); - - boolean result = testObject.validateLogin(); - - RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "GET"); - Map params = parseQueryParams(loginTokenRequest); - assertEquals("xml", params.get("format")); - assertEquals("query", params.get("action")); - assertEquals("userinfo", params.get("meta")); - - assertFalse(result); - } - - @Test - public void editToken() throws Exception { - server.enqueue(new MockResponse().setBody("")); - - String result = testObject.getEditToken(); - - RecordedRequest loginTokenRequest = assertBasicRequestParameters(server, "GET"); - Map params = parseQueryParams(loginTokenRequest); - assertEquals("xml", params.get("format")); - assertEquals("tokens", params.get("action")); - assertEquals("edit", params.get("type")); - - assertEquals("baz", result); - } - - @Test - public void fileExistsWithName_FileNotFound() throws Exception { - server.enqueue(new MockResponse().setBody(" ")); - - boolean result = testObject.fileExistsWithName("foo"); - - RecordedRequest request = assertBasicRequestParameters(server, "GET"); - Map params = parseQueryParams(request); - assertEquals("xml", params.get("format")); - assertEquals("query", params.get("action")); - assertEquals("imageinfo", params.get("prop")); - assertEquals("File:foo", params.get("titles")); - - assertFalse(result); - } - - @Test - public void getUploadCount() throws InterruptedException { - server.enqueue(new MockResponse().setBody("23\n")); - - TestObserver testObserver = testObject.getUploadCount("testUsername").test(); - - RecordedRequest request = server.takeRequest(); - Map params = parseQueryParams(request); - assertEquals("testUsername", params.get("user")); - - assertEquals(1, testObserver.valueCount()); - assertEquals(23, (int)testObserver.values().get(0)); - } - - private RecordedRequest assertBasicRequestParameters(MockWebServer server, String method) throws InterruptedException { - RecordedRequest request = server.takeRequest(); - assertEquals("/", request.getRequestUrl().encodedPath()); - assertEquals(method, request.getMethod()); - assertEquals("Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE, request.getHeader("User-Agent")); - if ("POST".equals(method)) { - assertEquals("application/x-www-form-urlencoded", request.getHeader("Content-Type")); - } - return request; - } - - private Map parseQueryParams(RecordedRequest request) { - Map result = new HashMap<>(); - HttpUrl url = request.getRequestUrl(); - Set params = url.queryParameterNames(); - for (String name : params) { - result.put(name, url.queryParameter(name)); - } - return result; - } - - private Map parseBody(String body) throws UnsupportedEncodingException { - String[] props = body.split("&"); - Map result = new HashMap<>(); - for (String prop : props) { - String[] pair = prop.split("="); - result.put(pair[0], URLDecoder.decode(pair[1], "utf-8")); - } - return result; - } -} diff --git a/app/src/test/java/fr/free/nrw/commons/utils/StringSortingUtilsTest.java b/app/src/test/java/fr/free/nrw/commons/utils/StringSortingUtilsTest.java deleted file mode 100644 index 6384fb096..000000000 --- a/app/src/test/java/fr/free/nrw/commons/utils/StringSortingUtilsTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package fr.free.nrw.commons.utils; - -import org.junit.Assert; -import org.junit.Test; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -public class StringSortingUtilsTest { - - @Test - public void testSortingNumbersBySimilarity() throws Exception { - List actualList = Arrays.asList("1234567", "4567", "12345", "123", "1234"); - List expectedList = Arrays.asList("1234", "12345", "123", "1234567", "4567"); - - Collections.sort(actualList, StringSortingUtils.sortBySimilarity("1234")); - Assert.assertEquals(expectedList, actualList); - } - - @Test - public void testSortingTextBySimilarity() throws Exception { - List actualList = Arrays.asList("The quick brown fox", - "quick brown fox", - "The", - "The quick ", - "The fox", - "brown fox", - "fox"); - List expectedList = Arrays.asList("The", - "The fox", - "The quick ", - "The quick brown fox", - "quick brown fox", - "brown fox", - "fox"); - - Collections.sort(actualList, StringSortingUtils.sortBySimilarity("The")); - Assert.assertEquals(expectedList, actualList); - } -} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/FileUtilsTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/FileUtilsTest.kt new file mode 100644 index 000000000..8be291d4d --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/FileUtilsTest.kt @@ -0,0 +1,33 @@ +package fr.free.nrw.commons + +import fr.free.nrw.commons.upload.FileUtils +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.* + +class FileUtilsTest { + @Test + fun copiedFileIsIdenticalToSource() { + val source = File.createTempFile("temp", "") + val dest = File.createTempFile("temp", "") + writeToFile(source, "Hello, World") + + FileUtils.copy(FileInputStream(source), FileOutputStream(dest)) + + assertEquals(getString(source), getString(dest)) + } + + private fun writeToFile(file: File, s: String) { + val buf = BufferedOutputStream(FileOutputStream(file)) + buf.write(s.toByteArray()) + buf.close() + } + + private fun getString(file: File): String { + val bytes = ByteArray(file.length().toInt()) + val buf = BufferedInputStream(FileInputStream(file)) + buf.read(bytes, 0, bytes.size) + buf.close() + return String(bytes) + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/LatLngTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/LatLngTests.kt new file mode 100644 index 000000000..efafa4e3a --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/LatLngTests.kt @@ -0,0 +1,64 @@ +package fr.free.nrw.commons + +import fr.free.nrw.commons.location.LatLng +import org.junit.Assert.assertEquals +import org.junit.Test + +class LatLngTests { + @Test + fun testZeroZero() { + val place = LatLng(0.0, 0.0, 0f) + assertPrettyCoordinateString("0.0 N, 0.0 E", place) + } + + @Test + fun testAntipode() { + val place = LatLng(0.0, 180.0, 0f) + assertPrettyCoordinateString("0.0 N, 180.0 W", place) + } + + @Test + fun testNorthPole() { + val place = LatLng(90.0, 0.0, 0f) + assertPrettyCoordinateString("90.0 N, 0.0 E", place) + } + + @Test + fun testSouthPole() { + val place = LatLng(-90.0, 0.0, 0f) + assertPrettyCoordinateString("90.0 S, 0.0 E", place) + } + + @Test + fun testLargerNumbers() { + val place = LatLng(120.0, 380.0, 0f) + assertPrettyCoordinateString("90.0 N, 20.0 E", place) + } + + @Test + fun testNegativeNumbers() { + val place = LatLng(-120.0, -30.0, 0f) + assertPrettyCoordinateString("90.0 S, 30.0 W", place) + } + + @Test + fun testTooBigWestValue() { + val place = LatLng(20.0, -190.0, 0f) + assertPrettyCoordinateString("20.0 N, 170.0 E", place) + } + + @Test + fun testRounding() { + val place = LatLng(0.1234567, -0.33333333, 0f) + assertPrettyCoordinateString("0.1235 N, 0.3333 W", place) + } + + @Test + fun testRoundingAgain() { + val place = LatLng(-0.000001, -0.999999, 0f) + assertPrettyCoordinateString("0.0 S, 1.0 W", place) + } + + private fun assertPrettyCoordinateString(expected: String, place: LatLng) = + assertEquals(expected, place.prettyCoordinateString) +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/LengthUtilsTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/LengthUtilsTest.kt new file mode 100644 index 000000000..63b0b4d0a --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/LengthUtilsTest.kt @@ -0,0 +1,46 @@ +package fr.free.nrw.commons + +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.utils.LengthUtils +import org.junit.Assert.assertEquals +import org.junit.Test + +class LengthUtilsTest { + @Test + fun testZeroDistance() { + val pointA = LatLng(0.0, 0.0, 0f) + val pointB = LatLng(0.0, 0.0, 0f) + assertDistanceBetween("0m", pointA, pointB) + } + + @Test + fun testOneDegreeOnEquator() { + val pointA = LatLng(0.0, 0.0, 0f) + val pointB = LatLng(0.0, 1.0, 0f) + assertDistanceBetween("111.2km", pointA, pointB) + } + + @Test + fun testOneDegreeFortyFiveDegrees() { + val pointA = LatLng(45.0, 0.0, 0f) + val pointB = LatLng(45.0, 1.0, 0f) + assertDistanceBetween("78.6km", pointA, pointB) + } + + @Test + fun testOneDegreeSouthPole() { + val pointA = LatLng(-90.0, 0.0, 0f) + val pointB = LatLng(-90.0, 1.0, 0f) + assertDistanceBetween("0m", pointA, pointB) + } + + @Test + fun testPoleToPole() { + val pointA = LatLng(90.0, 0.0, 0f) + val pointB = LatLng(-90.0, 0.0, 0f) + assertDistanceBetween("20,015.1km", pointA, pointB) + } + + private fun assertDistanceBetween(expected: String, pointA: LatLng, pointB: LatLng) = + assertEquals(expected, LengthUtils.formatDistanceBetween(pointA, pointB)) +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/MediaTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/MediaTest.kt new file mode 100644 index 000000000..f75c34568 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/MediaTest.kt @@ -0,0 +1,23 @@ +package fr.free.nrw.commons + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = intArrayOf(21), application = TestCommonsApplication::class) +class MediaTest { + @Test + fun displayTitleShouldStripExtension() { + val m = Media("File:Example.jpg") + assertEquals("Example", m.displayTitle) + } + + @Test + fun displayTitleShouldUseSpaceForUnderscore() { + val m = Media("File:Example 1_2.jpg") + assertEquals("Example 1 2", m.displayTitle) + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/NearbyControllerTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/NearbyControllerTest.kt new file mode 100644 index 000000000..fc0be84d0 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/NearbyControllerTest.kt @@ -0,0 +1,35 @@ +package fr.free.nrw.commons + +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.nearby.NearbyController.loadAttractionsFromLocationToBaseMarkerOptions +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = intArrayOf(21), application = TestCommonsApplication::class) +class NearbyControllerTest { + + @Test + fun testNullAttractions() { + val location = LatLng(0.0, 0.0, 0f) + + val options = loadAttractionsFromLocationToBaseMarkerOptions( + location, null, RuntimeEnvironment.application) + + assertEquals(0, options.size.toLong()) + } + + @Test + fun testEmptyList() { + val location = LatLng(0.0, 0.0, 0f) + + val options = loadAttractionsFromLocationToBaseMarkerOptions( + location, emptyList(), RuntimeEnvironment.application) + + assertEquals(0, options.size.toLong()) + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/PageTitleTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/PageTitleTest.kt new file mode 100644 index 000000000..28112b196 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/PageTitleTest.kt @@ -0,0 +1,67 @@ +package fr.free.nrw.commons + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.net.URLEncoder + +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = intArrayOf(21), application = TestCommonsApplication::class) +class PageTitleTest { + @Test + fun displayTextShouldNotBeUnderscored() { + val pageTitle = PageTitle("Ex_1 ") + assertEquals("Ex 1", pageTitle.displayText) + } + + @Test + fun moreThanTwoColons() { + val pageTitle = PageTitle("File:sample:a.jpg") + assertEquals("File:Sample:a.jpg", pageTitle.prefixedText) + } + + @Test + fun getTextShouldReturnWithoutNamespace() { + val pageTitle = PageTitle("File:sample.jpg") + assertEquals("Sample.jpg", pageTitle.text) + } + + + @Test + fun capitalizeNameAfterNamespace() { + val pageTitle = PageTitle("File:sample.jpg") + assertEquals("File:Sample.jpg", pageTitle.prefixedText) + } + + @Test + fun prefixedTextShouldBeUnderscored() { + val pageTitle = PageTitle("Ex 1 ") + assertEquals("Ex_1", pageTitle.prefixedText) + } + + @Test + fun getMobileUriForTest() { + val pageTitle = PageTitle("Test") + assertEquals(BuildConfig.MOBILE_HOME_URL + "Test", pageTitle.mobileUri.toString()) + } + + @Test + fun spaceBecomesUnderscoreInUri() { + val pageTitle = PageTitle("File:Ex 1.jpg") + assertEquals(BuildConfig.HOME_URL + "File:Ex_1.jpg", pageTitle.canonicalUri.toString()) + } + + @Test + fun leaveSubpageNamesUncapitalizedInUri() { + val pageTitle = PageTitle("User:Ex/subpage") + assertEquals(BuildConfig.HOME_URL + "User:Ex/subpage", pageTitle.canonicalUri.toString()) + } + + @Test + fun unicodeUri() { + val pageTitle = PageTitle("User:例") + assertEquals(BuildConfig.HOME_URL + "User:" + URLEncoder.encode("例", "utf-8"), pageTitle.canonicalUri.toString()) + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt new file mode 100644 index 000000000..84f6d5f10 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt @@ -0,0 +1,82 @@ +package fr.free.nrw.commons + +import android.content.ContentProviderClient +import android.content.Context +import android.content.SharedPreferences +import android.support.v4.util.LruCache +import com.google.gson.Gson +import com.nhaarman.mockito_kotlin.mock +import com.squareup.leakcanary.RefWatcher +import fr.free.nrw.commons.auth.AccountUtil +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.data.DBOpenHelper +import fr.free.nrw.commons.di.CommonsApplicationComponent +import fr.free.nrw.commons.di.CommonsApplicationModule +import fr.free.nrw.commons.di.DaggerCommonsApplicationComponent +import fr.free.nrw.commons.location.LocationServiceManager +import fr.free.nrw.commons.mwapi.MediaWikiApi +import fr.free.nrw.commons.nearby.NearbyPlaces +import fr.free.nrw.commons.upload.UploadController + +class TestCommonsApplication : CommonsApplication() { + private var mockApplicationComponent: CommonsApplicationComponent? = null + + override fun onCreate() { + if (mockApplicationComponent == null) { + mockApplicationComponent = DaggerCommonsApplicationComponent.builder() + .appModule(MockCommonsApplicationModule(this)).build() + } + super.onCreate() + } + + // No leakcanary in unit tests. + override fun setupLeakCanary(): RefWatcher = RefWatcher.DISABLED +} + +@Suppress("MemberVisibilityCanBePrivate") +class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModule(appContext) { + val accountUtil: AccountUtil = mock() + val appSharedPreferences: SharedPreferences = mock() + val defaultSharedPreferences: SharedPreferences = mock() + val categorySharedPreferences: SharedPreferences = mock() + val otherSharedPreferences: SharedPreferences = mock() + val uploadController: UploadController = mock() + val mockSessionManager: SessionManager = mock() + val locationServiceManager: LocationServiceManager = mock() + val mockDbOpenHelper: DBOpenHelper = mock() + val nearbyPlaces: NearbyPlaces = mock() + val lruCache: LruCache = mock() + val gson: Gson = Gson() + val categoryClient: ContentProviderClient = mock() + val contributionClient: ContentProviderClient = mock() + val modificationClient: ContentProviderClient = mock() + val uploadPrefs: SharedPreferences = mock() + + override fun provideCategoryContentProviderClient(context: Context?): ContentProviderClient = categoryClient + + override fun provideContributionContentProviderClient(context: Context?): ContentProviderClient = contributionClient + + override fun provideModificationContentProviderClient(context: Context?): ContentProviderClient = modificationClient + + override fun providesDirectNearbyUploadPreferences(context: Context?): SharedPreferences = uploadPrefs + + override fun providesAccountUtil(context: Context): AccountUtil = accountUtil + + override fun providesApplicationSharedPreferences(context: Context): SharedPreferences = appSharedPreferences + + override fun providesDefaultSharedPreferences(context: Context): SharedPreferences = defaultSharedPreferences + + override fun providesOtherSharedPreferences(context: Context): SharedPreferences = otherSharedPreferences + + override fun providesUploadController(sessionManager: SessionManager, sharedPreferences: SharedPreferences, context: Context): UploadController = uploadController + + override fun providesSessionManager(context: Context, mediaWikiApi: MediaWikiApi, sharedPreferences: SharedPreferences): SessionManager = mockSessionManager + + override fun provideLocationServiceManager(context: Context): LocationServiceManager = locationServiceManager + + override fun provideDBOpenHelper(context: Context): DBOpenHelper = mockDbOpenHelper + + override fun provideNearbyPlaces(): NearbyPlaces = nearbyPlaces + + override fun provideLruCache(): LruCache = lruCache +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/UtilsFixExtensionTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/UtilsFixExtensionTest.kt new file mode 100644 index 000000000..0082b9d28 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/UtilsFixExtensionTest.kt @@ -0,0 +1,68 @@ +package fr.free.nrw.commons + +import fr.free.nrw.commons.Utils.fixExtension +import org.junit.Assert.assertEquals +import org.junit.Test + +class UtilsFixExtensionTest { + + @Test + fun jpegResultsInJpg() { + assertEquals("SampleFile.jpg", fixExtension("SampleFile.jpeg", "jpeg")) + } + + @Test + fun capitalJpegWithNoHintResultsInJpg() { + assertEquals("SampleFile.jpg", fixExtension("SampleFile.JPEG", null)) + } + + @Test + fun jpegWithBogusHintResultsInJpg() { + assertEquals("SampleFile.jpg", fixExtension("SampleFile.jpeg", null)) + } + + @Test + fun jpegToCapitalJpegResultsInJpg() { + assertEquals("SampleFile.jpg", fixExtension("SampleFile.jpeg", "JPEG")) + } + + @Test + fun jpgToJpegResultsInJpg() { + assertEquals("SampleFile.jpg", fixExtension("SampleFile.jpg", "jpeg")) + } + + @Test + fun jpegToJpgResultsInJpg() { + assertEquals("SampleFile.jpg", fixExtension("SampleFile.jpeg", "jpg")) + } + + @Test + fun jpgRemainsJpg() { + assertEquals("SampleFile.jpg", fixExtension("SampleFile.jpg", "jpg")) + } + + @Test + fun pngRemainsPng() { + assertEquals("SampleFile.png", fixExtension("SampleFile.png", "png")) + } + + @Test + fun jpgHintResultsInJpg() { + assertEquals("SampleFile.jpg", fixExtension("SampleFile", "jpg")) + } + + @Test + fun jpegHintResultsInJpg() { + assertEquals("SampleFile.jpg", fixExtension("SampleFile", "jpeg")) + } + + @Test + fun dotLessJpgToJpgResultsInJpg() { + assertEquals("SAMPLEjpg.jpg", fixExtension("SAMPLEjpg", "jpg")) + } + + @Test + fun inWordJpegToJpgResultsInJpg() { + assertEquals("X.jpeg.SAMPLE.jpg", fixExtension("X.jpeg.SAMPLE", "jpg")) + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt new file mode 100644 index 000000000..b64d3b8aa --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDaoTest.kt @@ -0,0 +1,257 @@ +package fr.free.nrw.commons.category + +import android.content.ContentProviderClient +import android.content.ContentValues +import android.database.Cursor +import android.database.MatrixCursor +import android.database.sqlite.SQLiteDatabase +import android.os.RemoteException +import com.nhaarman.mockito_kotlin.* +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.category.CategoryContentProvider.BASE_URI +import fr.free.nrw.commons.category.CategoryContentProvider.uriForId +import fr.free.nrw.commons.category.CategoryDao.Table.* +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.util.* + +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = [21], application = TestCommonsApplication::class) +class CategoryDaoTest { + + private val columns = arrayOf(COLUMN_ID, COLUMN_NAME, COLUMN_LAST_USED, COLUMN_TIMES_USED) + private val client: ContentProviderClient = mock() + private val database: SQLiteDatabase = mock() + private val captor = argumentCaptor() + private val queryCaptor = argumentCaptor>() + + private lateinit var testObject: CategoryDao + + @Before + fun setUp() { + testObject = CategoryDao { client } + } + + @Test + fun createTable() { + onCreate(database) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + + @Test + fun deleteTable() { + onDelete(database) + inOrder(database) { + verify(database).execSQL(DROP_TABLE_STATEMENT) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + } + + @Test + fun migrateTableVersionFrom_v1_to_v2() { + onUpdate(database, 1, 2) + // Table didnt exist before v5 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v2_to_v3() { + onUpdate(database, 2, 3) + // Table didnt exist before v5 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v3_to_v4() { + onUpdate(database, 3, 4) + // Table didnt exist before v5 + verifyZeroInteractions(database) + } + + @Test + fun migrateTableVersionFrom_v4_to_v5() { + onUpdate(database, 4, 5) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + + @Test + fun migrateTableVersionFrom_v5_to_v6() { + onUpdate(database, 5, 6) + // Table didnt change in version 6 + verifyZeroInteractions(database) + } + + @Test + fun createFromCursor() { + createCursor(1).let { cursor -> + cursor.moveToFirst() + testObject.fromCursor(cursor).let { + assertEquals(uriForId(1), it.contentUri) + assertEquals("foo", it.name) + assertEquals(123, it.lastUsed.time) + assertEquals(2, it.timesUsed) + } + } + } + + @Test + fun saveExistingCategory() { + createCursor(1).let { + val category = testObject.fromCursor(it.apply { moveToFirst() }) + + testObject.save(category) + + verify(client).update(eq(category.contentUri), captor.capture(), isNull(), isNull()) + captor.firstValue.let { cv -> + assertEquals(3, cv.size()) + assertEquals(category.name, cv.getAsString(COLUMN_NAME)) + assertEquals(category.lastUsed.time, cv.getAsLong(COLUMN_LAST_USED)) + assertEquals(category.timesUsed, cv.getAsInteger(COLUMN_TIMES_USED)) + } + } + } + + @Test + fun saveNewCategory() { + val contentUri = CategoryContentProvider.uriForId(111) + whenever(client.insert(isA(), isA())).thenReturn(contentUri) + val category = Category(null, "foo", Date(234L), 1) + + testObject.save(category) + + verify(client).insert(eq(BASE_URI), captor.capture()) + captor.firstValue.let { cv -> + assertEquals(3, cv.size()) + assertEquals(category.name, cv.getAsString(COLUMN_NAME)) + assertEquals(category.lastUsed.time, cv.getAsLong(COLUMN_LAST_USED)) + assertEquals(category.timesUsed, cv.getAsInteger(COLUMN_TIMES_USED)) + assertEquals(contentUri, category.contentUri) + } + } + + @Test(expected = RuntimeException::class) + fun testSaveTranslatesRemoteExceptions() { + whenever(client.insert(isA(), isA())).thenThrow(RemoteException("")) + testObject.save(Category()) + } + + @Test + fun whenTheresNoDataFindReturnsNull_nullCursor() { + whenever(client.query(any(), any(), any(), any(), any())).thenReturn(null) + assertNull(testObject.find("foo")) + } + + @Test + fun whenTheresNoDataFindReturnsNull_emptyCursor() { + whenever(client.query(any(), any(), any(), any(), any())).thenReturn(createCursor(0)) + assertNull(testObject.find("foo")) + } + + @Test + fun cursorsAreClosedAfterUse() { + val mockCursor: Cursor = mock() + whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(mockCursor) + whenever(mockCursor.moveToFirst()).thenReturn(false) + + testObject.find("foo") + + verify(mockCursor).close() + } + + @Test + fun findCategory() { + whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1)) + + val category = testObject.find("foo") + assertNotNull(category) + + assertEquals(uriForId(1), category?.contentUri) + assertEquals("foo", category?.name) + assertEquals(123L, category?.lastUsed?.time) + assertEquals(2, category?.timesUsed) + + verify(client).query( + eq(BASE_URI), + eq(ALL_FIELDS), + eq("$COLUMN_NAME=?"), + queryCaptor.capture(), + isNull() + ) + assertEquals("foo", queryCaptor.firstValue[0]) + } + + @Test(expected = RuntimeException::class) + fun findCategoryTranslatesExceptions() { + whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenThrow(RemoteException("")) + testObject.find("foo") + } + + @Test(expected = RuntimeException::class) + fun recentCategoriesTranslatesExceptions() { + whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenThrow(RemoteException("")) + testObject.recentCategories(1) + } + + @Test + fun recentCategoriesReturnsEmptyList_nullCursor() { + whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenReturn(null) + assertTrue(testObject.recentCategories(1).isEmpty()) + } + + @Test + fun recentCategoriesReturnsEmptyList_emptyCursor() { + whenever(client.query(any(), any(), any(), any(), any())).thenReturn(createCursor(0)) + assertTrue(testObject.recentCategories(1).isEmpty()) + } + + @Test + fun cursorsAreClosedAfterRecentCategoriesQuery() { + val mockCursor: Cursor = mock() + whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenReturn(mockCursor) + whenever(mockCursor.moveToFirst()).thenReturn(false) + + testObject.recentCategories(1) + + verify(mockCursor).close() + } + + @Test + fun recentCategoriesReturnsLessThanLimit() { + whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenReturn(createCursor(1)) + + val result = testObject.recentCategories(10) + + assertEquals(1, result.size) + assertEquals("foo", result[0]) + + verify(client).query( + eq(BASE_URI), + eq(ALL_FIELDS), + isNull(), + queryCaptor.capture(), + eq("$COLUMN_LAST_USED DESC") + ) + assertEquals(0, queryCaptor.firstValue.size) + } + + @Test + fun recentCategoriesHonorsLimit() { + whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenReturn(createCursor(10)) + + val result = testObject.recentCategories(5) + + assertEquals(5, result.size) + } + + private fun createCursor(rowCount: Int) = MatrixCursor(columns, rowCount).apply { + for (i in 0 until rowCount) { + addRow(listOf("1", "foo", "123", "2")) + } + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionDaoTest.kt new file mode 100644 index 000000000..762e0bb85 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionDaoTest.kt @@ -0,0 +1,335 @@ +package fr.free.nrw.commons.contributions + +import android.content.ContentProviderClient +import android.content.ContentValues +import android.database.MatrixCursor +import android.database.sqlite.SQLiteDatabase +import android.net.Uri +import android.os.RemoteException +import com.nhaarman.mockito_kotlin.* +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.contributions.Contribution.* +import fr.free.nrw.commons.contributions.ContributionDao.Table +import fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI +import fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.util.* + +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = [21], application = TestCommonsApplication::class) +class ContributionDaoTest { + private val localUri = "http://example.com/" + private val client: ContentProviderClient = mock() + private val database: SQLiteDatabase = mock() + private val captor = argumentCaptor() + + private lateinit var contentUri: Uri + private lateinit var testObject: ContributionDao + + @Before + fun setUp() { + contentUri = uriForId(111) + testObject = ContributionDao { client } + } + + @Test + fun createTable() { + Table.onCreate(database) + verify(database).execSQL(Table.CREATE_TABLE_STATEMENT) + } + + @Test + fun deleteTable() { + Table.onDelete(database) + + inOrder(database) { + verify(database).execSQL(Table.DROP_TABLE_STATEMENT) + verify(database).execSQL(Table.CREATE_TABLE_STATEMENT) + } + } + + @Test + fun upgradeDatabase_v1_to_v2() { + Table.onUpdate(database, 1, 2) + + inOrder(database) { + verify(database).execSQL(Table.ADD_DESCRIPTION_FIELD) + verify(database).execSQL(Table.ADD_CREATOR_FIELD) + } + } + + @Test + fun upgradeDatabase_v2_to_v3() { + Table.onUpdate(database, 2, 3) + + inOrder(database) { + verify(database).execSQL(Table.ADD_MULTIPLE_FIELD) + verify(database).execSQL(Table.SET_DEFAULT_MULTIPLE) + } + } + + @Test + fun upgradeDatabase_v3_to_v4() { + Table.onUpdate(database, 3, 4) + + // No changes + verifyZeroInteractions(database) + } + + @Test + fun upgradeDatabase_v4_to_v5() { + Table.onUpdate(database, 4, 5) + + // No changes + verifyZeroInteractions(database) + } + + @Test + fun upgradeDatabase_v5_to_v6() { + Table.onUpdate(database, 5, 6) + + inOrder(database) { + verify(database).execSQL(Table.ADD_WIDTH_FIELD) + verify(database).execSQL(Table.SET_DEFAULT_WIDTH) + verify(database).execSQL(Table.ADD_HEIGHT_FIELD) + verify(database).execSQL(Table.SET_DEFAULT_HEIGHT) + verify(database).execSQL(Table.ADD_LICENSE_FIELD) + verify(database).execSQL(Table.SET_DEFAULT_LICENSE) + } + } + + @Test + fun saveNewContribution_nonNullFields() { + whenever(client.insert(isA(), isA())).thenReturn(contentUri) + val contribution = createContribution(true, null, null, null, null) + + testObject.save(contribution) + + assertEquals(contentUri, contribution.contentUri) + verify(client).insert(eq(BASE_URI), captor.capture()) + captor.firstValue.let { + // Long fields + assertEquals(222L, it.getAsLong(Table.COLUMN_LENGTH)) + assertEquals(321L, it.getAsLong(Table.COLUMN_TIMESTAMP)) + assertEquals(333L, it.getAsLong(Table.COLUMN_TRANSFERRED)) + + // Integer fields + assertEquals(STATE_COMPLETED, it.getAsInteger(Table.COLUMN_STATE)) + assertEquals(640, it.getAsInteger(Table.COLUMN_WIDTH)) + assertEquals(480, it.getAsInteger(Table.COLUMN_HEIGHT)) + + // String fields + assertEquals(SOURCE_CAMERA, it.getAsString(Table.COLUMN_SOURCE)) + assertEquals("desc", it.getAsString(Table.COLUMN_DESCRIPTION)) + assertEquals("create", it.getAsString(Table.COLUMN_CREATOR)) + assertEquals("007", it.getAsString(Table.COLUMN_LICENSE)) + } + } + + @Test + fun saveNewContribution_nullableFieldsAreNull() { + whenever(client.insert(isA(), isA())).thenReturn(contentUri) + val contribution = createContribution(true, null, null, null, null) + + testObject.save(contribution) + + assertEquals(contentUri, contribution.contentUri) + verify(client).insert(eq(BASE_URI), captor.capture()) + captor.firstValue.let { + // Nullable fields are absent if null + assertFalse(it.containsKey(Table.COLUMN_LOCAL_URI)) + assertFalse(it.containsKey(Table.COLUMN_IMAGE_URL)) + assertFalse(it.containsKey(Table.COLUMN_UPLOADED)) + } + } + + @Test + fun saveNewContribution_nullableImageUrlUsesFileAsBackup() { + whenever(client.insert(isA(), isA())).thenReturn(contentUri) + val contribution = createContribution(true, null, null, null, "file") + + testObject.save(contribution) + + assertEquals(contentUri, contribution.contentUri) + verify(client).insert(eq(BASE_URI), captor.capture()) + captor.firstValue.let { + // Nullable fields are absent if null + assertFalse(it.containsKey(Table.COLUMN_LOCAL_URI)) + assertFalse(it.containsKey(Table.COLUMN_UPLOADED)) + assertEquals(Utils.makeThumbBaseUrl("file"), it.getAsString(Table.COLUMN_IMAGE_URL)) + } + } + + @Test + fun saveNewContribution_nullableFieldsAreNonNull() { + whenever(client.insert(isA(), isA())).thenReturn(contentUri) + val contribution = createContribution(true, Uri.parse(localUri), + "image", Date(456L), null) + + testObject.save(contribution) + + assertEquals(contentUri, contribution.contentUri) + verify(client).insert(eq(BASE_URI), captor.capture()) + captor.firstValue.let { + assertEquals(localUri, it.getAsString(Table.COLUMN_LOCAL_URI)) + assertEquals("image", it.getAsString(Table.COLUMN_IMAGE_URL)) + assertEquals(456L, it.getAsLong(Table.COLUMN_UPLOADED)) + } + } + + @Test + fun saveNewContribution_booleanEncodesTrue() { + whenever(client.insert(isA(), isA())).thenReturn(contentUri) + val contribution = createContribution(true, null, null, null, null) + + testObject.save(contribution) + + assertEquals(contentUri, contribution.contentUri) + verify(client).insert(eq(BASE_URI), captor.capture()) + + // Boolean true --> 1 for ths encoding scheme + assertEquals("Boolean true should be encoded as 1", 1, + captor.firstValue.getAsInteger(Table.COLUMN_MULTIPLE)) + } + + @Test + fun saveNewContribution_booleanEncodesFalse() { + whenever(client.insert(isA(), isA())).thenReturn(contentUri) + val contribution = createContribution(false, null, null, null, null) + + testObject.save(contribution) + + assertEquals(contentUri, contribution.contentUri) + verify(client).insert(eq(BASE_URI), captor.capture()) + + // Boolean true --> 1 for ths encoding scheme + assertEquals("Boolean false should be encoded as 0", 0, + captor.firstValue.getAsInteger(Table.COLUMN_MULTIPLE)) + } + + @Test + fun saveExistingContribution() { + val contribution = createContribution(false, null, null, null, null) + contribution.contentUri = contentUri + + testObject.save(contribution) + + verify(client).update(eq(contentUri), isA(), isNull(), isNull()) + } + + @Test(expected = RuntimeException::class) + fun saveTranslatesExceptions() { + whenever(client.insert(isA(), isA())).thenThrow(RemoteException("")) + + testObject.save(createContribution(false, null, null, null, null)) + } + + @Test(expected = RuntimeException::class) + fun deleteTranslatesExceptions() { + whenever(client.delete(anyOrNull(), anyOrNull(), anyOrNull())).thenThrow(RemoteException("")) + + val contribution = createContribution(false, null, null, null, null) + contribution.contentUri = contentUri + testObject.delete(contribution) + } + + @Test(expected = RuntimeException::class) + fun exceptionThrownWhenAttemptingToDeleteUnsavedContribution() { + testObject.delete(createContribution(false, null, null, null, null)) + } + + @Test + fun deleteExistingContribution() { + val contribution = createContribution(false, null, null, null, null) + contribution.contentUri = contentUri + + testObject.delete(contribution) + + verify(client).delete(eq(contentUri), isNull(), isNull()) + } + + @Test + fun createFromCursor() { + val created = 321L + val uploaded = 456L + createCursor(created, uploaded, false, localUri).let { mc -> + testObject.fromCursor(mc).let { + assertEquals(uriForId(111), it.contentUri) + assertEquals("file", it.filename) + assertEquals(localUri, it.localUri.toString()) + assertEquals("image", it.imageUrl) + assertEquals(created, it.timestamp.time) + assertEquals(created, it.dateCreated.time) + assertEquals(STATE_QUEUED, it.state) + assertEquals(222L, it.dataLength) + assertEquals(uploaded, it.dateUploaded?.time) + assertEquals(88L, it.transferred) + assertEquals(SOURCE_GALLERY, it.source) + assertEquals("desc", it.description) + assertEquals("create", it.creator) + assertEquals(640, it.width) + assertEquals(480, it.height) + assertEquals("007", it.license) + } + } + } + + @Test + fun createFromCursor_nullableTimestamps() { + createCursor(0L, 0L, false, localUri).let { mc -> + testObject.fromCursor(mc).let { + assertNull(it.timestamp) + assertNull(it.dateCreated) + assertNull(it.dateUploaded) + } + } + } + + @Test + fun createFromCursor_nullableLocalUri() { + createCursor(0L, 0L, false, "").let { mc -> + testObject.fromCursor(mc).let { + assertNull(it.localUri) + assertNull(it.dateCreated) + assertNull(it.dateUploaded) + } + } + } + + @Test + fun createFromCursor_booleanEncoding() { + val mcFalse = createCursor(0L, 0L, false, localUri) + assertFalse(testObject.fromCursor(mcFalse).multiple) + + val mcHammer = createCursor(0L, 0L, true, localUri) + assertTrue(testObject.fromCursor(mcHammer).multiple) + } + + private fun createCursor(created: Long, uploaded: Long, multiple: Boolean, localUri: String) = + MatrixCursor(Table.ALL_FIELDS, 1).apply { + addRow(listOf("111", "file", localUri, "image", + created, STATE_QUEUED, 222L, uploaded, 88L, SOURCE_GALLERY, "desc", + "create", if (multiple) 1 else 0, 640, 480, "007")) + moveToFirst() + } + + private fun createContribution(isMultiple: Boolean, localUri: Uri?, imageUrl: String?, dateUploaded: Date?, filename: String?) = + Contribution(localUri, imageUrl, filename, "desc", 222L, Date(321L), dateUploaded, + "create", "edit", "coords").apply { + state = STATE_COMPLETED + transferred = 333L + source = SOURCE_CAMERA + license = "007" + multiple = isMultiple + timestamp = Date(321L) + width = 640 + height = 480 // VGA should be enough for anyone, right? + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/modifications/ModifierSequenceDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/modifications/ModifierSequenceDaoTest.kt new file mode 100644 index 000000000..bf0ddf772 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/modifications/ModifierSequenceDaoTest.kt @@ -0,0 +1,156 @@ +package fr.free.nrw.commons.modifications + +import android.content.ContentProviderClient +import android.content.ContentValues +import android.database.MatrixCursor +import android.database.sqlite.SQLiteDatabase +import android.net.Uri +import android.os.RemoteException +import com.nhaarman.mockito_kotlin.* +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.modifications.ModificationsContentProvider.BASE_URI +import fr.free.nrw.commons.modifications.ModifierSequenceDao.Table.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = [21], application = TestCommonsApplication::class) +class ModifierSequenceDaoTest { + + private val mediaUrl = "http://example.com/" + private val columns = arrayOf(COLUMN_ID, COLUMN_MEDIA_URI, COLUMN_DATA) + private val client: ContentProviderClient = mock() + private val database: SQLiteDatabase = mock() + private val contentValuesCaptor = argumentCaptor() + + private lateinit var testObject: ModifierSequenceDao + + @Before + fun setUp() { + testObject = ModifierSequenceDao { client } + } + + @Test + fun createFromCursorWithEmptyModifiers() { + testObject.fromCursor(createCursor("")).let { + assertEquals(mediaUrl, it.mediaUri.toString()) + assertEquals(BASE_URI.buildUpon().appendPath("1").toString(), it.contentUri.toString()) + assertTrue(it.modifiers.isEmpty()) + } + } + + @Test + fun createFromCursorWtihCategoryModifier() { + val cursor = createCursor("{\"name\": \"CategoriesModifier\", \"data\": {}}") + + val seq = testObject.fromCursor(cursor) + + assertEquals(1, seq.modifiers.size) + assertTrue(seq.modifiers[0] is CategoryModifier) + } + + @Test + fun createFromCursorWithRemoveModifier() { + val cursor = createCursor("{\"name\": \"TemplateRemoverModifier\", \"data\": {}}") + + val seq = testObject.fromCursor(cursor) + + assertEquals(1, seq.modifiers.size) + assertTrue(seq.modifiers[0] is TemplateRemoveModifier) + } + + @Test + fun deleteSequence() { + whenever(client.delete(isA(), isNull(), isNull())).thenReturn(1) + val seq = testObject.fromCursor(createCursor("")) + + testObject.delete(seq) + + verify(client).delete(eq(seq.contentUri), isNull(), isNull()) + } + + @Test(expected = RuntimeException::class) + fun deleteTranslatesRemoteExceptions() { + whenever(client.delete(isA(), isNull(), isNull())).thenThrow(RemoteException("")) + val seq = testObject.fromCursor(createCursor("")) + + testObject.delete(seq) + } + + @Test + fun saveExistingSequence() { + val modifierJson = "{\"name\":\"CategoriesModifier\",\"data\":{}}" + val expectedData = "{\"modifiers\":[$modifierJson]}" + val cursor = createCursor(modifierJson) + val seq = testObject.fromCursor(cursor) + + testObject.save(seq) + + verify(client).update(eq(seq.contentUri), contentValuesCaptor.capture(), isNull(), isNull()) + contentValuesCaptor.firstValue.let { + assertEquals(2, it.size()) + assertEquals(mediaUrl, it.get(COLUMN_MEDIA_URI)) + assertEquals(expectedData, it.get(COLUMN_DATA)) + } + } + + @Test + fun saveNewSequence() { + val expectedContentUri = BASE_URI.buildUpon().appendPath("1").build() + whenever(client.insert(isA(), isA())).thenReturn(expectedContentUri) + val seq = ModifierSequence(Uri.parse(mediaUrl)) + + testObject.save(seq) + + assertEquals(expectedContentUri.toString(), seq.contentUri.toString()) + verify(client).insert(eq(ModificationsContentProvider.BASE_URI), contentValuesCaptor.capture()) + contentValuesCaptor.firstValue.let { + assertEquals(2, it.size()) + assertEquals(mediaUrl, it.get(COLUMN_MEDIA_URI)) + assertEquals("{\"modifiers\":[]}", it.get(COLUMN_DATA)) + } + } + + @Test(expected = RuntimeException::class) + fun saveTranslatesRemoteExceptions() { + whenever(client.insert(isA(), isA())).thenThrow(RemoteException("")) + testObject.save(ModifierSequence(Uri.parse(mediaUrl))) + } + + @Test + fun createTable() { + onCreate(database) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + + @Test + fun updateTable() { + onUpdate(database, 1, 2) + + inOrder(database) { + verify(database).execSQL(DROP_TABLE_STATEMENT) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + } + + @Test + fun deleteTable() { + onDelete(database) + + inOrder(database) { + verify(database).execSQL(DROP_TABLE_STATEMENT) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + } + + private fun createCursor(modifierJson: String) = MatrixCursor(columns, 1).apply { + addRow(listOf("1", mediaUrl, "{\"modifiers\": [$modifierJson]}")) + moveToFirst() + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt new file mode 100644 index 000000000..85f1ed98e --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt @@ -0,0 +1,251 @@ +package fr.free.nrw.commons.mwapi + +import android.content.SharedPreferences +import android.os.Build +import android.preference.PreferenceManager +import com.google.gson.Gson +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.TestCommonsApplication +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import java.net.URLDecoder +import java.util.* + +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = intArrayOf(21), application = TestCommonsApplication::class) +class ApacheHttpClientMediaWikiApiTest { + + private lateinit var testObject: ApacheHttpClientMediaWikiApi + private lateinit var server: MockWebServer + private lateinit var wikidataServer: MockWebServer + private lateinit var sharedPreferences: SharedPreferences + private lateinit var categoryPreferences: SharedPreferences + + @Before + fun setUp() { + server = MockWebServer() + wikidataServer = MockWebServer() + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application) + categoryPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application) + testObject = ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.hostName + ":" + server.port + "/", "http://" + wikidataServer.hostName + ":" + wikidataServer.port + "/", sharedPreferences, categoryPreferences, Gson()) + testObject.setWikiMediaToolforgeUrl("http://" + server.hostName + ":" + server.port + "/") + } + + @After + fun teardown() { + server.shutdown() + } + + @Test + fun authCookiesAreHandled() { + assertEquals("", testObject.authCookie) + + testObject.authCookie = "cookie=chocolate-chip" + + assertEquals("cookie=chocolate-chip", testObject.authCookie) + } + + @Test + fun simpleLoginWithWrongPassword() { + server.enqueue(MockResponse().setBody("")) + server.enqueue(MockResponse().setBody("")) + + val result = testObject.login("foo", "bar") + + assertBasicRequestParameters(server, "POST").let { loginTokenRequest -> + parseBody(loginTokenRequest.body.readUtf8()).let { body -> + assertEquals("xml", body["format"]) + assertEquals("query", body["action"]) + assertEquals("login", body["type"]) + assertEquals("tokens", body["meta"]) + } + } + + assertBasicRequestParameters(server, "POST").let { loginRequest -> + parseBody(loginRequest.body.readUtf8()).let { body -> + assertEquals("1", body["rememberMe"]) + assertEquals("foo", body["username"]) + assertEquals("bar", body["password"]) + assertEquals("baz", body["logintoken"]) + assertEquals("https://commons.wikimedia.org", body["loginreturnurl"]) + assertEquals("xml", body["format"]) + } + } + + assertEquals("wrongpassword", result) + } + + @Test + fun simpleLogin() { + server.enqueue(MockResponse().setBody("")) + server.enqueue(MockResponse().setBody("")) + + val result = testObject.login("foo", "bar") + + assertBasicRequestParameters(server, "POST").let { loginTokenRequest -> + parseBody(loginTokenRequest.body.readUtf8()).let { body -> + assertEquals("xml", body["format"]) + assertEquals("query", body["action"]) + assertEquals("login", body["type"]) + assertEquals("tokens", body["meta"]) + } + } + + assertBasicRequestParameters(server, "POST").let { loginRequest -> + parseBody(loginRequest.body.readUtf8()).let { body -> + assertEquals("1", body["rememberMe"]) + assertEquals("foo", body["username"]) + assertEquals("bar", body["password"]) + assertEquals("baz", body["logintoken"]) + assertEquals("https://commons.wikimedia.org", body["loginreturnurl"]) + assertEquals("xml", body["format"]) + } + } + + assertEquals("PASS", result) + } + + @Test + fun twoFactorLogin() { + server.enqueue(MockResponse().setBody("")) + server.enqueue(MockResponse().setBody("")) + + val result = testObject.login("foo", "bar", "2fa") + + assertBasicRequestParameters(server, "POST").let { loginTokenRequest -> + parseBody(loginTokenRequest.body.readUtf8()).let { body -> + assertEquals("xml", body["format"]) + assertEquals("query", body["action"]) + assertEquals("login", body["type"]) + assertEquals("tokens", body["meta"]) + } + } + + assertBasicRequestParameters(server, "POST").let { loginRequest -> + parseBody(loginRequest.body.readUtf8()).let { body -> + assertEquals("true", body["rememberMe"]) + assertEquals("foo", body["username"]) + assertEquals("bar", body["password"]) + assertEquals("baz", body["logintoken"]) + assertEquals("true", body["logincontinue"]) + assertEquals("2fa", body["OATHToken"]) + assertEquals("xml", body["format"]) + } + } + + assertEquals("PASS", result) + } + + @Test + fun validateLoginForLoggedInUser() { + server.enqueue(MockResponse().setBody("")) + + val result = testObject.validateLogin() + + assertBasicRequestParameters(server, "GET").let { loginTokenRequest -> + parseQueryParams(loginTokenRequest).let { body -> + assertEquals("xml", body["format"]) + assertEquals("query", body["action"]) + assertEquals("userinfo", body["meta"]) + } + } + + assertTrue(result) + } + + @Test + fun validateLoginForLoggedOutUser() { + server.enqueue(MockResponse().setBody("")) + + val result = testObject.validateLogin() + + assertBasicRequestParameters(server, "GET").let { loginTokenRequest -> + parseQueryParams(loginTokenRequest).let { params -> + assertEquals("xml", params["format"]) + assertEquals("query", params["action"]) + assertEquals("userinfo", params["meta"]) + } + } + + assertFalse(result) + } + + @Test + fun editToken() { + server.enqueue(MockResponse().setBody("")) + + val result = testObject.editToken + + assertBasicRequestParameters(server, "GET").let { loginTokenRequest -> + parseQueryParams(loginTokenRequest).let { params -> + assertEquals("xml", params["format"]) + assertEquals("tokens", params["action"]) + assertEquals("edit", params["type"]) + } + } + + assertEquals("baz", result) + } + + @Test + fun fileExistsWithName_FileNotFound() { + server.enqueue(MockResponse().setBody(" ")) + + val result = testObject.fileExistsWithName("foo") + + assertBasicRequestParameters(server, "GET").let { request -> + parseQueryParams(request).let { params -> + assertEquals("xml", params["format"]) + assertEquals("query", params["action"]) + assertEquals("imageinfo", params["prop"]) + assertEquals("File:foo", params["titles"]) + } + } + + assertFalse(result) + } + + @Test + fun getUploadCount() { + server.enqueue(MockResponse().setBody("23\n")) + + val testObserver = testObject.getUploadCount("testUsername").test() + + assertEquals("testUsername", parseQueryParams(server.takeRequest())["user"]) + assertEquals(1, testObserver.valueCount()) + assertEquals(23, testObserver.values()[0]) + } + + private fun assertBasicRequestParameters(server: MockWebServer, method: String): RecordedRequest = server.takeRequest().let { + assertEquals("/", it.requestUrl.encodedPath()) + assertEquals(method, it.method) + assertEquals("Commons/${BuildConfig.VERSION_NAME} (https://mediawiki.org/wiki/Apps/Commons) Android/${Build.VERSION.RELEASE}", + it.getHeader("User-Agent")) + if ("POST" == method) { + assertEquals("application/x-www-form-urlencoded", it.getHeader("Content-Type")) + } + return it + } + + private fun parseQueryParams(request: RecordedRequest) = HashMap().apply { + request.requestUrl.let { + it.queryParameterNames().forEach { name -> put(name, it.queryParameter(name)) } + } + } + + private fun parseBody(body: String): Map = HashMap().apply { + body.split("&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray().forEach { prop -> + val pair = prop.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + put(pair[0], URLDecoder.decode(pair[1], "utf-8")) + } + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/CategoryApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/CategoryApiTest.kt new file mode 100644 index 000000000..76f34d55d --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/CategoryApiTest.kt @@ -0,0 +1,178 @@ +package fr.free.nrw.commons.mwapi + +import com.google.gson.Gson +import fr.free.nrw.commons.mwapi.model.Page +import fr.free.nrw.commons.mwapi.model.PageCategory +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class CategoryApiTest { + private lateinit var server: MockWebServer + private lateinit var url: String + private lateinit var categoryApi: CategoryApi + + @Before + fun setUp() { + server = MockWebServer() + url = "http://${server.hostName}:${server.port}/" + categoryApi = CategoryApi(OkHttpClient.Builder().build(), Gson(), HttpUrl.parse(url)) + } + + @After + fun teardown() { + server.shutdown() + } + + @Test + fun apiReturnsEmptyListWhenError() { + server.enqueue(MockResponse().setResponseCode(400).setBody("")) + + assertTrue(categoryApi.request("foo").blockingGet().isEmpty()) + } + + @Test + fun apiReturnsEmptyWhenTheresNoQuery() { + server.success(emptyMap()) + + assertTrue(categoryApi.request("foo").blockingGet().isEmpty()) + } + + @Test + fun apiReturnsEmptyWhenQueryHasNoPages() { + server.success(mapOf("query" to emptyMap())) + + assertTrue(categoryApi.request("foo").blockingGet().isEmpty()) + } + + @Test + fun apiReturnsEmptyWhenQueryHasPagesButTheyreEmpty() { + server.success(mapOf("query" to + mapOf("pages" to emptyList()))) + + assertTrue(categoryApi.request("foo").blockingGet().isEmpty()) + } + + @Test + fun singlePageSingleCategory() { + server.success(mapOf("query" to + mapOf("pages" to listOf( + page(listOf("one")) + )))) + + val response = categoryApi.request("foo").blockingGet() + + assertEquals(1, response.size) + assertEquals("one", response[0]) + } + + @Test + fun multiplePagesSingleCategory() { + server.success(mapOf("query" to + mapOf("pages" to listOf( + page(listOf("one")), + page(listOf("two")) + )))) + + val response = categoryApi.request("foo").blockingGet() + + assertEquals(2, response.size) + assertEquals("one", response[0]) + assertEquals("two", response[1]) + } + + @Test + fun singlePageMultipleCategories() { + server.success(mapOf("query" to + mapOf("pages" to listOf( + page(listOf("one", "two")) + )))) + + val response = categoryApi.request("foo").blockingGet() + + assertEquals(2, response.size) + assertEquals("one", response[0]) + assertEquals("two", response[1]) + } + + @Test + fun multiplePagesMultipleCategories() { + server.success(mapOf("query" to + mapOf("pages" to listOf( + page(listOf("one", "two")), + page(listOf("three", "four")) + )))) + + val response = categoryApi.request("foo").blockingGet() + + assertEquals(4, response.size) + assertEquals("one", response[0]) + assertEquals("two", response[1]) + assertEquals("three", response[2]) + assertEquals("four", response[3]) + } + + @Test + fun multiplePagesMultipleCategories_duplicatesRemoved() { + server.success(mapOf("query" to + mapOf("pages" to listOf( + page(listOf("one", "two", "three")), + page(listOf("three", "four", "one")) + )))) + + val response = categoryApi.request("foo").blockingGet() + + assertEquals(4, response.size) + assertEquals("one", response[0]) + assertEquals("two", response[1]) + assertEquals("three", response[2]) + assertEquals("four", response[3]) + } + + @Test + fun requestSendsWhatWeExpect() { + server.success(mapOf("query" to mapOf("pages" to emptyList()))) + + val coords = "foo,bar" + categoryApi.request(coords).blockingGet() + + server.takeRequest().let { request -> + assertEquals("GET", request.method) + assertEquals("/w/api.php", request.requestUrl.encodedPath()) + request.requestUrl.let { url -> + assertEquals("query", url.queryParameter("action")) + assertEquals("categories|coordinates|pageprops", url.queryParameter("prop")) + assertEquals("json", url.queryParameter("format")) + assertEquals("!hidden", url.queryParameter("clshow")) + assertEquals("type|name|dim|country|region|globe", url.queryParameter("coprop")) + assertEquals(coords, url.queryParameter("codistancefrompoint")) + assertEquals("geosearch", url.queryParameter("generator")) + assertEquals(coords, url.queryParameter("ggscoord")) + assertEquals("10000", url.queryParameter("ggsradius")) + assertEquals("10", url.queryParameter("ggslimit")) + assertEquals("6", url.queryParameter("ggsnamespace")) + assertEquals("type|name|dim|country|region|globe", url.queryParameter("ggsprop")) + assertEquals("all", url.queryParameter("ggsprimary")) + assertEquals("2", url.queryParameter("formatversion")) + } + } + } + + private fun page(catList: List) = Page().apply { + categories = catList.map { + PageCategory().apply { + title = "Category:$it" + } + }.toTypedArray() + } +} + +fun MockWebServer.success(response: Map) { + enqueue(MockResponse().setResponseCode(200).setBody(Gson().toJson(response))) +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/ApiResponseTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/ApiResponseTest.kt new file mode 100644 index 000000000..41406a894 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/ApiResponseTest.kt @@ -0,0 +1,28 @@ +package fr.free.nrw.commons.mwapi.model + +import org.junit.Assert.* +import org.junit.Test + +class ApiResponseTest { + @Test + fun hasPages_whenQueryIsNull() { + val response = ApiResponse() + assertFalse(response.hasPages()) + } + + @Test + fun hasPages_whenPagesIsNull() { + val response = ApiResponse() + response.query = Query() + response.query.pages = null + assertFalse(response.hasPages()) + } + + @Test + fun hasPages_defaultsToSafeValue() { + val response = ApiResponse() + response.query = Query() + assertNotNull(response.query.pages) + assertTrue(response.hasPages()) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/PageCategoryTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/PageCategoryTest.kt new file mode 100644 index 000000000..fcc3d408f --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/PageCategoryTest.kt @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.mwapi.model + +import org.junit.Assert.assertEquals +import org.junit.Test + +class PageCategoryTest { + @Test + fun stripPrefix_whenPresent() { + val testObject = PageCategory() + testObject.title = "Category:Foo" + assertEquals("Foo", testObject.withoutPrefix()) + } + + @Test + fun stripPrefix_prefixAbsent() { + val testObject = PageCategory() + testObject.title = "Foo_Bar" + assertEquals("Foo_Bar", testObject.withoutPrefix()) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/PageTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/PageTest.kt new file mode 100644 index 000000000..4179b4fb5 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/PageTest.kt @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.mwapi.model + +import org.junit.Assert.assertNotNull +import org.junit.Test + +class PageTest { + @Test + fun categoriesDefaultToSafeValue() { + val page = Page() + assertNotNull(page.getCategories()) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/GpsCategoryModelTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/GpsCategoryModelTest.kt new file mode 100644 index 000000000..cd2d77ada --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/GpsCategoryModelTest.kt @@ -0,0 +1,77 @@ +package fr.free.nrw.commons.upload + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class GpsCategoryModelTest { + + private lateinit var testObject : GpsCategoryModel + + @Before + fun setUp() { + testObject = GpsCategoryModel() + } + + @Test + fun initiallyTheModelIsEmpty() { + assertFalse(testObject.gpsCatExists) + assertTrue(testObject.categoryList.isEmpty()) + } + + @Test + fun addingCategoriesToTheModel() { + testObject.add("one") + assertTrue(testObject.gpsCatExists) + assertFalse(testObject.categoryList.isEmpty()) + assertEquals(listOf("one"), testObject.categoryList) + } + + @Test + fun duplicatesAreIgnored() { + testObject.add("one") + testObject.add("one") + assertEquals(listOf("one"), testObject.categoryList) + } + + @Test + fun modelProtectsAgainstExternalModification() { + testObject.add("one") + + val list = testObject.categoryList + list.add("two") + + assertEquals(listOf("one"), testObject.categoryList) + } + + @Test + fun clearingTheModel() { + testObject.add("one") + + testObject.clear() + assertFalse(testObject.gpsCatExists) + assertTrue(testObject.categoryList.isEmpty()) + + testObject.add("two") + assertEquals(listOf("two"), testObject.categoryList) + } + + @Test + fun settingTheListHandlesNull() { + testObject.add("one") + + testObject.categoryList = null + + assertFalse(testObject.gpsCatExists) + assertTrue(testObject.categoryList.isEmpty()) + } + + @Test + fun setttingTheListOverwritesExistingValues() { + testObject.add("one") + + testObject.categoryList = listOf("two") + + assertEquals(listOf("two"), testObject.categoryList) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/utils/StringSortingUtilsTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/utils/StringSortingUtilsTest.kt new file mode 100644 index 000000000..83e2f7169 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/utils/StringSortingUtilsTest.kt @@ -0,0 +1,41 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.utils.StringSortingUtils.sortBySimilarity +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.Collections.sort + +class StringSortingUtilsTest { + + @Test + fun testSortingNumbersBySimilarity() { + val actualList = listOf("1234567", "4567", "12345", "123", "1234") + val expectedList = listOf("1234", "12345", "123", "1234567", "4567") + + sort(actualList, sortBySimilarity("1234")) + + assertEquals(expectedList, actualList) + } + + @Test + fun testSortingTextBySimilarity() { + val actualList = listOf("The quick brown fox", + "quick brown fox", + "The", + "The quick ", + "The fox", + "brown fox", + "fox") + val expectedList = listOf("The", + "The fox", + "The quick ", + "The quick brown fox", + "quick brown fox", + "brown fox", + "fox") + + sort(actualList, sortBySimilarity("The")) + + assertEquals(expectedList, actualList) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index b0e8b6718..42dfeec75 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.1.51' + ext.kotlin_version = '1.2.31' repositories { jcenter() mavenCentral() diff --git a/gradle.properties b/gradle.properties index 850003852..05aa34949 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,17 +14,17 @@ # org.gradle.parallel=true #Thu Mar 01 15:28:48 IST 2018 systemProp.http.proxyPort=0 -compileSdkVersion=android-26 +compileSdkVersion=android-27 android.useDeprecatedNdk=true BUTTERKNIFE_VERSION=8.6.0 org.gradle.jvmargs=-Xmx1536M -buildToolsVersion=26.0.2 -targetSdkVersion=25 +buildToolsVersion=27.0.0 +targetSdkVersion=27 #TODO: Temporary disabled. https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#aapt2 #Refer to PR: https://github.com/commons-app/apps-android-commons/pull/932 android.enableAapt2=false -SUPPORT_LIB_VERSION=26.0.2 +SUPPORT_LIB_VERSION=27.1.1 minSdkVersion=15 systemProp.http.proxyHost= LEAK_CANARY=1.5.4 diff --git a/script/style/checkstyle.xml b/script/style/checkstyle.xml index cb0b13dca..216d1bce2 100644 --- a/script/style/checkstyle.xml +++ b/script/style/checkstyle.xml @@ -7,6 +7,8 @@ Modified from https://github.com/checkstyle/checkstyle/blob/master/src/main/resources/google_checks.xml. Modifications are: - doubled each value in Indentation + - exceptions for Butter Knife and Espresso + - larger (max)LineLength Checkstyle configuration that checks the Google coding conventions from Google Java Style that can be found at https://google.github.io/styleguide/javaguide.html. @@ -44,7 +46,7 @@ - + @@ -56,7 +58,7 @@ - +