Responsible for checking if the picture that the user is trying to upload is useful or not. Will attempt to filter
- * away completely black,fuzzy/blurry pictures(for now).
- *
- * todo: Detect selfies?
- */
-
-public class DetectUnwantedPicturesAsync extends AsyncTask {
-
- private final String imageMediaFilePath;
- public final WeakReference activityWeakReference;
-
- DetectUnwantedPicturesAsync(WeakReference activityWeakReference, String imageMediaFilePath) {
- //this.callback = callback;
- this.imageMediaFilePath = imageMediaFilePath;
- this.activityWeakReference = activityWeakReference;
- }
-
- @Override
- protected ImageUtils.Result doInBackground(Void... voids) {
- try {
- Timber.d("FilePath: " + imageMediaFilePath);
- if (imageMediaFilePath == null) {
- return ImageUtils.Result.IMAGE_OK;
- }
-
- BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(imageMediaFilePath,false);
-
- return ImageUtils.checkIfImageIsTooDark(decoder);
- } catch (IOException ioe) {
- Timber.e(ioe, "IO Exception");
- return ImageUtils.Result.IMAGE_OK;
- }
- }
-
- @Override
- protected void onPostExecute(ImageUtils.Result result) {
- super.onPostExecute(result);
- Activity activity = activityWeakReference.get();
-
- if (result != ImageUtils.Result.IMAGE_OK) {
- //show appropriate error message
- String errorMessage = result == ImageUtils.Result.IMAGE_DARK ? activity.getString(R.string.upload_image_too_dark) : activity.getString(R.string.upload_image_blurry);
- AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(activity);
- errorDialogBuilder.setMessage(errorMessage);
- errorDialogBuilder.setTitle(activity.getString(R.string.warning));
- errorDialogBuilder.setPositiveButton(activity.getString(R.string.no), (dialogInterface, i) -> {
- //user does not wish to upload the picture, take them back to ContributionsActivity
- Intent intent = new Intent(activity, ContributionsActivity.class);
- dialogInterface.dismiss();
- activity.startActivity(intent);
- });
- errorDialogBuilder.setNegativeButton(activity.getString(R.string.yes), (dialogInterface, i) -> {
- //user wishes to go ahead with the upload of this picture, just dismiss this dialog
- dialogInterface.dismiss();
- });
-
- AlertDialog errorDialog = errorDialogBuilder.create();
- if (!activity.isFinishing()) {
- errorDialog.show();
- }
- }
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DexterPermissionObtainer.java b/app/src/main/java/fr/free/nrw/commons/upload/DexterPermissionObtainer.java
new file mode 100644
index 000000000..64f483572
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/DexterPermissionObtainer.java
@@ -0,0 +1,153 @@
+package fr.free.nrw.commons.upload;
+
+import android.app.Activity;
+
+import com.karumi.dexter.Dexter;
+import com.karumi.dexter.DexterBuilder;
+import com.karumi.dexter.listener.PermissionDeniedResponse;
+import com.karumi.dexter.listener.PermissionGrantedResponse;
+import com.karumi.dexter.listener.single.BasePermissionListener;
+
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.utils.DialogUtil;
+import fr.free.nrw.commons.utils.ExternalStorageUtils;
+import fr.free.nrw.commons.utils.PermissionUtils;
+import io.reactivex.Completable;
+import io.reactivex.subjects.CompletableSubject;
+import timber.log.Timber;
+
+public class DexterPermissionObtainer {
+ private final String requestedPermission;
+ private android.app.AlertDialog storagePermissionInfoDialog;
+ private DexterBuilder dexterStoragePermissionBuilder;
+
+ private PermissionDeniedResponse permissionDeniedResponse;
+
+ private boolean storagePromptInProgress;
+
+ private final String rationaleTitle;
+ private final String rationaleText;
+
+ private Activity activity;
+
+ private CompletableSubject storagePromptObservable;
+
+ /**
+ * @param activity The activity that is requesting the permission
+ * @param requestedPermission The permission being requested in the form of Manifest.permission.*
+ * @param rationaleTitle The title of the rationale dialog
+ * @param rationaleText The text inside the rationale dialog
+ */
+ DexterPermissionObtainer(Activity activity, String requestedPermission, String rationaleTitle, String rationaleText) {
+ this.activity = activity;
+ this.rationaleTitle = rationaleTitle;
+ this.rationaleText = rationaleText;
+ this.requestedPermission = requestedPermission;
+ this.storagePromptObservable = CompletableSubject.create();
+ initPermissionsRationaleDialog();
+ }
+
+ /**
+ * Checks if storage permissions are obtained, prompts the users to grant storage permissions if necessary.
+ * When storage permission is present, onPermissionObtained is called.
+ */
+ Completable confirmStoragePermissions() {
+ if (ExternalStorageUtils.isStoragePermissionGranted(activity)) {
+ Timber.i("Storage permissions already granted.");
+ storagePromptObservable.onComplete();
+ } else if (!storagePromptInProgress) {
+ if (storagePromptObservable.hasComplete()) {
+ storagePromptObservable = CompletableSubject.create();
+ }
+ //If permission is not there, ask for it
+ storagePromptInProgress = true;
+ askDexterToHandleExternalStoragePermission();
+ }
+ return storagePromptObservable;
+ }
+
+
+ /**
+ * To be called when the user returns to the original activity after manually enabling storage permissions.
+ */
+ void onManualPermissionReturned() {
+ //OnActivity result, no matter what the result is, our function can handle that.
+ askDexterToHandleExternalStoragePermission();
+ }
+
+ /**
+ * This method initialised the Dexter's permission builder (if not already initialised). Also makes sure that the builder is initialised
+ * only once, otherwise we would'nt know on which instance of it, the user is working on. And after the builder is initialised, it checks
+ * for the required permission and then handles the permission status, thanks to Dexter's appropriate callbacks.
+ */
+ private void askDexterToHandleExternalStoragePermission() {
+ Timber.d("External storage permission is being requested");
+ if (null == dexterStoragePermissionBuilder) {
+ dexterStoragePermissionBuilder = Dexter.withActivity(activity)
+ .withPermission(requestedPermission)
+ .withListener(new BasePermissionListener() {
+ @Override
+ public void onPermissionGranted(PermissionGrantedResponse response) {
+ Timber.d("User has granted us the permission for writing the external storage");
+ //If permission is granted, well and good
+ storagePromptInProgress = false;
+ storagePromptObservable.onComplete();
+ //onPermissionObtained.run();
+ }
+
+ @Override
+ public void onPermissionDenied(PermissionDeniedResponse response) {
+ Timber.d("User has granted us the permission for writing the external storage");
+ //If permission is not granted in whatsoever scenario, we show him a dialog stating why we need the permission
+ permissionDeniedResponse = response;
+ if (null != storagePermissionInfoDialog && !storagePermissionInfoDialog
+ .isShowing()) {
+ storagePermissionInfoDialog.show();
+ }
+ }
+ });
+ }
+ dexterStoragePermissionBuilder.check();
+ }
+
+ /**
+ * We have agreed to show a dialog showing why we need a particular permission.
+ * This method is used to initialise the dialog which is going to show the permission's rationale.
+ * The dialog is initialised along with a callback for positive and negative user actions.
+ */
+ private void initPermissionsRationaleDialog() {
+ if (storagePermissionInfoDialog == null) {
+ storagePermissionInfoDialog = DialogUtil
+ .getAlertDialogWithPositiveAndNegativeCallbacks(
+ activity,
+ rationaleTitle, rationaleText,
+ R.drawable.ic_launcher, new DialogUtil.Callback() {
+ @Override
+ public void onPositiveButtonClicked() {
+ //If the user is willing to give us the permission
+ //But had somehow previously choose never ask again, we take him to app settings to manually enable permission
+ if (null == permissionDeniedResponse) {
+ //Dexter returned null, lets see if this ever happens
+ Timber.w("Dexter returned null as permissionDeniedResponse");
+ } else if (permissionDeniedResponse.isPermanentlyDenied()) {
+ PermissionUtils.askUserToManuallyEnablePermissionFromSettings(activity);
+ Timber.i("Permission permanently denied.");
+ } else {
+ //or if we still have chance to show runtime permission dialog, we show him that.
+ askDexterToHandleExternalStoragePermission();
+ Timber.d("Asking via Dexter for permission.");
+ }
+ }
+
+ @Override
+ public void onNegativeButtonClicked() {
+ //This was the behaviour as of now, I was planning to maybe snack him with some message
+ //and then call finish after some time, or may be it could be associated with some action
+ // on the snack. If the user does not want us to give the permission, even after showing
+ // rationale dialog, lets not trouble him any more.
+ activity.finish();
+ }
+ });
+ }
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java
deleted file mode 100644
index f74c40867..000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.support.v7.app.AlertDialog;
-
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.contributions.ContributionsActivity;
-import fr.free.nrw.commons.mwapi.MediaWikiApi;
-import timber.log.Timber;
-
-/**
- * Sends asynchronous queries to the Commons MediaWiki API to check that file doesn't already exist
- * Displays a warning to the user if the file already exists on Commons
- */
-public class ExistingFileAsync extends AsyncTask {
-
- interface Callback {
- void onResult(Result result);
- }
-
- public enum Result {
- NO_DUPLICATE,
- DUPLICATE_PROCEED,
- DUPLICATE_CANCELLED
- }
-
- private final WeakReference activity;
- private final MediaWikiApi api;
- private final String fileSha1;
- private final WeakReference context;
- private final Callback callback;
-
- public ExistingFileAsync(WeakReference activity, String fileSha1, WeakReference context, Callback callback, MediaWikiApi mwApi) {
- this.activity = activity;
- this.fileSha1 = fileSha1;
- this.context = context;
- this.callback = callback;
- this.api = mwApi;
- }
-
- @Override
- protected void onPreExecute() {
- super.onPreExecute();
- }
-
- @Override
- protected Boolean doInBackground(Void... voids) {
-
- // https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba
- boolean fileExists;
- try {
- String fileSha1 = this.fileSha1;
- fileExists = api.existingFile(fileSha1);
- } catch (IOException e) {
- Timber.e(e, "IO Exception: ");
- return false;
- }
-
- Timber.d("File already exists in Commons: %s", fileExists);
- return fileExists;
- }
-
- @Override
- protected void onPostExecute(Boolean fileExists) {
- super.onPostExecute(fileExists);
-
- // If file exists, display warning to user.
- // Use soft warning for now (user able to choose to proceed) until have determined that implementation works without bugs
- if (fileExists) {
- AlertDialog.Builder builder = new AlertDialog.Builder(context.get());
- builder.setMessage(R.string.file_exists)
- .setTitle(R.string.warning);
- builder.setPositiveButton(R.string.no, (dialog, id) -> {
- //Go back to ContributionsActivity
- Intent intent = new Intent(context.get(), ContributionsActivity.class);
- context.get().startActivity(intent);
- callback.onResult(Result.DUPLICATE_CANCELLED);
- });
- builder.setNegativeButton(R.string.yes, (dialog, id) -> callback.onResult(Result.DUPLICATE_PROCEED));
-
- AlertDialog dialog = builder.create();
- if (!activity.get().isFinishing()) {
- dialog.show();
- }
- } else {
- callback.onResult(Result.NO_DUPLICATE);
- }
- }
-}
\ No newline at end of file
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
index b29d686f5..c5b6df227 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java
@@ -1,21 +1,21 @@
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.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
+import android.support.annotation.NonNull;
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;
@@ -44,89 +44,44 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
@Inject
@Named("default_preferences")
SharedPreferences prefs;
- private Uri mediaUri;
+ private String filePath;
private ContentResolver contentResolver;
private GPSExtractor imageObj;
private Context context;
private String decimalCoords;
- private boolean haveCheckedForOtherImages = false;
- private String filePath;
+ private ExifInterface exifInterface;
private boolean useExtStorage;
- private boolean cacheFound;
+ private boolean haveCheckedForOtherImages = false;
private GPSExtractor tempImageObj;
- FileProcessor(Uri mediaUri, ContentResolver contentResolver, Context context) {
- this.mediaUri = mediaUri;
+ FileProcessor(@NonNull String filePath, ContentResolver contentResolver, Context context) {
+ this.filePath = filePath;
this.contentResolver = contentResolver;
this.context = context;
ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this);
+ try {
+ exifInterface=new ExifInterface(filePath);
+ } catch (IOException e) {
+ Timber.e(e);
+ }
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) {
+ GPSExtractor processFileCoordinates(SimilarImageInterface similarImageInterface) {
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());
- }
- } else {
- String filePath = getPathOfMediaOrCopy();
- if (filePath != null) {
- imageObj = new GPSExtractor(filePath);
- }
- }
-
- decimalCoords = imageObj.getCoords();
- if (decimalCoords == null || !imageObj.imageCoordsExists) {
- //Find other photos taken around the same time which has gps coordinates
- if (!haveCheckedForOtherImages)
- findOtherImages();// Do not do repeat the process
- } else {
- useImageCoords();
- }
-
- } catch (FileNotFoundException e) {
- Timber.w("File not found: " + mediaUri, e);
+ imageObj = new GPSExtractor(exifInterface);
+ decimalCoords = imageObj.getCoords();
+ if (decimalCoords == null || !imageObj.imageCoordsExists) {
+ //Find other photos taken around the same time which has gps coordinates
+ if (!haveCheckedForOtherImages)
+ findOtherImages(similarImageInterface);// Do not do repeat the process
+ } else {
+ useImageCoords();
}
+
return imageObj;
}
@@ -136,10 +91,10 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
/**
* Find other images around the same location that were taken within the last 20 sec
- *
+ * @param similarImageInterface
*/
- private void findOtherImages() {
- Timber.d("filePath" + getPathOfMediaOrCopy());
+ private void findOtherImages(SimilarImageInterface similarImageInterface) {
+ Timber.d("filePath" + filePath);
long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created
File folder = new File(filePath.substring(0, filePath.lastIndexOf('/')));
@@ -154,7 +109,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos
ParcelFileDescriptor descriptor = null;
try {
- descriptor = contentResolver.openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r");
+ descriptor = contentResolver.openFileDescriptor(Uri.fromFile(file), "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
@@ -173,12 +128,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
if (tempImageObj.getCoords() != 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");
+ similarImageInterface.showSimilarImageFragment(filePath, file.getAbsolutePath());
break;
}
}
@@ -210,7 +160,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
// 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())
@@ -223,7 +172,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
);
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);
}
@@ -232,20 +180,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
}
}
- 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;
@@ -259,4 +193,4 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
Timber.d("EXIF from imageObj");
useImageCoords();
}
-}
+}
\ No newline at end of file
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 0cd45c189..9401c941e 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
@@ -1,6 +1,7 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
+import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.SharedPreferences;
@@ -12,6 +13,7 @@ import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
+import android.provider.OpenableColumns;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -33,6 +35,8 @@ import java.util.Date;
import timber.log.Timber;
+import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext;
+
public class FileUtils {
/**
@@ -76,21 +80,32 @@ public class FileUtils {
/**
* 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;
- }
+ @NonNull
+ static String createExternalCopyPathAndCopy(Uri uri, ContentResolver contentResolver) throws IOException {
+ FileDescriptor fileDescriptor = contentResolver.openFileDescriptor(uri, "r").getFileDescriptor();
+ String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + "." + getFileExt(uri, contentResolver);
+ File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
+ newFile.mkdir();
+ FileUtils.copy(fileDescriptor, copyPath);
+ Timber.d("Filepath (copied): %s", copyPath);
+ return copyPath;
+ }
+
+ /**
+ * 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
+ */
+ @NonNull
+ static String createCopyPathAndCopy(Uri uri, Context context) throws IOException {
+ FileDescriptor fileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r").getFileDescriptor();
+ String copyPath = context.getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + "." + getFileExt(uri, context.getContentResolver());
+ FileUtils.copy(fileDescriptor, copyPath);
+ Timber.d("Filepath (copied): %s", copyPath);
+ return copyPath;
}
/**
@@ -121,13 +136,13 @@ public class FileUtils {
if ("primary".equalsIgnoreCase(type)) {
returnPath = Environment.getExternalStorageDirectory() + "/" + split[1];
}
- } else if (isDownloadsDocument(uri)) { // DownloadsProvider
+ } else if (isDownloadsDocument(uri)) { // DownloadsProvider
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/document"), Long.valueOf(id));
- returnPath = getDataColumn(context, contentUri, null, null);
+ returnPath = getDataColumn(context, contentUri, null, null);
} else if (isMediaDocument(uri)) { // MediaProvider
final String docId = DocumentsContract.getDocumentId(uri);
@@ -166,7 +181,7 @@ public class FileUtils {
returnPath = uri.getPath();
}
- if(returnPath == null) {
+ if (returnPath == null) {
//fetching path may fail depending on the source URI and all hope is lost
//so we will create and use a copy of the file, which seems to work
String copyPath = null;
@@ -304,6 +319,7 @@ 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
*/
@@ -330,6 +346,7 @@ public class FileUtils {
/**
* Deletes files.
+ *
* @param file context
*/
public static boolean deleteFile(File file) {
@@ -355,7 +372,7 @@ public class FileUtils {
commonsAppDirectory.mkdir();
}
- File logsFile = new File(commonsAppDirectory,"logs.txt");
+ File logsFile = new File(commonsAppDirectory, "logs.txt");
if (logsFile.exists()) {
//old logs file is useless
logsFile.delete();
@@ -377,4 +394,39 @@ public class FileUtils {
}
}
+ public static String getFilename(Uri uri, ContentResolver contentResolver) {
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN)
+ return "";
+ String result = null;
+ if (uri.getScheme().equals("content")) {
+ try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
+ }
+ }
+ }
+ if (result == null) {
+ result = uri.getPath();
+ int cut = result.lastIndexOf('/');
+ if (cut != -1) {
+ result = result.substring(cut + 1);
+ }
+ }
+ return result;
+ }
+
+ public static String getFileExt(String fileName){
+ //Default file extension
+ String extension=".jpg";
+
+ int i = fileName.lastIndexOf('.');
+ if (i > 0) {
+ extension = fileName.substring(i+1);
+ }
+ return extension;
+ }
+
+ public static String getFileExt(Uri uri, ContentResolver contentResolver) {
+ return getFileExt(getFilename(uri, contentResolver));
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java
index e45b31f05..a6b150c42 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java
@@ -16,11 +16,22 @@ import timber.log.Timber;
*/
public class GPSExtractor {
- private ExifInterface exif;
+ public static final GPSExtractor DUMMY= new GPSExtractor();
private double decLatitude;
private double decLongitude;
public boolean imageCoordsExists;
+ private String latitude;
+ private String longitude;
+ private String latitudeRef;
+ private String longitudeRef;
+ private String decimalCoords;
+ /**
+ * Dummy constructor.
+ */
+ private GPSExtractor(){
+
+ }
/**
* Construct from the file descriptor of the image (only for API 24 or newer).
* @param fileDescriptor the file descriptor of the image
@@ -28,7 +39,8 @@ public class GPSExtractor {
@RequiresApi(24)
public GPSExtractor(@NonNull FileDescriptor fileDescriptor) {
try {
- exif = new ExifInterface(fileDescriptor);
+ ExifInterface exif = new ExifInterface(fileDescriptor);
+ processCoords(exif);
} catch (IOException | IllegalArgumentException e) {
Timber.w(e);
}
@@ -41,47 +53,53 @@ public class GPSExtractor {
*/
public GPSExtractor(@NonNull String path) {
try {
- exif = new ExifInterface(path);
+ ExifInterface exif = new ExifInterface(path);
+ processCoords(exif);
} catch (IOException | IllegalArgumentException e) {
Timber.w(e);
}
}
+ /**
+ * Construct from the file path of the image.
+ * @param exif exif interface of the image
+ *
+ */
+ public GPSExtractor(@NonNull ExifInterface exif){
+ processCoords(exif);
+ }
+
+ private void processCoords(ExifInterface exif){
+ //If image has no EXIF data and user has enabled GPS setting, get user's location
+ //Always return null as a temporary fix for #1599
+ if (exif != null && exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) != null) {
+ //If image has EXIF data, extract image coords
+ imageCoordsExists = true;
+ Timber.d("EXIF data has location info");
+
+ latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
+ latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
+ longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
+ longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
+ }
+ }
+
/**
* Extracts geolocation (either of image from EXIF data, or of user)
* @return coordinates as string (needs to be passed as a String in API query)
*/
@Nullable
public String getCoords() {
- String latitude;
- String longitude;
- String latitudeRef;
- String longitudeRef;
- String decimalCoords;
+ if(decimalCoords!=null){
+ return decimalCoords;
+ }else if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) {
+ Timber.d("Latitude: %s %s", latitude, latitudeRef);
+ Timber.d("Longitude: %s %s", longitude, longitudeRef);
- //If image has no EXIF data and user has enabled GPS setting, get user's location
- //TODO: Always return null as a temporary fix for #1599
- if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) {
- return null;
+ decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef);
+ return decimalCoords;
} else {
- //If image has EXIF data, extract image coords
- imageCoordsExists = true;
- Timber.d("EXIF data has location info");
-
- latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
- latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
- longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
- longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
-
- if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) {
- Timber.d("Latitude: %s %s", latitude, latitudeRef);
- Timber.d("Longitude: %s %s", longitude, longitudeRef);
-
- decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef);
- return decimalCoords;
- } else {
- return null;
- }
+ return null;
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java b/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java
new file mode 100644
index 000000000..ff100e16e
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java
@@ -0,0 +1,52 @@
+package fr.free.nrw.commons.upload;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Point;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.Display;
+
+/**
+ * Created by Ilgaz Er on 8/7/2018.
+ */
+public class HeightLimitedRecyclerView extends RecyclerView {
+
+ int height;
+
+
+ public HeightLimitedRecyclerView(Context context) {
+ super(context);
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ ((Activity) getContext()).getWindowManager()
+ .getDefaultDisplay()
+ .getMetrics(displayMetrics);
+ height=displayMetrics.heightPixels;
+ }
+
+ public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ ((Activity) getContext()).getWindowManager()
+ .getDefaultDisplay()
+ .getMetrics(displayMetrics);
+ height=displayMetrics.heightPixels;
+ }
+
+ public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ ((Activity) getContext()).getWindowManager()
+ .getDefaultDisplay()
+ .getMetrics(displayMetrics);
+ height=displayMetrics.heightPixels;
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ heightSpec = MeasureSpec.makeMeasureSpec((int) (height*0.3), MeasureSpec.AT_MOST);
+ super.onMeasure(widthSpec, heightSpec);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Language.java b/app/src/main/java/fr/free/nrw/commons/upload/Language.java
index 8d4b27239..ab03a4db7 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/Language.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/Language.java
@@ -3,7 +3,6 @@ package fr.free.nrw.commons.upload;
import java.util.Locale;
class Language {
-
private Locale locale;
private boolean isSet = false;
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
deleted file mode 100644
index 1349ed773..000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java
+++ /dev/null
@@ -1,493 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.Manifest;
-import android.Manifest.permission;
-import android.app.AlertDialog;
-import android.app.ProgressDialog;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.database.DataSetObserver;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.ParcelFileDescriptor;
-import android.support.annotation.Nullable;
-import android.support.v4.app.ActivityCompat;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.content.ContextCompat;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.AdapterView;
-import android.widget.Toast;
-
-import com.karumi.dexter.Dexter;
-import com.karumi.dexter.DexterBuilder;
-import com.karumi.dexter.listener.PermissionDeniedResponse;
-import com.karumi.dexter.listener.PermissionGrantedResponse;
-import com.karumi.dexter.listener.single.BasePermissionListener;
-
-import java.io.FileNotFoundException;
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-import butterknife.ButterKnife;
-import fr.free.nrw.commons.BuildConfig;
-import fr.free.nrw.commons.CommonsApplication;
-import fr.free.nrw.commons.Media;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.auth.AuthenticatedActivity;
-import fr.free.nrw.commons.auth.SessionManager;
-import fr.free.nrw.commons.category.CategorizationFragment;
-import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
-import fr.free.nrw.commons.contributions.Contribution;
-import fr.free.nrw.commons.media.MediaDetailPagerFragment;
-import fr.free.nrw.commons.modifications.CategoryModifier;
-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.MediaWikiApi;
-import fr.free.nrw.commons.utils.ContributionUtils;
-import fr.free.nrw.commons.utils.DialogUtil;
-import fr.free.nrw.commons.utils.DialogUtil.Callback;
-import fr.free.nrw.commons.utils.ExternalStorageUtils;
-import fr.free.nrw.commons.utils.PermissionUtils;
-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,
- FragmentManager.OnBackStackChangedListener,
- MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
- OnCategoriesSaveHandler,
- ActivityCompat.OnRequestPermissionsResultCallback{
-
- @Inject
- MediaWikiApi mwApi;
- @Inject
- SessionManager sessionManager;
- @Inject
- UploadController uploadController;
- @Inject
- ModifierSequenceDao modifierSequenceDao;
- @Inject
- @Named("default_preferences")
- SharedPreferences prefs;
-
- private ArrayList photosList = null;
-
- private MultipleUploadListFragment uploadsList;
- private MediaDetailPagerFragment mediaDetails;
- private CategorizationFragment categorizationFragment;
-
- private boolean locationPermitted = false;
- private boolean isMultipleUploadsPrepared = false;
- private boolean isMultipleUploadsFinalised = false; // Checks is user clicked to upload button or regret before this phase
- private final String TAG="#MultipleShareActivity#";
- private AlertDialog storagePermissionInfoDialog;
- private DexterBuilder dexterStoragePermissionBuilder;
-
- private PermissionDeniedResponse permissionDeniedResponse;
-
- @Override
- public Media getMediaAtPosition(int i) {
- return photosList.get(i);
- }
-
- @Override
- public int getTotalMediaCount() {
- if (photosList == null) {
- return 0;
- }
- return photosList.size();
- }
-
- @Override
- public void notifyDatasetChanged() {
- if (uploadsList != null) {
- uploadsList.notifyDatasetChanged();
- }
- }
-
- @Override
- public void registerDataSetObserver(DataSetObserver observer) {
- // fixme implement me if needed
- }
-
- @Override
- public void unregisterDataSetObserver(DataSetObserver observer) {
- // fixme implement me if needed
- }
-
- @Override
- public void onItemClick(AdapterView> adapterView, View view, int index, long item) {
- showDetail(index);
- }
-
- @Override
- public void OnMultipleUploadInitiated() {
- // No need to request external permission here, because if user can reach this point, then she permission granted
- Timber.d("OnMultipleUploadInitiated");
- multipleUploadBegins();
- }
-
- private void multipleUploadBegins() {
-
- Timber.d("Multiple upload begins");
- final ProgressDialog dialog = new ProgressDialog(this);
- dialog.setIndeterminate(false);
- dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
- dialog.setMax(photosList.size());
- dialog.setTitle(getResources().getQuantityString(R.plurals.starting_multiple_uploads, photosList.size(), photosList.size()));
- dialog.show();
-
- for (int i = 0; i < photosList.size(); i++) {
- Contribution up = photosList.get(i);
- final int uploadCount = i + 1; // Goddamn Java
-
- uploadController.startUpload(up, contribution -> {
- dialog.setProgress(uploadCount);
- if (uploadCount == photosList.size()) {
- dialog.dismiss();
- Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG);
- startingToast.show();
- }
- });
- }
-
- uploadsList.setImageOnlyMode(true);
-
- categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization");
- if (categorizationFragment == null) {
- categorizationFragment = new CategorizationFragment();
- }
- // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
- View target = getCurrentFocus();
- if (target != null) {
- InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
- if (imm != null)
- imm.hideSoftInputFromWindow(target.getWindowToken(), 0);
- }
- getSupportFragmentManager().beginTransaction()
- .add(R.id.uploadsFragmentContainer, categorizationFragment, "categorization")
- .commitAllowingStateLoss();
- isMultipleUploadsFinalised = true;
- //See http://stackoverflow.com/questions/7469082/getting-exception-illegalstateexception-can-not-perform-this-action-after-onsa
- }
-
- @Override
- public void onCategoriesSave(List categories) {
- if (categories.size() > 0) {
- for (Contribution contribution : photosList) {
- ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
-
- categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
- categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
-
- modifierSequenceDao.save(categoriesSequence);
- }
- }
- // FIXME: Make sure that the content provider is up
- // This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
- ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default!
- finish();
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- if (mediaDetails.isVisible()) {
- getSupportFragmentManager().popBackStack();
- }
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.activity_multiple_uploads);
- ButterKnife.bind(this);
- initDrawer();
- initPermissionsRationaleDialog();
-
- if (savedInstanceState != null) {
- photosList = savedInstanceState.getParcelableArrayList("uploadsList");
- }
-
- getSupportFragmentManager().addOnBackStackChangedListener(this);
- requestAuthToken();
-
- //TODO: 15/10/17 should location permission be explicitly requested if not provided?
- //check if location permission is enabled
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
- {
- locationPermitted = true;
- }
- }
- }
-
-
- /**
- * We have agreed to show a dialog showing why we need a particular permission.
- * This method is used to initialise the dialog which is going to show the permission's rationale.
- * The dialog is initialised along with a callback for positive and negative user actions.
- */
- private void initPermissionsRationaleDialog() {
- if (storagePermissionInfoDialog == null) {
- storagePermissionInfoDialog = DialogUtil
- .getAlertDialogWithPositiveAndNegativeCallbacks(
- MultipleShareActivity.this,
- getString(R.string.storage_permission), getString(
- R.string.write_storage_permission_rationale_for_image_share),
- R.drawable.ic_launcher, new Callback() {
- @Override
- public void onPositiveButtonClicked() {
- //If the user is willing to give us the permission
- //But had somehow previously choose never ask again, we take him to app settings to manually enable permission
- if(null== permissionDeniedResponse){
- //Dexter returned null, lets see if this ever happens
- return;
- }
- else if (permissionDeniedResponse.isPermanentlyDenied()) {
- PermissionUtils.askUserToManuallyEnablePermissionFromSettings(MultipleShareActivity.this);
- } else {
- //or if we still have chance to show runtime permission dialog, we show him that.
- askDexterToHandleExternalStoragePermission();
- }
- }
-
- @Override
- public void onNegativeButtonClicked() {
- //This was the behaviour as of now, I was planning to maybe snack him with some message
- //and then call finish after some time, or may be it could be associated with some action on the snack
- //If the user does not want us to give the permission, even after showing rationale dialog, lets not trouble him anymore
- finish();
- }
- });
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- getSupportFragmentManager().removeOnBackStackChangedListener(this);
- uploadController.cleanup();
- }
-
- private void showDetail(int i) {
- if (mediaDetails == null || !mediaDetails.isVisible()) {
- mediaDetails = new MediaDetailPagerFragment(true, false);
- getSupportFragmentManager()
- .beginTransaction()
- .replace(R.id.uploadsFragmentContainer, mediaDetails)
- .addToBackStack(null)
- .commit();
- getSupportFragmentManager().executePendingTransactions();
- }
- mediaDetails.showImage(i);
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- /* This will be true if permission request is granted before we request. Otherwise we will
- * explicitly call operations under this method again.
- */
- if (isMultipleUploadsPrepared) {
- super.onSaveInstanceState(outState);
- Timber.d("onSaveInstanceState multiple uploads is prepared, permission granted");
- outState.putParcelableArrayList("uploadsList", photosList);
- } else {
- Timber.d("onSaveInstanceState multiple uploads is not prepared, permission not granted");
- return;
- }
- }
-
- @Override
- protected void onAuthCookieAcquired(String authCookie) {
- // Multiple uploads prepared boolean is used to decide when to call multipleUploadsBegin()
- isMultipleUploadsFinalised = false;
- isMultipleUploadsPrepared = false;
- mwApi.setAuthCookie(authCookie);
- if (!ExternalStorageUtils.isStoragePermissionGranted(this)) {
- //If permission is not there, handle the negative cases
- askDexterToHandleExternalStoragePermission();
- isMultipleUploadsPrepared = false;
- return; // Postpone operation to do after gettion permission
- } else {
- isMultipleUploadsPrepared = true;
- prepareMultipleUploadList();
- }
- }
-
- /**
- * This method initialised the Dexter's permission builder (if not already initialised). Also makes sure that the builder is initialised
- * only once, otherwise we would'nt know on which instance of it, the user is working on. And after the builder is initialised, it checks for the required
- * permission and then handles the permission status, thanks to Dexter's appropriate callbacks.
- */
- private void askDexterToHandleExternalStoragePermission() {
- Timber.d(TAG, "External storage permission is being requested");
- if (null == dexterStoragePermissionBuilder) {
- dexterStoragePermissionBuilder = Dexter.withActivity(this)
- .withPermission(permission.WRITE_EXTERNAL_STORAGE)
- .withListener(new BasePermissionListener() {
- @Override
- public void onPermissionGranted(PermissionGrantedResponse response) {
- Timber.d(TAG,"User has granted us the permission for writing the external storage");
- //If permission is granted, well and good
- prepareMultipleUploadList();
- }
-
- @Override
- public void onPermissionDenied(PermissionDeniedResponse response) {
- Timber.d(TAG,"User has granted us the permission for writing the external storage");
- //If permission is not granted in whatsoever scenario, we show him a dialog stating why we need the permission
- permissionDeniedResponse=response;
- if (null != storagePermissionInfoDialog && !storagePermissionInfoDialog
- .isShowing()) {
- storagePermissionInfoDialog.show();
- }
- }
- });
- }
- dexterStoragePermissionBuilder.check();
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) {
- //OnActivity result, no matter what the result is, our function can handle that.
- askDexterToHandleExternalStoragePermission();
- }
- }
-
- /**
- * Prepares a list from files will be uploaded. Saves these files temporarily to external
- * storage. Adds them to uploads list
- */
- private void prepareMultipleUploadList() {
- Intent intent = getIntent();
-
- if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
- if (photosList == null) {
- photosList = new ArrayList<>();
- ArrayList urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
- for (int i = 0; i < urisList.size(); i++) {
- Contribution up = new Contribution();
- Uri uri = urisList.get(i);
- // Use temporarily saved file Uri instead
- uri = ContributionUtils.saveFileBeingUploadedTemporarily(this, uri);
- up.setLocalUri(uri);
- up.setTag("mimeType", intent.getType());
- up.setTag("sequence", i);
- up.setSource(Contribution.SOURCE_EXTERNAL);
- up.setMultiple(true);
- String imageGpsCoordinates = extractImageGpsData(uri);
- if (imageGpsCoordinates != null) {
- Timber.d("GPS data for image found!");
- up.setDecimalCoords(imageGpsCoordinates);
- }
- photosList.add(up);
- }
- }
-
- uploadsList = (MultipleUploadListFragment) getSupportFragmentManager().findFragmentByTag("uploadsList");
- if (uploadsList == null) {
- uploadsList = new MultipleUploadListFragment();
- getSupportFragmentManager()
- .beginTransaction()
- .add(R.id.uploadsFragmentContainer, uploadsList, "uploadsList")
- .commit();
- }
- setTitle(getResources().getQuantityString(R.plurals.multiple_uploads_title, photosList.size(), photosList.size()));
- uploadController.prepareService();
- }
- }
-
- @Override
- protected void onAuthFailure() {
- Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG);
- failureToast.show();
- finish();
- }
-
- @Override
- public void onBackStackChanged() {
- getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible());
- }
-
- /**
- * Will attempt to extract the gps coordinates using exif data or by using the current
- * location if available for the image who's imageUri has been provided.
- * @param imageUri The uri of the image who's GPS coordinates data we wish to extract
- * @return GPS coordinates as a String as is returned by {@link GPSExtractor}
- */
- @Nullable
- private String extractImageGpsData(Uri imageUri) {
- Timber.d("Entering extractImagesGpsData");
-
- if (imageUri == null) {
- //now why would you do that???
- return null;
- }
-
- GPSExtractor gpsExtractor = null;
-
- try {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(imageUri,"r");
- if (fd != null) {
- gpsExtractor = new GPSExtractor(fd.getFileDescriptor());
- }
- } else {
- String filePath = FileUtils.getPath(this,imageUri);
- if (filePath != null) {
- gpsExtractor = new GPSExtractor(filePath);
- }
- }
-
- if (gpsExtractor != null) {
- //get image coordinates from exif data or user location
- return gpsExtractor.getCoords();
- }
-
- } catch (FileNotFoundException fnfe) {
- Timber.w(fnfe);
- return null;
- }
-
- return null;
- }
-
- // If on back pressed before sharing
- @Override
- public void onBackPressed() {
- super.onBackPressed();
- }
-
- @Override
- protected void onStop() {
- // Remove saved files if activity is stopped before upload operation, ie user changed mind
- if (!isMultipleUploadsFinalised) {
- if (photosList != null) {
- for (Contribution contribution : photosList) {
- Timber.d("User changed mind, didn't click to upload button, deleted file: "+contribution.getLocalUri());
- ContributionUtils.removeTemporaryFile(contribution.getLocalUri());
- }
- }
- }
- super.onStop();
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java
deleted file mode 100644
index 0dfdf5589..000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java
+++ /dev/null
@@ -1,254 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.content.Context;
-import android.graphics.Point;
-import android.net.Uri;
-import android.os.Bundle;
-import android.support.graphics.drawable.VectorDrawableCompat;
-import android.support.v4.app.Fragment;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.util.DisplayMetrics;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.BaseAdapter;
-import android.widget.EditText;
-import android.widget.FrameLayout;
-import android.widget.GridView;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
-import com.facebook.drawee.view.SimpleDraweeView;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import dagger.android.support.AndroidSupportInjection;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.contributions.Contribution;
-import fr.free.nrw.commons.media.MediaDetailPagerFragment;
-import fr.free.nrw.commons.utils.ViewUtil;
-
-public class MultipleUploadListFragment extends Fragment {
-
- public interface OnMultipleUploadInitiatedHandler {
- void OnMultipleUploadInitiated();
- }
-
- @BindView(R.id.multipleShareBackground)
- GridView photosGrid;
-
- @BindView(R.id.multipleBaseTitle)
- EditText baseTitle;
-
- private PhotoDisplayAdapter photosAdapter;
- private TitleTextWatcher textWatcher = new TitleTextWatcher();
-
- private Point photoSize;
- private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
- private OnMultipleUploadInitiatedHandler multipleUploadInitiatedHandler;
-
- private boolean imageOnlyMode;
-
- private static class UploadHolderView {
- private Uri imageUri;
- private SimpleDraweeView image;
- private TextView title;
- private RelativeLayout overlay;
- }
-
- @Override
- public void onAttach(Context context) {
- AndroidSupportInjection.inject(this);
- super.onAttach(context);
- }
-
- private class PhotoDisplayAdapter extends BaseAdapter {
-
- @Override
- public int getCount() {
- return detailProvider.getTotalMediaCount();
- }
-
- @Override
- public Object getItem(int i) {
- return detailProvider.getMediaAtPosition(i);
- }
-
- @Override
- public long getItemId(int i) {
- return i;
- }
-
- @Override
- public View getView(int i, View view, ViewGroup viewGroup) {
- UploadHolderView holder;
-
- if (view == null) {
- view = LayoutInflater.from(getContext()).inflate(R.layout.layout_upload_item, viewGroup, false);
- holder = new UploadHolderView();
- holder.image = view.findViewById(R.id.uploadImage);
- holder.title = view.findViewById(R.id.uploadTitle);
- holder.overlay = view.findViewById(R.id.uploadOverlay);
-
- holder.image.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, photoSize.y));
- holder.image.setHierarchy(GenericDraweeHierarchyBuilder
- .newInstance(getResources())
- .setPlaceholderImage(VectorDrawableCompat.create(getResources(),
- R.drawable.ic_image_black_24dp, getContext().getTheme()))
- .setFailureImage(VectorDrawableCompat.create(getResources(),
- R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
- .build());
- view.setTag(holder);
- } else {
- holder = (UploadHolderView) view.getTag();
- }
-
- Contribution up = (Contribution) this.getItem(i);
-
- if (holder.imageUri == null || !holder.imageUri.equals(up.getLocalUri())) {
- holder.image.setImageURI(up.getLocalUri().toString());
- holder.imageUri = up.getLocalUri();
- }
-
- if (!imageOnlyMode) {
- holder.overlay.setVisibility(View.VISIBLE);
- holder.title.setText(up.getFilename());
- } else {
- holder.overlay.setVisibility(View.GONE);
- }
-
- return view;
- }
- }
-
- @Override
- public void onStop() {
- super.onStop();
-
- // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
- View target = getActivity().getCurrentFocus();
- ViewUtil.hideKeyboard(target);
- }
-
- // FIXME: Wrong result type
- private Point calculatePicDimension(int count) {
- DisplayMetrics screenMetrics = getResources().getDisplayMetrics();
- int screenWidth = screenMetrics.widthPixels;
- int screenHeight = screenMetrics.heightPixels;
-
- int picWidth = Math.min((int) Math.sqrt(screenWidth * screenHeight / count), screenWidth);
- picWidth = Math.min((int) (192 * screenMetrics.density), Math.max((int) (120 * screenMetrics.density), picWidth / 48 * 48));
- int picHeight = Math.min(picWidth, (int) (192 * screenMetrics.density)); // Max Height is same as Contributions list
-
- return new Point(picWidth, picHeight);
- }
-
- public void notifyDatasetChanged() {
- if (photosAdapter != null) {
- photosAdapter.notifyDataSetChanged();
- }
- }
-
- public void setImageOnlyMode(boolean mode) {
- imageOnlyMode = mode;
- if (imageOnlyMode) {
- baseTitle.setVisibility(View.GONE);
- } else {
- baseTitle.setVisibility(View.VISIBLE);
- }
- photosAdapter.notifyDataSetChanged();
- photosGrid.setEnabled(!mode);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View view = inflater.inflate(R.layout.fragment_multiple_uploads_list, container, false);
- ButterKnife.bind(this,view);
- photosAdapter = new PhotoDisplayAdapter();
- photosGrid.setAdapter(photosAdapter);
- photosGrid.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
- photoSize = calculatePicDimension(detailProvider.getTotalMediaCount());
- photosGrid.setColumnWidth(photoSize.x);
-
- baseTitle.addTextChangedListener(textWatcher);
-
- baseTitle.setOnFocusChangeListener((v, hasFocus) -> {
- if (!hasFocus) {
- ViewUtil.hideKeyboard(v);
- }
- });
-
- return view;
- }
-
- @Override
- public void onDestroyView() {
- baseTitle.removeTextChangedListener(textWatcher);
- super.onDestroyView();
- }
-
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- super.onCreateOptionsMenu(menu, inflater);
- menu.clear();
- inflater.inflate(R.menu.fragment_multiple_upload_list, menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_upload_multiple:
- if (baseTitle.getText().toString().trim().isEmpty()) {
- Toast.makeText(getContext(), R.string.add_set_name_toast, Toast.LENGTH_LONG).show();
- return false;
- }
- multipleUploadInitiatedHandler.OnMultipleUploadInitiated();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity();
- multipleUploadInitiatedHandler = (OnMultipleUploadInitiatedHandler) getActivity();
-
- setHasOptionsMenu(true);
- }
-
- private class TitleTextWatcher implements TextWatcher {
- @Override
- public void beforeTextChanged(CharSequence charSequence, int i1, int i2, int i3) {
- }
-
- @Override
- public void onTextChanged(CharSequence charSequence, int i1, int i2, int i3) {
- for (int i = 0; i < detailProvider.getTotalMediaCount(); i++) {
- Contribution up = (Contribution) detailProvider.getMediaAtPosition(i);
- Boolean isDirty = (Boolean) up.getTag("isDirty");
- if (isDirty == null || !isDirty) {
- if (!TextUtils.isEmpty(charSequence)) {
- up.setFilename(charSequence.toString() + " - " + ((Integer) up.getTag("sequence") + 1));
- } else {
- up.setFilename("");
- }
- }
- }
- detailProvider.notifyDatasetChanged();
- }
-
- @Override
- public void afterTextChanged(Editable editable) {
- }
- }
-}
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
deleted file mode 100644
index 6757c8419..000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java
+++ /dev/null
@@ -1,674 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.Manifest;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.graphics.Bitmap;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Environment;
-import android.support.annotation.NonNull;
-import android.support.annotation.RequiresApi;
-import android.support.design.widget.FloatingActionButton;
-import android.support.graphics.drawable.VectorDrawableCompat;
-import android.support.v4.app.ActivityCompat;
-import android.support.v4.content.ContextCompat;
-import android.view.KeyEvent;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.animation.DecelerateInterpolator;
-import android.widget.FrameLayout;
-import android.widget.Toast;
-
-import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
-import com.facebook.drawee.view.SimpleDraweeView;
-import com.github.chrisbanes.photoview.PhotoView;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.ref.WeakReference;
-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.BuildConfig;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.auth.AuthenticatedActivity;
-import fr.free.nrw.commons.auth.LoginActivity;
-import fr.free.nrw.commons.auth.SessionManager;
-import fr.free.nrw.commons.caching.CacheController;
-import fr.free.nrw.commons.category.CategorizationFragment;
-import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
-import fr.free.nrw.commons.contributions.Contribution;
-import fr.free.nrw.commons.modifications.CategoryModifier;
-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.ContributionUtils;
-import fr.free.nrw.commons.utils.ExternalStorageUtils;
-import fr.free.nrw.commons.utils.ViewUtil;
-import timber.log.Timber;
-
-import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.DUPLICATE_PROCEED;
-import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE;
-import static fr.free.nrw.commons.upload.FileUtils.getSHA1;
-import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
-
-/**
- * Activity for the title/desc screen after image is selected. Also starts processing image
- * GPS coordinates or user location (if enabled in Settings) for category suggestions.
- */
-public class ShareActivity
- extends AuthenticatedActivity
- implements SingleUploadFragment.OnUploadActionInitiated,
- OnCategoriesSaveHandler,
- ActivityCompat.OnRequestPermissionsResultCallback {
-
- private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4;
- //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
- CacheController cacheController;
- @Inject
- SessionManager sessionManager;
- @Inject
- UploadController uploadController;
- @Inject
- ModifierSequenceDao modifierSequenceDao;
- @Inject
- CategoryApi apiCall;
- @Inject @Named("application_preferences") SharedPreferences applicationPrefs;
- @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 Uri contentProviderUri;
- private Contribution contribution;
- 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 boolean duplicateCheckPassed = false;
- private boolean isNearbyUpload = false;
- private Animator CurrentAnimator;
- private long ShortAnimationDuration;
- private boolean isFABOpen = false;
- private float startScaleFinal;
- private Bundle savedInstanceState;
- private boolean isUploadFinalised = false; // Checks is user clicked to upload button or regret before this phase
- private boolean isZoom = false;
-
-
-
- /**
- * Called when user taps the submit button.
- * Requests Storage permission, if needed.
- */
-
- @Override
- public void uploadActionInitiated(String title, String description) {
-
- this.title = title;
- this.description = description;
-
-
- if (sessionManager.getCurrentAccount() != null) {
- 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);
- } else {
- uploadBegins();
- }
- } else {
- uploadBegins();
- }
- }
- else //Send user to login activity
- {
- Toast.makeText(this, "You need to login first!", Toast.LENGTH_SHORT).show();
- Intent loginIntent = new Intent(ShareActivity.this, LoginActivity.class);
- startActivity(loginIntent);
- }
- }
-
- /**
- * 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() {
- return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri)
- && (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
- != PackageManager.PERMISSION_GRANTED);
- //return false;
- }
-
-
- /**
- * Called after permission checks are done.
- * Gets file metadata for category suggestions, displays toast, caches categories found, calls uploadController
- */
-
- private void uploadBegins() {
- fileObj.processFileCoordinates(locationPermitted);
-
- Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG);
- startingToast.show();
-
- if (!fileObj.isCacheFound()) {
- //Has to be called after apiCall.request()
- cacheController.cacheCategory();
- Timber.d("Cache the categories found");
- }
-
- uploadController.startUpload(title, contentProviderUri, mediaUri, description, mimeType, source, decimalCoords, wikiDataEntityId, c -> {
- ShareActivity.this.contribution = c;
- showPostUpload();
- });
- isUploadFinalised = true;
- }
-
- /**
- * Starts CategorizationFragment after uploadBegins.
- */
-
- private void showPostUpload() {
- if (categorizationFragment == null) {
- categorizationFragment = new CategorizationFragment();
- }
- getSupportFragmentManager().beginTransaction()
- .replace(R.id.single_upload_fragment_container, categorizationFragment, "categorization")
- .commit();
- }
-
- /**
- * Send categories to modifications queue after they are selected
- *
- * @param categories categories selected
- */
- @Override
- public void onCategoriesSave(List categories) {
- if (categories.size() > 0) {
- ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
-
- categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
- categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
- modifierSequenceDao.save(categoriesSequence);
- }
-
- // FIXME: Make sure that the content provider is up
- // This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
- ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default!
-
- finish();
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- if (contribution != null) {
- outState.putParcelable("contribution", contribution);
- }
- }
-
- @Override
- protected void onAuthCookieAcquired(String authCookie) {
- mwApi.setAuthCookie(authCookie);
- }
-
- @Override
- protected void onAuthFailure() {
- Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG);
- failureToast.show();
- finish();
- }
-
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- isUploadFinalised = false;
- setContentView(R.layout.activity_share);
- ButterKnife.bind(this);
- initBack();
- backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder
- .newInstance(getResources())
- .setPlaceholderImage(VectorDrawableCompat.create(getResources(),
- R.drawable.ic_image_black_24dp, getTheme()))
- .setFailureImage(VectorDrawableCompat.create(getResources(),
- R.drawable.ic_error_outline_black_24dp, getTheme()))
- .build());
- if (!ExternalStorageUtils.isStoragePermissionGranted(this)) {
- this.savedInstanceState = savedInstanceState;
- ExternalStorageUtils.requestExternalStoragePermission(this);
- return; // Postpone operation to do after getting permission
- } else {
- receiveImageIntent();
- createContributionWithReceivedIntent(savedInstanceState);
- }
- }
-
- @Override
- protected void onStop() {
- // If upload is not finalised with failure or success, but contribution is created,
- // we have to remove temp file, to prevent using unnecessary memory
- if (!isUploadFinalised) {
- if (mediaUri != null) {
- ContributionUtils.removeTemporaryFile(mediaUri);
- }
- }
- super.onStop();
- }
-
- private void createContributionWithReceivedIntent(Bundle savedInstanceState) {
- if (savedInstanceState != null) {
- contribution = savedInstanceState.getParcelable("contribution");
- }
-
- requestAuthToken();
- Timber.d("Uri: %s", mediaUri.toString());
- Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory());
-
- 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();
- if (sessionManager.getCurrentAccount() == null) {
- Toast.makeText(this, getString(R.string.login_alert_message), Toast.LENGTH_SHORT).show();
- applicationPrefs.edit().putBoolean("login_skipped", false).apply();
- Intent loginIntent = new Intent(ShareActivity.this, LoginActivity.class);
- startActivity(loginIntent);
- }
- }
-
- /**
- * Receive intent from ContributionController.java when user selects picture to upload
- */
- private void receiveImageIntent() {
- Intent intent = getIntent();
-
- if (Intent.ACTION_SEND.equals(intent.getAction())) {
- mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
- contentProviderUri = mediaUri;
- mediaUri = ContributionUtils.saveFileBeingUploadedTemporarily(this, mediaUri);
-
- if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
- source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
- } else {
- source = Contribution.SOURCE_EXTERNAL;
- }
-
- boolean isDirectUpload = intent.getBooleanExtra("isDirectUpload", false);
-
- if (isDirectUpload) {
- Timber.d("This was initiated by a direct upload from Nearby");
- isNearbyUpload = true;
- wikiDataEntityId = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF);
- Timber.d("Received wikiDataEntityId from contribution controller %s", wikiDataEntityId);
- }
- mimeType = intent.getType();
- }
-
- if (mediaUri != null) {
- backgroundImageView.setImageURI(mediaUri);
- }
- }
-
- /**
- * Function to display the zoom and map FAB
- */
- private void showFABMenu() {
- isFABOpen = true;
-
- if (gpsObj != null && gpsObj.imageCoordsExists)
- mapButton.setVisibility(View.VISIBLE);
- zoomInButton.setVisibility(View.VISIBLE);
-
- mainFab.animate().rotationBy(180);
- mapButton.animate().translationY(-getResources().getDimension(R.dimen.second_fab));
- zoomInButton.animate().translationY(-getResources().getDimension(R.dimen.first_fab));
- }
-
- /**
- * Function to close the zoom and map FAB
- */
- private void closeFABMenu() {
- isFABOpen = false;
- mainFab.animate().rotationBy(-180);
- mapButton.animate().translationY(0);
- zoomInButton.animate().translationY(0).setListener(new Animator.AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animator) {
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- if (!isFABOpen) {
- mapButton.setVisibility(View.GONE);
- zoomInButton.setVisibility(View.GONE);
- }
- }
-
- @Override
- public void onAnimationCancel(Animator animator) {
- }
-
- @Override
- public void onAnimationRepeat(Animator animator) {
- }
- });
- }
-
- /**
- * Checks if upload was initiated via Nearby
- *
- * @return true if upload was initiated via Nearby
- */
- protected boolean isNearbyUpload() {
- return isNearbyUpload;
- }
-
- /**
- * Handles 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) {
- if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- Timber.d("onRequestPermissionsResult external storage permission granted");
- // You can receive image intent and save image to a temp file only if ext storage permission is granted
- receiveImageIntent();
- createContributionWithReceivedIntent(savedInstanceState);
-
- if (requestCode == REQUEST_PERM_ON_SUBMIT_STORAGE) {
- checkIfFileExists();
- //Uploading only begins if storage permission granted from arrow icon
- uploadBegins();
- }
-
- } else {
- finish();
- }
- }
-
- /**
- * Check if file user wants to upload already exists on Commons
- */
- private void checkIfFileExists() {
- 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);
- 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);
- }
- }
-
- @Override
- public void onPause() {
- super.onPause();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- uploadController.cleanup();
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- if (categorizationFragment != null && categorizationFragment.isVisible()) {
- categorizationFragment.showBackButtonDialog();
- } else {
- onBackPressed();
- }
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- /**
- * Allows zooming in to the image about to be uploaded. Called when zoom FAB is tapped
- */
- private void zoomImageFromThumb(final View thumbView, Uri imageuri) {
- // If there's an animation in progress, cancel it immediately and proceed with this one.
- if (CurrentAnimator != null) {
- CurrentAnimator.cancel();
- }
- isZoom = true;
- ViewUtil.hideKeyboard(ShareActivity.this.findViewById(R.id.titleEdit));
- closeFABMenu();
- mainFab.setVisibility(View.GONE);
-
- InputStream input = null;
- try {
- input = this.getContentResolver().openInputStream(imageuri);
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- }
-
- Zoom zoomObj = new Zoom(thumbView, flContainer, this.getContentResolver());
- Bitmap scaledImage = zoomObj.createScaledImage(input, imageuri);
-
- // Load the high-resolution "zoomed-in" image.
- expandedImageView.setImageBitmap(scaledImage);
- float startScale = zoomObj.adjustStartEndBounds(startBounds, finalBounds, globalOffset);
-
- // Hide the thumbnail and show the zoomed-in view. When the animation
- // begins, it will position the zoomed-in view in the place of the
- // thumbnail.
- thumbView.setAlpha(0f);
- expandedImageView.setVisibility(View.VISIBLE);
- zoomOutButton.setVisibility(View.VISIBLE);
- zoomInButton.setVisibility(View.GONE);
-
- // Set the pivot point for SCALE_X and SCALE_Y transformations
- // to the top-left corner of the zoomed-in view (the default
- // is the center of the view).
- expandedImageView.setPivotX(0f);
- expandedImageView.setPivotY(0f);
-
- // Construct and run the parallel animation of the four translation and
- // scale properties (X, Y, SCALE_X, and SCALE_Y).
- AnimatorSet set = new AnimatorSet();
- set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left, finalBounds.left))
- .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top, finalBounds.top))
- .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f))
- .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f));
- set.setDuration(ShortAnimationDuration);
- set.setInterpolator(new DecelerateInterpolator());
- set.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- CurrentAnimator = null;
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- CurrentAnimator = null;
- }
- });
- set.start();
- CurrentAnimator = set;
-
- // Upon clicking the zoomed-in image, it should zoom back down
- // to the original bounds and show the thumbnail instead of
- // the expanded image.
- startScaleFinal = startScale;
- }
-
- /**
- * Called when user taps the ^ FAB button, expands to show Zoom and Map
- */
- @OnClick(R.id.main_fab)
- public void onMainFabClicked() {
- if (!isFABOpen) {
- showFABMenu();
- } else {
- closeFABMenu();
- }
- }
-
- @OnClick(R.id.media_upload_zoom_in)
- public void onZoomInFabClicked() {
- try {
- zoomImageFromThumb(backgroundImageView, mediaUri);
- } catch (Exception e) {
- Timber.e(e);
- }
- }
-
- @OnClick(R.id.media_upload_zoom_out)
- public void onZoomOutFabClicked() {
- if (CurrentAnimator != null) {
- CurrentAnimator.cancel();
- }
- isZoom = false;
- zoomOutButton.setVisibility(View.GONE);
- mainFab.setVisibility(View.VISIBLE);
-
- // Animate the four positioning/sizing properties in parallel,
- // back to their original values.
- AnimatorSet set = new AnimatorSet();
- set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left))
- .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top))
- .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScaleFinal))
- .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScaleFinal));
-
- set.setDuration(ShortAnimationDuration);
- set.setInterpolator(new DecelerateInterpolator());
- set.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- //background image view is thumbView
- backgroundImageView.setAlpha(1f);
- expandedImageView.setVisibility(View.GONE);
- CurrentAnimator = null;
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- //background image view is thumbView
- backgroundImageView.setAlpha(1f);
- expandedImageView.setVisibility(View.GONE);
- CurrentAnimator = null;
- }
- });
- set.start();
- CurrentAnimator = set;
- }
-
- @OnClick(R.id.media_map)
- public void onFabShowMapsClicked() {
- if (gpsObj != null && gpsObj.imageCoordsExists) {
- Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude());
- Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
- mapIntent.setPackage("com.google.android.apps.maps");
- startActivity(mapIntent);
- }
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_BACK:
- if(isZoom) {
- onZoomOutFabClicked();
- return true;
- }
- }
- return super.onKeyDown(keyCode,event);
-
- }
-}
-
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.java b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.java
new file mode 100644
index 000000000..6436f621b
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.java
@@ -0,0 +1,5 @@
+package fr.free.nrw.commons.upload;
+
+public interface SimilarImageInterface {
+ void showSimilarImageFragment(String originalFilePath, String possibleFilePath);
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java
deleted file mode 100644
index 9ea2f23f4..000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java
+++ /dev/null
@@ -1,389 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.annotation.SuppressLint;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
-import android.support.v4.view.ViewCompat;
-import android.support.v7.app.AlertDialog;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.text.Editable;
-import android.text.Html;
-import android.text.TextWatcher;
-import android.text.method.LinkMovementMethod;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.Spinner;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-
-import java.lang.reflect.Type;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Locale;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import butterknife.OnClick;
-import butterknife.OnItemSelected;
-import butterknife.OnTouch;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.Utils;
-import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
-import fr.free.nrw.commons.settings.Prefs;
-import fr.free.nrw.commons.utils.ViewUtil;
-import timber.log.Timber;
-
-import static android.view.MotionEvent.ACTION_UP;
-
-public class SingleUploadFragment extends CommonsDaggerSupportFragment {
-
- @BindView(R.id.titleEdit) EditText titleEdit;
- @BindView(R.id.rv_descriptions) RecyclerView rvDescriptions;
- @BindView(R.id.titleDescButton) Button titleDescButton;
- @BindView(R.id.share_license_summary) TextView licenseSummaryView;
- @BindView(R.id.licenseSpinner) Spinner licenseSpinner;
-
-
- @Inject @Named("default_preferences") SharedPreferences prefs;
- @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
-
- private String license;
- private OnUploadActionInitiated uploadActionInitiatedHandler;
- private TitleTextWatcher textWatcher = new TitleTextWatcher();
- private DescriptionsAdapter descriptionsAdapter;
-
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- inflater.inflate(R.menu.activity_share, menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- //What happens when the 'submit' icon is tapped
- case R.id.menu_upload_single:
-
- if (titleEdit.getText().toString().trim().isEmpty()) {
- Toast.makeText(getContext(), R.string.add_title_toast, Toast.LENGTH_LONG).show();
- return false;
- }
-
- String title = titleEdit.getText().toString();
- String descriptionsInVariousLanguages = getDescriptionsInAppropriateFormat();
-
- //Save the title/desc in short-lived cache so next time this fragment is loaded, we can access these
- prefs.edit()
- .putString("Title", title)
- .putString("Desc", new Gson().toJson(descriptionsAdapter
- .getDescriptions()))//Description, now is not just a string, its a list of description objects
- .apply();
-
- uploadActionInitiatedHandler
- .uploadActionInitiated(title, descriptionsInVariousLanguages);
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- private String getDescriptionsInAppropriateFormat() {
- List descriptions = descriptionsAdapter.getDescriptions();
- StringBuilder descriptionsInAppropriateFormat = new StringBuilder();
- for (Description description : descriptions) {
- String individualDescription = String.format("{{%s|1=%s}}", description.getLanguageId(),
- description.getDescriptionText());
- descriptionsInAppropriateFormat.append(individualDescription);
- }
- return descriptionsInAppropriateFormat.toString();
-
- }
-
- private List getDescriptions() {
- List descriptions = descriptionsAdapter.getDescriptions();
- return descriptions;
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.fragment_single_upload, container, false);
- ButterKnife.bind(this, rootView);
-
- initRecyclerView();
-
- Intent activityIntent = getActivity().getIntent();
- if (activityIntent.hasExtra("title")) {
- titleEdit.setText(activityIntent.getStringExtra("title"));
- }
- if (activityIntent.hasExtra("description") && descriptionsAdapter.getDescriptions() != null
- && descriptionsAdapter.getDescriptions().size() > 0) {
- descriptionsAdapter.getDescriptions().get(0)
- .setDescriptionText(activityIntent.getStringExtra("description"));
- descriptionsAdapter.notifyItemChanged(0);
- }
-
-
- ArrayList licenseItems = new ArrayList<>();
- licenseItems.add(getString(R.string.license_name_cc0));
- licenseItems.add(getString(R.string.license_name_cc_by));
- licenseItems.add(getString(R.string.license_name_cc_by_sa));
- licenseItems.add(getString(R.string.license_name_cc_by_four));
- licenseItems.add(getString(R.string.license_name_cc_by_sa_four));
-
- license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
-
- // If this is a direct upload from Nearby, autofill title and desc fields with the Place's values
- boolean isNearbyUpload = ((ShareActivity) getActivity()).isNearbyUpload();
-
- if (isNearbyUpload) {
- String imageTitle = directPrefs.getString("Title", "");
- String imageDesc = directPrefs.getString("Desc", "");
- String imageCats = directPrefs.getString("Category", "");
- Timber.d("Image title: " + imageTitle + ", image desc: " + imageDesc + ", image categories: " + imageCats);
- titleEdit.setText(imageTitle);
- if (descriptionsAdapter.getDescriptions() != null
- && descriptionsAdapter.getDescriptions().size() > 0) {
- descriptionsAdapter.getDescriptions().get(0).setDescriptionText(imageDesc);
- descriptionsAdapter.notifyItemChanged(0);
- }
- }
-
- // check if this is the first time we have uploaded
- if (prefs.getString("Title", "").trim().length() == 0
- && prefs.getString("Desc", "").trim().length() == 0) {
- titleDescButton.setVisibility(View.GONE);
- }
-
- Timber.d(license);
-
- ArrayAdapter adapter;
- if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme", false)) {
- // dark theme
- adapter = new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_dropdown_item, licenseItems);
- } else {
- // light theme
- adapter = new ArrayAdapter<>(getActivity(), R.layout.light_simple_spinner_dropdown_item, licenseItems);
- }
-
- licenseSpinner.setAdapter(adapter);
-
- int position = licenseItems.indexOf(getString(Utils.licenseNameFor(license)));
-
- // Check position is valid
- if (position < 0) {
- Timber.d("Invalid position: %d. Using default license", position);
- position = 4;
- }
-
- Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(license)));
- licenseSpinner.setSelection(position);
-
- titleEdit.addTextChangedListener(textWatcher);
-
- titleEdit.setOnFocusChangeListener((v, hasFocus) -> {
- if (!hasFocus) {
- ViewUtil.hideKeyboard(v);
- }
- });
-
- setLicenseSummary(license);
-
- return rootView;
- }
-
- private void initRecyclerView() {
- descriptionsAdapter = new DescriptionsAdapter();
- descriptionsAdapter.setCallback(this::showInfoAlert);
- descriptionsAdapter.setLanguages(getLocaleSupportedByDevice());
- rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
- rvDescriptions.setAdapter(descriptionsAdapter);
- }
-
- private List getLocaleSupportedByDevice() {
- List languages = new ArrayList<>();
- Locale[] localesArray = Locale.getAvailableLocales();
- List locales = Arrays.asList(localesArray);
- for (Locale locale : locales) {
- languages.add(new Language(locale));
- }
- return languages;
- }
-
- @Override
- public void onDestroyView() {
- titleEdit.removeTextChangedListener(textWatcher);
- super.onDestroyView();
- }
-
- @OnItemSelected(R.id.licenseSpinner)
- void onLicenseSelected(AdapterView> parent, View view, int position, long id) {
- String licenseName = parent.getItemAtPosition(position).toString();
-
- // Set selected color to white because it should be readable on random images.
- TextView selectedText = (TextView) licenseSpinner.getChildAt(0);
- if (selectedText != null) {
- selectedText.setTextColor(Color.WHITE);
- selectedText.setBackgroundColor(Color.TRANSPARENT);
- }
-
- String license;
- if (getString(R.string.license_name_cc0).equals(licenseName)) {
- license = Prefs.Licenses.CC0;
- } else if (getString(R.string.license_name_cc_by).equals(licenseName)) {
- license = Prefs.Licenses.CC_BY_3;
- } else if (getString(R.string.license_name_cc_by_sa).equals(licenseName)) {
- license = Prefs.Licenses.CC_BY_SA_3;
- } else if (getString(R.string.license_name_cc_by_four).equals(licenseName)) {
- license = Prefs.Licenses.CC_BY_4;
- } else if (getString(R.string.license_name_cc_by_sa_four).equals(licenseName)) {
- license = Prefs.Licenses.CC_BY_SA_4;
- } else {
- throw new IllegalStateException("Unknown licenseName: " + licenseName);
- }
-
- setLicenseSummary(license);
- prefs.edit()
- .putString(Prefs.DEFAULT_LICENSE, license)
- .apply();
- }
-
-
- @OnClick(R.id.titleDescButton)
- void setTitleDescButton() {
- //Retrieve last title and desc entered
- String title = prefs.getString("Title", "");
- String descriptionJson = prefs.getString("Desc", "");
- Timber.d("Title: %s, Desc: %s", title, descriptionJson);
-
- titleEdit.setText(title);
- Type typeOfDest = new TypeToken>() {
- }.getType();
-
- List descriptions = new Gson().fromJson(descriptionJson, typeOfDest);
- descriptionsAdapter.setDescriptions(descriptions);
-
- }
-
- /**
- * Copied from https://stackoverflow.com/a/26269435/8065933
- */
- @OnTouch(R.id.titleEdit)
- boolean titleInfo(View view, MotionEvent motionEvent) {
- final int value;
- if (ViewCompat.getLayoutDirection(getView()) == ViewCompat.LAYOUT_DIRECTION_LTR) {
- value = titleEdit.getRight() - titleEdit.getCompoundDrawables()[2].getBounds().width();
- if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
- showInfoAlert(R.string.media_detail_title, R.string.title_info);
- return true;
- }
- }
- else {
- value = titleEdit.getLeft() + titleEdit.getCompoundDrawables()[0].getBounds().width();
- if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) {
- showInfoAlert(R.string.media_detail_title, R.string.title_info);
- return true;
- }
- }
- return false;
- }
-
- @SuppressLint("StringFormatInvalid")
- private void setLicenseSummary(String license) {
- String licenseHyperLink = ""+ getString(Utils.licenseNameFor(license)) + "
";
- licenseSummaryView.setMovementMethod(LinkMovementMethod.getInstance());
- licenseSummaryView.setText(Html.fromHtml(getString(R.string.share_license_summary, licenseHyperLink)));
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- setHasOptionsMenu(true);
- uploadActionInitiatedHandler = (OnUploadActionInitiated) getActivity();
- }
-
- @Override
- public void onStop() {
- super.onStop();
-
- // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
- View target = getActivity().getCurrentFocus();
- ViewUtil.hideKeyboard(target);
- }
-
- @NonNull
- private String licenseUrlFor(String license) {
- switch (license) {
- case Prefs.Licenses.CC_BY_3:
- return "https://creativecommons.org/licenses/by/3.0/";
- case Prefs.Licenses.CC_BY_4:
- return "https://creativecommons.org/licenses/by/4.0/";
- case Prefs.Licenses.CC_BY_SA_3:
- return "https://creativecommons.org/licenses/by-sa/3.0/";
- case Prefs.Licenses.CC_BY_SA_4:
- return "https://creativecommons.org/licenses/by-sa/4.0/";
- case Prefs.Licenses.CC0:
- return "https://creativecommons.org/publicdomain/zero/1.0/";
- }
- throw new RuntimeException("Unrecognized license value: " + license);
- }
-
- public interface OnUploadActionInitiated {
-
- void uploadActionInitiated(String title, String description);
- }
-
- private class TitleTextWatcher implements TextWatcher {
-
- @Override
- public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
- }
-
- @Override
- public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
- }
-
- @Override
- public void afterTextChanged(Editable editable) {
- if (getActivity() != null) {
- getActivity().invalidateOptionsMenu();
- }
- }
- }
-
-
- private void showInfoAlert (int titleStringID, int messageStringID){
- new AlertDialog.Builder(getContext())
- .setTitle(titleStringID)
- .setMessage(messageStringID)
- .setCancelable(true)
- .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
- .create()
- .show();
- }
-
- @OnClick(R.id.ll_add_description)
- public void onLLAddDescriptionClicked() {
- descriptionsAdapter.addDescription(new Description());
- rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1);
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java
index eebe6b4fa..97908fa67 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java
@@ -1,48 +1,88 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
+import android.graphics.Color;
+import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+import java.util.Locale;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.utils.BiMap;
public class SpinnerLanguagesAdapter extends ArrayAdapter {
private final int resource;
private final LayoutInflater layoutInflater;
- List languages;
+ private List languageNamesList;
+ private List languageCodesList;
+ private final BiMap selectedLanguages;
+ public String selectedLangCode="";
+
+
public SpinnerLanguagesAdapter(@NonNull Context context,
- int resource) {
+ int resource, BiMap selectedLanguages) {
super(context, resource);
this.resource = resource;
this.layoutInflater = LayoutInflater.from(context);
- languages = new ArrayList<>();
+ languageNamesList = new ArrayList<>();
+ languageCodesList = new ArrayList<>();
+ prepareLanguages();
+ this.selectedLanguages = selectedLanguages;
}
- public void setLanguages(List languages) {
- this.languages = languages;
+ private void prepareLanguages() {
+ List languages = getLocaleSupportedByDevice();
+
+ for(Language language: languages) {
+ if(!languageCodesList.contains(language.getLocale().getLanguage())) {
+ languageNamesList.add(language.getLocale().getDisplayName());
+ languageCodesList.add(language.getLocale().getLanguage());
+ }
+ }
+ }
+
+ private List getLocaleSupportedByDevice() {
+ List languages = new ArrayList<>();
+ Locale[] localesArray = Locale.getAvailableLocales();
+ for (Locale locale : localesArray) {
+ languages.add(new Language(locale));
+ }
+
+ Collections.sort(languages, (language, t1) -> language.getLocale().getDisplayName()
+ .compareTo(t1.getLocale().getDisplayName()));
+ return languages;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return !languageCodesList.get(position).isEmpty()&&
+ (!selectedLanguages.containsKey(languageCodesList.get(position)) ||
+ languageCodesList.get(position).equals(selectedLangCode));
}
@Override
public int getCount() {
- return languages.size();
+ return languageNamesList.size();
}
@Override
public View getDropDownView(int position, @Nullable View convertView,
- @NonNull ViewGroup parent) {
+ @NonNull ViewGroup parent) {
View view = layoutInflater.inflate(resource, parent, false);
ViewHolder holder = new ViewHolder(view);
holder.init(position, true);
@@ -75,19 +115,40 @@ public class SpinnerLanguagesAdapter extends ArrayAdapter {
}
public void init(int position, boolean isDropDownView) {
- Language language = languages.get(position);
if (!isDropDownView) {
view.setVisibility(View.GONE);
- tvLanguage.setText(
- language.getLocale().getLanguage());
+ if(languageCodesList.get(position).length()>2)
+ tvLanguage.setText(languageCodesList.get(position).subSequence(0,2));
+ else
+ tvLanguage.setText(languageCodesList.get(position));
+
} else {
view.setVisibility(View.VISIBLE);
- tvLanguage.setText(
- String.format("%s [%s]", language.getLocale().getDisplayName(),
- language.getLocale().getLanguage()));
+ if (languageCodesList.get(position).isEmpty()) {
+ tvLanguage.setText(languageNamesList.get(position));
+ tvLanguage.setTextColor(Color.GRAY);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ tvLanguage.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
+ }
+ } else {
+ tvLanguage.setText(
+ String.format("%s [%s]", languageNamesList.get(position), languageCodesList.get(position)));
+ if(selectedLanguages.containsKey(languageCodesList.get(position))&&
+ !languageCodesList.get(position).equals(selectedLangCode))
+ tvLanguage.setTextColor(Color.GRAY);
+ else
+ tvLanguage.setTextColor(Color.BLACK);
+ }
}
-
}
}
+ String getLanguageCode(int position) {
+ return languageCodesList.get(position);
+ }
+
+ int getIndexOfUserDefaultLocale(Context context) {
+ return languageCodesList.indexOf(context.getResources().getConfiguration().locale.getLanguage());
+ }
+
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java
new file mode 100644
index 000000000..8963d0e25
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java
@@ -0,0 +1,5 @@
+package fr.free.nrw.commons.upload;
+
+public interface ThumbnailClickedListener {
+ void thumbnailClicked(UploadModel.UploadItem content);
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Title.java b/app/src/main/java/fr/free/nrw/commons/upload/Title.java
new file mode 100644
index 000000000..e0781ce67
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/Title.java
@@ -0,0 +1,37 @@
+package fr.free.nrw.commons.upload;
+
+import android.text.TextUtils;
+
+import io.reactivex.subjects.BehaviorSubject;
+import timber.log.Timber;
+
+class Title{
+
+ private String titleText;
+ private boolean set;
+
+ @Override
+ public String toString() {
+ return titleText;
+ }
+
+ public void setTitleText(String titleText) {
+ this.titleText = titleText;
+
+ if (!TextUtils.isEmpty(titleText)) {
+ set = true;
+ }
+ }
+
+ public boolean isSet() {
+ return set;
+ }
+
+ public void setSet(boolean set) {
+ this.set = set;
+ }
+
+ public boolean isEmpty() {
+ return titleText==null || titleText.isEmpty();
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
new file mode 100644
index 000000000..2ea89e8e6
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
@@ -0,0 +1,607 @@
+package fr.free.nrw.commons.upload;
+
+import android.Manifest;
+import android.animation.LayoutTransition;
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.constraint.ConstraintLayout;
+import android.support.design.widget.TextInputLayout;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.CardView;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.Html;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ViewFlipper;
+
+import com.github.chrisbanes.photoview.PhotoView;
+import com.jakewharton.rxbinding2.view.RxView;
+import com.jakewharton.rxbinding2.widget.RxTextView;
+import com.pedrogomez.renderers.RVRendererAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import fr.free.nrw.commons.CommonsApplication;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.Utils;
+import fr.free.nrw.commons.auth.AuthenticatedActivity;
+import fr.free.nrw.commons.auth.LoginActivity;
+import fr.free.nrw.commons.category.CategoriesModel;
+import fr.free.nrw.commons.category.CategoryItem;
+import fr.free.nrw.commons.contributions.Contribution;
+import fr.free.nrw.commons.mwapi.MediaWikiApi;
+import fr.free.nrw.commons.utils.DialogUtil;
+import fr.free.nrw.commons.utils.StringUtils;
+import fr.free.nrw.commons.utils.ViewUtil;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.schedulers.Schedulers;
+import timber.log.Timber;
+
+import static fr.free.nrw.commons.utils.ImageUtils.Result;
+import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
+import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
+
+public class UploadActivity extends AuthenticatedActivity implements UploadView, SimilarImageInterface {
+ @Inject InputMethodManager inputMethodManager;
+ @Inject MediaWikiApi mwApi;
+ @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
+ @Inject UploadPresenter presenter;
+ @Inject CategoriesModel categoriesModel;
+
+ // Main GUI
+ @BindView(R.id.backgroundImage) PhotoView background;
+ @BindView(R.id.activity_upload_cards) ConstraintLayout cardLayout;
+ @BindView(R.id.view_flipper) ViewFlipper viewFlipper;
+
+ // Top Card
+ @BindView(R.id.top_card) CardView topCard;
+ @BindView(R.id.top_card_expand_button) ImageView topCardExpandButton;
+ @BindView(R.id.top_card_title) TextView topCardTitle;
+ @BindView(R.id.top_card_thumbnails) RecyclerView topCardThumbnails;
+
+ // Bottom Card
+ @BindView(R.id.bottom_card) CardView bottomCard;
+ @BindView(R.id.bottom_card_expand_button) ImageView bottomCardExpandButton;
+ @BindView(R.id.bottom_card_title) TextView bottomCardTitle;
+ @BindView(R.id.bottom_card_subtitle) TextView bottomCardSubtitle;
+ @BindView(R.id.bottom_card_next) Button next;
+ @BindView(R.id.bottom_card_previous) Button previous;
+ @BindView(R.id.bottom_card_add_desc) Button bottomCardAddDescription;
+
+ //Right Card
+ @BindView(R.id.right_card) CardView rightCard;
+ @BindView(R.id.right_card_expand_button) ImageView rightCardExpandButton;
+ @BindView(R.id.right_card_map_button) View rightCardMapButton;
+
+ // Category Search
+ @BindView(R.id.categories_title) TextView categoryTitle;
+ @BindView(R.id.category_next) Button categoryNext;
+ @BindView(R.id.category_previous) Button categoryPrevious;
+ @BindView(R.id.categoriesSearchInProgress) ProgressBar categoriesSearchInProgress;
+ @BindView(R.id.category_search) EditText categoriesSearch;
+ @BindView(R.id.category_search_container) TextInputLayout categoriesSearchContainer;
+ @BindView(R.id.categories) RecyclerView categoriesList;
+
+ // Final Submission
+ @BindView(R.id.license_title) TextView licenseTitle;
+ @BindView(R.id.share_license_summary) TextView licenseSummary;
+ @BindView(R.id.media_upload_policy) TextView licensePolicy;
+ @BindView(R.id.license_list) Spinner licenseSpinner;
+ @BindView(R.id.submit) Button submit;
+ @BindView(R.id.license_previous) Button licensePrevious;
+ @BindView(R.id.rv_descriptions) RecyclerView rvDescriptions;
+
+ private DescriptionsAdapter descriptionsAdapter;
+ private RVRendererAdapter categoriesAdapter;
+ private CompositeDisposable compositeDisposable;
+
+ DexterPermissionObtainer dexterPermissionObtainer;
+
+
+ @SuppressLint("CheckResult")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_upload);
+ ButterKnife.bind(this);
+ compositeDisposable = new CompositeDisposable();
+
+ configureLayout();
+ configureTopCard();
+ configureBottomCard();
+ initRecyclerView();
+ configureRightCard();
+ configureNavigationButtons();
+ configureCategories();
+ configureLicenses();
+
+ presenter.init();
+
+ dexterPermissionObtainer = new DexterPermissionObtainer(this,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ getString(R.string.storage_permission),
+ getString(R.string.write_storage_permission_rationale_for_image_share));
+
+ dexterPermissionObtainer.confirmStoragePermissions().subscribe(this::receiveSharedItems);
+ }
+
+ @Override
+ public boolean checkIfLoggedIn() {
+ if (!sessionManager.isUserLoggedIn()) {
+ Timber.d("Current account is null");
+ ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in));
+ Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class);
+ startActivity(loginIntent);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void onDestroy() {
+ presenter.cleanup();
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ checkIfLoggedIn();
+ compositeDisposable.add(
+ dexterPermissionObtainer.confirmStoragePermissions()
+ .subscribe(() -> presenter.addView(this)));
+ compositeDisposable.add(
+ RxTextView.textChanges(categoriesSearch)
+ .doOnEach(v -> categoriesSearchContainer.setError(null))
+ .takeUntil(RxView.detaches(categoriesSearch))
+ .debounce(500, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(filter -> updateCategoryList(filter.toString()), Timber::e)
+ );
+ }
+
+ @Override
+ protected void onPause() {
+ presenter.removeView();
+ compositeDisposable.dispose();
+ compositeDisposable = new CompositeDisposable();
+ super.onPause();
+ }
+
+ @Override
+ public void updateThumbnails(List uploads) {
+ int uploadCount = uploads.size();
+ topCardThumbnails.setAdapter(new UploadThumbnailsAdapterFactory(presenter::thumbnailClicked).create(uploads));
+ topCardTitle.setText(getResources().getQuantityString(R.plurals.upload_count_title, uploadCount, uploadCount));
+ }
+
+ @Override
+ public void updateRightCardContent(boolean gpsPresent) {
+ if(gpsPresent){
+ rightCardMapButton.setVisibility(View.VISIBLE);
+ }else{
+ rightCardMapButton.setVisibility(View.GONE);
+ }
+ //The card should be disabled if it has no buttons.
+ setRightCardVisibility(gpsPresent);
+ }
+
+ @Override
+ public void updateBottomCardContent(int currentStep,
+ int stepCount,
+ UploadModel.UploadItem uploadItem,
+ boolean isShowingItem) {
+ String cardTitle = getResources().getString(R.string.step_count, currentStep, stepCount);
+ String cardSubTitle = getResources().getString(R.string.image_in_set_label, currentStep);
+ bottomCardTitle.setText(cardTitle);
+ bottomCardSubtitle.setText(cardSubTitle);
+ categoryTitle.setText(cardTitle);
+ licenseTitle.setText(cardTitle);
+ if(isShowingItem) {
+ descriptionsAdapter.setItems(uploadItem.title, uploadItem.descriptions);
+ rvDescriptions.setAdapter(descriptionsAdapter);
+ }
+ }
+
+ @Override
+ public void updateLicenses(List licenses, String selectedLicense) {
+ ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, licenses);
+ licenseSpinner.setAdapter(adapter);
+
+ int position = licenses.indexOf(getString(Utils.licenseNameFor(selectedLicense)));
+
+ // Check position is valid
+ if (position < 0) {
+ Timber.d("Invalid position: %d. Using default license", position);
+ position = licenses.size() - 1;
+ }
+
+ Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(selectedLicense)));
+ licenseSpinner.setSelection(position);
+ }
+
+ @SuppressLint("StringFormatInvalid")
+ @Override
+ public void updateLicenseSummary(String selectedLicense) {
+ String licenseHyperLink = "" +
+ getString(Utils.licenseNameFor(selectedLicense)) + "
";
+ licenseSummary.setMovementMethod(LinkMovementMethod.getInstance());
+ licenseSummary.setText(
+ Html.fromHtml(
+ getString(R.string.share_license_summary, licenseHyperLink)));
+ }
+
+ @Override
+ public void updateTopCardContent() {
+ RecyclerView.Adapter adapter = topCardThumbnails.getAdapter();
+ if (adapter != null) {
+ adapter.notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void setNextEnabled(boolean available) {
+ next.setEnabled(available);
+ categoryNext.setEnabled(available);
+ }
+
+ @Override
+ public void setSubmitEnabled(boolean available) {
+ submit.setEnabled(available);
+ }
+
+ @Override
+ public void setPreviousEnabled(boolean available) {
+ previous.setEnabled(available);
+ categoryPrevious.setEnabled(available);
+ licensePrevious.setEnabled(available);
+ }
+
+ @Override
+ public void setTopCardState(boolean state) {
+ updateCardState(state, topCardExpandButton, topCardThumbnails);
+ }
+
+ @Override
+ public void setTopCardVisibility(boolean visible) {
+ topCard.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void setBottomCardVisibility(boolean visible) {
+ bottomCard.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void setRightCardVisibility(boolean visible) {
+ rightCard.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void setBottomCardVisibility(@UploadPage int page) {
+ if (page == TITLE_CARD) {
+ viewFlipper.setDisplayedChild(0);
+ } else if (page == CATEGORIES) {
+ viewFlipper.setDisplayedChild(1);
+ } else if (page == LICENSE) {
+ viewFlipper.setDisplayedChild(2);
+ dismissKeyboard();
+ } else if (page == PLEASE_WAIT) {
+ viewFlipper.setDisplayedChild(3);
+ }
+ }
+
+ @Override
+ public void setBottomCardState(boolean state) {
+ updateCardState(state, bottomCardExpandButton, rvDescriptions, previous, next, bottomCardAddDescription);
+ }
+
+ @Override
+ public void setRightCardState(boolean state) {
+ rightCardExpandButton.animate().rotation(rightCardExpandButton.getRotation() + (state ? -180 : 180)).start();
+ //Add all items in rightCard here
+ rightCardMapButton.setVisibility(state ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public void setBackground(Uri mediaUri) {
+ background.setImageURI(mediaUri);
+ }
+
+
+ @Override
+ public void dismissKeyboard() {
+ InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+
+ // verify if the soft keyboard is open
+ if (imm != null && imm.isAcceptingText() && getCurrentFocus() != null) {
+ imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
+ }
+ }
+
+ @Override
+ public void showBadPicturePopup(@Result int result) {
+ String errorMessageForResult = getErrorMessageForResult(this, result);
+ if (StringUtils.isNullOrWhiteSpace(errorMessageForResult)) {
+ return;
+ }
+
+ DialogUtil.showAlertDialog(this,
+ getString(R.string.warning),
+ errorMessageForResult,
+ () -> presenter.deletePicture(),
+ () -> presenter.keepPicture());
+ }
+
+ @Override
+ public void showDuplicatePicturePopup() {
+ DialogUtil.showAlertDialog(this,
+ getString(R.string.warning),
+ String.format(getString(R.string.upload_title_duplicate), presenter.getCurrentImageFileName()),
+ null,
+ () -> {
+ presenter.keepPicture();
+ presenter.handleNext(descriptionsAdapter.getTitle(), getDescriptions());
+ });
+ }
+
+ public void showNoCategorySelectedWarning() {
+ DialogUtil.showAlertDialog(this,
+ getString(R.string.no_categories_selected),
+ getString(R.string.no_categories_selected_warning_desc),
+ getString(R.string.no_go_back),
+ getString(R.string.yes_submit),
+ null,
+ () -> presenter.handleCategoryNext(categoriesModel, true));
+ }
+
+ @Override
+ public void launchMapActivity(String decCoords) {
+ Utils.handleGeoCoordinates(this, decCoords);
+ }
+
+ @Override
+ public void showErrorMessage(int resourceId) {
+ ViewUtil.showShortToast(this, resourceId);
+ }
+
+ @Override
+ public void initDefaultCategories() {
+ updateCategoryList("");
+ }
+
+ @Override
+ protected void onAuthCookieAcquired(String authCookie) {
+ mwApi.setAuthCookie(authCookie);
+ }
+
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) {
+ dexterPermissionObtainer.onManualPermissionReturned();
+ }
+ }
+
+
+ @Override
+ protected void onAuthFailure() {
+ Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG).show();
+ finish();
+ }
+
+ private void configureLicenses() {
+ licenseSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ String licenseName = parent.getItemAtPosition(position).toString();
+ presenter.selectLicense(licenseName);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+ presenter.selectLicense(null);
+ }
+ });
+ }
+
+ private void configureLayout() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ cardLayout.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
+ }
+ background.setScaleType(ImageView.ScaleType.CENTER_CROP);
+ background.setOnScaleChangeListener((scaleFactor, x, y) -> presenter.closeAllCards());
+ }
+
+ private void configureTopCard() {
+ topCardExpandButton.setOnClickListener(v -> presenter.toggleTopCardState());
+ topCardThumbnails.setLayoutManager(new LinearLayoutManager(this,
+ LinearLayoutManager.HORIZONTAL, false));
+ }
+
+ private void configureBottomCard() {
+ bottomCardExpandButton.setOnClickListener(v -> presenter.toggleBottomCardState());
+ bottomCardAddDescription.setOnClickListener(v -> addNewDescription());
+ }
+
+ private void addNewDescription() {
+ descriptionsAdapter.addDescription(new Description());
+ rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1);
+ }
+
+ private void configureRightCard() {
+ rightCardExpandButton.setOnClickListener(v -> presenter.toggleRightCardState());
+ rightCardMapButton.setOnClickListener(v -> presenter.openCoordinateMap());
+ }
+
+ private void configureNavigationButtons() {
+ // Navigation next / previous for each image as we're collecting title + description
+ next.setOnClickListener(v -> {
+ setTitleAndDescriptions();
+ presenter.handleNext(descriptionsAdapter.getTitle(),
+ descriptionsAdapter.getDescriptions());
+ });
+ previous.setOnClickListener(v -> presenter.handlePrevious());
+
+ // Next / previous for the category selection currentPage
+ categoryNext.setOnClickListener(v -> presenter.handleCategoryNext(categoriesModel, false));
+ categoryPrevious.setOnClickListener(v -> presenter.handlePrevious());
+
+ // Finally, the previous / submit buttons on the final currentPage of the wizard
+ licensePrevious.setOnClickListener(v -> presenter.handlePrevious());
+ submit.setOnClickListener(v -> {
+ Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG).show();
+ presenter.handleSubmit(categoriesModel);
+ finish();
+ });
+
+ }
+
+ private void setTitleAndDescriptions() {
+ List descriptions = descriptionsAdapter.getDescriptions();
+ Timber.d("Descriptions size is %d are %s", descriptions.size(), descriptions);
+ }
+
+ private void configureCategories() {
+ categoriesAdapter = new UploadCategoriesAdapterFactory(categoriesModel).create(new ArrayList<>());
+ categoriesList.setLayoutManager(new LinearLayoutManager(this));
+ categoriesList.setAdapter(categoriesAdapter);
+ }
+
+ @SuppressLint("CheckResult")
+ private void updateCategoryList(String filter) {
+ List imageTitleList = presenter.getImageTitleList();
+ Observable.fromIterable(categoriesModel.getSelectedCategories())
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .doOnSubscribe(disposable -> {
+ categoriesSearchInProgress.setVisibility(View.VISIBLE);
+ categoriesSearchContainer.setError(null);
+ categoriesAdapter.clear();
+ })
+ .observeOn(Schedulers.io())
+ .concatWith(
+ categoriesModel.searchAll(filter, imageTitleList)
+ .mergeWith(categoriesModel.searchCategories(filter, imageTitleList))
+ .concatWith(TextUtils.isEmpty(filter)
+ ? categoriesModel.defaultCategories(imageTitleList) : Observable.empty())
+ )
+ .filter(categoryItem -> !categoriesModel.containsYear(categoryItem.getName()))
+ .distinct()
+ .sorted(categoriesModel.sortBySimilarity(filter))
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ s -> categoriesAdapter.add(s),
+ Timber::e,
+ () -> {
+ categoriesAdapter.notifyDataSetChanged();
+ categoriesSearchInProgress.setVisibility(View.GONE);
+
+ if (categoriesAdapter.getItemCount() == categoriesModel.selectedCategoriesCount()
+ && !categoriesSearch.getText().toString().isEmpty()) {
+ categoriesSearchContainer.setError("No categories found");
+ }
+ }
+ );
+ }
+
+ private void receiveSharedItems() {
+ Intent intent = getIntent();
+ String mimeType = intent.getType();
+ String source;
+
+ if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
+ source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
+ } else {
+ source = Contribution.SOURCE_EXTERNAL;
+ }
+
+ if (Intent.ACTION_SEND.equals(intent.getAction())) {
+ Uri mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (intent.getBooleanExtra("isDirectUpload", false)) {
+ String imageTitle = directPrefs.getString("Title", "");
+ String imageDesc = directPrefs.getString("Desc", "");
+ Timber.i("Received direct upload with title %s and description %s", imageTitle, imageDesc);
+ String wikidataEntityIdPref = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF);
+ presenter.receiveDirect(mediaUri, mimeType, source, wikidataEntityIdPref, imageTitle, imageDesc);
+ } else {
+ Timber.i("Received single upload");
+ presenter.receive(mediaUri, mimeType, source);
+ }
+ } else if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
+ ArrayList urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ Timber.i("Received multiple upload %s", urisList.size());
+ presenter.receive(urisList, mimeType, source);
+ }
+ }
+
+ private void updateCardState(boolean state, ImageView button, View... content) {
+ button.animate().rotation(button.getRotation() + (state ? 180 : -180)).start();
+ if (content != null) {
+ for (View view : content) {
+ view.setVisibility(state ? View.VISIBLE : View.GONE);
+ }
+ }
+ }
+
+ @Override
+ public List getDescriptions() {
+ return descriptionsAdapter.getDescriptions();
+ }
+
+ private void initRecyclerView() {
+ descriptionsAdapter = new DescriptionsAdapter(this);
+ descriptionsAdapter.setCallback(this::showInfoAlert);
+ rvDescriptions.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
+ rvDescriptions.setAdapter(descriptionsAdapter);
+ addNewDescription();
+ }
+
+
+ private void showInfoAlert(int titleStringID, int messageStringId, String... formatArgs) {
+ new AlertDialog.Builder(this)
+ .setTitle(titleStringID)
+ .setMessage(getString(messageStringId, (Object[]) formatArgs))
+ .setCancelable(true)
+ .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
+ .create()
+ .show();
+ }
+
+ @Override
+ public void showSimilarImageFragment(String originalFilePath, String possibleFilePath) {
+ SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
+ Bundle args = new Bundle();
+ args.putString("originalImagePath", originalFilePath);
+ args.putString("possibleImagePath", possibleFilePath);
+ newFragment.setArguments(args);
+ newFragment.show(getSupportFragmentManager(), "dialog");
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesAdapterFactory.java
new file mode 100644
index 000000000..1797cbe80
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesAdapterFactory.java
@@ -0,0 +1,27 @@
+package fr.free.nrw.commons.upload;
+
+import com.pedrogomez.renderers.ListAdapteeCollection;
+import com.pedrogomez.renderers.RVRendererAdapter;
+import com.pedrogomez.renderers.RendererBuilder;
+
+import java.util.Collections;
+import java.util.List;
+
+import fr.free.nrw.commons.category.CategoryClickedListener;
+import fr.free.nrw.commons.category.CategoryItem;
+
+public class UploadCategoriesAdapterFactory {
+ private final CategoryClickedListener listener;
+
+ public UploadCategoriesAdapterFactory(CategoryClickedListener listener) {
+ this.listener = listener;
+ }
+
+ public RVRendererAdapter create(List placeList) {
+ RendererBuilder builder = new RendererBuilder()
+ .bind(CategoryItem.class, new UploadCategoriesRenderer(listener));
+ ListAdapteeCollection collection = new ListAdapteeCollection<>(
+ placeList != null ? placeList : Collections.emptyList());
+ return new RVRendererAdapter<>(builder, collection);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesRenderer.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesRenderer.java
new file mode 100644
index 000000000..d0862b964
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesRenderer.java
@@ -0,0 +1,52 @@
+package fr.free.nrw.commons.upload;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+
+import com.pedrogomez.renderers.Renderer;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.category.CategoryClickedListener;
+import fr.free.nrw.commons.category.CategoryItem;
+
+public class UploadCategoriesRenderer extends Renderer {
+ @BindView(R.id.tvName) CheckBox checkedView;
+ private final CategoryClickedListener listener;
+
+ UploadCategoriesRenderer(CategoryClickedListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
+ return layoutInflater.inflate(R.layout.layout_upload_categories_item, viewGroup, false);
+ }
+
+ @Override
+ protected void setUpView(View view) {
+ ButterKnife.bind(this, view);
+ }
+
+ @Override
+ protected void hookListeners(View view) {
+ view.setOnClickListener(v -> {
+ CategoryItem item = getContent();
+ item.setSelected(!item.isSelected());
+ checkedView.setChecked(item.isSelected());
+ if (listener != null) {
+ listener.categoryClicked(item);
+ }
+ });
+ }
+
+ @Override
+ public void render() {
+ CategoryItem item = getContent();
+ checkedView.setChecked(item.isSelected());
+ checkedView.setText(item.getName());
+ }
+}
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 222757f54..fd0563ab3 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
@@ -23,7 +23,6 @@ import java.io.InputStream;
import java.util.Date;
import java.util.concurrent.Executors;
-import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
@@ -87,49 +86,11 @@ public class UploadController {
/**
* Starts a new upload task.
- * @param title the title of the contribution
- * @param mediaUri the media URI of the contribution
- * @param description the description of the contribution
- * @param mimeType the MIME type of the contribution
- * @param source the source of the contribution
- * @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615")
- * @param wikiDataEntityId
- * @param onComplete the progress tracker
+ *
+ * @param contribution the contribution object
*/
- public void startUpload(String title, Uri contentProviderUri, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, String wikiDataEntityId, ContributionUploadProgress onComplete) {
- Contribution contribution;
-
-
- //TODO: Modify this to include coords
- contribution = new Contribution(mediaUri, null, title, description, -1,
- null, null, sessionManager.getCurrentAccount().name,
- CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords);
-
-
- contribution.setTag("mimeType", mimeType);
- contribution.setSource(source);
-
- Timber.d("Wikidata entity ID received from Share activity is %s", wikiDataEntityId);
- //TODO: Modify this to include coords
- Account currentAccount = sessionManager.getCurrentAccount();
- if(currentAccount == null) {
- Timber.d("Current account is null");
- ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in));
- sessionManager.forceLogin(context);
- return;
- }
- contribution = new Contribution(mediaUri, null, title, description, -1,
- null, null, currentAccount.name,
- CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords);
-
-
- contribution.setTag("mimeType", mimeType);
- contribution.setSource(source);
- contribution.setWikiDataEntityId(wikiDataEntityId);
- contribution.setContentProviderUri(contentProviderUri);
-
- //Calls the next overloaded method
- startUpload(contribution, onComplete);
+ public void startUpload(Contribution contribution) {
+ startUpload(contribution, c -> {});
}
/**
@@ -142,7 +103,14 @@ public class UploadController {
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
//Set creator, desc, and license
if (TextUtils.isEmpty(contribution.getCreator())) {
- contribution.setCreator(sessionManager.getCurrentAccount().name);
+ Account currentAccount = sessionManager.getCurrentAccount();
+ if (currentAccount == null) {
+ Timber.d("Current account is null");
+ ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in));
+ sessionManager.forceLogin(context);
+ return;
+ }
+ contribution.setCreator(currentAccount.name);
}
if (contribution.getDescription() == null) {
@@ -163,8 +131,6 @@ public class UploadController {
long length;
ContentResolver contentResolver = context.getContentResolver();
try {
-
- //TODO: understand do we really need this code
if (contribution.getDataLength() <= 0) {
Timber.d("UploadController/doInBackground, contribution.getLocalUri():" + contribution.getLocalUri());
AssetFileDescriptor assetFileDescriptor = contentResolver
@@ -218,7 +184,7 @@ public class UploadController {
contribution.setDateCreated(new Date());
}
}
- return contribution;
+ return contribution;
}
@Override
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java
new file mode 100644
index 000000000..46193e958
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java
@@ -0,0 +1,400 @@
+package fr.free.nrw.commons.upload;
+
+import android.annotation.SuppressLint;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.graphics.BitmapRegionDecoder;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import fr.free.nrw.commons.CommonsApplication;
+import fr.free.nrw.commons.auth.SessionManager;
+import fr.free.nrw.commons.contributions.Contribution;
+import fr.free.nrw.commons.mwapi.MediaWikiApi;
+import fr.free.nrw.commons.settings.Prefs;
+import fr.free.nrw.commons.utils.ImageUtils;
+import io.reactivex.Observable;
+import io.reactivex.Single;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.functions.Consumer;
+import io.reactivex.schedulers.Schedulers;
+import io.reactivex.subjects.BehaviorSubject;
+import timber.log.Timber;
+
+public class UploadModel {
+
+ private MediaWikiApi mwApi;
+ private static UploadItem DUMMY = new UploadItem(Uri.EMPTY, "", "", GPSExtractor.DUMMY, "", null,-1l) {
+ };
+ private final SharedPreferences prefs;
+ private final List licenses;
+ private String license;
+ private final Map licensesByName;
+ private List items = new ArrayList<>();
+ private boolean topCardState = true;
+ private boolean bottomCardState = true;
+ private boolean rightCardState = true;
+ private int currentStepIndex = 0;
+ private Context context;
+ private ContentResolver contentResolver;
+ private boolean useExtStorage;
+ private Disposable badImageSubscription;
+
+ @Inject
+ SessionManager sessionManager;
+ private Uri currentMediaUri;
+
+ @Inject
+ UploadModel(@Named("licenses") List licenses,
+ @Named("default_preferences") SharedPreferences prefs,
+ @Named("licenses_by_name") Map licensesByName,
+ Context context,
+ MediaWikiApi mwApi) {
+ this.licenses = licenses;
+ this.prefs = prefs;
+ this.license = Prefs.Licenses.CC_BY_SA_3;
+ this.licensesByName = licensesByName;
+ this.context = context;
+ this.mwApi = mwApi;
+ this.contentResolver = context.getContentResolver();
+ useExtStorage = this.prefs.getBoolean("useExternalStorage", false);
+ }
+
+ @SuppressLint("CheckResult")
+ void receive(List mediaUri, String mimeType, String source, SimilarImageInterface similarImageInterface) {
+ initDefaultValues();
+ Observable itemObservable = Observable.fromIterable(mediaUri)
+ .map(media -> {
+ currentMediaUri=media;
+ return cacheFileUpload(media);
+ })
+ .map(filePath -> {
+ long fileCreatedDate = getFileCreatedDate(currentMediaUri);
+ Uri uri = Uri.fromFile(new File(filePath));
+ FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context);
+ UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface),
+ FileUtils.getFileExt(filePath), null,fileCreatedDate);
+ Single.zip(
+ Single.fromCallable(() ->
+ new FileInputStream(filePath))
+ .map(FileUtils::getSHA1)
+ .map(mwApi::existingFile)
+ .map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK),
+ Single.fromCallable(() ->
+ new FileInputStream(filePath))
+ .map(file -> BitmapRegionDecoder.newInstance(file, false))
+ .map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
+ (dupe, dark) -> dupe | dark)
+ .observeOn(Schedulers.io())
+ .subscribe(item.imageQuality::onNext, Timber::e);
+ return item;
+ });
+ items = itemObservable.toList().blockingGet();
+ items.get(0).selected = true;
+ items.get(0).first = true;
+ }
+
+ @SuppressLint("CheckResult")
+ void receiveDirect(Uri media, String mimeType, String source, String wikidataEntityIdPref, String title, String desc, SimilarImageInterface similarImageInterface) {
+ initDefaultValues();
+ long fileCreatedDate = getFileCreatedDate(media);
+ String filePath = this.cacheFileUpload(media);
+ Uri uri = Uri.fromFile(new File(filePath));
+ FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context);
+ UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface),
+ FileUtils.getFileExt(filePath), wikidataEntityIdPref,fileCreatedDate);
+ item.title.setTitleText(title);
+ item.descriptions.get(0).setDescriptionText(desc);
+ //TODO figure out if default descriptions in other languages exist
+ item.descriptions.get(0).setLanguageCode("en");
+ Single.zip(
+ Single.fromCallable(() ->
+ new FileInputStream(filePath))
+ .map(FileUtils::getSHA1)
+ .map(mwApi::existingFile)
+ .map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK),
+ Single.fromCallable(() ->
+ new FileInputStream(filePath))
+ .map(file -> BitmapRegionDecoder.newInstance(file, false))
+ .map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
+ (dupe, dark) -> dupe | dark).subscribe(item.imageQuality::onNext);
+ items.add(item);
+ items.get(0).selected = true;
+ items.get(0).first = true;
+ }
+
+ private void initDefaultValues() {
+ currentStepIndex = 0;
+ topCardState = true;
+ bottomCardState = true;
+ rightCardState = true;
+ items = new ArrayList<>();
+ }
+
+ /**
+ * Get file creation date from uri from all possible content providers
+ * @param media
+ * @return
+ */
+ private long getFileCreatedDate(Uri media) {
+ try {
+ Cursor cursor = contentResolver.query(media, null, null, null, null);
+ if (cursor == null) {
+ return -1;//Could not fetch last_modified
+ }
+ //Content provider contracts for opening gallery from the app and that by sharing from gallery from outside are different and we need to handle both the cases
+ int lastModifiedColumnIndex = cursor.getColumnIndex("last_modified");//If gallery is opened from in app
+ if(lastModifiedColumnIndex==-1){
+ lastModifiedColumnIndex=cursor.getColumnIndex("datetaken");
+ }
+ //If both the content providers do not give the data, lets leave it to Jesus
+ if(lastModifiedColumnIndex==-1){
+ return -1l;
+ }
+ cursor.moveToFirst();
+ return cursor.getLong(lastModifiedColumnIndex);
+ } catch (Exception e) {
+ return -1;////Could not fetch last_modified
+ }
+ }
+
+ boolean isPreviousAvailable() {
+ return currentStepIndex > 0;
+ }
+
+ boolean isNextAvailable() {
+ return currentStepIndex < (items.size() + 1);
+ }
+
+ boolean isSubmitAvailable() {
+ int count = items.size();
+ boolean hasError = license == null;
+ for (int i = 0; i < count; i++) {
+ UploadItem item = items.get(i);
+ hasError |= item.error;
+ }
+ return !hasError;
+ }
+
+ int getCurrentStep() {
+ return currentStepIndex + 1;
+ }
+
+ int getStepCount() {
+ return items.size() + 2;
+ }
+
+ public int getCount() {
+ return items.size();
+ }
+
+ public List getUploads() {
+ return items;
+ }
+
+ boolean isTopCardState() {
+ return topCardState;
+ }
+
+ void setTopCardState(boolean topCardState) {
+ this.topCardState = topCardState;
+ }
+
+ boolean isBottomCardState() {
+ return bottomCardState;
+ }
+
+ void setRightCardState(boolean rightCardState) {
+ this.rightCardState = rightCardState;
+ }
+
+ boolean isRightCardState() {
+ return rightCardState;
+ }
+
+ void setBottomCardState(boolean bottomCardState) {
+ this.bottomCardState = bottomCardState;
+ }
+
+ public void next() {
+ if (badImageSubscription != null)
+ badImageSubscription.dispose();
+ markCurrentUploadVisited();
+ if (currentStepIndex < items.size() + 1) {
+ currentStepIndex++;
+ }
+ updateItemState();
+ }
+
+ public void setCurrentTitleAndDescriptions(Title title, List descriptions) {
+ setCurrentUploadTitle(title);
+ setCurrentUploadDescriptions(descriptions);
+ }
+
+ private void setCurrentUploadTitle(Title title) {
+ if (currentStepIndex < items.size() && currentStepIndex >= 0) {
+ items.get(currentStepIndex).title = title;
+ }
+ }
+
+ private void setCurrentUploadDescriptions(List descriptions) {
+ if (currentStepIndex < items.size() && currentStepIndex >= 0) {
+ items.get(currentStepIndex).descriptions = descriptions;
+ }
+ }
+
+ public void previous() {
+ if (badImageSubscription != null)
+ badImageSubscription.dispose();
+ markCurrentUploadVisited();
+ if (currentStepIndex > 0) {
+ currentStepIndex--;
+ }
+ updateItemState();
+ }
+
+ void jumpTo(UploadItem item) {
+ currentStepIndex = items.indexOf(item);
+ item.visited = true;
+ updateItemState();
+ }
+
+ UploadItem getCurrentItem() {
+ return isShowingItem() ? items.get(currentStepIndex) : DUMMY;
+ }
+
+ boolean isShowingItem() {
+ return currentStepIndex < items.size();
+ }
+
+ private void updateItemState() {
+ int count = items.size();
+ for (int i = 0; i < count; i++) {
+ UploadItem item = items.get(i);
+ item.selected = (currentStepIndex >= count || i == currentStepIndex);
+ item.error = item.title == null || item.title.isEmpty();
+ }
+ }
+
+ private void markCurrentUploadVisited() {
+ if (currentStepIndex < items.size() && currentStepIndex >= 0) {
+ items.get(currentStepIndex).visited = true;
+ }
+ }
+
+ public List getLicenses() {
+ return licenses;
+ }
+
+ String getSelectedLicense() {
+ return license;
+ }
+
+ void setSelectedLicense(String licenseName) {
+ this.license = licensesByName.get(licenseName);
+ }
+
+ Observable buildContributions(List categoryStringList) {
+ return Observable.fromIterable(items).map(item ->
+ {
+ Contribution contribution = new Contribution(item.mediaUri, null, item.title + "." + item.fileExt,
+ Description.formatList(item.descriptions), -1,
+ null, null, sessionManager.getUserName(),
+ CommonsApplication.DEFAULT_EDIT_SUMMARY, item.gpsCoords.getCoords());
+ contribution.setWikiDataEntityId(item.wikidataEntityId);
+ contribution.setCategories(categoryStringList);
+ contribution.setTag("mimeType", item.mimeType);
+ contribution.setSource(item.source);
+ contribution.setContentProviderUri(item.mediaUri);
+ if (item.createdTimestamp != -1l) {
+ contribution.setDateCreated(new Date(item.createdTimestamp));
+ //Set the date only if you have it, else the upload service is gonna try it the other way
+ }
+ return contribution;
+ });
+ }
+
+ /**
+ * Copy files into local storage and return file path
+ *
+ * @param media Uri of the file
+ * @return path of the enw file
+ */
+ private String cacheFileUpload(Uri media) {
+ try {
+ String copyPath;
+ if (useExtStorage)
+ copyPath = FileUtils.createExternalCopyPathAndCopy(media, contentResolver);
+ else
+ copyPath = FileUtils.createCopyPathAndCopy(media, context);
+ Timber.i("File path is " + copyPath);
+ return copyPath;
+ } catch (IOException e) {
+ Timber.w(e, "Error in copying URI " + media.getPath());
+ return null;
+ }
+ }
+
+ void keepPicture() {
+ items.get(currentStepIndex).imageQuality.onNext(ImageUtils.IMAGE_KEEP);
+ }
+
+ void deletePicture() {
+ badImageSubscription.dispose();
+ items.remove(currentStepIndex).imageQuality.onComplete();
+ updateItemState();
+ }
+
+ void subscribeBadPicture(Consumer consumer) {
+ badImageSubscription = getCurrentItem().imageQuality.subscribe(consumer, Timber::e);
+ }
+
+
+ @SuppressWarnings("WeakerAccess")
+ static class UploadItem {
+ public final Uri mediaUri;
+ public final String mimeType;
+ public final String source;
+ public final GPSExtractor gpsCoords;
+
+ public boolean selected = false;
+ public boolean first = false;
+ public String fileExt;
+ public BehaviorSubject imageQuality;
+ Title title;
+ List descriptions;
+ public String wikidataEntityId;
+ public boolean visited;
+ public boolean error;
+ public long createdTimestamp;
+
+ @SuppressLint("CheckResult")
+ UploadItem(Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords, String fileExt, @Nullable String wikidataEntityId, long createdTimestamp) {
+ title = new Title();
+ descriptions = new ArrayList<>();
+ descriptions.add(new Description());
+ this.wikidataEntityId = wikidataEntityId;
+ this.mediaUri = mediaUri;
+ this.mimeType = mimeType;
+ this.source = source;
+ this.gpsCoords = gpsCoords;
+ this.fileExt = fileExt;
+ imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT);
+ this.createdTimestamp=createdTimestamp;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java
new file mode 100644
index 000000000..74e3192bd
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java
@@ -0,0 +1,430 @@
+package fr.free.nrw.commons.upload;
+
+import android.annotation.SuppressLint;
+import android.net.Uri;
+
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.category.CategoriesModel;
+import fr.free.nrw.commons.contributions.Contribution;
+import fr.free.nrw.commons.mwapi.MediaWikiApi;
+import fr.free.nrw.commons.utils.ImageUtils;
+import io.reactivex.Completable;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.schedulers.Schedulers;
+import timber.log.Timber;
+
+import static fr.free.nrw.commons.upload.UploadModel.UploadItem;
+import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE;
+import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
+import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP;
+import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
+
+/**
+ * The MVP pattern presenter of Upload GUI
+ */
+@Singleton
+public class UploadPresenter {
+
+ private final UploadModel uploadModel;
+ private final UploadController uploadController;
+ private final MediaWikiApi mediaWikiApi;
+
+ private static final UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(),
+ new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null);
+ private UploadView view = DUMMY;
+
+ private static final SimilarImageInterface SIMILAR_IMAGE = (SimilarImageInterface) Proxy.newProxyInstance(SimilarImageInterface.class.getClassLoader(),
+ new Class[]{SimilarImageInterface.class}, (proxy, method, methodArgs) -> null);
+ private SimilarImageInterface similarImageInterface = SIMILAR_IMAGE;
+
+ @UploadView.UploadPage
+ private int currentPage = UploadView.PLEASE_WAIT;
+
+
+ @Inject
+ UploadPresenter(UploadModel uploadModel,
+ UploadController uploadController,
+ MediaWikiApi mediaWikiApi) {
+ this.uploadModel = uploadModel;
+ this.uploadController = uploadController;
+ this.mediaWikiApi = mediaWikiApi;
+ }
+
+ void receive(Uri mediaUri, String mimeType, String source) {
+ receive(Collections.singletonList(mediaUri), mimeType, source);
+ }
+
+ /**
+ * Passes the items received to {@link #uploadModel} and displays the items.
+ *
+ * @param media The Uri's of the media being uploaded.
+ * @param mimeType the mimeType of the files.
+ * @param source File source from {@link Contribution.FileSource}
+ */
+ @SuppressLint("CheckResult")
+ void receive(List media, String mimeType, @Contribution.FileSource String source) {
+ Completable.fromRunnable(() -> uploadModel.receive(media, mimeType, source, similarImageInterface))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> {
+ updateCards();
+ updateLicenses();
+ updateContent();
+ if (uploadModel.isShowingItem())
+ uploadModel.subscribeBadPicture(this::handleBadPicture);
+ }, Timber::e);
+ }
+
+ /**
+ * Passes the direct upload item received to {@link #uploadModel} and displays the items.
+ *
+ * @param media The Uri's of the media being uploaded.
+ * @param mimeType the mimeType of the files.
+ * @param source File source from {@link Contribution.FileSource}
+ */
+ @SuppressLint("CheckResult")
+ void receiveDirect(Uri media, String mimeType, @Contribution.FileSource String source, String wikidataEntityIdPref, String title, String desc) {
+ Completable.fromRunnable(() -> uploadModel.receiveDirect(media, mimeType, source, wikidataEntityIdPref, title, desc, similarImageInterface))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> {
+ updateCards();
+ updateLicenses();
+ updateContent();
+ if (uploadModel.isShowingItem())
+ uploadModel.subscribeBadPicture(this::handleBadPicture);
+ }, Timber::e);
+ }
+ /**
+ * Sets the license to parameter and updates {@link UploadActivity}
+ *
+ * @param licenseName license name
+ */
+ void selectLicense(String licenseName) {
+ uploadModel.setSelectedLicense(licenseName);
+ view.updateLicenseSummary(uploadModel.getSelectedLicense());
+ }
+
+ //region Wizard step management
+
+ /**
+ * Called by the next button in {@link UploadActivity}
+ */
+ @SuppressLint("CheckResult")
+ void handleNext(Title title,
+ List descriptions) {
+ validateCurrentItemTitle()
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(errorCode -> handleImage(errorCode, title, descriptions));
+ }
+
+ /**
+ * Called by the next button in {@link UploadActivity}
+ */
+ @SuppressLint("CheckResult")
+ void handleCategoryNext(CategoriesModel categoriesModel,
+ boolean noCategoryWarningShown) {
+ if (categoriesModel.selectedCategoriesCount() < 1 && !noCategoryWarningShown) {
+ view.showNoCategorySelectedWarning();
+ } else {
+ nextUploadedItem();
+ }
+ }
+
+ private void handleImage(Integer errorCode, Title title, List descriptions) {
+ switch (errorCode) {
+ case EMPTY_TITLE:
+ view.showErrorMessage(R.string.add_title_toast);
+ break;
+ case FILE_NAME_EXISTS:
+ if(getCurrentItem().imageQuality.getValue().equals(IMAGE_KEEP)) {
+ setTitleAndDescription(title, descriptions);
+ nextUploadedItem();
+ } else {
+ view.showDuplicatePicturePopup();
+ }
+ break;
+ case IMAGE_OK:
+ default:
+ setTitleAndDescription(title, descriptions);
+ nextUploadedItem();
+ }
+ }
+
+ private void nextUploadedItem() {
+ uploadModel.next();
+ updateContent();
+ if (uploadModel.isShowingItem()) {
+ uploadModel.subscribeBadPicture(this::handleBadPicture);
+ }
+ view.dismissKeyboard();
+ }
+
+ private void setTitleAndDescription(Title title, List descriptions) {
+ uploadModel.setCurrentTitleAndDescriptions(title, descriptions);
+ }
+
+ private Title getCurrentImageTitle() {
+ return getCurrentItem().title;
+ }
+
+ String getCurrentImageFileName() {
+ UploadItem currentItem = getCurrentItem();
+ return currentItem.title + "." + uploadModel.getCurrentItem().fileExt;
+ }
+
+ @SuppressLint("CheckResult")
+ private Observable validateCurrentItemTitle() {
+ Title title = getCurrentImageTitle();
+ if (title.isEmpty()) {
+ view.showErrorMessage(R.string.add_title_toast);
+ return Observable.just(EMPTY_TITLE);
+ }
+
+ return Observable.fromCallable(() -> mediaWikiApi.fileExistsWithName(getCurrentImageFileName()))
+ .subscribeOn(Schedulers.io())
+ .map(doesFileExist -> {
+ if (doesFileExist) {
+ return FILE_NAME_EXISTS;
+ }
+ return IMAGE_OK;
+ });
+ }
+
+ /**
+ * Called by the previous button in {@link UploadActivity}
+ */
+ void handlePrevious() {
+ uploadModel.previous();
+ updateContent();
+ if (uploadModel.isShowingItem()) {
+ uploadModel.subscribeBadPicture(this::handleBadPicture);
+ }
+ view.dismissKeyboard();
+ }
+
+ /**
+ * Called when one of the pictures on the top card is clicked on in {@link UploadActivity}
+ */
+ void thumbnailClicked(UploadItem item) {
+ uploadModel.jumpTo(item);
+ updateContent();
+ }
+
+ /**
+ * Called by the submit button in {@link UploadActivity}
+ */
+ @SuppressLint("CheckResult")
+ void handleSubmit(CategoriesModel categoriesModel) {
+ if (view.checkIfLoggedIn())
+ uploadModel.buildContributions(categoriesModel.getCategoryStringList())
+ .observeOn(Schedulers.io())
+ .subscribe(uploadController::startUpload);
+ }
+
+ /**
+ * Called by the map button on the right card in {@link UploadActivity}
+ */
+ void openCoordinateMap() {
+ GPSExtractor gpsObj = uploadModel.getCurrentItem().gpsCoords;
+ if (gpsObj != null && gpsObj.imageCoordsExists) {
+ view.launchMapActivity(gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude());
+ }
+ }
+
+
+ /**
+ * Called by the image processors when a result is obtained.
+ *
+ * @param result the result returned by the image procesors.
+ */
+ private void handleBadPicture(@ImageUtils.Result int result) {
+ view.showBadPicturePopup(result);
+ }
+
+ void keepPicture() {
+ uploadModel.keepPicture();
+ }
+
+ void deletePicture() {
+ if (uploadModel.getCount() == 1)
+ view.finish();
+ else {
+ uploadModel.deletePicture();
+ updateCards();
+ updateContent();
+ if (uploadModel.isShowingItem())
+ uploadModel.subscribeBadPicture(this::handleBadPicture);
+ view.dismissKeyboard();
+ }
+ }
+ //endregion
+
+ //region Top Bottom and Right card state management
+
+
+ /**
+ * Toggles the top card's state between open and closed.
+ */
+ void toggleTopCardState() {
+ uploadModel.setTopCardState(!uploadModel.isTopCardState());
+ view.setTopCardState(uploadModel.isTopCardState());
+ }
+
+ /**
+ * Toggles the bottom card's state between open and closed.
+ */
+ void toggleBottomCardState() {
+ uploadModel.setBottomCardState(!uploadModel.isBottomCardState());
+ view.setBottomCardState(uploadModel.isBottomCardState());
+ }
+
+ /**
+ * Toggles the right card's state between open and closed.
+ */
+ void toggleRightCardState() {
+ uploadModel.setRightCardState(!uploadModel.isRightCardState());
+ view.setRightCardState(uploadModel.isRightCardState());
+ }
+
+ /**
+ * Sets all the cards' states to closed.
+ */
+ void closeAllCards() {
+ if (uploadModel.isTopCardState()) {
+ uploadModel.setTopCardState(false);
+ view.setTopCardState(false);
+ }
+ if (uploadModel.isRightCardState()) {
+ uploadModel.setRightCardState(false);
+ view.setRightCardState(false);
+ }
+ if (uploadModel.isBottomCardState()) {
+ uploadModel.setBottomCardState(false);
+ view.setBottomCardState(false);
+ }
+ }
+ //endregion
+
+ //region View / Lifecycle management
+ public void init() {
+ uploadController.prepareService();
+ }
+
+ void cleanup() {
+ uploadController.cleanup();
+ }
+
+ void removeView() {
+ this.view = DUMMY;
+ }
+
+ void addView(UploadView view) {
+ this.view = view;
+
+ updateCards();
+ updateLicenses();
+ updateContent();
+ }
+
+
+ /**
+ * Updates the cards for when there is a change to the amount of items being uploaded.
+ */
+ private void updateCards() {
+ Timber.i("uploadModel.getCount():" + uploadModel.getCount());
+ view.updateThumbnails(uploadModel.getUploads());
+ view.setTopCardVisibility(uploadModel.getCount() > 1);
+ view.setBottomCardVisibility(uploadModel.getCount() > 0);
+ view.setTopCardState(uploadModel.isTopCardState());
+ view.setBottomCardState(uploadModel.isBottomCardState());
+ }
+
+ /**
+ * Sets the list of licences and the default license.
+ */
+ private void updateLicenses() {
+ String selectedLicense = uploadModel.getSelectedLicense();
+ view.updateLicenses(uploadModel.getLicenses(), selectedLicense);
+ view.updateLicenseSummary(selectedLicense);
+ }
+
+ /**
+ * Updates the cards and the background when a new currentPage is selected.
+ */
+ private void updateContent() {
+ Timber.i("Updating content for currentPage" + uploadModel.getCurrentStep());
+ view.setNextEnabled(uploadModel.isNextAvailable());
+ view.setPreviousEnabled(uploadModel.isPreviousAvailable());
+ view.setSubmitEnabled(uploadModel.isSubmitAvailable());
+
+ view.setBackground(uploadModel.getCurrentItem().mediaUri);
+
+ view.updateBottomCardContent(uploadModel.getCurrentStep(),
+ uploadModel.getStepCount(),
+ uploadModel.getCurrentItem(),
+ uploadModel.isShowingItem());
+
+ view.updateTopCardContent();
+
+ GPSExtractor gpsObj = uploadModel.getCurrentItem().gpsCoords;
+ view.updateRightCardContent(gpsObj != null && gpsObj.imageCoordsExists);
+
+ showCorrectCards(uploadModel.getCurrentStep(), uploadModel.getCount());
+ }
+
+ /**
+ * Updates the layout to show the correct bottom card.
+ *
+ * @param currentStep the current step
+ * @param uploadCount how many items are being uploaded
+ */
+ private void showCorrectCards(int currentStep, int uploadCount) {
+ if (uploadCount == 0) {
+ currentPage = UploadView.PLEASE_WAIT;
+ } else if (currentStep <= uploadCount) {
+ currentPage = UploadView.TITLE_CARD;
+ view.setTopCardVisibility(uploadModel.getCount() > 1);
+ } else if (currentStep == uploadCount + 1) {
+ currentPage = UploadView.CATEGORIES;
+ view.setTopCardVisibility(false);
+ view.setRightCardVisibility(false);
+ view.initDefaultCategories();
+ } else {
+ currentPage = UploadView.LICENSE;
+ view.setTopCardVisibility(false);
+ view.setRightCardVisibility(false);
+ }
+ view.setBottomCardVisibility(currentPage);
+ }
+
+ //endregion
+
+ /**
+ * @return the item currently being displayed
+ */
+ private UploadItem getCurrentItem() {
+ return uploadModel.getCurrentItem();
+ }
+
+ List getImageTitleList() {
+ List titleList = new ArrayList<>();
+ for (UploadItem item : uploadModel.getUploads()) {
+ if (item.title.isSet()) {
+ titleList.add(item.title.toString());
+ }
+ }
+ return titleList;
+ }
+
+}
\ No newline at end of file
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 488517121..e7920f317 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
@@ -36,7 +36,7 @@ import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionDao;
-import fr.free.nrw.commons.contributions.ContributionsActivity;
+import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.UploadResult;
@@ -62,7 +62,9 @@ public class UploadService extends HandlerService {
private NotificationCompat.Builder curProgressNotification;
private int toUpload;
- // The file names of unfinished uploads, used to prevent overwriting
+ /**
+ * The file names of unfinished uploads, used to prevent overwriting
+ */
private Set unfinishedUploads = new HashSet<>();
// DO NOT HAVE NOTIFICATION ID OF 0 FOR ANYTHING
@@ -193,7 +195,7 @@ public class UploadService extends HandlerService {
.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload))
.setOngoing(true)
.setProgress(100, 0, true)
- .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0))
+ .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0))
.setTicker(getString(R.string.upload_progress_notification_title_in_progress, contribution.getDisplayTitle()));
}
@@ -230,7 +232,7 @@ public class UploadService extends HandlerService {
//As the fileInputStream is null there's no point in continuing the upload process
//mwapi.upload accepts a NonNull input stream
- if(fileInputStream == null) {
+ if (fileInputStream == null) {
Timber.d("File not found");
return;
}
@@ -314,11 +316,12 @@ public class UploadService extends HandlerService {
}
@SuppressLint("StringFormatInvalid")
+ @SuppressWarnings("deprecation")
private void showFailedNotification(Contribution contribution) {
Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
.setAutoCancel(true)
- .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0))
+ .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0))
.setTicker(getString(R.string.upload_failed_notification_title, contribution.getDisplayTitle()))
.setContentTitle(getString(R.string.upload_failed_notification_title, contribution.getDisplayTitle()))
.setContentText(getString(R.string.upload_failed_notification_subtitle))
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java
new file mode 100644
index 000000000..64b873da0
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java
@@ -0,0 +1,49 @@
+package fr.free.nrw.commons.upload;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import com.facebook.drawee.view.SimpleDraweeView;
+import com.pedrogomez.renderers.Renderer;
+
+import fr.free.nrw.commons.R;
+
+class UploadThumbnailRenderer extends Renderer {
+ private ThumbnailClickedListener listener;
+ private SimpleDraweeView background;
+ private View space;
+ private ImageView error;
+
+ public UploadThumbnailRenderer(ThumbnailClickedListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ protected View inflate(LayoutInflater inflater, ViewGroup parent) {
+ return inflater.inflate(R.layout.item_upload_thumbnail, parent, false);
+ }
+
+ @Override
+ protected void setUpView(View rootView) {
+ error = rootView.findViewById(R.id.error);
+ space = rootView.findViewById(R.id.left_space);
+ background = rootView.findViewById(R.id.thumbnail);
+ }
+
+ @Override
+ protected void hookListeners(View rootView) {
+ background.setOnClickListener(v -> listener.thumbnailClicked(getContent()));
+ }
+
+ @Override
+ public void render() {
+ UploadModel.UploadItem content = getContent();
+ background.setImageURI(content.mediaUri);
+ background.setAlpha(content.selected ? 1.0f : 0.5f);
+ space.setVisibility(content.first ? View.VISIBLE : View.GONE);
+ error.setVisibility(content.visited && content.error ? View.VISIBLE : View.GONE);
+ }
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java
new file mode 100644
index 000000000..bc0a79c80
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java
@@ -0,0 +1,26 @@
+package fr.free.nrw.commons.upload;
+
+import com.pedrogomez.renderers.ListAdapteeCollection;
+import com.pedrogomez.renderers.RVRendererAdapter;
+import com.pedrogomez.renderers.RendererBuilder;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.inject.Inject;
+
+public class UploadThumbnailsAdapterFactory {
+ private ThumbnailClickedListener listener;
+
+ UploadThumbnailsAdapterFactory(ThumbnailClickedListener listener) {
+ this.listener = listener;
+ }
+
+ public RVRendererAdapter create(List placeList) {
+ RendererBuilder builder = new RendererBuilder()
+ .bind(UploadModel.UploadItem.class, new UploadThumbnailRenderer(listener));
+ ListAdapteeCollection collection = new ListAdapteeCollection<>(
+ placeList != null ? placeList : Collections.emptyList());
+ return new RVRendererAdapter<>(builder, collection);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java
new file mode 100644
index 000000000..410914446
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java
@@ -0,0 +1,82 @@
+package fr.free.nrw.commons.upload;
+
+import android.net.Uri;
+import android.support.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.util.List;
+
+import fr.free.nrw.commons.utils.ImageUtils;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+public interface UploadView {
+ // Dummy implementation of the view interface to allow us to have a 'null object pattern'
+ // in the presenter and avoid constant NULL checking.
+// UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(),
+// new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null);
+
+ List getDescriptions();
+
+ @Retention(SOURCE)
+ @IntDef({PLEASE_WAIT, TITLE_CARD, CATEGORIES, LICENSE})
+ @interface UploadPage {}
+
+ int PLEASE_WAIT = 0;
+
+ int TITLE_CARD = 1;
+ int CATEGORIES = 2;
+ int LICENSE = 3;
+
+ boolean checkIfLoggedIn();
+
+ void updateThumbnails(List uploads);
+
+ void setNextEnabled(boolean available);
+
+ void setSubmitEnabled(boolean available);
+
+ void setPreviousEnabled(boolean available);
+
+ void setTopCardState(boolean state);
+
+ void setRightCardVisibility(boolean visible);
+
+ void setBottomCardState(boolean state);
+
+ void setRightCardState(boolean bottomCardState);
+
+ void setBackground(Uri mediaUri);
+
+ void setTopCardVisibility(boolean visible);
+
+ void setBottomCardVisibility(boolean visible);
+
+ void setBottomCardVisibility(@UploadPage int page);
+
+ void updateRightCardContent(boolean gpsPresent);
+
+ void updateBottomCardContent(int currentStep, int stepCount, UploadModel.UploadItem uploadItem, boolean isShowingItem);
+
+ void updateLicenses(List licenses, String selectedLicense);
+
+ void updateLicenseSummary(String selectedLicense);
+
+ void updateTopCardContent();
+
+ void dismissKeyboard();
+
+ void showBadPicturePopup(@ImageUtils.Result int errorMessage);
+
+ void showDuplicatePicturePopup();
+
+ void finish();
+
+ void launchMapActivity(String decCoords);
+
+ void showErrorMessage(int resourceId);
+
+ void initDefaultCategories();
+
+ void showNoCategorySelectedWarning();
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java b/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java
index d3274bf2c..a28fde579 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java
@@ -7,62 +7,62 @@ import java.util.HashMap;
* info in the user language
*/
public class UrlLicense {
- HashMap urlLicense = new HashMap();
- public void initialize(){
- urlLicense.put("en","https://commons.wikimedia.org/wiki/Commons:Licensing");
- urlLicense.put("ar","https://commons.wikimedia.org/wiki/Commons:Licensing/ar");
- urlLicense.put("ast","https://commons.wikimedia.org/wiki/Commons:Licensing/ast");
- urlLicense.put("az","https://commons.wikimedia.org/wiki/Commons:Licensing/az");
- urlLicense.put("be","https://commons.wikimedia.org/wiki/Commons:Licensing/be");
- urlLicense.put("bg","https://commons.wikimedia.org/wiki/Commons:Licensing/bg");
- urlLicense.put("bn","https://commons.wikimedia.org/wiki/Commons:Licensing/bn");
- urlLicense.put("ca","https://commons.wikimedia.org/wiki/Commons:Licensing/ca");
- urlLicense.put("cs","https://commons.wikimedia.org/wiki/Commons:Licensing/cs");
- urlLicense.put("da","https://commons.wikimedia.org/wiki/Commons:Licensing/da");
- urlLicense.put("de","https://commons.wikimedia.org/wiki/Commons:Licensing/de");
- urlLicense.put("el","https://commons.wikimedia.org/wiki/Commons:Licensing/el");
- urlLicense.put("eo","https://commons.wikimedia.org/wiki/Commons:Licensing/eo");
- urlLicense.put("es","https://commons.wikimedia.org/wiki/Commons:Licensing/es");
- urlLicense.put("eu","https://commons.wikimedia.org/wiki/Commons:Licensing/eu");
- urlLicense.put("fa","https://commons.wikimedia.org/wiki/Commons:Licensing/fa");
- urlLicense.put("fi","https://commons.wikimedia.org/wiki/Commons:Licensing/fi");
- urlLicense.put("fr","https://commons.wikimedia.org/wiki/Commons:Licensing/fr");
- urlLicense.put("gl","https://commons.wikimedia.org/wiki/Commons:Licensing/gl");
- urlLicense.put("gsw","https://commons.wikimedia.org/wiki/Commons:Licensing/gsw");
- urlLicense.put("he","https://commons.wikimedia.org/wiki/Commons:Licensing/he");
- urlLicense.put("hi","https://commons.wikimedia.org/wiki/Commons:Licensing/hi");
- urlLicense.put("hu","https://commons.wikimedia.org/wiki/Commons:Licensing/hu");
- urlLicense.put("id","https://commons.wikimedia.org/wiki/Commons:Licensing/id");
- urlLicense.put("is","https://commons.wikimedia.org/wiki/Commons:Licensing/is");
- urlLicense.put("it","https://commons.wikimedia.org/wiki/Commons:Licensing/it");
- urlLicense.put("ja","https://commons.wikimedia.org/wiki/Commons:Licensing/ja");
- urlLicense.put("ka","https://commons.wikimedia.org/wiki/Commons:Licensing/ka");
- urlLicense.put("km","https://commons.wikimedia.org/wiki/Commons:Licensing/km");
- urlLicense.put("ko","https://commons.wikimedia.org/wiki/Commons:Licensing/ko");
- urlLicense.put("ku","https://commons.wikimedia.org/wiki/Commons:Licensing/ku");
- urlLicense.put("mk","https://commons.wikimedia.org/wiki/Commons:Licensing/mk");
- urlLicense.put("mr","https://commons.wikimedia.org/wiki/Commons:Licensing/mr");
- urlLicense.put("ms","https://commons.wikimedia.org/wiki/Commons:Licensing/ms");
- urlLicense.put("my","https://commons.wikimedia.org/wiki/Commons:Licensing/my");
- urlLicense.put("nl","https://commons.wikimedia.org/wiki/Commons:Licensing/nl");
- urlLicense.put("oc","https://commons.wikimedia.org/wiki/Commons:Licensing/oc");
- urlLicense.put("pl","https://commons.wikimedia.org/wiki/Commons:Licensing/pl");
- urlLicense.put("pt","https://commons.wikimedia.org/wiki/Commons:Licensing/pt");
- urlLicense.put("pt-br","https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br");
- urlLicense.put("ro","https://commons.wikimedia.org/wiki/Commons:Licensing/ro");
- urlLicense.put("ru","https://commons.wikimedia.org/wiki/Commons:Licensing/ru");
- urlLicense.put("scn","https://commons.wikimedia.org/wiki/Commons:Licensing/scn");
- urlLicense.put("sk","https://commons.wikimedia.org/wiki/Commons:Licensing/sk");
- urlLicense.put("sl","https://commons.wikimedia.org/wiki/Commons:Licensing/sl");
- urlLicense.put("sv","https://commons.wikimedia.org/wiki/Commons:Licensing/sv");
- urlLicense.put("tr","https://commons.wikimedia.org/wiki/Commons:Licensing/tr");
- urlLicense.put("uk","https://commons.wikimedia.org/wiki/Commons:Licensing/uk");
- urlLicense.put("ur","https://commons.wikimedia.org/wiki/Commons:Licensing/ur");
- urlLicense.put("vi","https://commons.wikimedia.org/wiki/Commons:Licensing/vi");
- urlLicense.put("zh","https://commons.wikimedia.org/wiki/Commons:Licensing/zh");
+ public static HashMap urlLicense = new HashMap<>();
+ static {
+ urlLicense.put("en", "https://commons.wikimedia.org/wiki/Commons:Licensing");
+ urlLicense.put("ar", "https://commons.wikimedia.org/wiki/Commons:Licensing/ar");
+ urlLicense.put("ast", "https://commons.wikimedia.org/wiki/Commons:Licensing/ast");
+ urlLicense.put("az", "https://commons.wikimedia.org/wiki/Commons:Licensing/az");
+ urlLicense.put("be", "https://commons.wikimedia.org/wiki/Commons:Licensing/be");
+ urlLicense.put("bg", "https://commons.wikimedia.org/wiki/Commons:Licensing/bg");
+ urlLicense.put("bn", "https://commons.wikimedia.org/wiki/Commons:Licensing/bn");
+ urlLicense.put("ca", "https://commons.wikimedia.org/wiki/Commons:Licensing/ca");
+ urlLicense.put("cs", "https://commons.wikimedia.org/wiki/Commons:Licensing/cs");
+ urlLicense.put("da", "https://commons.wikimedia.org/wiki/Commons:Licensing/da");
+ urlLicense.put("de", "https://commons.wikimedia.org/wiki/Commons:Licensing/de");
+ urlLicense.put("el", "https://commons.wikimedia.org/wiki/Commons:Licensing/el");
+ urlLicense.put("eo", "https://commons.wikimedia.org/wiki/Commons:Licensing/eo");
+ urlLicense.put("es", "https://commons.wikimedia.org/wiki/Commons:Licensing/es");
+ urlLicense.put("eu", "https://commons.wikimedia.org/wiki/Commons:Licensing/eu");
+ urlLicense.put("fa", "https://commons.wikimedia.org/wiki/Commons:Licensing/fa");
+ urlLicense.put("fi", "https://commons.wikimedia.org/wiki/Commons:Licensing/fi");
+ urlLicense.put("fr", "https://commons.wikimedia.org/wiki/Commons:Licensing/fr");
+ urlLicense.put("gl", "https://commons.wikimedia.org/wiki/Commons:Licensing/gl");
+ urlLicense.put("gsw", "https://commons.wikimedia.org/wiki/Commons:Licensing/gsw");
+ urlLicense.put("he", "https://commons.wikimedia.org/wiki/Commons:Licensing/he");
+ urlLicense.put("hi", "https://commons.wikimedia.org/wiki/Commons:Licensing/hi");
+ urlLicense.put("hu", "https://commons.wikimedia.org/wiki/Commons:Licensing/hu");
+ urlLicense.put("id", "https://commons.wikimedia.org/wiki/Commons:Licensing/id");
+ urlLicense.put("is", "https://commons.wikimedia.org/wiki/Commons:Licensing/is");
+ urlLicense.put("it", "https://commons.wikimedia.org/wiki/Commons:Licensing/it");
+ urlLicense.put("ja", "https://commons.wikimedia.org/wiki/Commons:Licensing/ja");
+ urlLicense.put("ka", "https://commons.wikimedia.org/wiki/Commons:Licensing/ka");
+ urlLicense.put("km", "https://commons.wikimedia.org/wiki/Commons:Licensing/km");
+ urlLicense.put("ko", "https://commons.wikimedia.org/wiki/Commons:Licensing/ko");
+ urlLicense.put("ku", "https://commons.wikimedia.org/wiki/Commons:Licensing/ku");
+ urlLicense.put("mk", "https://commons.wikimedia.org/wiki/Commons:Licensing/mk");
+ urlLicense.put("mr", "https://commons.wikimedia.org/wiki/Commons:Licensing/mr");
+ urlLicense.put("ms", "https://commons.wikimedia.org/wiki/Commons:Licensing/ms");
+ urlLicense.put("my", "https://commons.wikimedia.org/wiki/Commons:Licensing/my");
+ urlLicense.put("nl", "https://commons.wikimedia.org/wiki/Commons:Licensing/nl");
+ urlLicense.put("oc", "https://commons.wikimedia.org/wiki/Commons:Licensing/oc");
+ urlLicense.put("pl", "https://commons.wikimedia.org/wiki/Commons:Licensing/pl");
+ urlLicense.put("pt", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt");
+ urlLicense.put("pt-br", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br");
+ urlLicense.put("ro", "https://commons.wikimedia.org/wiki/Commons:Licensing/ro");
+ urlLicense.put("ru", "https://commons.wikimedia.org/wiki/Commons:Licensing/ru");
+ urlLicense.put("scn", "https://commons.wikimedia.org/wiki/Commons:Licensing/scn");
+ urlLicense.put("sk", "https://commons.wikimedia.org/wiki/Commons:Licensing/sk");
+ urlLicense.put("sl", "https://commons.wikimedia.org/wiki/Commons:Licensing/sl");
+ urlLicense.put("sv", "https://commons.wikimedia.org/wiki/Commons:Licensing/sv");
+ urlLicense.put("tr", "https://commons.wikimedia.org/wiki/Commons:Licensing/tr");
+ urlLicense.put("uk", "https://commons.wikimedia.org/wiki/Commons:Licensing/uk");
+ urlLicense.put("ur", "https://commons.wikimedia.org/wiki/Commons:Licensing/ur");
+ urlLicense.put("vi", "https://commons.wikimedia.org/wiki/Commons:Licensing/vi");
+ urlLicense.put("zh", "https://commons.wikimedia.org/wiki/Commons:Licensing/zh");
}
- public String getLicenseUrl ( String language){
- if(urlLicense.containsKey(language)) {
+ public static String getLicenseUrl ( String language){
+ if (urlLicense.containsKey(language)) {
return urlLicense.get(language);
} else {
return urlLicense.get("en");
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java
new file mode 100644
index 000000000..92f9f1935
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java
@@ -0,0 +1,30 @@
+package fr.free.nrw.commons.utils;
+
+import android.support.annotation.NonNull;
+import android.text.Editable;
+import android.text.TextWatcher;
+
+public class AbstractTextWatcher implements TextWatcher {
+ private final TextChange textChange;
+
+ public AbstractTextWatcher(@NonNull TextChange textChange) {
+ this.textChange = textChange;
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ textChange.onTextChanged(s.toString());
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+
+ public interface TextChange {
+ void onTextChanged(String value);
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/BiMap.java b/app/src/main/java/fr/free/nrw/commons/utils/BiMap.java
new file mode 100644
index 000000000..227f5f024
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/utils/BiMap.java
@@ -0,0 +1,41 @@
+package fr.free.nrw.commons.utils;
+
+import java.util.HashMap;
+import java.util.Set;
+
+/**
+ * HashMap that can be searched in both the forward and reverse directions.
+ */
+public class BiMap {
+
+ private HashMap map = new HashMap();
+ private HashMap inversedMap = new HashMap();
+
+ public void put(K k, V v) {
+ map.put(k, v);
+ inversedMap.put(v, k);
+ }
+
+ public V get(K k) {
+ return map.get(k);
+ }
+
+ public K getKey(V v) {
+ return inversedMap.get(v);
+ }
+
+ public Set getEntrySet(){
+ return inversedMap.keySet();
+ }
+
+ public void remove(K k){
+ inversedMap.remove(map.remove(k));
+ }
+
+
+ public boolean containsKey(V v){
+ return inversedMap.containsKey(v);
+ }
+
+}
+
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ContributionListViewUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ContributionListViewUtils.java
new file mode 100644
index 000000000..6dc9625d7
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/utils/ContributionListViewUtils.java
@@ -0,0 +1,39 @@
+package fr.free.nrw.commons.utils;
+
+import android.util.Log;
+import android.view.View;
+
+/**
+ * This class includes utilities for contribution list fragment indicators, such as number of
+ * uploads, notification and nearby cards and their progress bar behind them.
+ */
+public class ContributionListViewUtils {
+
+ /**
+ * Sets indicator and progress bar visibility according to 3 states, data is ready to display,
+ * data still loading, both should be invisible because media details fragment is visible
+ * @param indicator this can be numOfUploads text view, notification/nearby card views
+ * @param progressBar this is the progress bar behind indicators, displays they are loading
+ * @param isIndicatorReady is indicator fetched the information will be displayed
+ * @param isBothInvisible true if contribution list fragment is not active (ie. Media Details Fragment is active)
+ */
+ public static void setIndicatorVisibility(View indicator, View progressBar, boolean isIndicatorReady, boolean isBothInvisible) {
+ if (indicator!=null && progressBar!=null) {
+ if (isIndicatorReady) {
+ // Indicator ready, display them
+ indicator.setVisibility(View.VISIBLE);
+ progressBar.setVisibility(View.GONE);
+ } else {
+ if (isBothInvisible) {
+ // Media Details Fragment is visible, hide both
+ indicator.setVisibility(View.GONE);
+ progressBar.setVisibility(View.GONE);
+ } else {
+ // Indicator is not ready, still loading
+ indicator.setVisibility(View.GONE);
+ progressBar.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java
index 2e4592e40..f68037488 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java
@@ -5,6 +5,7 @@ import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.content.Context;
+import android.content.DialogInterface;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
@@ -114,7 +115,49 @@ public class DialogUtil {
.setIcon(iconResourceId).create();
return alertDialog;
+ }
+ public static void showAlertDialog(Activity activity,
+ String title,
+ String message,
+ final Runnable onPositiveBtnClick,
+ final Runnable onNegativeBtnClick) {
+ showAlertDialog(activity,
+ title,
+ message,
+ activity.getString(R.string.no),
+ activity.getString(R.string.yes),
+ onPositiveBtnClick,
+ onNegativeBtnClick);
+ }
+
+ public static void showAlertDialog(Activity activity,
+ String title,
+ String message,
+ String positiveButtonText,
+ String negativeButtonText,
+ final Runnable onPositiveBtnClick,
+ final Runnable onNegativeBtnClick) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(title);
+ builder.setMessage(message);
+
+ builder.setPositiveButton(positiveButtonText, (dialogInterface, i) -> {
+ dialogInterface.dismiss();
+ if (onPositiveBtnClick != null) {
+ onPositiveBtnClick.run();
+ }
+ });
+
+ builder.setNegativeButton(negativeButtonText, (DialogInterface dialogInterface, int i) -> {
+ dialogInterface.dismiss();
+ if (onNegativeBtnClick != null) {
+ onNegativeBtnClick.run();
+ }
+ });
+
+ AlertDialog dialog = builder.create();
+ showSafely(activity, dialog);
}
public interface Callback {
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
index d8e569564..26ab5b2ca 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java
@@ -66,7 +66,7 @@ public class FileUtils {
*/
public static boolean checkIfDirectoryExists(String pathToCheck) {
File director = new File(pathToCheck);
- if(director.exists() && director.isDirectory()) {
+ if (director.exists() && director.isDirectory()) {
return true;
} else {
return false;
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 460046bab..e6cc2fc5d 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
@@ -7,6 +7,7 @@ import android.graphics.BitmapRegionDecoder;
import android.graphics.Color;
import android.graphics.Rect;
import android.net.Uri;
+import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.facebook.common.executors.CallerThreadExecutor;
@@ -20,6 +21,8 @@ import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import fr.free.nrw.commons.R;
import timber.log.Timber;
@@ -30,20 +33,44 @@ import timber.log.Timber;
public class ImageUtils {
- public enum Result {
- IMAGE_DARK,
- IMAGE_OK
+ public static final int IMAGE_DARK = 1;
+ public static final int IMAGE_BLURRY = 1 << 1;
+ public static final int IMAGE_DUPLICATE = 1 << 2;
+ public static final int IMAGE_OK = 0;
+ public static final int IMAGE_KEEP = -1;
+ public static final int IMAGE_WAIT = -2;
+ public static final int EMPTY_TITLE = -3;
+ public static final int FILE_NAME_EXISTS = -4;
+ public static final int NO_CATEGORY_SELECTED = -5;
+
+ @IntDef(
+ flag = true,
+ value = {
+ IMAGE_DARK,
+ IMAGE_BLURRY,
+ IMAGE_DUPLICATE,
+ IMAGE_OK,
+ IMAGE_KEEP,
+ IMAGE_WAIT,
+ EMPTY_TITLE,
+ FILE_NAME_EXISTS,
+ NO_CATEGORY_SELECTED
+ }
+ )
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Result {
}
/**
* @param bitmapRegionDecoder BitmapRegionDecoder for the image we wish to process
- * @return Result.IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
- * Result.IMAGE_DARK if image is too dark
+ * @return IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
+ * IMAGE_DARK if image is too dark
*/
- public static Result checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
+ public static @Result
+ int checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
if (bitmapRegionDecoder == null) {
Timber.e("Expected bitmapRegionDecoder was null");
- return Result.IMAGE_OK;
+ return IMAGE_OK;
}
int loadImageHeight = bitmapRegionDecoder.getHeight();
@@ -59,10 +86,10 @@ public class ImageUtils {
Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect,null);
if (checkIfImageIsDark(processBitmap)) {
- return Result.IMAGE_DARK;
+ return IMAGE_DARK;
}
- return Result.IMAGE_OK;
+ return IMAGE_OK;
}
/**
@@ -132,8 +159,9 @@ public class ImageUtils {
/**
* 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
+ *
+ * @param context context
+ * @param imageUrl Url of the image
*/
public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) {
Timber.d("Trying to set wallpaper from url %s", imageUrl.toString());
@@ -150,7 +178,7 @@ public class ImageUtils {
@Override
public void onNewResultImpl(@Nullable Bitmap bitmap) {
- if (dataSource.isFinished() && bitmap != null){
+ if (dataSource.isFinished() && bitmap != null) {
Timber.d("Bitmap loaded from url %s", imageUrl.toString());
setWallpaper(context, Bitmap.createBitmap(bitmap));
dataSource.close();
@@ -173,7 +201,29 @@ public class ImageUtils {
wallpaperManager.setBitmap(bitmap);
ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_successfully));
} catch (IOException e) {
- Timber.e(e,"Error setting wallpaper");
+ Timber.e(e, "Error setting wallpaper");
}
}
+
+ public static String getErrorMessageForResult(Context context, @Result int result) {
+ String errorMessage;
+ if (result == ImageUtils.IMAGE_DARK)
+ errorMessage = context.getString(R.string.upload_image_problem_dark);
+ else if (result == ImageUtils.IMAGE_BLURRY)
+ errorMessage = context.getString(R.string.upload_image_problem_blurry);
+ else if (result == ImageUtils.IMAGE_DUPLICATE)
+ errorMessage = context.getString(R.string.upload_image_problem_duplicate);
+ else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY))
+ errorMessage = context.getString(R.string.upload_image_problem_dark_blurry);
+ else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_DUPLICATE))
+ errorMessage = context.getString(R.string.upload_image_problem_dark_duplicate);
+ else if (result == (ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE))
+ errorMessage = context.getString(R.string.upload_image_problem_blurry_duplicate);
+ else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE))
+ errorMessage = context.getString(R.string.upload_image_problem_dark_blurry_duplicate);
+ else
+ return "";
+
+ return errorMessage;
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java
index 3429ef403..d76e2ff5f 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java
@@ -9,8 +9,14 @@ import android.support.v4.content.ContextCompat;
import fr.free.nrw.commons.CommonsApplication;
+
public class PermissionUtils {
+ public static final int CAMERA_PERMISSION_FROM_CONTRIBUTION_LIST = 100;
+ public static final int GALLERY_PERMISSION_FROM_CONTRIBUTION_LIST = 101;
+ public static final int CAMERA_PERMISSION_FROM_NEARBY_MAP = 102;
+ public static final int GALLERY_PERMISSION_FROM_NEARBY_MAP = 103;
+
/**
* This method can be used by any activity which requires a permission which has been blocked(marked never ask again by the user)
It open the app settings from where the user can manually give us the required permission.
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java
index e409b856d..1b321ec07 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java
@@ -44,4 +44,4 @@ public class StringSortingUtils {
double distanceBetweenStrings = new Levenshtein().distance(longer, shorter);
return (longerLength - distanceBetweenStrings) / (double) longerLength;
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java
index 0eb8216e4..0f93e65ef 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java
@@ -12,4 +12,8 @@ public class StringUtils {
return Html.fromHtml(source).toString();
}
}
+
+ public static boolean isNullOrWhiteSpace(String value) {
+ return value == null || value.trim().isEmpty();
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/UriDeserializer.java b/app/src/main/java/fr/free/nrw/commons/utils/UriDeserializer.java
index ad37b27fc..9be11d295 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/UriDeserializer.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/UriDeserializer.java
@@ -15,4 +15,4 @@ public class UriDeserializer implements JsonDeserializer {
final JsonDeserializationContext context) throws JsonParseException {
return Uri.parse(src.getAsString());
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java
index af71e826d..aef3dddb0 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java
@@ -2,10 +2,13 @@ package fr.free.nrw.commons.utils;
import android.app.Activity;
import android.content.Context;
+import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.view.Display;
import android.view.View;
+import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
+import android.widget.PopupWindow;
import android.widget.Toast;
public class ViewUtil {
@@ -15,7 +18,7 @@ public class ViewUtil {
public static final String SHOWCASE_VIEW_ID_3 = "SHOWCASE_VIEW_ID_3";
public static void showSnackbar(View view, int messageResourceId) {
- if(view.getContext() == null) {
+ if (view.getContext() == null) {
return;
}
@@ -30,9 +33,33 @@ public class ViewUtil {
ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_LONG).show());
}
+ public static void showLongToast(Context context, @StringRes int stringResourceId) {
+ if (context == null) {
+ return;
+ }
+
+ ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_LONG).show());
+ }
+
+ public static void showShortToast(Context context, String text) {
+ if (context == null) {
+ return;
+ }
+
+ ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_SHORT).show());
+ }
+
+ public static void showShortToast(Context context, @StringRes int stringResourceId) {
+ if (context == null) {
+ return;
+ }
+
+ ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_SHORT).show());
+ }
+
public static boolean isPortrait(Context context) {
Display orientation = ((Activity)context).getWindowManager().getDefaultDisplay();
- if(orientation.getWidth() < orientation.getHeight()){
+ if (orientation.getWidth() < orientation.getHeight()){
return true;
} else {
return false;
@@ -49,4 +76,15 @@ public class ViewUtil {
}
}
+ public static void displayPopupWindow(View anchorView, Context context, View popupWindowLayout, String text) {
+
+ PopupWindow popup = new PopupWindow(context);
+ popup.setContentView(popupWindowLayout);
+ // Closes the popup window when touch outside of it - when looses focus
+ popup.setOutsideTouchable(true);
+ popup.setFocusable(true);
+ // Show anchored to button
+ popup.showAsDropDown(anchorView);
+ }
+
}
diff --git a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java
index 22bea67b3..fc87f7abb 100644
--- a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java
+++ b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java
@@ -128,4 +128,4 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
public void onDisabled(Context context) {
// Enter relevant functionality for when the last widget is disabled
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java
index 407c24711..a97d0eded 100644
--- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java
+++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java
@@ -17,4 +17,4 @@ public class WikidataEditListenerImpl extends WikidataEditListener {
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
index 0a7c0b8ec..6b0c52cb3 100644
--- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java
+++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java
@@ -48,12 +48,12 @@ public class WikidataEditService {
* @param fileName
*/
public void createClaimWithLogging(String wikidataEntityId, String fileName) {
- if(wikidataEntityId == null) {
+ if (wikidataEntityId == null) {
Timber.d("Skipping creation of claim as Wikidata entity ID is null");
return;
}
- if(fileName == null) {
+ if (fileName == null) {
Timber.d("Skipping creation of claim as fileName entity ID is null");
return;
}
diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-hdpi/ic_arrow_back_white.png
new file mode 100644
index 000000000..0b78dc8c4
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_arrow_back_white.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art.png
new file mode 100644
index 000000000..d974d19d5
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art_dot.png
new file mode 100644
index 000000000..b03310f55
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art_dot.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-mdpi/ic_arrow_back_white.png
new file mode 100644
index 000000000..8d1142f31
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_arrow_back_white.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art.png
new file mode 100644
index 000000000..f9cb42735
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art_dot.png
new file mode 100644
index 000000000..ebb4f86c0
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art_dot.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-xhdpi/ic_arrow_back_white.png
new file mode 100644
index 000000000..f782543d3
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_arrow_back_white.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art.png
new file mode 100644
index 000000000..62369be0a
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art_dot.png
new file mode 100644
index 000000000..c6be29890
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art_dot.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png
new file mode 100644
index 000000000..2fe75945f
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art.png
new file mode 100644
index 000000000..907c922fc
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art_dot.png
new file mode 100644
index 000000000..239de1d73
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art_dot.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white.png
new file mode 100644
index 000000000..575d5075c
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art.png
new file mode 100644
index 000000000..88ba092c1
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art_dot.png
new file mode 100644
index 000000000..c5a5e3f1f
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art_dot.png differ
diff --git a/app/src/main/res/drawable/ic_error_red_24dp.xml b/app/src/main/res/drawable/ic_error_red_24dp.xml
new file mode 100644
index 000000000..e1569395b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_error_red_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_expand_less_black_24dp.xml b/app/src/main/res/drawable/ic_expand_less_black_24dp.xml
new file mode 100644
index 000000000..3afdf9682
--- /dev/null
+++ b/app/src/main/res/drawable/ic_expand_less_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_expand_less_white_24dp.xml b/app/src/main/res/drawable/ic_expand_less_white_24dp.xml
new file mode 100644
index 000000000..d58421a2f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_expand_less_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_expand_more_black_24dp.xml b/app/src/main/res/drawable/ic_expand_more_black_24dp.xml
new file mode 100644
index 000000000..8d57dbc10
--- /dev/null
+++ b/app/src/main/res/drawable/ic_expand_more_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_expand_more_white_24dp.xml b/app/src/main/res/drawable/ic_expand_more_white_24dp.xml
new file mode 100644
index 000000000..fd3ce4a46
--- /dev/null
+++ b/app/src/main/res/drawable/ic_expand_more_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_location_white_24dp.xml b/app/src/main/res/drawable/ic_location_white_24dp.xml
new file mode 100644
index 000000000..aea56cb5a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_location_white_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_notifications_active_white_24dp.xml b/app/src/main/res/drawable/ic_notifications_active_white_24dp.xml
new file mode 100644
index 000000000..5d6921643
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notifications_active_white_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_notifications_white_24dp.xml b/app/src/main/res/drawable/ic_notifications_white_24dp.xml
new file mode 100644
index 000000000..120895c4f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notifications_white_24dp.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/app/src/main/res/layout/activity_contributions.xml b/app/src/main/res/layout/activity_contributions.xml
index 51a48f0a5..2e6e92728 100644
--- a/app/src/main/res/layout/activity_contributions.xml
+++ b/app/src/main/res/layout/activity_contributions.xml
@@ -1,34 +1,51 @@
+ android:layout_height="match_parent"
+ android:background="?attr/contributionsListBackground"
+ >
+ android:layout_height="match_parent"
+ android:gravity="center_horizontal"
+ >
+
+
+ android:layout_below="@id/tab_layout">
-
+ android:layout_height="match_parent" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_upload_bottom_card.xml b/app/src/main/res/layout/activity_upload_bottom_card.xml
new file mode 100644
index 000000000..273835b5d
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload_bottom_card.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upload_categories.xml b/app/src/main/res/layout/activity_upload_categories.xml
new file mode 100644
index 000000000..b3ae7c8a2
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload_categories.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upload_license.xml b/app/src/main/res/layout/activity_upload_license.xml
new file mode 100644
index 000000000..0f3391128
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload_license.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upload_please_wait.xml b/app/src/main/res/layout/activity_upload_please_wait.xml
new file mode 100644
index 000000000..e74d576e4
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload_please_wait.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upload_right_card.xml b/app/src/main/res/layout/activity_upload_right_card.xml
new file mode 100644
index 000000000..56fb96880
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload_right_card.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_upload_top_card.xml b/app/src/main/res/layout/activity_upload_top_card.xml
new file mode 100644
index 000000000..4e93d2a31
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload_top_card.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/custom_nearby_tab_layout.xml b/app/src/main/res/layout/custom_nearby_tab_layout.xml
new file mode 100644
index 000000000..7f6ca0f08
--- /dev/null
+++ b/app/src/main/res/layout/custom_nearby_tab_layout.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_categorization.xml b/app/src/main/res/layout/fragment_categorization.xml
deleted file mode 100644
index 58a768094..000000000
--- a/app/src/main/res/layout/fragment_categorization.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_contributions.xml b/app/src/main/res/layout/fragment_contributions.xml
index a016d752c..dd1959178 100644
--- a/app/src/main/res/layout/fragment_contributions.xml
+++ b/app/src/main/res/layout/fragment_contributions.xml
@@ -1,51 +1,22 @@
-
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical">
-
+
-
+
+
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_contributions_list.xml b/app/src/main/res/layout/fragment_contributions_list.xml
new file mode 100644
index 000000000..719283336
--- /dev/null
+++ b/app/src/main/res/layout/fragment_contributions_list.xml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_nearby.xml b/app/src/main/res/layout/fragment_nearby.xml
index ef580fe99..4269f135b 100644
--- a/app/src/main/res/layout/fragment_nearby.xml
+++ b/app/src/main/res/layout/fragment_nearby.xml
@@ -1,13 +1,161 @@
-
-
-
+
+ android:layout_height="match_parent">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/app/src/main/res/layout/fragment_nearby_list.xml b/app/src/main/res/layout/fragment_nearby_list.xml
new file mode 100644
index 000000000..ef580fe99
--- /dev/null
+++ b/app/src/main/res/layout/fragment_nearby_list.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_single_upload.xml b/app/src/main/res/layout/fragment_single_upload.xml
index 196760bb0..d5cba971d 100644
--- a/app/src/main/res/layout/fragment_single_upload.xml
+++ b/app/src/main/res/layout/fragment_single_upload.xml
@@ -6,13 +6,13 @@
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"
+ android:nestedScrollingEnabled="false"
android:paddingBottom="@dimen/small_gap"
android:paddingEnd="@dimen/standard_gap"
android:paddingLeft="@dimen/standard_gap"
android:paddingRight="@dimen/standard_gap"
android:paddingStart="@dimen/standard_gap"
android:paddingTop="@dimen/small_gap"
- android:nestedScrollingEnabled="false"
android:theme="@style/DarkAppTheme">
-
-
-
-
+ android:orientation="vertical">
+
+
+
+
+
+
+
+
+ android:gravity="center"
+ android:text="@string/share_license_summary"
+ android:textColorLink="@color/button_blue" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_categories_item.xml b/app/src/main/res/layout/layout_categories_item.xml
index d301e9098..6fd154344 100644
--- a/app/src/main/res/layout/layout_categories_item.xml
+++ b/app/src/main/res/layout/layout_categories_item.xml
@@ -7,6 +7,6 @@
android:checked="false"
android:gravity="center_vertical"
android:padding="@dimen/tiny_gap"
- android:theme="@style/DarkAppTheme">
+ android:textColor="@color/primaryDarkColor">
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_upload_categories_item.xml b/app/src/main/res/layout/layout_upload_categories_item.xml
new file mode 100644
index 000000000..7379fb199
--- /dev/null
+++ b/app/src/main/res/layout/layout_upload_categories_item.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/nearby_card_view.xml b/app/src/main/res/layout/nearby_card_view.xml
new file mode 100644
index 000000000..fe52e219a
--- /dev/null
+++ b/app/src/main/res/layout/nearby_card_view.xml
@@ -0,0 +1,96 @@
+
+
+
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/nearby_info_popup_layout.xml b/app/src/main/res/layout/nearby_info_popup_layout.xml
new file mode 100644
index 000000000..04c1a2d54
--- /dev/null
+++ b/app/src/main/res/layout/nearby_info_popup_layout.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/row_item_description.xml b/app/src/main/res/layout/row_item_description.xml
index 8b49f0902..f626abcfa 100644
--- a/app/src/main/res/layout/row_item_description.xml
+++ b/app/src/main/res/layout/row_item_description.xml
@@ -1,36 +1,35 @@
-
-
-
+ android:orientation="horizontal"
+ android:weightSum="8">
-
+
-
-
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/row_item_title.xml b/app/src/main/res/layout/row_item_title.xml
new file mode 100644
index 000000000..3a2262067
--- /dev/null
+++ b/app/src/main/res/layout/row_item_title.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/app/src/main/res/menu/contribution_activity_notification_menu.xml b/app/src/main/res/menu/contribution_activity_notification_menu.xml
new file mode 100644
index 000000000..b480d917b
--- /dev/null
+++ b/app/src/main/res/menu/contribution_activity_notification_menu.xml
@@ -0,0 +1,14 @@
+
diff --git a/app/src/main/res/menu/drawer.xml b/app/src/main/res/menu/drawer.xml
index 2adccb8e2..70ac42550 100644
--- a/app/src/main/res/menu/drawer.xml
+++ b/app/src/main/res/menu/drawer.xml
@@ -11,11 +11,6 @@
android:id="@+id/action_home"
android:icon="@drawable/ic_home_black_24dp"
android:title="@string/navigation_item_home" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 17febe602..7a12bd3e9 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -240,8 +240,8 @@
خطأ أثناء تخزين الصور
عنوان وصفي فريد للملف، والذي سيكون بمثابة اسم الملف، يمكنك استخدام لغة واضحة مع مسافات، لا تقم بتضمين امتداد الملف
يُرجَى وصف الوسائط قدر الإمكان: أين تم التقاطها؟ ما تظهر؟ ما هو السياق؟ يُرجَى وصف الأشياء أو الأشخاص، اكشف المعلومات التي لا يمكن تخمينها بسهولة، على سبيل المثال الوقت في اليوم إذا كان منظرا طبيعيا، إذا أظهرت الوسائط شيئا غير عادي، فيُرجَى توضيح ما يجعله غير عادي.
- هذه الصورة مظلمة للغاية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا كومنز للصور ذات القيمة الموسوعية فقط.
- هذه الصورة ضبابية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا كومنز للصور ذات القيمة الموسوعية فقط.
+ هذه الصورة مظلمة للغاية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا كومنز للصور ذات القيمة الموسوعية فقط.
+ هذه الصورة ضبابية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا كومنز للصور ذات القيمة الموسوعية فقط.
إعطاء السماح
استخدم تخزينا خارجيا
احفظ الصور الملتقطة بالكاميرا داخل التطبيق على جهازك
diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml
index 38526b387..d8d36226c 100644
--- a/app/src/main/res/values-ast/strings.xml
+++ b/app/src/main/res/values-ast/strings.xml
@@ -168,7 +168,7 @@
Llugares cercanos
Nun s\'alcontraron llugares cercanos
Avisu
- Esti ficheru yá esiste\'n Commons. ¿Confirmes que quies siguir?
+ Esti ficheru yá esiste\'n Commons. ¿Confirmes que quies siguir?
Sí
Non
Títulu
@@ -230,8 +230,8 @@
Error al poner les fotos na caché
Un títulu descriptivu únicu pal ficheru, que sirvirá para da-y nome al mesmu. Puede usase llinguaxe normal con espacios. Nun incluyas la estensión del ficheru
Por favor, describi l\'elementu multimedia tantu como sía posible: ¿ónde se tomó?, ¿qué amuesa?, ¿cuál ye\'l contestu? Por favor, describi los oxetos o persones. Revela la información que nun pueda aldovinase de mou cenciellu, por casu el momentu del día si ye un paisaxe. Si\'l mediu amuesa daqué desacostumao, esplica qué lo fai raro.
- Esta imaxe ye escura enforma, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.
- Esta imaxe ta borrosa, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.
+ Esta imaxe ye escura enforma, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.
+ Esta imaxe ta borrosa, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.
Dar permisu
Usar almacenamientu esternu
Guardar nel preséu les imaxes tomaes cola cámara de la app
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 cd36cebd4..83bdd49f9 100644
--- a/app/src/main/res/values-b+sr+Latn/strings.xml
+++ b/app/src/main/res/values-b+sr+Latn/strings.xml
@@ -169,7 +169,7 @@
Mesta u blizini
Nisu pronađena obližnja mesta
Upozorenje
- Ova datoteka je već dostupna na Ostavi. Da li ste sigurni da želite da nastavite?
+ Ova datoteka je već dostupna na Ostavi. Da li ste sigurni da želite da nastavite?
Da
Ne
Naslov
diff --git a/app/src/main/res/values-ba/strings.xml b/app/src/main/res/values-ba/strings.xml
index 53916bbe2..58ae9cd4b 100644
--- a/app/src/main/res/values-ba/strings.xml
+++ b/app/src/main/res/values-ba/strings.xml
@@ -157,7 +157,7 @@
Мотлаҡ булмаған рөхсәт: категория тәҡдиме өсөн ошо урынды алыу
Яҡындағы урындар
Яҡындағы урындар табылманы
- Был файл Викискладта бар. Дауам итергә ризаһыңмы?
+ Был файл Викискладта бар. Дауам итергә ризаһыңмы?
Атама
Мәғлүмәт йөрөтөүсенең атамаһы
Мәғлүмәт йөрөтөүсене һүрәтләү ошонда яҙыла.Уның ярайһы уҡ оҙон, хатта бер-нисә юлға һуҙылып китеүе лә бар. Шулай булһа ла ул бик матур күренер тип уйлайбыҙ.
@@ -204,8 +204,8 @@
Рәсемде кэшлағандағы хата
Файлдың исеме булараҡ һаҡланасаҡ үҙенсәлекле һәртәләү. Тәбиғи телегеҙҙе, һүҙҙәр араһын айырып, ҡулланырға була. Зинһар, файл киңәйтеүҙәрен күрһәтмәгеҙ.
Зинһар, тейәләсәк файлды тәфсирләп һүрәтлә:ҡайҙа төшөрөлгән? нимә һәрәтләнә? һүрәт нимәне аңлата? Рәсемдәге кешеләр йәки объекттарҙы ла һүрәтлә. Һүрәткә ҡарап ҡына белеп булмаған мәғлүмәттәрҙе өҫтә: мәҫәлән, тәүлектең ниндәй мәлендә, ҡасан төшөрөлгән был файл. Әгәр ғәҙәти булмаған әйбер төшөрөлһә, уның нимәһе шаҡ ҡатырғанын аңлат.
- Был рәсем бик ҡараңғы күренә. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.
- Был рәсем асыҡ түгел. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.
+ Был рәсем бик ҡараңғы күренә. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.
+ Был рәсем асыҡ түгел. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.
Рөхсәт бирәм
Тышҡы һаҡлағысты ҡуллан
Ҡулайламаның камераһы ярҙамында төшөрөлгән һүрәттәрҙе һаҡлау
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 0c8b40906..b6295448b 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -76,7 +76,7 @@
Обновяване
Добре
Предупреждение
- Този файл вече съществува в Общомедия. Наистина ли искате да продължите?
+ Този файл вече съществува в Общомедия. Наистина ли искате да продължите?
Да
Не
Заглавие
diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml
index c4f886ca2..76bdae2c6 100644
--- a/app/src/main/res/values-bn/strings.xml
+++ b/app/src/main/res/values-bn/strings.xml
@@ -178,7 +178,7 @@
কাছাকাছি স্থান
কাছাকাছি কোন স্থান পাওয়া যায়নি
সতর্কীকরণ
- এই ফাইলটি ইতিমধ্যে কমন্সে বিদ্যমান। আপনি কি নিশ্চিত আপনি সামনে এগুতে চান?
+ এই ফাইলটি ইতিমধ্যে কমন্সে বিদ্যমান। আপনি কি নিশ্চিত আপনি সামনে এগুতে চান?
হ্যাঁ
না
শিরোনাম
@@ -239,8 +239,8 @@
ছবি আনার সময় ত্রুটি
ফাইলের একটি স্বতন্ত্র বর্ণনামূলক নাম যা ফাইলের নাম হিসাবে কাজ করবে। অাপনি সাধারণ ভাষা ব্যবহার করতে পারেন শূন্যস্থানসহ। ফাইলের এক্সটেনশন যুক্ত করবেন না।
যতটা সম্ভব মিডিয়াটি বর্ণনা করুন: এটি কোথায় ধারণ করা হয়েছিল? এটি কি প্রদর্শন করে? এটির প্রসঙ্গ কি? ধারণকৃত বস্তু অথবা ব্যক্তির বর্ণনা করুন। সহজে অনুমান করা যায়না সেরকম তথ্য উদঘাটন করুন, উদাহরণস্বরূপ, যদি ল্যান্ডস্কেপ হয় তাহলে দিবসকালের সময় দিন।
- এই ছবিটি খুবই অন্ধকারময়, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।
- এই ছবিটি অস্পষ্ট, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।
+ এই ছবিটি খুবই অন্ধকারময়, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।
+ এই ছবিটি অস্পষ্ট, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।
অনুমতি দিন
বাহ্যিক সঞ্চয়স্থান ব্যবহার করুন
অাপনার ডিভাইসের নিজস্ব ক্যামেরায় ধারণকৃত ছবি সংরক্ষণ করুন
@@ -294,4 +294,10 @@
নির্বাচিত ছবি
স্তর
কমন্স বিজ্ঞপ্তি
+ বুকমার্ক
+ বুকমার্ক
+ চিত্র
+ অবস্থান
+ বুকমার্ক
+ বুকমার্ক
diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml
index 90ede0b76..5135a7236 100644
--- a/app/src/main/res/values-br/strings.xml
+++ b/app/src/main/res/values-br/strings.xml
@@ -8,8 +8,11 @@
* Y-M D
-->
+ Ergerzhout
+ Ergerzhout
Neuz
Hollek
+ Sonjoù
Lec\'hiadur
Commons
•
@@ -25,7 +28,7 @@
Kevreet oc\'h !
Kudenn gevreañ !
N\'eo ket bet kavet ar restr. Klask gant unan all.
- Dilesadur c\'hwitet!
+ Dilesadur c\'hwitet, kevreit en-dro mar plij
Kroget da enporzhiañ!
%1$s bet enporzhiet !
Pouezit evit gwelet hoc\'h enporzhiadenn
@@ -53,6 +56,7 @@
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 hag 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.
@@ -64,6 +68,7 @@
Klask rummadoù
Enrollañ
Freskaat
+ Roll
Diwerededkaet eo ar GPS war hoc\'h ardivink.\nHa c\'hoant ho peus da weredekaat anezhañ ?
Gweredekaat ar GPS
Enporzhiadenn ebet c\'hoazh !
@@ -85,6 +90,7 @@
Rummadoù
Arventennoù
En em enskrivañ
+ Rummad
Diwar-benn
Meziant frank a wirioù embannet dindan <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">an Aotre-implijout Apache v2</a>. Merkoù kenwerzhel eus Diazezadur Wikimedia eo Wikimedia Commons hag e logoioù. Gallout a reont bezañ implijet gant aotre an Diazezadur. N\'omp nag aprouet gant Diazezadur Wiikimedia na stag outañ.
Krouiñ ur <a href=\"https://github.com/commons-app/apps-android-commons/issues\">gemennadenn GitHub nevez</a> evit kelaouiñ a-zivout un draen bennak pe bet kinnigoù.
@@ -146,26 +152,28 @@
Diarbenn an dafar gwarezet a vez kavet ganeoc\'h war ar Genrouedad, hag ivez ar skeudennoù skritelloù, ar goloioù levrioù ha kement zo…
Ha soñjal a rit eo mat ?
Ya !
+ <u>Gouzout hiroc\'h</u>
Rummadoù
O kargañ…
Hini ebet diuzet
Deskrivadur ebet
Aotre-implijout dizanv
Freskaat
- Aotre rekis : lenn ur stokañ diavaez. Hep se, n\'hall ket an arload mont en-dro.
- Aotre ret ; skrivañ war al lec\'h stokañ diavaez. Ne c\'hall ket an arload mont en-dro hep an dra-se.
+ Aotre rekis : lenn ur stokañ diavaez. N\'hall ket an arload tizhout ho palier hep an dra-se.
+ Aotre ret ; skrivañ war al lec\'h stokañ diavaez. Ne c\'hall ket an arload tizhout ho kamera hep an dra-se.
Aotre diret : kaout al lec\'hiadur red evit kinnig rummadoù
Mat eo
Lec\'hioù nes
N\'eus bet kavet netra tostik
Diwallit
- Emañ ar restr-mañ war Commons c\'hoazh. Ha sur oc\'h e fell deoc\'h kenderc\'hel ?
+ Emañ ar restr-mañ war Commons c\'hoazh. Ha sur oc\'h e fell deoc\'h kenderc\'hel ?
Ya
Ket
Titl
Titl ar media
Deskrivadur
Amañ e lakaer titl ar media. Gallout a ra bezañ hir-mat ha mont dre meur a linenn. Spi hon eus e vo bravik an disoc\'h koulskoude.
+ Aozer
Deiziad enporzhiañ
Aotre-implijout
Daveennoù
@@ -211,12 +219,40 @@
N\'eus bet kavet deskrivadur ebet
Pajennad restroù Commons
Elfenn Wikidata
+ Pennad Wikipedia
Reiñ ar gwir
Implijout ar stokañ diavaez
Enrollit ar skeudennoù tennet gant luc\'hskeudennerez ho penveg
Kevreit ouzh ho kont
+ Fazi ! N\'eo ket bet kavet an URL
+ Kinnig evit lemel
+ Meneget eo bet ar skeudenn evit lemel.
+ Diskwel er merdeer
+ Lezel a-gostez
+ Kevreañ
N\'eo ket cheñchet al lec\'hiadur.
Kaout urzhioù
Lenn ar pennad
+ WIKIDATA
+ WIKIPEDIA
+ <u>FAG</u>
+ Lezel an tutorial a-gostez
+ <u>Treiñ</u>
+ Yezhoù
+ Kenderc\'hel
+ Nullañ
+ Klask en-dro
+ Mat eo !
+ N\'eus bet kavet skeudenn ebet !
+ Ur fazi zo c\'hoarvezet p\'eo bet karget ar skeudennoù.
+ Enporzhiet gant:%1$s
+ Stanket oc\'h bet ha ne c\'hallit ket mui kemmañ war Commons
Skeudenn an deiz
+ Skeudenn an deiz
+ Klask
+ Klask e Commons
+ N\'eus bet kavet skeudenn ebet hag a sell ouzh 1$s
+ Klask
+ Enklaskoù nevez :
+ Rekedoù enklask diwezhañ
diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml
index 1a8253fa3..1c456a749 100644
--- a/app/src/main/res/values-bs/strings.xml
+++ b/app/src/main/res/values-bs/strings.xml
@@ -142,7 +142,7 @@
Mjesta u blizini
Nema okolnih mjesta
Upozorenje
- Ova datoteka već postoji na Commonsu. Jeste li sigurni da želite nastaviti?
+ Ova datoteka već postoji na Commonsu. Jeste li sigurni da želite nastaviti?
Da
Ne
Naslov
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index d0027df81..951fbce09 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -139,7 +139,7 @@
Llocs propers
No s\'han trobat llocs propers
Avís
- El fitxer ja existeix a Commons. Segur que voleu procedir?
+ El fitxer ja existeix a Commons. Segur que voleu procedir?
Sí
No
Títol
diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml
index 5f892b17f..36697a1a1 100644
--- a/app/src/main/res/values-ckb/strings.xml
+++ b/app/src/main/res/values-ckb/strings.xml
@@ -41,7 +41,7 @@
باشە
شوێنە نزیکەکان
ئاگاداری
- ئەم پەڕگەیە لەسەر کۆمنز ھەیە. دڵنیایت کە دەتەوێت بەردەوام بیت؟
+ ئەم پەڕگەیە لەسەر کۆمنز ھەیە. دڵنیایت کە دەتەوێت بەردەوام بیت؟
بەڵێ
نەخێر
ناونیشان
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 635c85300..cecfce340 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -167,7 +167,7 @@
Vyhněte se dílům chráněným autorským právem, která jste našli na internetu, jako i obrázkům plakátů, přebalů knih apod.
Myslíte, že rozumíte?
Ano!
-
+ <u>Více informací</u>
Kategorie
Načítá se…
Nevybrány
@@ -181,7 +181,7 @@
Místa v okolí
Poblíž nebylo nic nalezeno
Upozornění
- Tento soubor již na Commons existuje. Jste si jist, že chcete pokračovat?
+ Tento soubor již na Commons existuje. Jste si jist, že chcete pokračovat?
Ano
Ne
Název
@@ -243,14 +243,14 @@
Chyba při meziukládání obrázků
Unikátní a popisný název pro daný soubor, který bude sloužit jako název souboru. Můžete použít běžný psaný jazyk s mezerami; nezahrnujte koncovku souboru.
Popište prosím obrázek, jak jen to je možné: Kde byl pořízen? Co znázorňuje? Jaký je kontext obrázku? Popisujte prosím významné předměty nebo osoby na obrázku a nezapomeňte na informace, které není možné snadno odhadnout ze samotného obrázku, jako je například denní doba, pokud jde o krajinu. Pokud je na obrázku něco neobvyklého, popište, co to dělá neobvyklým.
- Tento obrázek je příliš tmavý, jste si jist/a, že ho chcete nahrát? Wikimedia Commons slouží jenom pro obrázky s encyklopedickou hodnotou.
- Tento obrázek je rozmazaný, jste si jist/a, že ho chcete nahrát? Wikimedia Commons slouží jenom pro obrázky s encyklopedickou hodnotou.
+ Tento obrázek je příliš tmavý, jste si jist/a, že ho chcete nahrát? Wikimedia Commons slouží jenom pro obrázky s encyklopedickou hodnotou.
+ Tento obrázek je rozmazaný, jste si jist/a, že ho chcete nahrát? Wikimedia Commons slouží jenom pro obrázky s encyklopedickou hodnotou.
Dát povolení
Použít externí úložiště
Uložit obrázky pořízené fotoaparátem, jenž je součástí této aplikace
Přihlásit se k účtu
Odeslat log
- Odeslat log vývojářům e-mailem
+ Odeslat log vývojářům e-mailem za účelem odstranění problémů s aplikací. Poznámka: Logy mohou obsahovat identifikační údaje.
Nebyl nalezen žádný webový prohlížeč k otevření URL
Chyba! URL nenalezeno
Navrhnout na smazání
@@ -262,6 +262,8 @@
Opravdu chcete přeskočit přihlášení?
Nebude možné nahrávat obrázky.
K použití této funkce se musíte přihlásit
+ Zkopírovat wikitext do schránky
+ Wikitext byl zkopírován do schránky
Vaše umístění se nezměnilo.
Umístění není dostupné.
Vyžadováno povolení k zobrazení seznamu blízkých míst
@@ -282,7 +284,7 @@
Internet je nedostupný
Internet je dostupný
Při načítání oznámení došlo k chybě
- Nebyly nalezeny žádné oznámení
+ Nebyla nalezena žádné oznámení
<u>Přeložit</u>
Jazyky
Vyberte jazyk, pro který chcete odeslat překlady
@@ -351,5 +353,18 @@
Počet obrázků, které jste nahrál/a na Commons, ať už jakýmkoliv způsobem
Procentuální množství obrázků, které jste nahrál/a na Commons a nebyly smazány
Počet obrázků, které jste nahrál/a na Commons a jsou použity v projektech Wikimedia
+ Chyba!
Notifikace z Commons
+ Povolení k ukládání
+ Potřebujeme povolení k přístupu k externí paměti vašeho zařízení, abychom mohli nahrávat obrázky.
+ Záložky
+ Záložky
+ Obrázky
+ Místa
+ Přidat/Odstranit ze záložek
+ Záložky
+ Nemáte zatím žádné záložky
+ Záložky
+ Záznam logu započal. Prosím RESTARTUJTE aplikaci, proveďte akci, kterou si přejete zaznamenat, a potom znovu odešlete log.
+ Vítejte na Commons!\n\nNahrajte svá první média pomocí tlačítek fotoaparátu nebo galerie nahoře.
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index a065b878f..af29073ab 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -174,7 +174,7 @@
Steder i nærheden
Ingen steder i nærheden fundet
Advarsel
- Denne fil findes allerede på Commons. Er du sikker på, at du ønsker at fortsætte?
+ Denne fil findes allerede på Commons. Er du sikker på, at du ønsker at fortsætte?
Ja
Nej
Titel
@@ -235,8 +235,8 @@
Fejl under mellemlagring af billeder
En unik beskrivelse for filen, som vil fungere som et filnavn. Du kan bruge normalt sprog med mellemrum. Udelad filendelsen.
Beskriv mediet så godt som muligt: Hvor blev det taget? Hvad viser det? Hvad er konteksten? Beskriv objekterne eller personerne. Giv information som ikke nemt kan gættes, for eksempel hvornår på dagen billedet blev taget, om det er et landskabsbillede. Om billedet viser noget usædvanligt, forklar hvad som gør det usædvanlig.
- Billedet er for mørkt. Er du sikker på, at du ønsker at overføre det? Wikimedia Commons er kun for billeder encyklopædisk værdi.
- Dette billede er sløret. Er du sikker på, at du ønsker at overføre det? Wikimedia Commons er kun for billeder med encyklopædisk værdi.
+ Billedet er for mørkt. Er du sikker på, at du ønsker at overføre det? Wikimedia Commons er kun for billeder encyklopædisk værdi.
+ Dette billede er sløret. Er du sikker på, at du ønsker at overføre det? Wikimedia Commons er kun for billeder med encyklopædisk værdi.
Giv tilladelse
Brug eksternt lager
Gem billeder taget med din enheds program på kameraet
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index f4f078e31..77220b744 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -173,7 +173,7 @@
Orte in der Nähe
Keine Orte in der Nähe gefunden
Warnung
- Diese Datei ist bereits auf Commons vorhanden. Bist du sicher, dass du fortfahren möchtest?
+ Diese Datei ist bereits auf Commons vorhanden. Bist du sicher, dass du fortfahren möchtest?
Ja
Nein
Titel
@@ -235,8 +235,8 @@
Fehler beim Zwischenspeichern der Bilder
Ein eindeutiger beschreibender Titel für die Datei, der als Dateiname dient. Du kannst Klartext mit Leerzeichen verwenden. Gib nicht die Dateierweiterung mit an.
Bitte beschreibe das Medium so gut wie möglich: Wo wurde es aufgenommen? Was zeigt es? Was ist der Kontext? Bitte beschreibe die Objekte oder Personen. Zeige Informationen auf, die nicht einfach erraten werden können, zum Beispiel die Tageszeit, falls es eine Landschaft ist. Falls das Medium etwas Ungewöhnliches zeigt, erkläre bitte, was es ungewöhnlich macht.
- Dieses Bild ist zu dunkel. Bist du sicher, dass du es hochladen möchtest? Wikimedia Commons ist nur für Bilder mit enzyklopädischem Wert gedacht.
- Dieses Bild ist verschwommen. Bist du sicher, dass du es hochladen möchtest? Wikimedia Commons ist nur für Bilder mit enzyklopädischem Wert gedacht.
+ Dieses Bild ist zu dunkel. Bist du sicher, dass du es hochladen möchtest? Wikimedia Commons ist nur für Bilder mit enzyklopädischem Wert gedacht.
+ Dieses Bild ist verschwommen. Bist du sicher, dass du es hochladen möchtest? Wikimedia Commons ist nur für Bilder mit enzyklopädischem Wert gedacht.
Berechtigung geben
Externen Speicher verwenden
Mit der In-App-Kamera aufgenommene Bilder auf deinem Gerät speichern
diff --git a/app/src/main/res/values-diq/strings.xml b/app/src/main/res/values-diq/strings.xml
index a312fcc9d..d10389038 100644
--- a/app/src/main/res/values-diq/strings.xml
+++ b/app/src/main/res/values-diq/strings.xml
@@ -81,6 +81,7 @@
Kategoriyi
Sazi
Qeyd be
+ Kategoriye
Heq te cı
Qandê yew <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub-cıkewtış</a>ê neweyi rê rapor û teklifan bıaferne.
<u>Politikaya nımıtışi</u>
@@ -137,7 +138,11 @@
Keye
Bar ke
Veciyayış
+ Cı kewe
+ Zıwani
Bıtexelne
Anciya bıcerrebne
Mı fehm kerd!
+ Cıgeyrayış
+ Cıgeyrayış
diff --git a/app/src/main/res/values-dty/strings.xml b/app/src/main/res/values-dty/strings.xml
index 09c0426ab..94f6fa993 100644
--- a/app/src/main/res/values-dty/strings.xml
+++ b/app/src/main/res/values-dty/strings.xml
@@ -46,7 +46,7 @@
हुन्छ
नज्यूकाऽ ठउरअन
चेतावनी
- यो फाइल कमन्स मी पैली बठेइ छ। तम पक्का छऽ कि तम ऐतिर जान चाहन्छऽ?
+ यो फाइल कमन्स मी पैली बठेइ छ। तम पक्का छऽ कि तम ऐतिर जान चाहन्छऽ?
हो
नाइँ
शीर्षक
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 8928400b3..6228fff6b 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -176,7 +176,7 @@
Κοντινοί Τόποι
Δεν βρέθηκαν τόποι εδώ κοντά
Προειδοποίηση
- Αυτό το αρχείο υπάρχει ήδη στα Commons. Είστε σίγουρος ότι θέλετε να συνεχίσετε;
+ Αυτό το αρχείο υπάρχει ήδη στα Commons. Είστε σίγουρος ότι θέλετε να συνεχίσετε;
Ναι
Όχι
Τίτλος
@@ -238,8 +238,8 @@
Υπήρξε σφάλμα κατά την σκίαση εικόνων
Ένας μοναδικός τίτλος περιγραφής του φακέλλου, που θα χρησιμεύσει ως όνομα φακέλλου. Μπορείτε να χρησιμοποιήσετε τις ήδη υπάρχουσες γλώσσες με διαστήματα. Μην συμπεριλάβετε την επέκταση φακέλλου
\nΠαρακαλώ περιγράψετε τα μέσα το δυνατό περισσότερο : Πού οδηγήθηκε αυτό; Τι δείχνει; Ποιο είναι το περιεχόμενο του; Παρακαλώ περιγράψετε τα αντικείμενα ή τα πρόσωπα. Αποκαλύψετε πληροφορίες που δεν μπορούν εύκολο να μαντέψει κανείς, για παράδειγμα την ώρα εντός της ημέρας αν πρόκειται για τοπίο. Αν τα μέσα δείξουν κάτι ασύνηθες, παρακαλώ εξηγήστε τι το καθιστά μη συνηθισμένα.
- Αυτή η εικόνα είναι πολύ σκοτεινή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.
- Αυτή η εικόνα είναι θολή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.
+ Αυτή η εικόνα είναι πολύ σκοτεινή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.
+ Αυτή η εικόνα είναι θολή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.
Χορηγήστε άδεια
Χρησιμοποιήσετε την εξωτερική αποθήκευση
Αποθηκεύσετε εικόνες που παίρνονται στην κάμερα εφαρμογής στην συσκευή σας
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 0929b0c58..d3d4388a1 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -181,7 +181,7 @@
Lugares cercanos
No se encontraron lugares cercanos
Atención
- Este archivo ya existe en Commons. ¿Confirmas que quieres continuar?
+ Este archivo ya existe en Commons. ¿Confirmas que quieres continuar?
Sí
No
Título
@@ -243,8 +243,8 @@
Error al almacenar imágenes en la antememoria
Un título único descriptivo para el archivo, que servirá como un nombre de archivo. Puede usar un lenguaje claro con espacios. No incluya la extensión del archivo.
Por favor, describa el elemento multimedia tanto como sea posible: ¿dónde fue tomado?, ¿qué muestra?, ¿cuál es el contexto? Por favor, describa los objetos o personas. Ofrezca la información que no puede ser inferida tan fácilmente, por ejemplo el momento del día si es un paisaje. Si el medio muestra algo inusual, explique qué lo hace insual.
- Esta imagen es demasiado oscura. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.
- Esta imagen se ve borrosa. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.
+ Esta imagen es demasiado oscura. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.
+ Esta imagen se ve borrosa. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.
Otorgar permiso
Utilizar almacenamiento externo
Guardar en el dispositivo imágenes capturadas con la cámara de la aplicación
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index 1f7fb5347..eb964e1a2 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -172,7 +172,7 @@
Gertuko lekuak
Ez da hurbileko lekurik aurkitu
Oharra
- Fifxategia dagoeneko Commonsen existitzen da. Ziur zaude jarraitu nahi duzula?
+ Fifxategia dagoeneko Commonsen existitzen da. Ziur zaude jarraitu nahi duzula?
Bai
Ez
Izenburua
@@ -229,8 +229,8 @@
Argazkiak hartzerakoan sortutako akatsa
Fitxategi izenburu deskribatzaile bakarra, fitxategi-izen gisa balioko duena. Hizkuntza arrunta erabil dezakezu espazioekin. Ez sartu fitxategiaren luzapena.
Mesedez, deskribatu multimedia elementua ahal duzun gehien: non hartu zen? zer erakusten du? zein da bere testuingurua? Mesedez, objektuak eta pertsonak deskribatu. Eman asmatzeko erraza ez den informazioa, adibidez, paisaia bat izatekotan, eguneko zein orudtan hartu den. Multimediak zerbait berezia erakusten badu, mesedez azaldu zerk egiten duen berezia.
- Argazkia ilunegia da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.
- Argazkia lausoa da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.
+ Argazkia ilunegia da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.
+ Argazkia lausoa da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.
Baimena eman
Kanpo-biltegia erabili
Aplikazioaren kamerarekin ateratako argazkiak zure gailuan gorde
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 6eb562557..62313d209 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -2,7 +2,9 @@
+ Explorare
+ Explorare
Aspect
General
+ Feedback
Locație
Commons
•
Setări
+ Încarcă la Commons
Nume de utilizator
Parolă
+ Intrați în contul dumneavoastră Commons Beta
Autentificare
V-aţi uitat parola?
Înregistrare
@@ -86,11 +91,13 @@
Categorii
Setări
Înregistrare
+ Imagini recomandate
Categorie
Despre
Aplicația Wikimedia Commons este un software cu sursă deschisă creat și menținut de bursieri și voluntari ai comunității Wikimedia. Fundația Wikimedia nu este implicată în crearea, dezvoltarea sau mentenanța aplicației.
Creați un nou <a href=\"https://github.com/commons-app/apps-android-commons/issues\">tichet pe GitHub</a> pentru a raporta buguri sau sugestii.
- <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">Politica de confidențialitate</a>
+ <u>Politica de confidențialitate</u>
+ <u>Autori</u>
Despre
Trimitere reacții (prin e-mail)
Niciun client de email instalat
@@ -101,7 +108,8 @@
Revocare
Această imagine va fi licențiată sub %1$s
Descarcă
- Licență
+ Licență implicită
+ Folosiți titlul/descrierea anterioară
Obține în mod automat locația curentă
Mod de noapte
Folosește o temă întunecată
@@ -151,9 +159,16 @@
Titlul fișierului
Descriere
Descrierea fișierului vine aici. Aceasta poate fi destul de lungă și va trebuie distribuită pe mai multe linii. Sperăm totuși că arată bine.
+ Autor
+ Data încărcării
+ Licență
+ Coordonate
+ Nefurnizat
Deveniți beta-tester
Abonați-vă la canalul nostru beta pe Google Play și a obține acces rapid la noi caracteristici și corecții de erori
Code 2FA
+ Limită maximă
+ Nu pot afișa mai mult de 500
Logoul Commons
Site-ul Commons
Pagina de Facebook a Commons
@@ -180,4 +195,36 @@
Tutorial
Notificări
Element Wikidata
+ Vizualizează cu navigatorul
+ Omite
+ Autentificare
+ <u>Dă-ne o notă</u>
+ <u>FAQ</u>
+ Sari peste tutorial
+ Nu aveți internet
+ Aveți internet
+ <u>Tranduceți</u>
+ Limbi
+ Încărcată de: %1$s
+ Imaginea zilei
+ Imaginea zilei
+ Căutare
+ Căutare pe Commons
+ Căutare
+ Căutări recente:
+ Quiz
+ Întrebare
+ Rezultat
+ Continuați
+ Răspuns corect
+ Răspuns greșit
+ +Adaugă descriere
+ Semne de carte
+ Semne de carte
+ Imagini
+ Locuri
+ Adaugă/scoate din semnele de carte
+ Semne de carte
+ Nu ați adăugat niciun semn de carte
+ Semne de carte
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 7b05363ab..26e0285ba 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -186,7 +186,7 @@
Места поблизости
Места поблизости не найдены
Внимание
- Этот файл уже существует на Викискладе. Вы уверены, что хотите продолжить?
+ Этот файл уже существует на Викискладе. Вы уверены, что хотите продолжить?
Да
Нет
Название
@@ -248,8 +248,8 @@
Ошибка при кэшировании картинок
Уникальное описание, которое будет сохранено как имя файла. Вы можете использовать естественный язык, разделяя слова пробелами. Пожалуйста, не указывайте расширение файла.
Пожалуйста, подробно опишите загружаемый файл: где он был снят? что на нём изображено? каков его контекст? Пожалуйста опишите изображённых персон или объекты. Добавьте информацию, о которой нельзя легко догадаться, например, время суток, когда снимался файл. Если снято что-то необычное, постарайтесь пояснить, что именно в этом необычного.
- Это изображение слишком тёмное. Вы уверены, что хотите его загрузить? Викисклад подходит только для фотографий, имеющих энциклопедическую ценность.
- Это изображение размыто. Вы уверены, что хотите его загрузить? Викисклад подходит только для фотографий, имеющих энциклопедическую ценность.
+ Это изображение слишком тёмное. Вы уверены, что хотите его загрузить? Викисклад подходит только для фотографий, имеющих энциклопедическую ценность.
+ Это изображение размыто. Вы уверены, что хотите его загрузить? Викисклад подходит только для фотографий, имеющих энциклопедическую ценность.
Разрешить
Использовать внешнее хранилище
Сохранять изображения, сделанные с помощью встроенной камеры на устройстве
diff --git a/app/src/main/res/values-sd/strings.xml b/app/src/main/res/values-sd/strings.xml
index 5b07e722c..5660a7c89 100644
--- a/app/src/main/res/values-sd/strings.xml
+++ b/app/src/main/res/values-sd/strings.xml
@@ -161,7 +161,7 @@
ويجھڙائيءَ ۾ جڳھون
آپاس وارو جڳهه نه لڌو
چتاءُ
- فائل اڳ ئي العام ۾ موجود آھي. ڇا توھان کي پڪ آھي تہ توھان اڳتي وڌڻ ٿا چاھيو؟
+ فائل اڳ ئي العام ۾ موجود آھي. ڇا توھان کي پڪ آھي تہ توھان اڳتي وڌڻ ٿا چاھيو؟
ها
نہ
عنوان
diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml
index 6fb06002a..3b2fde0fb 100644
--- a/app/src/main/res/values-si/strings.xml
+++ b/app/src/main/res/values-si/strings.xml
@@ -131,7 +131,7 @@
අවට ස්ථාන
ළඟපාත තැන් කිසිවක් සොයා ගැනීමට නොහැකි විය
අනතුරු හැඟවීමයි
- මෙම ගොනුව දැනටමත් කොමන්ස් හි ඇත. ඔබට ඉදිරියට යෑම ගැන විශ්වාසද?
+ මෙම ගොනුව දැනටමත් කොමන්ස් හි ඇත. ඔබට ඉදිරියට යෑම ගැන විශ්වාසද?
ඔව්
නැත
මාතෘකාව
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index ae91a8d25..2b7190fa7 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -139,7 +139,7 @@
Miesta v okolí
V okolí sa nenašli žiadne miesta
Upozornenie
- Tento súbor už na Commons existuje. Ste si istí, že chcete pokračovať?
+ Tento súbor už na Commons existuje. Ste si istí, že chcete pokračovať?
Áno
Nie
Názov
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index 1964ed2e3..6e3c5811f 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -176,7 +176,7 @@
Места у близини
Нису пронађена оближња места
Упозорење
- Ова датотека је већ доступна на Остави. Да ли сте сигурни да желите да наставите?
+ Ова датотека је већ доступна на Остави. Да ли сте сигурни да желите да наставите?
Да
Не
Наслов
@@ -238,8 +238,8 @@
Грешка при кеширању слика
Јединствен описни наслов за датотеку, који ће бити име датотеке. Можете да користите обични језик са размацима. Не треба уносити екстензију датотеке
Молимо да опишете датотеку колико је то могуће: Где је направљена? Шта приказује? Шта је контекст? Опишите објекте и/или особе. Откријте информације које се не могу лако погодити, на пример доба дана ако је у питању пејзаж. Ако датотека приказује нешто необично, молимо да објасните шта је то чини необичном.
- Ова слика је претамна; да ли сте сигурни да је желите отпремити? Викимедијина остава је само за слике са енциклопедијском вредношћу.
- Ова слика је мутна; да ли сте сигурни да је желите отпремити? Викимедијина остава је само за слике са енциклопедијском вредношћу.
+ Ова слика је претамна; да ли сте сигурни да је желите отпремити? Викимедијина остава је само за слике са енциклопедијском вредношћу.
+ Ова слика је мутна; да ли сте сигурни да је желите отпремити? Викимедијина остава је само за слике са енциклопедијском вредношћу.
Давање дозволе
Употреба спољашње меморије
Спремање слика направљених камером апликације на Вашем уређају
diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml
index 20378a409..9d7720a4f 100644
--- a/app/src/main/res/values-su/strings.xml
+++ b/app/src/main/res/values-su/strings.xml
@@ -169,7 +169,7 @@
Tempat Sabudeureun
Teu kapanggih tempat sabudeureun
Mangkahadé
- Ieu berkas geus aya di Commons. Rék diteruskeun baé ieu téh?
+ Ieu berkas geus aya di Commons. Rék diteruskeun baé ieu téh?
Nya
Henteu
Judul
@@ -230,8 +230,8 @@
Kasalahan nalika muat gambar
Judul déskriptif anu unik pikeun berkas, anu bakal miboga fungsi minangka ngaran berkas. Anjeun bisa maké basa basajan kalawan spasi. Ulah ngawuwuhkeun éksténsi berkas
Pék émbarkeun wincikan média saloba-lobabana: Dimana éta dicokot? Naon nu titojokeunna? Naon kontéksna? Pék jéntrékeun obyék atawa jalmana. Ébré informasi anu teu gampang kajudi, kawas wayah mun éta mangrupa pamandangan. Ari média nu némbongkeun perkara nu teu guyub, pék jéntrékeun naon nu ngabalukarkeun éta téh teu guyub.
- Ieu gambar poék teuing, rék diunggah waé? Wikimedia Commons téh ukur pikeun gambar anu boga ajén énsiklopédik.
- Potona teu cékas, rék diunggah waé? Wikimedia Commons mah ukur pikeun gambar anu boga ajén énsiklopédik.
+ Ieu gambar poék teuing, rék diunggah waé? Wikimedia Commons téh ukur pikeun gambar anu boga ajén énsiklopédik.
+ Potona teu cékas, rék diunggah waé? Wikimedia Commons mah ukur pikeun gambar anu boga ajén énsiklopédik.
Béré idin
Paké panyimpenan éksternal
Simpen gambar nu nyomotna ku aplikasi kaména na parangkat anjeun
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 9a8cd7659..c60cf229b 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -174,7 +174,7 @@
Platser i närheten
Inga platser i närheten hittades
Varning
- Denna fil finns redan på Commons. Är du säker på att du vill fortsätta?
+ Denna fil finns redan på Commons. Är du säker på att du vill fortsätta?
Ja
Nej
Titel
@@ -236,8 +236,8 @@
Fel uppstod när bilder cachelagras
En unik beskrivande titel för filen, som kommer att fungera som ett filnamn. Du kan använda klarspråk med mellanslag. Ta inte med filändelsen
Beskriv mediafilen så mycket som möjligt. Var togs den? Vad visar den? Vad är sammanhanget? Beskriv föremålen eller personerna. Ge information som inte kan gissas fram, t.ex. tidpunkten om det är ett landskap. Om mediafilen visar någonting ovanligt, förklara vad som gör den ovanlig.
- Denna bild är för mörk, är du säker på att du vill ladda upp den? Wikimedia Commons är endast till för bilder med encyklopediskt värde.
- Denna bild är suddig, är du säker på att du vill ladda upp den? Wikimedia Commons är endast till för bilder med encyklopediskt värde.
+ Denna bild är för mörk, är du säker på att du vill ladda upp den? Wikimedia Commons är endast till för bilder med encyklopediskt värde.
+ Denna bild är suddig, är du säker på att du vill ladda upp den? Wikimedia Commons är endast till för bilder med encyklopediskt värde.
Ge behörighet
Använd extern lagring
Spara bilder som tas med kameran i appen på din enhet
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
index 9a7ec69c0..3206dcf4f 100644
--- a/app/src/main/res/values-th/strings.xml
+++ b/app/src/main/res/values-th/strings.xml
@@ -155,7 +155,7 @@
สถานที่ใกล้เคียง
ไม่พบสถานที่ใกล้เคียง
คำเตือน
- ไฟล์นี้มีอยู่แล้วบนคอมมอนส์ คุณแน่ใจหรือว่าคุณต้องการดำเนินการต่อ?
+ ไฟล์นี้มีอยู่แล้วบนคอมมอนส์ คุณแน่ใจหรือว่าคุณต้องการดำเนินการต่อ?
ใช่
ไม่
ชื่อเรื่อง
@@ -213,8 +213,8 @@
ข้อผิดพลาดขณะแคชภาพ
ชื่อเรื่องที่อธิบายลักษณะเฉพาะของไฟล์ ซึ่งจะใช้เป็นชื่อไฟล์ คุณอาจใช้ภาษาธรรมดาที่มีเว้นวรรคก็ได้ อย่ารวมนามสกุลไฟล์
โปรดอธิบายสื่อดังกล่าวให้มากที่สุดเท่าที่จะได้: สื่อนี้ถูกถ่ายที่ไหน? สื่อนี้แสดงถึงอะไร? บริบทคืออะไร? โปรดอธิบายถึงวัตถุหรือบุคคล เปิดเผยข้อมูลที่ไม่อาจคาดเดาได้อย่างง่ายดาย เช่น เวลาที่ถ่าย หากเป็นภาพทิวทัศน์ หากสื่อแสดงถึงสิ่งที่ไม่ธรรมดา โปรดอธิบายว่าอะไรทำให้สื่อดังกล่าวไม่ธรรมดา
- ภาพนี้มืดเกินไป คุณแน่ใจหรือว่าคุณต้องการอัปโหลดภาพนี้? วิกิมีเดียคอมมอนส์นั้นมีไว้สำหรับรูปภาพที่มีคุณค่าในทางสารานุกรมเท่านั้น
- ภาพนี้มัว คุณแน่ใจหรือว่าคุณต้องการอัปโหลดภาพนี้? วิกิมีเดียคอมมอนส์นั้นมีไว้สำหรับรูปภาพที่มีคุณค่าในทางสารานุกรมเท่านั้น
+ ภาพนี้มืดเกินไป คุณแน่ใจหรือว่าคุณต้องการอัปโหลดภาพนี้? วิกิมีเดียคอมมอนส์นั้นมีไว้สำหรับรูปภาพที่มีคุณค่าในทางสารานุกรมเท่านั้น
+ ภาพนี้มัว คุณแน่ใจหรือว่าคุณต้องการอัปโหลดภาพนี้? วิกิมีเดียคอมมอนส์นั้นมีไว้สำหรับรูปภาพที่มีคุณค่าในทางสารานุกรมเท่านั้น
ให้สิทธิ์
ใช้ที่จัดเก็บข้อมูลภายนอก
บันทึกรูปภาพที่ถ่ายด้วยกล้องในแอปบนอุปกรณ์ของคุณ
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 58c5133c2..583619c1d 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -180,7 +180,7 @@
Yakındaki yerler
Yakınlardaki yer bulunamadı
Uyarı
- Bu dosya zaten Commons\'da var. Devam etmek istediğinizden emin misiniz?
+ Bu dosya zaten Commons\'da var. Devam etmek istediğinizden emin misiniz?
Evet
Hayır
Başlık
@@ -242,8 +242,8 @@
Resimler önbelleğe alınırken hata oluştu
Dosya için dosya adı olarak kullanılacak benzersiz açıklayıcı bir başlık olmalıdır. Boşluk bırakarak sade bir dil kullanabilirsiniz. Dosya uzantısını dahil etmeyin
Lütfen medyayı mümkün olduğunca açıklayın: Nerede çekildi? Ne gösteriyor? Bağlam nedir? Lütfen nesneleri veya kişileri tanımlayın. Kolay tahmin edilemeyen bilgileri açıklayın, örneğin bir manzara ise günün saatini belirtin. Medya alışılmadık bir şey gösteriyorsa lütfen olağandışı yapan şeyleri açıklayın.
- Bu fotoğraf çok karanlık, yine de yüklemek istiyor musunuz? Wikimedia Commons yalnızca ansiklopedik değeri olan fotoğraflar içindir.
- Bu fotoğraf bulanık, yine de yüklemek istiyor musunuz? Wikimedia Commons yalnızca ansiklopedik değeri olan fotoğraflar içindir.
+ Bu fotoğraf çok karanlık, yine de yüklemek istiyor musunuz? Wikimedia Commons yalnızca ansiklopedik değeri olan fotoğraflar içindir.
+ Bu fotoğraf bulanık, yine de yüklemek istiyor musunuz? Wikimedia Commons yalnızca ansiklopedik değeri olan fotoğraflar içindir.
İzin ver
Harici depolamayı kullanın
Uygulama kamerası kullanıldığında çekilen fotoğrafları cihazına kaydedin
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 5b3df71c9..42283caec 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -185,7 +185,7 @@
Місця поблизу
Не знайдено місць поблизу
Попередження
- Цей файл вже існує на Вікісховищі. Ви впевнені, що хочете продовжити?
+ Цей файл вже існує на Вікісховищі. Ви впевнені, що хочете продовжити?
Так
Ні
Назва
@@ -247,8 +247,8 @@
Помилка кешування зображень
Унікальна описова назва файлу. Ви можете використовувати простий текст з пробілами. Не вказуйте розширення файлу
Будь ласка, докладно опишіть файл: де його було зроблено? що на ньому зображено? який контекст? Будь ласка, опишіть об\'єкти чи осіб. Додайте інформацію, яку не можна легко здогадатися, наприклад, пору доби для фотографії пейзажу. Якщо зображено щось незвичайне, спробуйте пояснити, що робить його незвичайним.
- Це зображення надто темне. Ви упевнені, що хочете його завантажити? Вікісховище призначене лише для зображень, що мають енциклопедичну цінність.
- Це зображення розмите. Ви упевнені, що хочете його завантажити? Вікісховище призначене лише для зображень, що мають енциклопедичну цінність.
+ Це зображення надто темне. Ви упевнені, що хочете його завантажити? Вікісховище призначене лише для зображень, що мають енциклопедичну цінність.
+ Це зображення розмите. Ви упевнені, що хочете його завантажити? Вікісховище призначене лише для зображень, що мають енциклопедичну цінність.
Надати дозвіл
Використовувати зовнішнє сховище
Зберігати зображення, виконані вбудованою камерою Вашого пристрою
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index a55d54f7f..aad679fe5 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -165,7 +165,7 @@
Nơi Lân cận
Không tìm thấy nơi lân cận
Cảnh báo
- Tập tin này đã tồn tại ở Commons. Bạn có chắc chắn muốn tiếp tục?
+ Tập tin này đã tồn tại ở Commons. Bạn có chắc chắn muốn tiếp tục?
Có
Không
Tiêu đề
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index bf3815db9..bb9ebb9f6 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -179,7 +179,7 @@
附近地点
找不到附近地点
警告
- 此文件在共享资源已存在。您确定要继续么?
+ 此文件在共享资源已存在。您确定要继续么?
是
否
标题
@@ -241,8 +241,8 @@
缓存图片时出错
用于文件的唯一描述性标题,它将作为文件名使用。您可以使用有空格的简明语言。请不要包含文件扩展名
请尽可能详细地描述媒体:拍摄在何地?显示什么?例文是什么?请描述对象或个人。透露一些不易猜想到的信息,例如这幅风景画的具体日期时间。如果媒体显示了一些不寻常的事物,请说明为什么它显得不寻常。
- 此图片太暗,您确定要上传它么?维基共享资源只接受对百科全书有价值的图片。
- 此图片模糊不清,您确定要上传它么?维基共享资源只接受对百科全书有价值的图片。
+ 此图片太暗,您确定要上传它么?维基共享资源只接受对百科全书有价值的图片。
+ 此图片模糊不清,您确定要上传它么?维基共享资源只接受对百科全书有价值的图片。
提供权限
使用外部存储
在您的设备上,使用应用中的照相机保存照片
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index a4c9ef716..c305de897 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -16,6 +16,12 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index bad958661..ce164ac76 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -3,6 +3,8 @@
#303030
#fafafa
+ #eaeaea
+ #1a1a1a
#0c609c
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index bac816cd4..34ee3e6dc 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -26,6 +26,7 @@
14sp
15dp
25dp
+ 12sp
- @string/contributions_subtitle_zero
- - %1$d upload
- - %1$d uploads
+ - (%1$d)
+ - (%1$d)
- Starting %1$d upload
@@ -167,7 +167,7 @@
Nearby Places
No nearby places found
Warning
- This file already exists on Commons. Are you sure you want to proceed?
+ This file already exists on Commons. Are you sure you want to proceed?
Yes
No
Title
@@ -235,6 +235,15 @@
Please describe the media as much as possible: Where was it taken? What does it show? What is the context? Please describe the objects or persons. Reveal information that can not be easily guessed, for instance the time of day if it is a landscape. If the media shows something unusual, please explain what makes it unusual.
This picture is too dark, are you sure you want to upload it? Wikimedia Commons is only for pictures with encyclopedic value.
This picture is blurry, are you sure you want to upload it? Wikimedia Commons is only for pictures with encyclopedic value.
+
+ This file already exists on Commons. Are you sure you want to proceed?
+ This picture is too dark, are you sure you want to upload it? Wikimedia Commons is only for pictures with encyclopedic value.
+ This picture is blurry, are you sure you want to upload it? Wikimedia Commons is only for pictures with encyclopedic value.
+ This picture is too dark and already exists on Commons, are you sure you want to upload it? Please only upload pictures with encyclopedic value.
+ This picture is blurry and already exists on Commons, are you sure you want to upload it? Please only upload pictures with encyclopedic value.
+ This picture is too dark and is blurry, are you sure you want to upload it? Please only upload pictures with encyclopedic value.
+ This picture is too dark, is blurry and already exists on Commons, are you sure you want to upload it? Please only upload pictures with encyclopedic value.
+
Give permission
Use external storage
Save pictures taken with the in-app camera on your device
@@ -355,8 +364,30 @@
Error occurred!
Commons Notification
+
+ Contributions
+ Nearby
+ Notifications
+ Display nearby notification
+ Tap here to see the nearest place that needs pictures
+ No nearby places found close to you
+ List
+
Storage Permission
We need your permission to access the external storage of your device in order to upload images.
+ You won\'t see the nearest place that needs pictures anymore. However, you can re-enable this notification in Settings if you wish.
+
+ Step %1$d of %2$d
+ Image %1$d in set
+ Next
+ Previous
+ Submit
+ A file with the file name %1$s exists. Are you sure you want to proceed?
+ No compatible map application could be found on your device. Please install a map application to use this feature.
+
+ - %1$d Upload
+ - %1$d Uploads
+
Bookmarks
Bookmarks
Pictures
@@ -366,7 +397,6 @@
You haven\'t added any bookmarks
Bookmarks
Log collection started. Please RESTART the app, perform action that you wish to log, and then tap \'Send log file\' again
- Welcome to Commons!\n Upload your first media by touching the camera or gallery icon above.
I uploaded it by mistake
I did not know it would be publicly visible
@@ -377,4 +407,23 @@
,used in
articles
+ Welcome to Commons!\n
+Upload your first media by touching the camera or gallery icon above.
+
+ Worldwide
+ America
+ Europe
+ Middle East
+ Africa
+ Asia
+ Pacific
+
+ No Categories Selected
+ Images without categories are rarely usable. Are you sure you want to submit without selecting categories?
+
+ Yes, Submit
+ No, Go Back
+
+ (For all images in set)
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 26d9c357b..70bec86a3 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,6 +1,8 @@
+
+
+
+