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/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 9d7150008..37e104d14 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,7 @@ +## Title (required) + +Fixes #{GitHub issue number and title (Please do not forget adding title) } + ## Description (required) Fixes #{GitHub issue number and title} @@ -12,4 +16,4 @@ Tested on {API level & name of device/emulator}, with {build variant, e.g. ProdD {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._ +_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 5d37f8f54..9300cd9aa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,6 @@ dependencies { implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar' implementation 'in.yuvi:http.fluent:1.3' implementation 'com.github.chrisbanes:PhotoView:2.0.0' - implementation 'com.android.volley:volley:1.0.0' implementation 'ch.acra:acra:4.9.2' implementation 'org.mediawiki:api:1.3' implementation 'commons-codec:commons-codec:1.10' @@ -20,7 +19,7 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.5.1' implementation 'info.debatty:java-string-similarity:0.24' implementation 'com.borjabravo:readmoretextview:2.1.0' - implementation 'com.android.support.constraint:constraint-layout:1.0.2' + implementation 'com.android.support.constraint:constraint-layout:1.1.0' implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.4.1@aar'){ transitive=true } @@ -69,10 +68,16 @@ dependencies { 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.2-alpha1' + 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" @@ -117,7 +122,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" @@ -130,6 +135,7 @@ android { productFlavors { prod { 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/\"" @@ -146,6 +152,7 @@ android { beta { // What values do we need to hit the BETA versions of the site / api ? 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/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/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 17f6770d2..80bc59c27 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" /> 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..2b2cad5fe 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; @@ -21,13 +20,14 @@ import java.io.File; import javax.inject.Inject; import javax.inject.Named; +import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.category.CategoryDao; 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/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/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index d0fb628e3..b19d70a4e 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 @@ -271,11 +271,11 @@ public class LoginActivity extends AccountAuthenticatorActivity { showMessageAndCancelDialog(R.string.login_failed_network); } 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(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(Locale.getDefault()).contains("throttle".toLowerCase())) { // Matches unknown throttle error codes 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 e804189ab..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 @@ -39,7 +39,7 @@ 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.GpsCategoryModel; import fr.free.nrw.commons.utils.StringSortingUtils; import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; @@ -73,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; @@ -253,7 +254,6 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment { } private Observable defaultCategories() { - Observable directCat = directCategories(); if (hasDirectCategories) { Timber.d("Image has direct Cat"); @@ -287,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/CategoryImagesListFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java index 3b6734edd..a44e19a29 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java @@ -224,4 +224,14 @@ public class CategoryImagesListFragment extends DaggerFragment { 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/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/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index 2b6c56c43..4aae422e3 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 @@ -127,7 +127,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/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/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 49c422633..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 @@ -284,6 +284,7 @@ public class LocationServiceManager implements LocationListener { LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving LOCATION_NOT_CHANGED, - PERMISSION_JUST_GRANTED + 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 9614c4f00..54bb5981c 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 @@ -35,6 +35,9 @@ import java.util.Locale; import javax.inject.Inject; import javax.inject.Provider; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import fr.free.nrw.commons.License; import fr.free.nrw.commons.LicenseList; import fr.free.nrw.commons.Media; @@ -56,16 +59,16 @@ import static android.widget.Toast.LENGTH_SHORT; public class MediaDetailFragment extends CommonsDaggerSupportFragment { private boolean editable; - private boolean isFeaturedMedia; + private boolean isCategoryImage; private MediaDetailPagerFragment.MediaDetailProvider detailProvider; private int index; - public static MediaDetailFragment forMedia(int index, boolean editable, boolean isFeaturedMedia) { + public static MediaDetailFragment forMedia(int index, boolean editable, boolean isCategoryImage) { MediaDetailFragment mf = new MediaDetailFragment(); Bundle state = new Bundle(); state.putBoolean("editable", editable); - state.putBoolean("isFeaturedMedia", isFeaturedMedia); + state.putBoolean("isCategoryImage", isCategoryImage); state.putInt("index", index); state.putInt("listIndex", 0); state.putInt("listTop", 0); @@ -128,7 +131,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { super.onSaveInstanceState(outState); outState.putInt("index", index); outState.putBoolean("editable", editable); - outState.putBoolean("isFeaturedMedia", isFeaturedMedia); + outState.putBoolean("isCategoryImage", isCategoryImage); getScrollPosition(); outState.putInt("listTop", initialListTop); @@ -144,12 +147,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { if (savedInstanceState != null) { editable = savedInstanceState.getBoolean("editable"); - isFeaturedMedia = savedInstanceState.getBoolean("isFeaturedMedia"); + isCategoryImage = savedInstanceState.getBoolean("isCategoryImage"); index = savedInstanceState.getInt("index"); initialListTop = savedInstanceState.getInt("listTop"); } else { editable = getArguments().getBoolean("editable"); - isFeaturedMedia = getArguments().getBoolean("isFeaturedMedia"); + isCategoryImage = getArguments().getBoolean("isCategoryImage"); index = getArguments().getInt("index"); initialListTop = 0; } @@ -161,7 +164,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { ButterKnife.bind(this,view); - if (isFeaturedMedia){ + if (isCategoryImage){ authorLayout.setVisibility(VISIBLE); } else { authorLayout.setVisibility(GONE); @@ -328,7 +331,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { if (!TextUtils.isEmpty(licenseLink(media))) { openWebBrowser(licenseLink(media)); } else { - if(isFeaturedMedia) { + 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); @@ -503,8 +506,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { if (media.getRequestedDeletion()){ delete.setVisibility(GONE); nominatedForDeletion.setVisibility(VISIBLE); - } - else{ + } 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 62d1261cf..2c58e8dbb 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 @@ -2,9 +2,11 @@ package fr.free.nrw.commons.media; import android.annotation.SuppressLint; import android.app.DownloadManager; +import android.app.WallpaperManager; import android.content.Intent; import android.content.SharedPreferences; import android.database.DataSetObserver; +import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -26,6 +28,8 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Toast; +import java.io.IOException; + import butterknife.BindView; import butterknife.ButterKnife; import javax.inject.Inject; @@ -38,12 +42,15 @@ 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; import static android.content.Intent.ACTION_VIEW; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.widget.Toast.LENGTH_SHORT; +import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext; public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener { @@ -140,6 +147,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()); @@ -155,6 +166,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. 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 6629d0933..96bf9cbcf 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 @@ -25,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; @@ -62,6 +64,7 @@ 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 defaultPreferences; private SharedPreferences categoryPreferences; @@ -69,6 +72,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { public ApacheHttpClientMediaWikiApi(Context context, String apiURL, + String wikidatApiURL, SharedPreferences defaultPreferences, SharedPreferences categoryPreferences, Gson gson) { @@ -82,6 +86,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent()); httpClient = new DefaultHttpClient(cm, params); api = new MWApi(apiURL, httpClient); + wikidataApi = new MWApi(wikidatApiURL, httpClient); this.defaultPreferences = defaultPreferences; this.categoryPreferences = categoryPreferences; this.gson = gson; @@ -206,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") @@ -351,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) { @@ -444,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) { @@ -586,6 +692,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 c0bd2fd87..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 @@ -27,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; @@ -49,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/NearbyActivity.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java index 35e15b0d9..df31a8761 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 @@ -42,11 +42,13 @@ 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; @@ -55,8 +57,12 @@ 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; @@ -76,6 +82,8 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp LocationServiceManager locationManager; @Inject NearbyController nearbyController; + @Inject WikidataEditListener wikidataEditListener; + @Inject @Named("application_preferences") SharedPreferences applicationPrefs; private LatLng curLatLng; @@ -110,6 +118,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp initBottomSheetBehaviour(); initDrawer(); + wikidataEditListener.setAuthenticationStateListener(this); } private void resumeFragment() { @@ -219,7 +228,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp //Still need to check if GPS is enabled checkGps(); lastKnownLocation = locationManager.getLKL(); - refreshView(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED); + refreshView(PERMISSION_JUST_GRANTED); } else { //If permission not granted, go to page that says Nearby Places cannot be displayed hideProgressBar(); @@ -279,7 +288,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)) { @@ -305,7 +314,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp } } } else { - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } } @@ -314,7 +323,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); } } @@ -373,8 +382,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)); } @@ -390,7 +398,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; } @@ -403,12 +411,13 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp registerLocationUpdates(); LatLng lastLocation = locationManager.getLastLocation(); - if (curLatLng != null && curLatLng.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; } curLatLng = lastLocation; - if (locationChangeType.equals(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED)) { + if (locationChangeType.equals(PERMISSION_JUST_GRANTED)) { curLatLng = lastKnownLocation; } @@ -417,8 +426,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp return; } - if (locationChangeType.equals(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) - || locationChangeType.equals(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED)) { + if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED) + || locationChangeType.equals(PERMISSION_JUST_GRANTED) + || locationChangeType.equals(MAP_UPDATED)) { progressBar.setVisibility(View.VISIBLE); //TODO: This hack inserts curLatLng before populatePlaces is called (see #1440). Ideally a proper fix should be found @@ -440,7 +450,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp progressBar.setVisibility(View.GONE); }); } else if (locationChangeType - .equals(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) { + .equals(LOCATION_SLIGHTLY_CHANGED)) { Gson gson = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriSerializer()) .create(); @@ -685,12 +695,12 @@ 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) { @@ -700,4 +710,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp 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/NearbyListFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java index 1be2a8689..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; @@ -47,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); @@ -137,7 +146,7 @@ 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); 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 69041d286..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 @@ -731,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(); } @@ -766,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); 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 d05d81251..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 { 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/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 6dd6056f7..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 { 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 612b86458..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 @@ -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 9c31e2b4d..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, 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 cfcec1da5..638ad6a10 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,66 +1,52 @@ package fr.free.nrw.commons.upload; import android.Manifest; -import android.app.Activity; 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.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Bitmap; -import android.graphics.BitmapRegionDecoder; 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.provider.MediaStore; 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.v4.graphics.BitmapCompat; -import android.util.Log; import android.view.MenuItem; import android.view.View; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; -import android.widget.TextView; import android.widget.Toast; -import butterknife.BindView; -import butterknife.OnClick; 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; @@ -73,13 +59,15 @@ 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.mwapi.CategoryApi; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.utils.ViewUtil; import timber.log.Timber; +import android.support.design.widget.FloatingActionButton; 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 @@ -88,30 +76,13 @@ import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE; public class ShareActivity extends AuthenticatedActivity implements SingleUploadFragment.OnUploadActionInitiated, - OnCategoriesSaveHandler,SimilarImageDialogFragment.onResponse { - - @BindView(R.id.container) - FrameLayout flContainer; - @BindView(R.id.backgroundImage) - SimpleDraweeView backgroundImageView; - @BindView(R.id.media_map) - FloatingActionButton mapsFragment; //Lets stick to camelCase - @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 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 @@ -123,45 +94,53 @@ 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 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; - - //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(); private float startScaleFinal; - /** * Called when user taps the submit button. + * Requests Storage permission, if needed. */ @Override public void uploadActionInitiated(String title, String description) { @@ -170,8 +149,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); @@ -183,34 +160,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(); @@ -220,6 +207,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) { @@ -257,9 +249,6 @@ public class ShareActivity finish(); } - protected boolean isNearbyUpload() { - return isNearbyUpload; - } @Override public void onCreate(Bundle savedInstanceState) { @@ -276,7 +265,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())) { @@ -296,231 +332,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(); - mapsFragment.setVisibility(View.VISIBLE); - if( imageObj == null || imageObj.imageCoordsExists != true){ - mapsFragment.setVisibility(View.INVISIBLE); - } - } - /* + /** * Function to display the zoom and map FAB */ - private void showFABMenu(){ - isFABOpen=true; + private void showFABMenu() { + isFABOpen = true; - if( imageObj != null && imageObj.imageCoordsExists == true) - mapsFragment.setVisibility(View.VISIBLE); + if (gpsObj != null && gpsObj.imageCoordsExists) + mapButton.setVisibility(View.VISIBLE); zoomInButton.setVisibility(View.VISIBLE); mainFab.animate().rotationBy(180); - mapsFragment.animate().translationY(-getResources().getDimension(R.dimen.second_fab)); + 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 + /** + * Function to close the zoom and map FAB */ - private void closeFABMenu(){ - isFABOpen=false; + private void closeFABMenu() { + isFABOpen = false; mainFab.animate().rotationBy(-180); - mapsFragment.animate().translationY(0); + 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){ - mapsFragment.setVisibility(View.GONE); + 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(); - } - - /* - * to display permission snackbar in share activity + /** + * Displays Snackbar to ask for location permissions */ - private Snackbar requestPermissionUsingSnackBar(String rationale, - final String[] perms, - final int code) { + 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)); @@ -528,203 +433,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; - } - - //I might not be supposed to change it, but still, I saw it - @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(); - - } - - /** - * 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"); @@ -751,125 +497,32 @@ public class ShareActivity return super.onOptionsItemSelected(item); } - /* - * Get SHA1 of file from input stream + /** + * Allows zooming in to the image about to be uploaded. Called when zoom FAB is tapped */ - private 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"); - } - } - } - - /* - * function to provide pinch zoom - */ - private void zoomImageFromThumb(final View thumbView, Uri imageuri ) { - // If there's an animation in progress, cancel it - // immediately and proceed with this one. + 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(); } ViewUtil.hideKeyboard(ShareActivity.this.findViewById(R.id.titleEdit | R.id.descEdit)); closeFABMenu(); mainFab.setVisibility(View.GONE); + InputStream input = null; - Bitmap scaled = null; try { input = this.getContentResolver().openInputStream(imageuri); } catch (FileNotFoundException e) { e.printStackTrace(); } - BitmapRegionDecoder decoder = null; - try { - decoder = BitmapRegionDecoder.newInstance(input, false); - } catch (IOException e) { - e.printStackTrace(); - } - Bitmap bitmap = decoder.decodeRegion(new Rect(10, 10, 50, 50), null); - try { - //Compress the Image - System.gc(); - Runtime rt = Runtime.getRuntime(); - long maxMemory = rt.freeMemory(); - bitmap = MediaStore.Images.Media.getBitmap(this.getContentResolver(), 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) { - } catch (NullPointerException e){ - scaled = bitmap; - } + + Zoom zoomObj = new Zoom(thumbView, flContainer, this.getContentResolver()); + Bitmap scaledImage = zoomObj.createScaledImage(input, imageuri); + // Load the high-resolution "zoomed-in" image. - expandedImageView.setImageBitmap(scaled); + expandedImageView.setImageBitmap(scaledImage); - - - // Calculate the starting and ending bounds for the zoomed-in image. - // This step involves lots of math. Yay, math. - // The start bounds are the global visible rectangle of the thumbnail, - // and the final bounds are the global visible rectangle of the container - // view. 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). - 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; - } + 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 @@ -888,15 +541,10 @@ public class ShareActivity // 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.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() { @@ -917,11 +565,10 @@ public class ShareActivity // to the original bounds and show the thumbnail instead of // the expanded image. startScaleFinal = startScale; - } - /* - * called when upper arrow floating button + /** + * Called when user taps the ^ FAB button, expands to show Zoom and Map */ @OnClick(R.id.main_fab) public void onMainFabClicked() { @@ -934,11 +581,10 @@ public class ShareActivity @OnClick(R.id.media_upload_zoom_in) public void onZoomInFabClicked() { - //This try catch block was originally holding the entire click listener on the fab button, I did not wanted to risk exceptions try { zoomImageFromThumb(backgroundImageView, mediaUri); } catch (Exception e) { - Log.i("exception", e.toString()); + Timber.e(e); } } @@ -953,17 +599,10 @@ public class ShareActivity // 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.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() { @@ -989,10 +628,8 @@ public class ShareActivity @OnClick(R.id.media_map) public void onFabShowMapsClicked() { - if (imageObj != null && imageObj.imageCoordsExists == true) { - Uri gmmIntentUri = Uri - .parse("google.streetview:cbll=" + imageObj.getDecLatitude() + "," + imageObj - .getDecLongitude()); + 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); 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..4c05eb7b0 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 @@ -18,6 +18,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.HashSet; +import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -36,6 +37,12 @@ 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.utils.ViewUtil; +import fr.free.nrw.commons.wikidata.WikidataEditListener; +import fr.free.nrw.commons.wikidata.WikidataEditService; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; import timber.log.Timber; public class UploadService extends HandlerService { @@ -49,8 +56,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 +144,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: @@ -253,6 +261,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/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..a091d7758 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,12 +1,34 @@ package fr.free.nrw.commons.utils; +import android.app.WallpaperManager; +import android.content.Context; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.BitmapRegionDecoder; import android.graphics.Color; import android.graphics.Rect; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +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; +import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext; + /** * Created by bluesir9 on 3/10/17. */ @@ -132,4 +154,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/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/menu/fragment_image_detail.xml b/app/src/main/res/menu/fragment_image_detail.xml index e864dddb2..70a35951a 100644 --- a/app/src/main/res/menu/fragment_image_detail.xml +++ b/app/src/main/res/menu/fragment_image_detail.xml @@ -15,6 +15,10 @@ android:id="@+id/menu_download_current_image" android:title="@string/menu_download" app:showAsAction="never" /> + العنوان الوصف لا يمكن تسجيل الدخول - فشل في شبكة الاتصال - لا يمكن تسجيل الدخول - فضلا تحقق من اسم المستخدم - لا يمكن تسجيل الدخول - فضلا تحقق من كلمة السر الكثير من المحاولات غير الناجحة. الرجاء المحاولة مرة أخرى في بضع دقائق. عذراً، لقد تم منع هذا المستخدم على كومنز يجب توفير رمز التحقق المزدوج. diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index 947bc441a..f7301c45d 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -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. @@ -273,4 +272,6 @@ Compartir app Nun s\'especificaron les coordenaes al escoyer la imaxe Error al llograr los llugares cercanos. + 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 0cd72a167..81b826d36 100644 --- a/app/src/main/res/values-b+sr+Latn/strings.xml +++ b/app/src/main/res/values-b+sr+Latn/strings.xml @@ -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. diff --git a/app/src/main/res/values-ba/strings.xml b/app/src/main/res/values-ba/strings.xml index 293dd3f73..f54a93831 100644 --- a/app/src/main/res/values-ba/strings.xml +++ b/app/src/main/res/values-ba/strings.xml @@ -50,8 +50,6 @@ Был файлдың атамаһын күрһәт Тасуирлама Инеп булмай - интернет хатаһы - Инмәнең - ҡулланыусы исемеңде тикшер - Инмәнең - серһуҙеңде тикшер Күп тапҡыр яңылыштың. Зинһар, бер-нисә минуттан тағы ла инеп ҡара Ғәфү итегеҙ, әммә был исемдәге ҡатнашыусыға Викискладҡа инеү тыйылған Ике тапҡыр раҫлай торған шәхси кодты яҙырға кәрәк diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index cfaf021f9..28b092add 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -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 7c76ebaea..69bb5fc33 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -57,8 +57,6 @@ এই ফাইলটির জন্য একটি শিরোনাম প্রদান করুন বিবরণ প্রবেশ করা যাচ্ছে না - নেটওয়ার্ক ব্যর্থতা - প্রবেশ করা যাচ্ছে না - অনুগ্রহ করে আপনার ব্যবহারকারী নাম পরীক্ষা করুন। - প্রবেশ করা যাচ্ছে না - অনুগ্রহ করে আপনার পাসওয়ার্ড পরীক্ষা করুন খুব বেশি অসফল প্রচেষ্টা। অনুগ্রহ করে কয়েক মিনিট পরে আবারও চেষ্টা করুন। দুঃখিত, এই ব্যবহারকারীকে কমন্সে বাধা দেয়া হয়েছে অাপনাকে অবশ্যই অাপনার দু\'স্তরের সত্যায়নকরণ কোড দিতে হবে। diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 8383f7cbe..2a7cd2284 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -52,8 +52,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. diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index 6c0097089..e6667a083 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -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. diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index f7f40f427..aaf5ca737 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -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/strings.xml b/app/src/main/res/values-cs/strings.xml index 6e2453d6d..eb87ab83e 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -61,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í. diff --git a/app/src/main/res/values-csb/strings.xml b/app/src/main/res/values-csb/strings.xml index 2d7b99904..f04df2055 100644 --- a/app/src/main/res/values-csb/strings.xml +++ b/app/src/main/res/values-csb/strings.xml @@ -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..ca00d1c4d 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -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. diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index e10a93850..fe194ce94 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -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. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9f47ce42e..f0eab6cb2 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -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. @@ -279,4 +278,6 @@ App teilen Während der Bildauswahl wurden keine Koordinaten angegeben Fehler beim Abrufen der Orte in der Nähe. + Hintergrundbild festlegen + Hintergrundbild erfolgreich festgelegt! diff --git a/app/src/main/res/values-diq/strings.xml b/app/src/main/res/values-diq/strings.xml index 53bd88bac..79da245fe 100644 --- a/app/src/main/res/values-diq/strings.xml +++ b/app/src/main/res/values-diq/strings.xml @@ -52,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 diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e372df836..876b76924 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -57,8 +57,7 @@ Παρακαλώ παρέχετε ένα τίτλο για αυτό το αρχείο Περιγραφή Δεν είναι δυνατή η σύνδεση - αποτυχία του δικτύου - Δεν είναι δυνατή η σύνδεση - ελέγξτε το όνομα χρήστη σας - Δεν είναι δυνατή η σύνδεση - παρακαλούμε ελέγξτε τον κωδικό σας + Αποτυχία σύνδεσης - παρακαλώ ελέγξτε το όνομα χρήστη και τον κωδικό σας Πάρα πολλές ανεπιτυχείς προσπάθειες. Παρακαλώ δοκιμάστε ξανά σε λίγα λεπτά. Συγνώμη, αυτός ο χρήστης έχει αποκλειστεί από τα Commons Πρέπει να δώσετε τον κωδικό πιστοποίησης με δύο παράγοντες @@ -283,4 +282,6 @@ Κοινοποίηση εφαρμογής Οι συντεταγμένες δεν ορίστηκαν κατά την διάρκεια της επιλογής εικόνας Σφάλμα κατά την εύρεση κοντινών μερών. + Ρύθμιση ταπετσαρίας + Η ταπετσαρία ρυθμίστηκε επιτυχώς! diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9e5dae650..1be0ebd1a 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,6 +1,7 @@ - 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 e833932a8..b0ea9ce0f 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -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. @@ -162,8 +160,8 @@ Ei kuvausta Tuntematon lisenssi Päivitä - Vaadittu oikeus: Ulkoisen tallennustilan luku. Appi ei voi päästä galleriaasi ilman tätä oikeutta. - Vaadittava lupa: Kirjoita ulkoiseen tallennustilaan. Appi ei voi päästä kameraasi ilman tätä oikeutta. + 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 @@ -262,4 +260,8 @@ 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..58e26797b 100644 --- a/app/src/main/res/values-fo/strings.xml +++ b/app/src/main/res/values-fo/strings.xml @@ -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 bfb14baf1..e0826cc00 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -65,8 +65,7 @@ Veuillez donner un titre à ce fichier Description Impossible de se connecter — panne de réseau - Impossible de se connecter — veuillez vérifier votre nom d’utilisateur - Impossible de se connecter — veuillez vérifier votre mot de passe + Impossible de se connecter — veuillez vérifier votre nom d’utilisateur et votre mot de passe Trop de tentatives infructueuses. Veuillez réessayer dans quelques minutes. Désolé, cet utilisateur a été bloqué dans Commons Vous devez fournir votre code d’authentification à deux facteurs. @@ -290,4 +289,6 @@ Partager les applications Les coordonnées n\'ont pas été spécifiées pendant la sélection de l\'image Erreur durant l\'exploration du voisinage. + Définir le papier-peint + Papier-peint configuré avec succès! diff --git a/app/src/main/res/values-frr/strings.xml b/app/src/main/res/values-frr/strings.xml index f4866e705..1355096c5 100644 --- a/app/src/main/res/values-frr/strings.xml +++ b/app/src/main/res/values-frr/strings.xml @@ -42,8 +42,6 @@ Tiitel Beskriiwang Bi\'t uunmeldin as wat skiaf gingen - näätwerk-feeler - Bi\'t uunmeldin as wat skiaf gingen - luke ans efter di brükernööm - Bi\'t uunmeldin as wat skiaf gingen - luke ans efter det paaswurd Tu fölsis fersoocht saner lok. Ferschük det uun hög minüüten noch ans nei. Didiar brüker as üüb Commons speret wurden. Dü skel dan code för\'t tau-straal-gudkäänen (2FA) uundu. diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 630311db4..1dcd7ecca 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -55,8 +55,6 @@ 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 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. diff --git a/app/src/main/res/values-haw/strings.xml b/app/src/main/res/values-haw/strings.xml index a81ab120d..674dc6dbe 100644 --- a/app/src/main/res/values-haw/strings.xml +++ b/app/src/main/res/values-haw/strings.xml @@ -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 69bb67655..0404cacd2 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -53,8 +53,6 @@ शीर्षक विवरण प्रवेश नहीं हो रहा - नेटवर्क विफल - प्रवेश नहीं हो रहा - कृपया अपना सदस्य नाम जाँचें - प्रवेश नहीं हो रहा - कृपया अपना पासवर्ड जाँचें ढेर सारे असफल प्रयास होने के कारण कुछ मिनटों के बाद प्रयास करें। क्षमा करें, यह सदस्य कॉमन्स में अवरोधित है आपको अपना दो कारक प्रमाणन कोड प्रदान करना होगा। 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..f675e2f77 --- /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..b6dbf026d 100644 --- a/app/src/main/res/values-hrx/strings.xml +++ b/app/src/main/res/values-hrx/strings.xml @@ -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..a8647fb56 100644 --- a/app/src/main/res/values-hsb/strings.xml +++ b/app/src/main/res/values-hsb/strings.xml @@ -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 d9580dca1..d259e4895 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -59,8 +59,7 @@ 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. @@ -255,15 +254,18 @@ 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..6a533ef67 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -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 f1b020965..985e858bf 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -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. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 06bfa701f..30a02428e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -53,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. diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 9ba67e4ee..e13626c29 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -58,8 +58,7 @@ נא לתת כותרת לקובץ הזה תיאור לא ניתן להיכנס – כשל בתקשורת - לא ניתן להיכנס – נא לבדוק את שם המשתמש שלך - לא ניתן להיכנס – נא לבדוק את הססמה שלך + לא ניתן להיכנס לחשבון – נא לבדוק את שם המשתמש ואת הסיסמה יותר מדי ניסיונות כושלים להיכנס. נא לנסות שוב בעוד מספר דקות. סליחה, החשבון הזה חסום בוויקישיתוף יש לספק את קוד האימות הדו־שלבי שלך. @@ -283,4 +282,6 @@ שיתוף היישום לא צוינו קואורדינטות בעת בחירת התמונה שגיאה באחזור המקומות בסביבתך. + הגדרת רקע + הרקע הוגדר בהצלחה! 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 6ff03ee0d..df73c9d46 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -58,8 +58,6 @@ ファイル名をつけてください 説明 ログインできません - ネットワークのエラーです - ログインできません - 利用者名を確認してください - ログインできません - パスワードを確認してください 失敗した回数が多すぎます。数分待ってからもう一度お試しください。 申し訳ありませんが、この利用者はコモンズでブロックされています。 2要素認証コードを提供する必要があります。 @@ -138,7 +136,7 @@ ウィキメディア・コモンズにはウィキペディアで使用する画像のほぼすべてが保管されています。 あなたの画像は世界中の人々が学習する助けになります! アップロードする画像はあなたご本人が撮影したものかあなたが単独で制作したものに限定します。 - 自然物 (動植物、山)\n• 道具 (自転車、駅)\n• 著名人 (市区村長・都道府県知事、自分が会ったオリンピック選手) + 自然 (動植物、山)\n• 道具 (自転車、駅)\n• 著名人 (市区村長・都道府県知事、自分が会ったオリンピック選手) 自然物 (動植物、山) 道具 (自転車、駅) 著名人 (市区村長・都道府県知事、自分が会ったオリンピック選手) diff --git a/app/src/main/res/values-jv/strings.xml b/app/src/main/res/values-jv/strings.xml index e051b6f97..a4a85c80f 100644 --- a/app/src/main/res/values-jv/strings.xml +++ b/app/src/main/res/values-jv/strings.xml @@ -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 diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index 2bb1c5191..60320dd9e 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -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 342c615b9..c88a34978 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -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. diff --git a/app/src/main/res/values-km/strings.xml b/app/src/main/res/values-km/strings.xml index c057e04c7..ff4a6ce79 100644 --- a/app/src/main/res/values-km/strings.xml +++ b/app/src/main/res/values-km/strings.xml @@ -35,8 +35,6 @@ ចំណងជើង បរិយាយ មិនអាចកត់ឈ្មោះចូល - បណ្តាញ network បរាជ័យ - មិនអាចកត់ឈ្មោះចូល - សូមពិនិត្យឈ្មោះអ្នកប្រើប្រាស់របស់អ្នក - មិនអាចកត់ឈ្មោះចូល - សូមពិនិត្យលេខសម្ងាត់របស់អ្នក ការព្យាយាមមិនបានសម្រេចមានចំនួនច្រើនដងពេក។ សូមព្យាយាមម្តងទៀតនៅប៉ុន្មាននាទីក្រោយ។ សូមអភ័យទោស អ្នកប្រើប្រាស់រូបនេះត្រូវបានហាមឃាត់នៅ Commons កត់ឈ្មោះចូលបរាជ័យ diff --git a/app/src/main/res/values-ko-rKP/strings.xml b/app/src/main/res/values-ko-rKP/strings.xml index 670479f75..ccbe86100 100644 --- a/app/src/main/res/values-ko-rKP/strings.xml +++ b/app/src/main/res/values-ko-rKP/strings.xml @@ -42,8 +42,6 @@ 제목 설명 가입할수 없습니다 - 망 오유입니다 - 가입할수 없습니다 - 사용자이름을 확인하세요 - 가입할수 없습니다 - 통행암호를 확인하세요 실패한 시도가 너무 많습니다. 몇분후에 다시 시도하세요. 죄송합니다, 이 사용자는 공용에서 차단되였습니다 두인자검증부호를 제공해야 합니다. diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 883665c85..4cd947699 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -58,8 +58,7 @@ 이 파일의 제목을 지정해 주십시오 설명 로그인할 수 없습니다 - 네트워크 오류입니다 - 로그인할 수 없습니다 - 사용자 이름을 확인하세요 - 로그인할 수 없습니다 - 비밀번호를 확인하세요 + 로그인할 수 없습니다 - 사용자 이름과 비밀번호를 확인해 주십시오 실패한 시도가 너무 많습니다. 몇 분 후에 다시 시도하세요. 죄송합니다, 이 사용자는 공용에서 차단되었습니다 2요소 인증 코드를 제공해야 합니다. @@ -276,4 +275,6 @@ 앱 공유 그림 선택 중에 좌표가 지정되지 않았습니다 주변 장소를 가져오는데 오류가 있습니다. + 배경화면 설정 + 배경화면을 성공적으로 설정했습니다! diff --git a/app/src/main/res/values-kum/strings.xml b/app/src/main/res/values-kum/strings.xml index 1630600cd..34db9a395 100644 --- a/app/src/main/res/values-kum/strings.xml +++ b/app/src/main/res/values-kum/strings.xml @@ -38,7 +38,7 @@ Тасвири Такрарламакъ Гери алмакъ - Юклемек + Эндирмек CC0 CC BY-SA 3.0 CC BY-SA 3.0 (Алмания) @@ -47,6 +47,7 @@ CC BY-SA 4.0 CC BY 4.0 CC Zero + Интернетден эндирген суратларынг Юклев уьлгю: Дюр! Категориялар diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index 6d78e91fe..33e79bd3c 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -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 d346b90bd..e39c06498 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -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 @@ -244,4 +242,6 @@ Keng Biller fonnt! Feeler beim Eropluede vu Biller. Eropgeluede vum: %1$s + 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 index a14add497..f92167522 100644 --- a/app/src/main/res/values-li/strings.xml +++ b/app/src/main/res/values-li/strings.xml @@ -50,8 +50,6 @@ Gaef estebleef \'ne naam veur dit bestandj Besjrieving Kan zich neet aanmelde - netwirkfout - Kan zich neet aanmelde - controleer de gebroekersnaam - Kan zich neet aanmelde - controleer die wachwaord 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. diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index c5c0813ab..7fc63d8b9 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -45,8 +45,6 @@ Pavadinimas Aprašymas Negalima prisijungti - tinklo klaida - Negalima prisijungti - prašome patikrinti savo vartotojo vardą - Negalima prisijungti - prašome patikrinti savo slaptažodį Per daug nesėkmingų bandymų. Pabandykite dar kartą po keleto minučių. Atsiprašome, šis vartotojas buvo užblokuotas Commons Prisijungti nepavyko diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index a344fcad2..66a48e0a0 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -49,8 +49,7 @@ Ставете ѝ наслов на податотеката Опис Не можам да Ве најавам — мрежата не работи - Не можам да Ве најавам — проверете си го корисничкото име - Не можам да Ве најавам — проверете си ја лозинката + Не можев да ве најавам. Проверете ги корисничкото име и лозинката. Направени се премногу неуспешни обиди. Обидете се пак за некоја минута. Нажалост, корисникот е блокиран на Ризницата Мора да го укажете вашиот код за двочинителска заверка. @@ -273,4 +272,6 @@ Сподели прилог Не беа укажани координати при изборот на сликата Грешка при добивањето на околните места. + Задај позадина + Позадината е успешно зададена! diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 632cc0265..c057fb068 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -50,8 +50,6 @@ ഈ പ്രമാണത്തിന് ഒരു തലക്കെട്ട് നൽകുക. വിവരണം പ്രവേശിക്കാനായില്ല - നെറ്റ്‌വർക്ക് പരാജയപ്പെട്ടു - പ്രവേശിക്കാനായില്ല - ദയവായി താങ്കളുടെ ഉപയോക്തൃനാമം പരിശോധിക്കുക - പ്രവേശിക്കാനായില്ല - ദയവായി താങ്കളുടെ രഹസ്യവാക്ക് പരിശോധിക്കുക നിരവധി വിജയകരമല്ലാത്ത ശ്രമങ്ങൾ നടന്നിരിക്കുന്നു. വീണ്ടും ശ്രമിക്കുന്നതിനു മുമ്പ് ഏതാനം മിനിറ്റുകൾ വിശ്രമിക്കുക. ക്ഷമിക്കുക, ഈ ഉപയോക്താവ് കോമൺസിൽ നിന്ന് തടയപ്പെട്ടിരിക്കുകയാണ് താങ്കളുടെ ദ്വി-ഘടക സാധൂകരണ കോഡ് നൽകുക. diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index b88e021e7..e0a15ad36 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -54,8 +54,6 @@ कृपया या फाईलसाठी शीर्षक प्रदान करा वर्णन सनोंद प्रवेश अशक्य - नेटवर्क नाही - सनोंद प्रवेश अशक्य - कृपया आपले सदस्यनाव तपासा - सनोंद प्रवेश अशक्य - कृपया आपला परवलीचा शब्द तपासा अनेक अयशस्वी प्रयत्न.काही मिनीटांनंतर पुन्हा प्रयत्न करा. माफ करा, कॉमन्सवर हा सदस्य प्रतिबंधित आहे आपण आपल्या दोन कारक प्रमाणिकरण कोड प्रदान करणे आवश्यक आहे. diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index c5c0f18ab..ef244d049 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -37,8 +37,6 @@ Tajuk Keterangan Tidak boleh log masuk - kegagalan rangkaian - Tidak dapat log masuk - Sila semak nama pengguna anda - Tidak dapat log masuk - Sila semak kata laluan anda Terlalu banyak cubaan yang tidak berjaya. Sila cuba lagi dalam beberapa minit Maaf, pengguna ini telah disekat di Commons Log masuk gagal diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 9774035ee..d2f622fce 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -49,8 +49,6 @@ Tittel Beskrivelse Innlogging feilet - nettverksproblem - Innlogging feilet - sjekk brukernavnet ditt - Innlogging feilet - sjekk passordet ditt For mange misslykkede forsøk. Vennligst prøv igjen om noen få minutter. Beklager, denne brukeren har blitt blokkert på Commons Du må oppgi tofaktorautentiseringskoden din. diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index db9d8d3fd..6cd959647 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -39,8 +39,6 @@ शीर्षक वर्णन प्रवेश गर्न असमर्थ - जडान खराबी - प्रवेश गर्न असमर्थ - कृपया तपाईंको प्रयोगकर्ता नाम जाँच गर्नुहोस् - प्रवेश गर्न असमर्थ - कृपया आफ्नो पासवर्ड जाँच गर्नुहोस धेरै असफल प्रयासहरू भए । कृपया केही मिनेट पछि पुन: प्रयास गर्नुहोस माफ गर्नुहोस, यो प्रयोगकर्तालाई कमोन्समा ब्लक गरिएको छ प्रवेश सफल हुन सकेन @@ -97,4 +95,6 @@ अज्ञान अनुमतिपत्र ताजागर्ने टगल दृश्य + भित्तेपत्र चयन गर्नुहोस् + भित्तेपत्र सफलतापूर्वक चयन भयो! diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index f2688cffa..b6cec0ebd 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -47,8 +47,6 @@ Naam Beschrijving Aanmelden niet mogelijk. Er is een probleem met het netwerk - Aanmelden niet mogelijk. Controleer uw gebruikersnaam - Aanmelden niet mogelijk. Controleer uw wachtwoord U hebt te vaak geprobeerd aan te melden. Probeer het over een aantal minuten opnieuw. Deze gebruiker is helaas geblokkeerd op Wikimedia Commons Aanmelden mislukt diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index 803293a55..2c2d94d70 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -43,8 +43,6 @@ Títol Descripcion Impossible de se connectar — pana de ret - Impossible de se connectar — verificatz vòstre nom d’utilizaire - Impossible de se connectar — verificatz vòstre senhal Tròp de temptativas infructuosas. Ensajatz tornarmai dins qualques minutas. O planhèm, aqueste utilizaire es estat blocat dins Commons Error de connexion diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index c1bc2b73f..a1e08984a 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -35,8 +35,6 @@ ଶିରୋନାମ ବିବରଣୀ ଲଗ ଇନ କରିବାରେ ବିଫଳ - ନେଟୱାର୍କରେ ଅସୁବିଧା - ଲଗ ଇନ କରିବାରେ ବିଫଳ - ଦୟାକରି ନିଜର ସଭ୍ୟ ନାମ ପରଖିନିଅନ୍ତୁ - ଲଗ ଇନ କରିବାରେ ବିଫଳ - ଦୟାକରି ନିଜର ପାସୱାର୍ଡ଼ ପରଖିନିଅନ୍ତୁ ଖୁବ ଅଧିକ ଅସଫଳ ଚେଷ୍ଟା । ଦୟାକରି କେଇ ମିନିଟ ଛାଡ଼ି ଚେଷ୍ଟା କରନ୍ତୁ କ୍ଷମା ଘେନିବେ, ଏହି ସଭ୍ୟଙ୍କୁ କମନ୍ସରେ ଅଟକାଯାଇଛି ଲଗଇନ ହେଲାନାହିଁ diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 6937ab75d..952ad877f 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -43,8 +43,6 @@ ਸਿਰਲੇਖ ਵੇਰਵਾ ਦਾਖ਼ਲਾ ਨਹੀਂ ਹੋ ਰਿਹਾ - ਨੈੱਟਵਰਕ ਫੇਲ੍ਹ ਹੋਇਆ ਹੈ - ਦਾਖ਼ਲਾ ਨਹੀਂ ਹੋ ਰਿਹਾ - ਆਪਣਾ ਵਰਤੋਂਕਾਰ ਨਾਂ ਚੈੱਕ ਕਰੋ - ਦਾਖ਼ਲਾ ਨਹੀਂ ਹੋ ਰਿਹਾ - ਆਪਣਾ ਪਾਸਵਰਡ ਚੈੱਕ ਕਰੋ ਜੀ ਬਹੁਤ ਸਾਰੀਆਂ ਅਸਫ਼ਲ ਕੋਸ਼ਿਸ਼ਾਂ। ਥੋੜ੍ਹੀ ਦੇਰ ਬਾਅਦ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ। ਅਫ਼ਸੋਸ, ਇਹ ਵਰਤੋਂਕਾਰ ਕਾਮਨਜ਼ ਉੱਤੇ ਬਲਾਕ ਕਰ ਦਿੱਤਾ ਗਿਆ ਹੈ ਦਾਖ਼ਲਾ ਫੇਲ੍ਹ ਹੋਇਆ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index ccc5f374f..d9eff3de6 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -60,8 +60,7 @@ Tytuł Opis Nie można zalogować - błąd sieci - Nie można zalogować - sprawdź nazwę użytkownika - Nie można zalogować - sprawdź hasło + Nie można się zalogować - sprawdź swoją nazwę użytkownika i hasło Zbyt wiele nieudanych prób zalogowania. Spróbuj ponownie za kilka minut. Przepraszamy, ten użytkownik został zablokowany na Commons Wprowadź swój kod dla dwuetapowej autoryzacji. @@ -229,4 +228,6 @@ Anuluj Nie znaleziono grafik! Wystąpił błąd podczas ładowania grafik. + Ustaw tapetę + Tapeta ustawiona pomyślnie! diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index 8b2bd292c..c68663306 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -49,8 +49,7 @@ Për piasì, ch\'a-j buta \'n tìtol a s\'archivi Descrission Impossìbil rintré ant ël sistema - la rej a marcia nen - Impossìbil rintré ant ël sistema - për piasì, ch\'a verìfica sò stranòm - Impossìbil rintré ant ël sistema - për piasì, ch\'a contròla soa ciav + Impossìbil rintré ant ël sistema - për piasì ch\'a contròla sò stranòm e soa ciav Tròpi tentativ falì. Për piasì, ch\'a preuva torna da-sì chèiche minute. An dëspias, s\'utent-sì a l\'é stàit blocà ansima a Commons A dev fornì sò còdes d\'autentificassion a doi fator. @@ -273,4 +272,6 @@ Partagé j\'aplicassion Le coordinà a son nen ëstàite spessificà durant la selession ëd la plancia Eror durant l\'esplorassion dj\'anviron. + Definì la tapissarìa + La tapissarìa a l\'é stàita definìa për da bin! diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 3e2b499e6..2010b9bd5 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -37,8 +37,6 @@ مهرباني وکړئ د دې دوتنې لپاره سرلیک چمتو کړئ څرگندونه د ننوتلو توان نلري - د شبکې ناکامي - د ننوتلو توان نلري - لطفاً خپل کارن نوم وګورئ - د ننوتلو توان نلري - لطفاً خپل پټنوم وګورئ ډیری ناکامه هڅې. لطفا څو دقیقې وروسته بیا هڅه وکړئ. بخښنه غواړو، په دي کارن د کامنز لخوا بنديز ولګول شو غونډال کې ننوتنه نابريالې شوه diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 849c4e6b1..f7fa873bf 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -61,8 +61,7 @@ Forneça um título para este arquivo Descrição Erro ao efetuar o login - falha na rede - Erro ao efetuar o login - confira seu nome de usuário - Erro ao efetuar o login - confira sua senha + Não é possível fazer o login - verifique seu nome de usuário e senha Muitas tentativas malsucedidas. Tente de novo daqui alguns minutos. Desculpe, esse usuário foi banido do Commons Você precisa fornecer o seu código de ativação de dois fatores. @@ -285,4 +284,6 @@ Compartilhar o aplicativo Não foram especificadas coordenadas durante a seleção da imagem Erro ao buscar lugares próximos. + Definir imagem de fundo + Imagem de fundo definida! diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index e59582fa0..22776341f 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -58,8 +58,7 @@ Forneça um título para este ficheiro, por favor Descrição Não foi possível iniciar sessão - falha de rede - Não foi possível iniciar sessão - verifique o seu nome de utilizador(a) - Não foi possível iniciar sessão - verifique a sua palavra-passe + Não foi possível iniciar sessão - verifique o seu nome de utilizador e a palavra-passe Demasiadas tentativas malsucedidas. Por favor, tente de novo dentro de alguns minutos. Desculpe, este utilizador foi bloqueado no Commons Precisa fornecer o seu código de ativação de dois fatores. @@ -222,7 +221,7 @@ Configurações Comentários Sair - Tutorial + Explicação Notificações Destacadas Os sítios aqui perto não podem ser apresentados sem permissões de localização @@ -284,4 +283,6 @@ Partilhar aplicação Não foram especificadas coordenadas durante a seleção da imagem Erro ao localizar locais próximos. + Definir imagem de fundo + Imagem de fundo definida! diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index d84ce08ab..fde276f7c 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -50,8 +50,6 @@ {{Identical|Title}} {{Identical|Description}} Error message shown to user when login can not be completed due to network issues. - Error message shown to user when login can not be completed because the user name is wrong. - Error message shown to user when login can not be completed beause the password is wrong Error message shown to user when login can not be completed because the user has attempted to login too many times in a short period of time, and hence been throttled. Error message shown to user when login can not be completed because the user is blocked on Wikimedia Commons {{Identical|Login failed}} diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index b87abfbbf..52b301fc8 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -44,8 +44,6 @@ Titlu Descriere Autentificare nereușită – defecțiune de rețea - Autentificare nereușită – verificați-vă numele de utilizator - Autentificare nereușită – verificați-vă parola Prea multe încercări nereușite. Încercați din nou peste câteva minute. Ne pare rău, acest utilizator a fost blocat la Commons Trebuie să introduceți tokenul de autentificare. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0d5a0ee3e..754fd228c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -62,8 +62,7 @@ Пожалуйста, укажите название этого файла Описание Не удаётся войти — сбой сети - Не удалось войти — пожалуйста, проверьте своё имя пользователя - Не удалось войти — пожалуйста, проверьте свой пароль + Не удаётся войти — проверьте ваше имя пользователя и пароль Слишком много неудачных попыток. Пожалуйста, попробуйте ещё раз через несколько минут. Извините, но участник с таким именем был заблокирован на Викискладе Вы должны ввести код двухфакторной аутентификации. @@ -291,4 +290,6 @@ Поделиться приложением Во время выбора изображения не были указаны координаты Ошибка получения мест поблизости + Сделать фоновой заставкой + Фоновая заставка успешно установлена! diff --git a/app/src/main/res/values-sd/strings.xml b/app/src/main/res/values-sd/strings.xml index 3ccc39dd3..07043e1bd 100644 --- a/app/src/main/res/values-sd/strings.xml +++ b/app/src/main/res/values-sd/strings.xml @@ -42,8 +42,6 @@ عنوان تشريح ناقابلِ داخل ٿيڻ - باھمڄار ناڪامي - ناقابلِ داخل ٿيڻ - براءِ مھرباني پنھنجو واپرائيندڙ-نانءُ چڪاسيو - ناقابل داخل ٿيڻ - براءِ مھرباني پنھنجو ڳجھولفظ چڪاسيو ھيڪانديون ناڪام ڪوششون. براءِ مھرباني ڪجھ منٽن کانپوءِ ٻيھر ڪوشش ڪريو. افسوس، ھي واپرائيندڙ العام تي بندشيل آھي داخل ٿيڻ ناڪام diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 4ae0db17c..cb553b304 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -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 90fac7f16..9ad6857ef 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -51,8 +51,6 @@ 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 diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 925b8a5ce..312cccb62 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -57,8 +57,6 @@ Унесите наслов за ову датотеку Опис Неуспешно пријављивање – грешка на мрежи - Неуспешно пријављивање – проверите Ваше корисничко име - Неуспешно пријављивање – проверите Вашу лозинку Превише неуспешних покушаја. Пробајте поново за неколико минута. Нажалост, овај корисник је блокиран на Остави Морате унети Ваш двофакторски код за аутентификацију. diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml index b2604eefd..d209cc1ad 100644 --- a/app/src/main/res/values-su/strings.xml +++ b/app/src/main/res/values-su/strings.xml @@ -12,15 +12,15 @@ Asup log Daptar Asup log - Tungguan heula… - Asup log suksés! - Asup log Gagal! + Tungguan… + Laksana login! + Gagal login! Berkas teu kapanggih. Coba berkas séjén. Oténtikasi gagal! - Ngamimitian ngunjal! + Mitembeyan ngunjal! %1$s diunjal! Toél pikeun némpo unjalan anjeun - Ngamimitian ngunjal %1$s + Ngamimitian %1$s ngunjal Ngunjal %1$s Méréskeun unjalan %1$s Ngunjal %1$s gagal @@ -43,8 +43,6 @@ Judul Pedaran Teu bisa login - gangguan jaringan - Teu bisa login - pariksa sandiasma - Teu bisa login - pariksa kecap sandi Loba teuing nu gagalna. Mangga cobian sababaraha menit deui mah Punten, ieu kontributor geus diblokir di Commons Anjeun kudu nyayagakeun kodeu oténtikasi dua faktor. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 1d2fc868e..e57c98b5b 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -55,8 +55,7 @@ Var god ange en titel för denna fil Beskrivning Det gick inte att logga in - nätverksfel - Det gick inte att logga in - var god kontrollera ditt användarnamn - Det gick inte att logga in - var god kontrollera ditt lösenord + Kunde inte logga in - kontrollera ditt användarnamn och lösenord För många misslyckade försök. Var god försök igen om några minuter. Tyvärr, denna användare har blockerats på Commons Du måste ange din tvåstegsverifieringskod. @@ -281,4 +280,6 @@ Dela app Koordinater specificerades inte vid bildvalet Fel uppstod när platser i närheten hämtades. + Ange som bakgrundsbild + Bakgrundsbilden ändrades! diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index b2ad478b0..59c919d35 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -38,8 +38,6 @@ శీర్షిక వివరణ లాగిన్ చెయ్యలేకపోయాం - నెట్‍వర్కు విఫలం - లాగిన్ చెయ్యలేకపోయాం - మీ వాడుకరిపేరును సరిచూసుకోండి - లాగిన్ చెయ్యలేకపోయాం - మీ సంకేతపదాన్ని సరిచూసుకోండి మరీ ఎక్కువ విఫల యత్నాలు చేసారు. కొద్ది నిముషాలాగి ప్రయత్నించండి ఈ వాడుకరి కామన్స్ లో నిరోధించబడ్డారు, సారీ. లాగిన్ విఫలమైంది diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 29b29d804..933f0c2d4 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -48,8 +48,6 @@ กรุณาระบุชืิ่อเรื่องของไฟล์นี้ คำอธิบาย ไม่สามารถเข้าสู่ระบบได้ - ความล้มเหลวของเครือข่าย - ไม่สามารถเข้าสู่ระบบได้ - กรุณาตรวจสอบชื่อผู้ใช้ของคุณ - ไม่สามารถเข้าสู่ระบบได้ - กรุณาตรวจสอบรหัสผ่านของคุณ จำนวนครั้งที่พยายามไม่สำเร็จมากเกินไป กรุณาลองอีกครั้งในอีกสักครู่ ขออภัย ผู้ใช้นี้ถูกบล็อกบนคอมมอนส์อยู่ คุณต้องระบุโค้ดการตรวจสอบความถูกต้องสองปัจจัยของคุณ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index c9dd60b41..c81a11055 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -57,8 +57,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. @@ -158,6 +157,7 @@ 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 @@ -281,4 +281,6 @@ Uygulamayı Paylaş Koordinatlar görüntü seçimi sırasında belirlenmedi Yakındaki yerler alınırken hata oluştu. + 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..4c55cb548 100644 --- a/app/src/main/res/values-ug/strings.xml +++ b/app/src/main/res/values-ug/strings.xml @@ -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 0e83e6be0..4b0286605 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -58,8 +58,7 @@ Будь ласка, вкажіть назву цього файлу Опис Неможливо увійти — збій у мережі - Неможливо увійти — будь ласка, перевірте своє ім\'я користувача - Неможливо увійти — будь ласка, перевірте свій пароль + Неможливо увійти — будь ласка перевірте ім\'я користувача та пароль Надто багато невдалих спроб. Будь ласка, спробуйте знову через кілька хвилин. Вибачте, цього користувача було заблоковано на Вікісховищі Ви повинні надати код двофакторної автентифікації. @@ -150,7 +149,7 @@ Корисні об\'єкти (велосипеди, залізничні станції) Відомі люди (ваш мер, спортсмен-олімпієць, якого ви зустріли) Будь ласка, НЕ завантажуйте: - - Селфі або фото своїх друзів \n- Зображення, які Ви завантажили з інтернету \n- Скріншоти патентованих програм + - Селфі або фото своїх друзів \n- Зображення, які Ви завантажили з інтернету \n- Знімки екрану пропрієтарних програм Селфі чи фото ваших друзів Зображення, які ви завантажили з інтернету Знімки екрану пропрієтарних програм @@ -162,7 +161,7 @@ Надсилайте Ваші зображення. Допоможіть оживити статті Вікіпедії! Зображення у Вікіпедії надходять з Вікісховища. Ваші зображення допомагають освіті людей у всьому світі. - Уникайте захищених авторським правом матеріалів, знайдених в Інтернеті, а також зображень плакатів, обкладинок книг і т. п. + Уникайте захищених авторським правом матеріалів, знайдених в Інтернеті, а також зображень плакатів, обкладинок книг, тощо. Ви це зрозуміли? Так! @@ -290,4 +289,6 @@ Поділитися програмою Під час вибору зображення не були вказані координати Помилка отримання місць поблизу. + Поставити шпалерами екрану + Шпалери екрану виставлено успішно! diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 4fdd67514..2620412d6 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -45,8 +45,6 @@ عنوان وضاحت لاگ ان ہونے میں ناکام - نیٹ ورک ناکامی - لاگ ان ہونے میں ناکام - براہ مہربانی اپنا صارف نام کی جانچ کریں - لاگ ان ہونے میں ناکام - براہ مہربانی - اپنے پاس ورڈ کی جانچ کریں بے شمار ناکام کوششیں کچھ منٹوں میں دوبارہ کوشش کریں۔ معذرت، یہ صارف کومنز پر بلاک کردیا گیا ہے آپ کو اپنے دو عامل کے تصدیق کوڈ فراہم کرنا چاہیے۔ diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 4cff15064..6e32bfdde 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,16 +1,24 @@ + 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ờ… @@ -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,7 +101,7 @@ 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ý @@ -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..d9c0b8f9d 100644 --- a/app/src/main/res/values-xmf/strings.xml +++ b/app/src/main/res/values-xmf/strings.xml @@ -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 4581d45ce..bd1242e12 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,7 +29,7 @@ 請稍候… 登入成功! 登入失敗! - 找不到檔案。請嘗試其它檔案看看。 + 找不到檔案。請試試看其它檔案。 未能核對身分! 開始上傳! 已上傳%1$s! @@ -42,7 +43,7 @@ 正在上載 %1$d 個檔案 正在上載 %1$d 個檔案 - 我的最近上傳 + 我最近的上傳 已佇列 失敗 %1$d%%完成 @@ -57,8 +58,7 @@ 請提供此檔案的標題 說明 無法登入-網路故障 - 無法登入-請檢查您的使用者名稱 - 無法登入-請檢查您的密碼 + 無法登入 - 請檢查您的使用者名稱與密碼 失敗次數過多。請於幾分鐘後重試。 很抱歉,該使用者已被維基共享資源封禁 必須提供您的雙重因素身分核對代碼。 @@ -110,7 +110,7 @@ 透過提交此圖片,我宣佈這是我個人創作的成品,且不包含受版權保護或自拍內容,並除此之外遵守<a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">維基媒體共享資源方針</a>。 下載 預設授權條款 - 使用先前標題/說明 + 使用先前標題、說明 自動獲取目前位置 若圖片未有地理標記,就以目前位置來作為分類建議。 夜間模式 @@ -155,9 +155,10 @@ 貢獻您的圖片,使維基百科的文章更加生動! 維基百科的圖片,來自維基共享資源。 您的圖片可以幫助教育世界各地的人。 - 避免使用受版權保護的材料,例如從互聯網找來的圖片、海報、書籍封面等 + 避免使用受版權保護的材料,例如從網際網路找來的圖片、海報、書籍封面等 明白了嗎? 是! + 此提示為空,可能無效。請見錯誤報告: https://github.com/commons-app/apps-android-commons/issues/1333 。 分類 載入中… 未選擇 @@ -243,6 +244,7 @@ 錯誤!查無 URL 提名刪除 此圖片已被提名刪除。 + 此提示為空,可能無效。請見錯誤報告: https://github.com/commons-app/apps-android-commons/issues/1333 。 於瀏覽器檢視 位置無法更改。 位置無效。 @@ -281,4 +283,6 @@ 分享應用程式 當選擇圖片時未指定座標 索取附近地點時出錯。 + 設定桌布 + 桌布設定成功! diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 2079eb925..445e0f82d 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -55,8 +55,7 @@ 请提供此文件的标题 说明 无法登录 - 网络故障 - 无法登录 - 请检查您的用户名 - 无法登录 - 请检查您的密码 + 无法登录——请检查您的用户名和密码 失败次数过多。请在几分钟后重试。 对不起,该用户已经被共享资源封禁 您必须提供您的双因素验证代码。 @@ -279,4 +278,6 @@ 分享应用 图片选择时,坐标并未指定 检索附近地点时出错。 + 设置墙纸 + 墙纸已成功设置! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6e30baa10..25934ab0c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -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. @@ -284,4 +283,8 @@ Coordinates were not specified during image selection Error fetching nearby places. + Image successfully added to %1$s on Wikidata! + Failed to update corresponding wiki data entity! + Set wallpaper + Wallpaper set successfully! diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt index b1de29143..84f6d5f10 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt @@ -1,5 +1,6 @@ package fr.free.nrw.commons +import android.content.ContentProviderClient import android.content.Context import android.content.SharedPreferences import android.support.v4.util.LruCache @@ -8,7 +9,6 @@ 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.caching.CacheController import fr.free.nrw.commons.data.DBOpenHelper import fr.free.nrw.commons.di.CommonsApplicationComponent import fr.free.nrw.commons.di.CommonsApplicationModule @@ -33,6 +33,7 @@ class TestCommonsApplication : CommonsApplication() { override fun setupLeakCanary(): RefWatcher = RefWatcher.DISABLED } +@Suppress("MemberVisibilityCanBePrivate") class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModule(appContext) { val accountUtil: AccountUtil = mock() val appSharedPreferences: SharedPreferences = mock() @@ -41,13 +42,23 @@ class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModu val otherSharedPreferences: SharedPreferences = mock() val uploadController: UploadController = mock() val mockSessionManager: SessionManager = mock() - val mediaWikiApi: MediaWikiApi = mock() val locationServiceManager: LocationServiceManager = mock() - val cacheController: CacheController = 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 @@ -61,12 +72,8 @@ class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModu override fun providesSessionManager(context: Context, mediaWikiApi: MediaWikiApi, sharedPreferences: SharedPreferences): SessionManager = mockSessionManager - override fun provideMediaWikiApi(context: Context, sharedPreferences: SharedPreferences, categorySharedPreferences: SharedPreferences, gson: Gson): MediaWikiApi = mediaWikiApi - override fun provideLocationServiceManager(context: Context): LocationServiceManager = locationServiceManager - override fun provideCacheController(): CacheController = cacheController - override fun provideDBOpenHelper(context: Context): DBOpenHelper = mockDbOpenHelper override fun provideNearbyPlaces(): NearbyPlaces = nearbyPlaces 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 index 686a90ef2..85f1ed98e 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt @@ -26,15 +26,17 @@ 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 + "/", sharedPreferences, categoryPreferences, Gson()) + testObject = ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.hostName + ":" + server.port + "/", "http://" + wikidataServer.hostName + ":" + wikidataServer.port + "/", sharedPreferences, categoryPreferences, Gson()) testObject.setWikiMediaToolforgeUrl("http://" + server.hostName + ":" + server.port + "/") } 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/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 @@ - +