Change the UTC Date format to yyyy/MM/dd HH:mm:ss

This commit is contained in:
ujjwal2900 2024-09-12 08:59:02 +05:30
commit 9e135310b2
87 changed files with 2977 additions and 1008 deletions

View file

@ -195,36 +195,4 @@ class MainActivityTest {
Espresso.pressBack()
UITestHelper.sleep(1000)
}
@Test
fun testLimitedConnectionModeToggle() {
val isEnabled = defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)
Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(R.id.toggle_limited_connection_mode),
childAtPosition(
childAtPosition(
ViewMatchers.withId(R.id.toolbar),
1
),
0
),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click())
UITestHelper.sleep(1000)
if (isEnabled) {
Assert.assertFalse(
defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)
)
} else {
Assert.assertTrue(
defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)
)
}
}
}

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
@ -13,66 +14,65 @@
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<queries>
<!-- Browser -->
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<!-- Google Maps -->
<package android:name="com.google.android.apps.maps" />
</queries>
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
</queries> <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
<uses-feature android:name="android.hardware.location.gps" />
<application
android:name=".CommonsApplication"
android:appComponentFactory="commons"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/LightAppTheme"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
tools:replace="android:appComponentFactory"
android:appComponentFactory="commons"
android:requestLegacyExternalStorage = "true"
tools:ignore="GoogleAppIndexingWarning">
android:theme="@style/LightAppTheme"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:appComponentFactory">
<activity
android:name=".nearby.WikidataFeedback"
android:exported="false" />
<activity
android:theme="@style/EditActivityTheme"
android:name=".upload.UploadProgressActivity"
android:exported="false" />
<activity
android:name=".description.DescriptionEditActivity"
android:exported="true" />
android:exported="true"
android:theme="@style/EditActivityTheme" />
<activity
android:name=".edit.EditActivity"
android:exported="false" />
<activity android:name="org.acra.dialog.CrashReportDialog"
android:process=":acra"
android:launchMode="singleInstance"
<activity
android:name="org.acra.dialog.CrashReportDialog"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true" />
android:finishOnTaskLaunch="true"
android:launchMode="singleInstance"
android:process=":acra" />
<activity
android:name=".media.ZoomableActivity"
android:label="Zoomable Activity"
android:configChanges="screenSize|keyboard|orientation"
android:label="Zoomable Activity"
android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
<activity android:name=".auth.LoginActivity"
<activity
android:name=".auth.LoginActivity"
android:exported="true">
<intent-filter>
<category android:name="android.intent.category.LAUNCHER" />
@ -80,21 +80,19 @@
<action android:name="android.intent.action.MAIN" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity android:name=".WelcomeActivity" />
<activity
android:hardwareAccelerated="false"
android:name=".upload.UploadActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboard"
android:exported="true"
android:hardwareAccelerated="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize"
>
android:windowSoftInputMode="adjustResize">
<intent-filter android:label="@string/intent_share_upload_label">
<action android:name="android.intent.action.SEND" />
@ -114,9 +112,9 @@
</activity>
<activity
android:name=".contributions.MainActivity"
android:configChanges="screenSize|keyboard|orientation"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:configChanges="screenSize|keyboard|orientation" />
android:label="@string/app_name" />
<activity
android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings" />
@ -124,57 +122,47 @@
android:name=".AboutActivity"
android:label="@string/title_activity_about"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".auth.SignupActivity"
android:configChanges="orientation|screenLayout|screenSize"
android:label="@string/title_activity_signup" />
<activity
android:name=".notification.NotificationActivity"
android:label="@string/navigation_item_notification" />
<activity android:name=".quiz.QuizActivity"
android:label="@string/quiz"/>
<activity android:name=".quiz.QuizResultActivity"
android:label="@string/result"/>
<activity
android:name=".quiz.QuizActivity"
android:label="@string/quiz" />
<activity
android:name=".quiz.QuizResultActivity"
android:label="@string/result" />
<activity
android:name=".customselector.ui.selector.CustomSelectorActivity"
android:label="@string/title_activity_custom_selector"
android:configChanges="screenSize|keyboard|orientation"
android:label="@string/title_activity_custom_selector"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".category.CategoryDetailsActivity"
android:label="@string/title_activity_featured_images"
android:configChanges="screenSize|keyboard|orientation"
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".explore.depictions.WikidataItemDetailsActivity"
android:label="@string/title_activity_featured_images"
android:configChanges="screenSize|keyboard|orientation"
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".explore.SearchActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/title_activity_search"
android:launchMode="singleTop"
android:configChanges="orientation|keyboardHidden|screenSize"
android:parentActivityName=".contributions.MainActivity"
/>
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".profile.ProfileActivity"
android:configChanges="orientation|screenSize|keyboard"
android:label="@string/Profile" />
<activity
android:name=".review.ReviewActivity"
android:label="@string/title_activity_review" />
<activity
android:name=".LocationPicker.LocationPickerActivity"
android:label="Location Picker" />
@ -186,11 +174,11 @@
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service
android:name="org.acra.sender.SenderService"
android:exported="false"
@ -205,42 +193,36 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name=".category.CategoryContentProvider"
android:authorities="${applicationId}.categories.contentprovider"
android:exported="false"
android:label="@string/provider_categories"
android:syncable="false" />
<provider
android:name=".explore.recentsearches.RecentSearchesContentProvider"
android:authorities="${applicationId}.explore.recentsearches.contentprovider"
android:exported="false"
android:label="@string/provider_searches"
android:syncable="false" />
<provider
android:name=".recentlanguages.RecentLanguagesContentProvider"
android:authorities="${applicationId}.recentlanguages.contentprovider"
android:exported="false"
android:label="@string/provider_recent_languages"
android:syncable="false" />
<provider
android:name=".bookmarks.pictures.BookmarkPicturesContentProvider"
android:authorities="${applicationId}.bookmarks.contentprovider"
android:exported="false"
android:label="@string/provider_bookmarks"
android:syncable="false" />
<provider
android:name=".bookmarks.locations.BookmarkLocationsContentProvider"
android:authorities="${applicationId}.bookmarks.locations.contentprovider"
android:exported="false"
android:label="@string/provider_bookmarks_location"
android:syncable="false" />
<provider
android:name=".bookmarks.items.BookmarkItemsContentProvider"
android:authorities="${applicationId}.bookmarks.items.contentprovider"
@ -248,7 +230,8 @@
android:label="@string/provider_bookmarks_location"
android:syncable="false" />
<receiver android:name=".widget.PicOfDayAppWidget"
<receiver
android:name=".widget.PicOfDayAppWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@ -259,8 +242,9 @@
android:resource="@xml/pic_of_day_app_widget_info" />
</receiver>
<uses-library android:name="org.apache.http.legacy" android:required="false" />
<uses-library
android:name="org.apache.http.legacy"
android:required="false" />
</application>
</manifest>

View file

@ -142,15 +142,7 @@ public class CommonsApplication extends MultiDexApplication {
@Inject
ContributionDao contributionDao;
/**
* In-memory list of contributions whose uploads have been paused by the user
*/
public static Map<String, Boolean> pauseUploads = new HashMap<>();
/**
* In-memory list of uploads that have been cancelled by the user
*/
public static HashSet<String> cancelledUploads = new HashSet<>();
public static Boolean isPaused = false;
/**
* Used to declare and initialize various components and dependencies

View file

@ -28,6 +28,7 @@ data class Contribution constructor(
var dateCreatedSource: String? = null,
var wikidataPlace: WikidataPlace? = null,
var chunkInfo: ChunkInfo? = null,
var errorInfo: String? = null,
/**
* @return array list of entityids for the depictions
*/
@ -42,6 +43,7 @@ data class Contribution constructor(
var dateCreated: Date? = null,
var dateCreatedString: String? = null,
var dateModified: Date? = null,
var dateUploadStarted: Date? = null,
var hasInvalidLocation : Int = 0,
var contentUri: Uri? = null,
var countryCode : String? = null,
@ -99,7 +101,6 @@ data class Contribution constructor(
const val STATE_QUEUED = 2
const val STATE_IN_PROGRESS = 3
const val STATE_PAUSED = 4
const val STATE_QUEUED_LIMITED_CONNECTION_MODE=5
/**
* Formatting captions to the Wikibase format for sending labels
@ -127,11 +128,8 @@ data class Contribution constructor(
return chunkInfo != null && chunkInfo!!.totalChunks == chunkInfo!!.indexOfNextChunkToUpload
}
fun isPaused(): Boolean {
return CommonsApplication.pauseUploads[pageId] ?: false
fun dateUploadStartedInMillis(): Long {
return dateUploadStarted!!.time
}
fun unpause() {
CommonsApplication.pauseUploads[pageId] = false
}
}

View file

@ -9,6 +9,10 @@ import android.content.Intent;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.paging.DataSource.Factory;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.filepicker.DefaultCallback;
import fr.free.nrw.commons.filepicker.FilePicker;
@ -25,6 +29,8 @@ import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
@ -39,10 +45,16 @@ public class ContributionController {
private boolean isInAppCameraUpload;
public LocationPermissionCallback locationPermissionCallback;
private LocationPermissionsHelper locationPermissionsHelper;
LiveData<PagedList<Contribution>> failedAndPendingContributionList;
LiveData<PagedList<Contribution>> pendingContributionList;
LiveData<PagedList<Contribution>> failedContributionList;
@Inject
LocationServiceManager locationManager;
@Inject
ContributionsRepository repository;
@Inject
public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) {
this.defaultKvStore = defaultKvStore;
@ -115,8 +127,8 @@ public class ContributionController {
}
/**
* Shows a dialog alerting the user about location services being off
* and asking them to turn it on
* Shows a dialog alerting the user about location services being off and asking them to turn it
* on
* TODO: Add a seperate callback in LocationPermissionsHelper for this.
* Ref: https://github.com/commons-app/apps-android-commons/pull/5494/files#r1510553114
*
@ -307,4 +319,60 @@ public class ContributionController {
isInAppCameraUpload = false; // reset the flag for next use
return shareIntent;
}
/**
* Fetches the contributions with the state "IN_PROGRESS", "QUEUED" and "PAUSED" and then it
* populates the `pendingContributionList`.
**/
void getPendingContributions() {
final PagedList.Config pagedListConfig =
(new PagedList.Config.Builder())
.setPrefetchDistance(50)
.setPageSize(10).build();
Factory<Integer, Contribution> factory;
factory = repository.fetchContributionsWithStates(
Arrays.asList(Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED,
Contribution.STATE_PAUSED));
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
pagedListConfig);
pendingContributionList = livePagedListBuilder.build();
}
/**
* Fetches the contributions with the state "FAILED" and populates the
* `failedContributionList`.
**/
void getFailedContributions() {
final PagedList.Config pagedListConfig =
(new PagedList.Config.Builder())
.setPrefetchDistance(50)
.setPageSize(10).build();
Factory<Integer, Contribution> factory;
factory = repository.fetchContributionsWithStates(
Collections.singletonList(Contribution.STATE_FAILED));
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
pagedListConfig);
failedContributionList = livePagedListBuilder.build();
}
/**
* Fetches the contributions with the state "IN_PROGRESS", "QUEUED", "PAUSED" and "FAILED" and
* then it populates the `failedAndPendingContributionList`.
**/
void getFailedAndPendingContributions() {
final PagedList.Config pagedListConfig =
(new PagedList.Config.Builder())
.setPrefetchDistance(50)
.setPageSize(10).build();
Factory<Integer, Contribution> factory;
factory = repository.fetchContributionsWithStates(
Arrays.asList(Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED,
Contribution.STATE_PAUSED, Contribution.STATE_FAILED));
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
pagedListConfig);
failedAndPendingContributionList = livePagedListBuilder.build();
}
}

View file

@ -13,6 +13,7 @@ import io.reactivex.Completable;
import io.reactivex.Single;
import java.util.Calendar;
import java.util.List;
import timber.log.Timber;
@Dao
public abstract class ContributionDao {
@ -27,6 +28,9 @@ public abstract class ContributionDao {
return Completable
.fromAction(() -> {
contribution.setDateModified(Calendar.getInstance().getTime());
if (contribution.getDateUploadStarted() == null) {
contribution.setDateUploadStarted(Calendar.getInstance().getTime());
}
saveSynchronous(contribution);
});
}
@ -44,11 +48,32 @@ public abstract class ContributionDao {
@Delete
public abstract void deleteSynchronous(Contribution contribution);
/**
* Deletes contributions with specific states from the database.
*
* @param states The states of the contributions to delete.
* @throws SQLiteException If an SQLite error occurs.
*/
@Query("DELETE FROM contribution WHERE state IN (:states)")
public abstract void deleteContributionsWithStatesSynchronous(List<Integer> states)
throws SQLiteException;
public Completable delete(final Contribution contribution) {
return Completable
.fromAction(() -> deleteSynchronous(contribution));
}
/**
* Deletes contributions with specific states from the database.
*
* @param states The states of the contributions to delete.
* @return A Completable indicating the result of the operation.
*/
public Completable deleteContributionsWithStates(List<Integer> states) {
return Completable
.fromAction(() -> deleteContributionsWithStatesSynchronous(states));
}
@Query("SELECT * from contribution WHERE media_filename=:fileName")
public abstract List<Contribution> getContributionWithTitle(String fileName);
@ -58,6 +83,26 @@ public abstract class ContributionDao {
@Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
public abstract Single<List<Contribution>> getContribution(List<Integer> states);
/**
* Gets contributions with specific states in descending order by the date they were uploaded.
*
* @param states The states of the contributions to fetch.
* @return A DataSource factory for paginated contributions with the specified states.
*/
@Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
public abstract DataSource.Factory<Integer, Contribution> getContributions(
List<Integer> states);
/**
* Gets contributions with specific states in ascending order by the date the upload started.
*
* @param states The states of the contributions to fetch.
* @return A DataSource factory for paginated contributions with the specified states.
*/
@Query("SELECT * from contribution WHERE state IN (:states) order by dateUploadStarted ASC")
public abstract DataSource.Factory<Integer, Contribution> getContributionsSortedByDateUploadStarted(
List<Integer> states);
@Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)")
public abstract Single<Integer> getPendingUploads(int[] toUpdateStates);
@ -67,6 +112,15 @@ public abstract class ContributionDao {
@Update
public abstract void updateSynchronous(Contribution contribution);
/**
* Updates the state of contributions with specific states.
*
* @param states The current states of the contributions to update.
* @param newState The new state to set.
*/
@Query("UPDATE contribution SET state = :newState WHERE state IN (:states)")
public abstract void updateContributionsState(List<Integer> states, int newState);
public Completable update(final Contribution contribution) {
return Completable
.fromAction(() -> {
@ -74,4 +128,18 @@ public abstract class ContributionDao {
updateSynchronous(contribution);
});
}
/**
* Updates the state of contributions with specific states asynchronously.
*
* @param states The current states of the contributions to update.
* @param newState The new state to set.
* @return A Completable indicating the result of the operation.
*/
public Completable updateContributionsWithStates(List<Integer> states, int newState) {
return Completable
.fromAction(() -> {
updateContributionsState(states, newState);
});
}
}

View file

@ -48,11 +48,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
binding = LayoutContributionBinding.bind(parent);
binding.retryButton.setOnClickListener(v -> retryUpload());
binding.cancelButton.setOnClickListener(v -> deleteUpload());
binding.contributionImage.setOnClickListener(v -> imageClicked());
binding.wikipediaButton.setOnClickListener(v -> wikipediaButtonClicked());
binding.pauseResumeButton.setOnClickListener(v -> onPauseResumeButtonClicked());
/* Set a dialog indicating that the upload is being paused. This is needed because pausing
an upload might take a dozen seconds. */
@ -80,9 +77,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
binding.contributionImage.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
binding.contributionImage.getHierarchy().setFailureImage(R.drawable.image_placeholder);
final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(),
contribution.getLocalUri());
if (!TextUtils.isEmpty(imageSource)) {
@ -90,79 +84,27 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
.setProgressiveRenderingEnabled(true)
.build();
}
else if (URLUtil.isFileUrl(imageSource)){
imageRequest=ImageRequest.fromUri(Uri.parse(imageSource));
}
else if(imageSource != null) {
} else if (URLUtil.isFileUrl(imageSource)) {
imageRequest = ImageRequest.fromUri(Uri.parse(imageSource));
} else if (imageSource != null) {
final File file = new File(imageSource);
imageRequest = ImageRequest.fromFile(file);
}
if(imageRequest != null){
if (imageRequest != null) {
binding.contributionImage.setImageRequest(imageRequest);
}
}
binding.contributionSequenceNumber.setText(String.valueOf(position + 1));
binding.contributionSequenceNumber.setVisibility(View.VISIBLE);
binding.wikipediaButton.setVisibility(View.GONE);
switch (contribution.getState()) {
case Contribution.STATE_COMPLETED:
binding.contributionState.setVisibility(View.GONE);
binding.contributionProgress.setVisibility(View.GONE);
binding.imageOptions.setVisibility(View.GONE);
binding.contributionState.setText("");
checkIfMediaExistsOnWikipediaPage(contribution);
break;
case Contribution.STATE_QUEUED:
case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE:
binding.contributionProgress.setVisibility(View.GONE);
binding.contributionState.setVisibility(View.VISIBLE);
binding.contributionState.setText(R.string.contribution_state_queued);
binding.imageOptions.setVisibility(View.GONE);
break;
case Contribution.STATE_IN_PROGRESS:
binding.contributionState.setVisibility(View.GONE);
binding.contributionProgress.setVisibility(View.VISIBLE);
binding.wikipediaButton.setVisibility(View.GONE);
binding.pauseResumeButton.setVisibility(View.VISIBLE);
binding.cancelButton.setVisibility(View.GONE);
binding.retryButton.setVisibility(View.GONE);
binding.imageOptions.setVisibility(View.VISIBLE);
final long total = contribution.getDataLength();
final long transferred = contribution.getTransferred();
if (transferred == 0 || transferred >= total) {
binding.contributionProgress.setIndeterminate(true);
} else {
binding.contributionProgress.setIndeterminate(false);
binding.contributionProgress.setProgress((int) (((double) transferred / (double) total) * 100));
}
break;
case Contribution.STATE_PAUSED:
binding.contributionProgress.setVisibility(View.GONE);
binding.contributionState.setVisibility(View.VISIBLE);
binding.contributionState.setText(R.string.paused);
binding.cancelButton.setVisibility(View.VISIBLE);
binding.retryButton.setVisibility(View.GONE);
binding.pauseResumeButton.setVisibility(View.VISIBLE);
binding.imageOptions.setVisibility(View.VISIBLE);
setResume();
if(pausingPopUp.isShowing()){
pausingPopUp.hide();
}
break;
case Contribution.STATE_FAILED:
binding.contributionState.setVisibility(View.VISIBLE);
binding.contributionState.setText(R.string.contribution_state_failed);
binding.contributionProgress.setVisibility(View.GONE);
binding.cancelButton.setVisibility(View.VISIBLE);
binding.retryButton.setVisibility(View.VISIBLE);
binding.pauseResumeButton.setVisibility(View.GONE);
binding.imageOptions.setVisibility(View.VISIBLE);
break;
}
}
/**
@ -196,8 +138,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
if (!mediaExists) {
binding.wikipediaButton.setVisibility(View.VISIBLE);
isWikipediaButtonDisplayed = true;
binding.cancelButton.setVisibility(View.GONE);
binding.retryButton.setVisibility(View.GONE);
binding.imageOptions.setVisibility(View.VISIBLE);
}
}
@ -217,20 +157,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
null;
}
/**
* Retry upload when it is failed
*/
public void retryUpload() {
callback.retryUpload(contribution);
}
/**
* Delete a failed upload attempt
*/
public void deleteUpload() {
callback.deleteUpload(contribution);
}
public void imageClicked() {
callback.openMediaDetail(position, isWikipediaButtonDisplayed);
}
@ -239,44 +165,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
callback.addImageToWikipedia(contribution);
}
/**
* Triggers a callback for pause/resume
*/
public void onPauseResumeButtonClicked() {
if (binding.pauseResumeButton.getTag().toString().equals("pause")) {
pause();
} else {
resume();
}
}
private void resume() {
callback.resumeUpload(contribution);
setPaused();
}
private void pause() {
pausingPopUp.show();
callback.pauseUpload(contribution);
setResume();
}
/**
* Update pause/resume button to show pause state
*/
private void setPaused() {
binding.pauseResumeButton.setImageResource(R.drawable.pause_icon);
binding.pauseResumeButton.setTag(parent.getContext().getString(R.string.pause));
}
/**
* Update pause/resume button to show resume state
*/
private void setResume() {
binding.pauseResumeButton.setImageResource(R.drawable.play_icon);
binding.pauseResumeButton.setTag(parent.getContext().getString(R.string.resume));
}
public ImageRequest getImageRequest() {
return imageRequest;
}

View file

@ -19,8 +19,5 @@ public class ContributionsContract {
Contribution getContributionsWithTitle(String uri);
void deleteUpload(Contribution contribution);
void saveContribution(Contribution contribution);
}
}

View file

@ -5,6 +5,7 @@ import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED;
import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL;
import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
import static fr.free.nrw.commons.utils.LengthUtils.computeBearing;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
@ -12,6 +13,7 @@ import android.Manifest;
import android.Manifest.permission;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
@ -25,6 +27,7 @@ import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
@ -44,6 +47,8 @@ import fr.free.nrw.commons.notification.models.Notification;
import fr.free.nrw.commons.notification.NotificationController;
import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.upload.UploadProgressActivity;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
@ -104,6 +109,8 @@ public class ContributionsFragment
LocationServiceManager locationManager;
@Inject
NotificationController notificationController;
@Inject
ContributionController contributionController;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
@ -113,10 +120,10 @@ public class ContributionsFragment
static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
private static final int MAX_RETRIES = 10;
public FragmentContributionsBinding binding;
@Inject ContributionsPresenter contributionsPresenter;
@Inject
ContributionsPresenter contributionsPresenter;
@Inject
SessionManager sessionManager;
@ -129,6 +136,12 @@ public class ContributionsFragment
public TextView notificationCount;
public TextView pendingUploadsCountTextView;
public TextView uploadsErrorTextView;
public ImageView pendingUploadsImageView;
private Campaign wlmCampaign;
String userName;
@ -150,10 +163,12 @@ public class ContributionsFragment
if (areAllGranted) {
onLocationPermissionGranted();
} else {
if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)
if (shouldShowRequestPermissionRationale(
Manifest.permission.ACCESS_FINE_LOCATION)
&& store.getBoolean("displayLocationPermissionForCardView", true)
&& !store.getBoolean("doNotAskForLocationPermission", false)
&& (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) {
&& (((MainActivity) getActivity()).activeFragment
== ActiveFragment.CONTRIBUTIONS)) {
binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION;
} else {
displayYouWontSeeNearbyMessage();
@ -198,11 +213,10 @@ public class ContributionsFragment
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
// Do not ask for permission on activity start again
store.putBoolean("displayLocationPermissionForCardView",false);
store.putBoolean("displayLocationPermissionForCardView", false);
}
});
if (savedInstanceState != null) {
mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager()
.findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
@ -212,9 +226,7 @@ public class ContributionsFragment
}
initFragments();
if(isUserProfile) {
binding.limitedConnectionEnabledLayout.setVisibility(View.GONE);
}else {
if (!isUserProfile) {
upDateUploadCount();
}
if (shouldShowMediaDetailsFragment) {
@ -230,7 +242,6 @@ public class ContributionsFragment
&& sessionManager.getCurrentAccount() != null && !isUserProfile) {
setUploadCount();
}
binding.limitedConnectionEnabledLayout.setOnClickListener(toggleDescriptionListener);
setHasOptionsMenu(true);
return binding.getRoot();
}
@ -258,10 +269,32 @@ public class ContributionsFragment
MenuItem notificationsMenuItem = menu.findItem(R.id.notifications);
final View notification = notificationsMenuItem.getActionView();
notificationCount = notification.findViewById(R.id.notification_count_badge);
MenuItem uploadMenuItem = menu.findItem(R.id.upload_tab);
final View uploadMenuItemActionView = uploadMenuItem.getActionView();
pendingUploadsCountTextView = uploadMenuItemActionView.findViewById(
R.id.pending_uploads_count_badge);
uploadsErrorTextView = uploadMenuItemActionView.findViewById(
R.id.uploads_error_count_badge);
pendingUploadsImageView = uploadMenuItemActionView.findViewById(
R.id.pending_uploads_image_view);
if (pendingUploadsImageView != null) {
pendingUploadsImageView.setOnClickListener(view -> {
startActivity(new Intent(getContext(), UploadProgressActivity.class));
});
}
if (pendingUploadsCountTextView != null) {
pendingUploadsCountTextView.setOnClickListener(view -> {
startActivity(new Intent(getContext(), UploadProgressActivity.class));
});
}
if (uploadsErrorTextView != null) {
uploadsErrorTextView.setOnClickListener(view -> {
startActivity(new Intent(getContext(), UploadProgressActivity.class));
});
}
notification.setOnClickListener(view -> {
NotificationActivity.startYourself(getContext(), "unread");
});
updateLimitedConnectionToggle(menu);
}
@SuppressLint("CheckResult")
@ -273,6 +306,33 @@ public class ContributionsFragment
throwable -> Timber.e(throwable, "Error occurred while loading notifications")));
}
/**
* Sets the visibility of the upload icon based on the number of failed and pending
* contributions.
*/
public void setUploadIconVisibility() {
contributionController.getFailedAndPendingContributions();
contributionController.failedAndPendingContributionList.observe(getViewLifecycleOwner(),
list -> {
updateUploadIcon(list.size());
});
}
/**
* Sets the count for the upload icon based on the number of pending and failed contributions.
*/
public void setUploadIconCount() {
contributionController.getPendingContributions();
contributionController.pendingContributionList.observe(getViewLifecycleOwner(),
list -> {
updatePendingIcon(list.size());
});
contributionController.getFailedContributions();
contributionController.failedContributionList.observe(getViewLifecycleOwner(), list -> {
updateErrorIcon(list.size());
});
}
public void scrollToTop() {
if (contributionsListFragment != null) {
contributionsListFragment.scrollToTop();
@ -289,29 +349,6 @@ public class ContributionsFragment
}
}
public void updateLimitedConnectionToggle(Menu menu) {
MenuItem checkable = menu.findItem(R.id.toggle_limited_connection_mode);
boolean isEnabled = store
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false);
checkable.setChecked(isEnabled);
if (binding!=null) {
binding.limitedConnectionEnabledLayout.setVisibility(isEnabled ? View.VISIBLE : View.GONE);
}
checkable.setIcon((isEnabled) ? R.drawable.ic_baseline_cloud_off_24:R.drawable.ic_baseline_cloud_queue_24);
checkable.setOnMenuItemClickListener(new OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
((MainActivity) getActivity()).toggleLimitedConnectionMode();
boolean isEnabled = store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false);
binding.limitedConnectionEnabledLayout.setVisibility(isEnabled ? View.VISIBLE : View.GONE);
checkable.setIcon((isEnabled) ? R.drawable.ic_baseline_cloud_off_24:R.drawable.ic_baseline_cloud_queue_24);
return false;
}
});
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
@ -355,7 +392,7 @@ public class ContributionsFragment
}
private void setupViewForMediaDetails() {
if (binding!=null) {
if (binding != null) {
binding.campaignsView.setVisibility(View.GONE);
}
}
@ -465,7 +502,7 @@ public class ContributionsFragment
contributionsPresenter.onAttachView(this);
locationManager.addLocationListener(this);
if (binding==null) {
if (binding == null) {
return;
}
@ -484,7 +521,8 @@ public class ContributionsFragment
} catch (Exception e) {
Timber.e(e);
}
if (binding.cardViewNearby.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) {
if (binding.cardViewNearby.cardViewVisibilityState
== NearbyNotificationCardView.CardViewVisibilityState.READY) {
binding.cardViewNearby.setVisibility(View.VISIBLE);
}
@ -494,16 +532,19 @@ public class ContributionsFragment
}
// Notification Count and Campaigns should not be set, if it is used in User Profile
if(!isUserProfile) {
if (!isUserProfile) {
setNotificationCount();
fetchCampaigns();
setUploadIconVisibility();
setUploadIconCount();
}
}
mSensorManager.registerListener(this, mLight, SensorManager.SENSOR_DELAY_UI);
}
private void checkPermissionsAndShowNearbyCardView() {
if (PermissionUtils.hasPermission(getActivity(), new String[]{Manifest.permission.ACCESS_FINE_LOCATION})) {
if (PermissionUtils.hasPermission(getActivity(),
new String[]{Manifest.permission.ACCESS_FINE_LOCATION})) {
onLocationPermissionGranted();
} else if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)
&& store.getBoolean("displayLocationPermissionForCardView", true)
@ -636,14 +677,14 @@ public class ContributionsFragment
*/
private void fetchCampaigns() {
if (Utils.isMonumentsEnabled(new Date())) {
if (binding!=null) {
if (binding != null) {
binding.campaignsView.setCampaign(wlmCampaign);
binding.campaignsView.setVisibility(View.VISIBLE);
}
} else if (store.getBoolean(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE, true)) {
presenter.getCampaigns();
} else {
if (binding!=null) {
if (binding != null) {
binding.campaignsView.setVisibility(View.GONE);
}
}
@ -657,7 +698,7 @@ public class ContributionsFragment
@Override
public void showCampaigns(Campaign campaign) {
if (campaign != null && !isUserProfile) {
if (binding!=null) {
if (binding != null) {
binding.campaignsView.setCampaign(campaign);
}
}
@ -676,67 +717,6 @@ public class ContributionsFragment
}
}
/**
* Restarts the upload process for a contribution
*
* @param contribution
*/
public void restartUpload(Contribution contribution) {
contribution.setState(Contribution.STATE_QUEUED);
contributionsPresenter.saveContribution(contribution);
Timber.d("Restarting for %s", contribution.toString());
}
/**
* Retry upload when it is failed
*
* @param contribution contribution to be retried
*/
@Override
public void retryUpload(Contribution contribution) {
if (NetworkUtils.isInternetConnectionEstablished(getContext())) {
if (contribution.getState() == STATE_PAUSED
|| contribution.getState() == Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) {
restartUpload(contribution);
} else if (contribution.getState() == STATE_FAILED) {
int retries = contribution.getRetries();
// TODO: Improve UX. Additional details: https://github.com/commons-app/apps-android-commons/pull/5257#discussion_r1304662562
/* Limit the number of retries for a failed upload
to handle cases like invalid filename as such uploads
will never be successful */
if (retries < MAX_RETRIES) {
contribution.setRetries(retries + 1);
Timber.d("Retried uploading %s %d times", contribution.getMedia().getFilename(),
retries + 1);
restartUpload(contribution);
} else {
// TODO: Show the exact reason for failure
Toast.makeText(getContext(),
R.string.retry_limit_reached, Toast.LENGTH_SHORT).show();
}
} else {
Timber.d("Skipping re-upload for non-failed %s", contribution.toString());
}
} else {
ViewUtil.showLongToast(getContext(), R.string.this_function_needs_network_connection);
}
}
/**
* Pauses the upload
*
* @param contribution
*/
@Override
public void pauseUpload(Contribution contribution) {
//Pause the upload in the global singleton
CommonsApplication.pauseUploads.put(contribution.getPageId(), true);
//Retain the paused state in DB
contribution.setState(STATE_PAUSED);
contributionsPresenter.saveContribution(contribution);
}
/**
* Notify the viewpager that number of items have changed.
*/
@ -747,6 +727,54 @@ public class ContributionsFragment
}
}
/**
* Updates the visibility and text of the pending uploads count TextView based on the given
* count.
*
* @param pendingCount The number of pending uploads.
*/
public void updatePendingIcon(int pendingCount) {
if (pendingUploadsCountTextView != null) {
if (pendingCount != 0) {
pendingUploadsCountTextView.setVisibility(View.VISIBLE);
pendingUploadsCountTextView.setText(String.valueOf(pendingCount));
} else {
pendingUploadsCountTextView.setVisibility(View.INVISIBLE);
}
}
}
/**
* Updates the visibility and text of the error uploads TextView based on the given count.
*
* @param errorCount The number of error uploads.
*/
public void updateErrorIcon(int errorCount) {
if (uploadsErrorTextView != null) {
if (errorCount != 0) {
uploadsErrorTextView.setVisibility(View.VISIBLE);
uploadsErrorTextView.setText(String.valueOf(errorCount));
} else {
uploadsErrorTextView.setVisibility(View.GONE);
}
}
}
/**
* Updates the visibility of the pending uploads ImageView based on the given count.
*
* @param count The number of pending uploads.
*/
public void updateUploadIcon(int count) {
if (pendingUploadsImageView != null) {
if (count != 0) {
pendingUploadsImageView.setVisibility(View.VISIBLE);
} else {
pendingUploadsImageView.setVisibility(View.GONE);
}
}
}
/**
* Replace whatever is in the current contributionsFragmentContainer view with
* mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects
@ -782,7 +810,8 @@ public class ContributionsFragment
public boolean backButtonClicked() {
if (mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible()) {
if (store.getBoolean("displayNearbyCardView", true) && !isUserProfile) {
if (binding.cardViewNearby.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) {
if (binding.cardViewNearby.cardViewVisibilityState
== NearbyNotificationCardView.CardViewVisibilityState.READY) {
binding.cardViewNearby.setVisibility(View.VISIBLE);
}
} else {
@ -829,6 +858,60 @@ public class ContributionsFragment
}
/**
* Restarts the upload process for a contribution
*
* @param contribution
*/
public void restartUpload(Contribution contribution) {
contribution.setDateUploadStarted(Calendar.getInstance().getTime());
if (contribution.getState() == Contribution.STATE_FAILED) {
if (contribution.getErrorInfo() == null) {
contribution.setChunkInfo(null);
contribution.setTransferred(0);
}
contributionsPresenter.checkDuplicateImageAndRestartContribution(contribution);
} else {
contribution.setState(Contribution.STATE_QUEUED);
contributionsPresenter.saveContribution(contribution);
Timber.d("Restarting for %s", contribution.toString());
}
}
/**
* Retry upload when it is failed
*
* @param contribution contribution to be retried
*/
public void retryUpload(Contribution contribution) {
if (NetworkUtils.isInternetConnectionEstablished(getContext())) {
if (contribution.getState() == STATE_PAUSED) {
restartUpload(contribution);
} else if (contribution.getState() == STATE_FAILED) {
int retries = contribution.getRetries();
// TODO: Improve UX. Additional details: https://github.com/commons-app/apps-android-commons/pull/5257#discussion_r1304662562
/* Limit the number of retries for a failed upload
to handle cases like invalid filename as such uploads
will never be successful */
if (retries < MAX_RETRIES) {
contribution.setRetries(retries + 1);
Timber.d("Retried uploading %s %d times", contribution.getMedia().getFilename(),
retries + 1);
restartUpload(contribution);
} else {
// TODO: Show the exact reason for failure
Toast.makeText(getContext(),
R.string.retry_limit_reached, Toast.LENGTH_SHORT).show();
}
} else {
Timber.d("Skipping re-upload for non-failed %s", contribution.toString());
}
} else {
ViewUtil.showLongToast(getContext(), R.string.this_function_needs_network_connection);
}
}
/**
* Reload media detail fragment once media is nominated
*
@ -844,21 +927,6 @@ public class ContributionsFragment
}
}
// click listener to toggle description that means uses can press the limited connection
// banner and description will hide. Tap again to show description.
private View.OnClickListener toggleDescriptionListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
View view2 = binding.limitedConnectionDescriptionTextView;
if (view2.getVisibility() == View.GONE) {
view2.setVisibility(View.VISIBLE);
} else {
view2.setVisibility(View.GONE);
}
}
};
/**
* When the device rotates, rotate the Nearby banner's compass arrow in tandem.
*/

View file

@ -70,16 +70,8 @@ public class ContributionsListAdapter extends
public interface Callback {
void retryUpload(Contribution contribution);
void deleteUpload(Contribution contribution);
void openMediaDetail(int contribution, boolean isWikipediaPageExists);
void addImageToWikipedia(Contribution contribution);
void pauseUpload(Contribution contribution);
void resumeUpload(Contribution contribution);
}
}

View file

@ -17,7 +17,5 @@ public class ContributionsListContract {
}
public interface UserActionListener extends BasePresenter<View> {
void deleteUpload(Contribution contribution);
}
}

View file

@ -19,7 +19,7 @@ import android.view.animation.AnimationUtils;
import android.widget.LinearLayout;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@ -30,11 +30,11 @@ import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
import androidx.recyclerview.widget.RecyclerView.ItemAnimator;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import androidx.recyclerview.widget.SimpleItemAnimator;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.databinding.FragmentContributionsListBinding;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.media.MediaClient;
@ -42,7 +42,6 @@ import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.SystemThemeUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import javax.inject.Inject;
@ -56,7 +55,7 @@ import fr.free.nrw.commons.wikidata.model.WikiSite;
*/
public class ContributionsListFragment extends CommonsDaggerSupportFragment implements
ContributionsListContract.View, ContributionsListAdapter.Callback,
ContributionsListContract.View, Callback,
WikipediaInstructionsDialogFragment.Callback {
private static final String RV_STATE = "rv_scroll_state";
@ -81,7 +80,6 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
private Animation rotate_forward;
private Animation rotate_backward;
private boolean isFabOpen;
@VisibleForTesting
protected RecyclerView rvContributionsList;
@ -99,7 +97,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
private String userName;
private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestMultiplePermissions(),
new RequestMultiplePermissions(),
new ActivityResultCallback<Map<String, Boolean>>() {
@Override
public void onActivityResult(Map<String, Boolean> result) {
@ -151,7 +149,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
contributionsListPresenter.onAttachView(this);
binding.fabCustomGallery.setOnClickListener(v -> launchCustomSelector());
binding.fabCustomGallery.setOnLongClickListener(view -> {
ViewUtil.showShortToast(getContext(),R.string.custom_selector_title);
ViewUtil.showShortToast(getContext(), R.string.custom_selector_title);
return true;
});
@ -160,7 +158,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
binding.fabLayout.setVisibility(VISIBLE);
} else {
binding.tvContributionsOfUser.setVisibility(VISIBLE);
binding.tvContributionsOfUser.setText(getString(R.string.contributions_of_user, userName));
binding.tvContributionsOfUser.setText(
getString(R.string.contributions_of_user, userName));
binding.fabLayout.setVisibility(GONE);
}
@ -305,7 +304,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
public void onConfigurationChanged(final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// check orientation
binding.fabLayout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
binding.fabLayout.setOrientation(
newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
rvContributionsList
.setLayoutManager(
@ -326,7 +326,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
animateFAB(isFabOpen);
});
binding.fabCamera.setOnLongClickListener(view -> {
ViewUtil.showShortToast(getContext(),R.string.add_contribution_from_camera);
ViewUtil.showShortToast(getContext(), R.string.add_contribution_from_camera);
return true;
});
binding.fabGallery.setOnClickListener(view -> {
@ -334,7 +334,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
animateFAB(isFabOpen);
});
binding.fabGallery.setOnLongClickListener(view -> {
ViewUtil.showShortToast(getContext(),R.string.menu_from_gallery);
ViewUtil.showShortToast(getContext(), R.string.menu_from_gallery);
return true;
});
}
@ -415,30 +415,6 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
}
}
@Override
public void retryUpload(final Contribution contribution) {
if (null != callback) {//Just being safe, ideally they won't be called when detached
callback.retryUpload(contribution);
}
}
@Override
public void deleteUpload(final Contribution contribution) {
DialogUtil.showAlertDialog(getActivity(),
String.format(Locale.getDefault(),
getString(R.string.cancelling_upload)),
String.format(Locale.getDefault(),
getString(R.string.cancel_upload_dialog)),
String.format(Locale.getDefault(), getString(R.string.yes)), String.format(Locale.getDefault(), getString(R.string.no)),
() -> {
ViewUtil.showShortToast(getContext(), R.string.cancelling_upload);
contributionsListPresenter.deleteUpload(contribution);
CommonsApplication.cancelledUploads.add(contribution.getPageId());
}, () -> {
// Do nothing
});
}
@Override
public void openMediaDetail(final int position, boolean isWikipediaButtonDisplayed) {
if (null != callback) {//Just being safe, ideally they won't be called when detached
@ -463,28 +439,6 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
});
}
/**
* Pauses the current upload
*
* @param contribution
*/
@Override
public void pauseUpload(Contribution contribution) {
ViewUtil.showShortToast(getContext(), R.string.pausing_upload);
callback.pauseUpload(contribution);
}
/**
* Resumes the current upload
*
* @param contribution
*/
@Override
public void resumeUpload(Contribution contribution) {
ViewUtil.showShortToast(getContext(), R.string.resuming_upload);
callback.retryUpload(contribution);
}
/**
* Display confirmation dialog with instructions when the user tries to add image to wikipedia
*
@ -536,13 +490,10 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
void notifyDataSetChanged();
void retryUpload(Contribution contribution);
void showDetail(int position, boolean isWikipediaButtonDisplayed);
void pauseUpload(Contribution contribution);
// Notify the viewpager that number of items have changed.
void viewPagerNotifyDataSetChanged();
}
}

View file

@ -10,6 +10,8 @@ import fr.free.nrw.commons.contributions.ContributionsListContract.UserActionLis
import fr.free.nrw.commons.di.CommonsApplicationModule;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import java.util.Arrays;
import java.util.Collections;
import javax.inject.Inject;
import javax.inject.Named;
@ -36,7 +38,7 @@ public class ContributionsListPresenter implements UserActionListener {
this.contributionBoundaryCallback = contributionBoundaryCallback;
this.repository = repository;
this.ioThreadScheduler = ioThreadScheduler;
this.contributionsRemoteDataSource=contributionsRemoteDataSource;
this.contributionsRemoteDataSource = contributionsRemoteDataSource;
compositeDisposable = new CompositeDisposable();
}
@ -71,10 +73,12 @@ public class ContributionsListPresenter implements UserActionListener {
} else {
contributionBoundaryCallback.setUserName(userName);
shouldSetBoundaryCallback = true;
factory = repository.fetchContributions();
factory = repository.fetchContributionsWithStates(
Collections.singletonList(Contribution.STATE_COMPLETED));
}
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory, pagedListConfig);
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
pagedListConfig);
if (shouldSetBoundaryCallback) {
livePagedListBuilder.setBoundaryCallback(contributionBoundaryCallback);
}
@ -89,15 +93,4 @@ public class ContributionsListPresenter implements UserActionListener {
contributionBoundaryCallback.dispose();
}
/**
* Delete a failed contribution from the local db
*/
@Override
public void deleteUpload(final Contribution contribution) {
compositeDisposable.add(repository
.deleteContributionFromDB(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
}

View file

@ -1,16 +1,14 @@
package fr.free.nrw.commons.contributions;
import androidx.paging.DataSource.Factory;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import io.reactivex.Completable;
import io.reactivex.Single;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import io.reactivex.Single;
/**
* The LocalDataSource class for Contributions
*/
@ -43,12 +41,14 @@ class ContributionsLocalDataSource {
/**
* Get contribution object from cursor
*
* @param uri
* @return
*/
public Contribution getContributionWithFileName(final String uri) {
final List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle(uri);
if(!contributionWithUri.isEmpty()){
final List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle(
uri);
if (!contributionWithUri.isEmpty()) {
return contributionWithUri.get(0);
}
return null;
@ -56,6 +56,7 @@ class ContributionsLocalDataSource {
/**
* Remove a contribution from the contributions table
*
* @param contribution
* @return
*/
@ -63,15 +64,48 @@ class ContributionsLocalDataSource {
return contributionDao.delete(contribution);
}
/**
* Deletes contributions with specific states.
*
* @param states The states of the contributions to delete.
* @return A Completable indicating the result of the operation.
*/
public Completable deleteContributionsWithStates(List<Integer> states) {
return contributionDao.deleteContributionsWithStates(states);
}
public Factory<Integer, Contribution> getContributions() {
return contributionDao.fetchContributions();
}
/**
* Fetches contributions with specific states.
*
* @param states The states of the contributions to fetch.
* @return A DataSource factory for paginated contributions with the specified states.
*/
public Factory<Integer, Contribution> getContributionsWithStates(List<Integer> states) {
return contributionDao.getContributions(states);
}
/**
* Fetches contributions with specific states sorted by the date the upload started.
*
* @param states The states of the contributions to fetch.
* @return A DataSource factory for paginated contributions with the specified states sorted by
* date upload started.
*/
public Factory<Integer, Contribution> getContributionsWithStatesSortedByDateUploadStarted(
List<Integer> states) {
return contributionDao.getContributionsSortedByDateUploadStarted(states);
}
public Single<List<Long>> saveContributions(final List<Contribution> contributions) {
final List<Contribution> contributionList = new ArrayList<>();
for(final Contribution contribution: contributions) {
final Contribution oldContribution = contributionDao.getContribution(contribution.getPageId());
if(oldContribution != null) {
for (final Contribution contribution : contributions) {
final Contribution oldContribution = contributionDao.getContribution(
contribution.getPageId());
if (oldContribution != null) {
contribution.setWikidataPlace(oldContribution.getWikidataPlace());
}
contributionList.add(contribution);
@ -84,10 +118,14 @@ class ContributionsLocalDataSource {
}
public void set(final String key, final long value) {
defaultKVStore.putLong(key,value);
defaultKVStore.putLong(key, value);
}
public Completable updateContribution(final Contribution contribution) {
return contributionDao.update(contribution);
}
public Completable updateContributionsWithStates(List<Integer> states, int newState) {
return contributionDao.updateContributionsWithStates(states, newState);
}
}

View file

@ -1,21 +1,26 @@
package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
import androidx.work.ExistingWorkPolicy;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.repository.UploadRepository;
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
/**
* The presenter class for Contributions
*/
public class ContributionsPresenter implements UserActionListener {
private final ContributionsRepository repository;
private final ContributionsRepository contributionsRepository;
private final UploadRepository uploadRepository;
private final Scheduler ioThreadScheduler;
private CompositeDisposable compositeDisposable;
private ContributionsContract.View view;
@ -25,15 +30,17 @@ public class ContributionsPresenter implements UserActionListener {
@Inject
ContributionsPresenter(ContributionsRepository repository,
UploadRepository uploadRepository,
@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) {
this.repository = repository;
this.ioThreadScheduler=ioThreadScheduler;
this.contributionsRepository = repository;
this.uploadRepository = uploadRepository;
this.ioThreadScheduler = ioThreadScheduler;
}
@Override
public void onAttachView(ContributionsContract.View view) {
this.view = view;
compositeDisposable=new CompositeDisposable();
compositeDisposable = new CompositeDisposable();
}
@Override
@ -44,20 +51,31 @@ public class ContributionsPresenter implements UserActionListener {
@Override
public Contribution getContributionsWithTitle(String title) {
return repository.getContributionWithFileName(title);
return contributionsRepository.getContributionWithFileName(title);
}
/**
* Delete a failed contribution from the local db
* @param contribution
* Checks if a contribution is a duplicate and restarts the contribution process if it is not.
*
* @param contribution The contribution to check and potentially restart.
*/
@Override
public void deleteUpload(Contribution contribution) {
compositeDisposable.add(repository
public void checkDuplicateImageAndRestartContribution(Contribution contribution) {
compositeDisposable.add(uploadRepository
.checkDuplicateImage(contribution.getLocalUriPath().getPath())
.subscribeOn(ioThreadScheduler)
.subscribe(imageCheckResult -> {
if (imageCheckResult == IMAGE_OK) {
contribution.setState(Contribution.STATE_QUEUED);
saveContribution(contribution);
} else {
Timber.e("Contribution already exists");
compositeDisposable.add(contributionsRepository
.deleteContributionFromDB(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
}));
}
/**
* Update the contribution's state in the databse, upon completion, trigger the workmanager to
@ -65,9 +83,8 @@ public class ContributionsPresenter implements UserActionListener {
*
* @param contribution
*/
@Override
public void saveContribution(Contribution contribution) {
compositeDisposable.add(repository
compositeDisposable.add(contributionsRepository
.save(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest(

View file

@ -29,6 +29,7 @@ public class ContributionsRepository {
/**
* Deletes a failed upload from DB
*
* @param contribution
* @return
*/
@ -36,8 +37,19 @@ public class ContributionsRepository {
return localDataSource.deleteContribution(contribution);
}
/**
* Deletes contributions from the database with specific states.
*
* @param states The states of the contributions to delete.
* @return A Completable indicating the result of the operation.
*/
public Completable deleteContributionsFromDBWithStates(List<Integer> states) {
return localDataSource.deleteContributionsWithStates(states);
}
/**
* Get contribution object with title
*
* @param fileName
* @return
*/
@ -49,19 +61,52 @@ public class ContributionsRepository {
return localDataSource.getContributions();
}
/**
* Fetches contributions with specific states.
*
* @param states The states of the contributions to fetch.
* @return A DataSource factory for paginated contributions with the specified states.
*/
public Factory<Integer, Contribution> fetchContributionsWithStates(List<Integer> states) {
return localDataSource.getContributionsWithStates(states);
}
/**
* Fetches contributions with specific states sorted by the date the upload started.
*
* @param states The states of the contributions to fetch.
* @return A DataSource factory for paginated contributions with the specified states sorted by
* date upload started.
*/
public Factory<Integer, Contribution> fetchContributionsWithStatesSortedByDateUploadStarted(
List<Integer> states) {
return localDataSource.getContributionsWithStatesSortedByDateUploadStarted(states);
}
public Single<List<Long>> save(List<Contribution> contributions) {
return localDataSource.saveContributions(contributions);
}
public Completable save(Contribution contributions){
public Completable save(Contribution contributions) {
return localDataSource.saveContributions(contributions);
}
public void set(String key, long value) {
localDataSource.set(key,value);
localDataSource.set(key, value);
}
public Completable updateContribution(Contribution contribution) {
return localDataSource.updateContribution(contribution);
}
/**
* Updates the state of contributions with specific states.
*
* @param states The current states of the contributions to update.
* @param newState The new state to set.
* @return A Completable indicating the result of the operation.
*/
public Completable updateContributionsWithStates(List<Integer> states, int newState) {
return localDataSource.updateContributionsWithStates(states, newState);
}
}

View file

@ -41,11 +41,14 @@ import fr.free.nrw.commons.notification.NotificationController;
import fr.free.nrw.commons.quiz.QuizChecker;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.upload.UploadActivity;
import fr.free.nrw.commons.upload.UploadProgressActivity;
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Completable;
import io.reactivex.schedulers.Schedulers;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
@ -144,16 +147,16 @@ public class MainActivity extends BaseActivity
applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false);
applicationKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", false);
}
if(savedInstanceState == null){
if (savedInstanceState == null) {
//starting a fresh fragment.
// Open Last opened screen if it is Contributions or Nearby, otherwise Contributions
if(applicationKvStore.getBoolean("last_opened_nearby")){
if (applicationKvStore.getBoolean("last_opened_nearby")) {
setTitle(getString(R.string.nearby_fragment));
showNearby();
loadFragment(NearbyParentFragment.newInstance(),false);
}else{
loadFragment(NearbyParentFragment.newInstance(), false);
} else {
setTitle(getString(R.string.contributions_fragment));
loadFragment(ContributionsFragment.newInstance(),false);
loadFragment(ContributionsFragment.newInstance(), false);
}
}
setUpPager();
@ -165,7 +168,8 @@ public class MainActivity extends BaseActivity
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
PermissionUtils.checkPermissionsAndPerformAction(
this,
() -> {},
() -> {
},
R.string.media_location_permission_denied,
R.string.add_location_manually,
permission.ACCESS_MEDIA_LOCATION);
@ -179,7 +183,8 @@ public class MainActivity extends BaseActivity
}
private void setUpPager() {
binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(navListener = (item) -> {
binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(
navListener = (item) -> {
if (!item.getTitle().equals(getString(R.string.more))) {
// do not change title for more fragment
setTitle(item.getTitle());
@ -193,18 +198,18 @@ public class MainActivity extends BaseActivity
}
private void setUpLoggedOutPager() {
loadFragment(ExploreFragment.newInstance(),false);
loadFragment(ExploreFragment.newInstance(), false);
binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(item -> {
if (!item.getTitle().equals(getString(R.string.more))) {
// do not change title for more fragment
setTitle(item.getTitle());
}
Fragment fragment = NavTabLoggedOut.of(item.getOrder()).newInstance();
return loadFragment(fragment,true);
return loadFragment(fragment, true);
});
}
private boolean loadFragment(Fragment fragment,boolean showBottom ) {
private boolean loadFragment(Fragment fragment, boolean showBottom) {
//showBottom so that we do not show the bottom tray again when constructing
//from the saved instance state.
if (fragment instanceof ContributionsFragment) {
@ -234,7 +239,8 @@ public class MainActivity extends BaseActivity
bookmarkFragment = (BookmarkFragment) fragment;
activeFragment = ActiveFragment.BOOKMARK;
} else if (fragment == null && showBottom) {
if (applicationKvStore.getBoolean("login_skipped") == true) { // If logged out, more sheet is different
if (applicationKvStore.getBoolean("login_skipped")
== true) { // If logged out, more sheet is different
MoreBottomSheetLoggedOutFragment bottomSheet = new MoreBottomSheetLoggedOutFragment();
bottomSheet.show(getSupportFragmentManager(),
"MoreBottomSheetLoggedOut");
@ -264,28 +270,30 @@ public class MainActivity extends BaseActivity
}
/**
* Adds number of uploads next to tab text "Contributions" then it will look like
* "Contributions (NUMBER)"
* Adds number of uploads next to tab text "Contributions" then it will look like "Contributions
* (NUMBER)"
*
* @param uploadCount
*/
public void setNumOfUploads(int uploadCount) {
if (activeFragment == ActiveFragment.CONTRIBUTIONS) {
setTitle(getResources().getString(R.string.contributions_fragment) +" "+ (
setTitle(getResources().getString(R.string.contributions_fragment) + " " + (
!(uploadCount == 0) ?
getResources()
.getQuantityString(R.plurals.contributions_subtitle,
uploadCount, uploadCount):getString(R.string.contributions_subtitle_zero)));
uploadCount, uploadCount)
: getString(R.string.contributions_subtitle_zero)));
}
}
/**
* Resume the uploads that got stuck because of the app being killed
* or the device being rebooted.
*
* Resume the uploads that got stuck because of the app being killed or the device being
* rebooted.
* <p>
* When the app is terminated or the device is restarted, contributions remain in the
* 'STATE_IN_PROGRESS' state. This status persists and doesn't change during these events.
* So, retrieving contributions labeled as 'STATE_IN_PROGRESS'
* from the database will provide the list of uploads that appear as stuck on opening the app again
* 'STATE_IN_PROGRESS' state. This status persists and doesn't change during these events. So,
* retrieving contributions labeled as 'STATE_IN_PROGRESS' from the database will provide the
* list of uploads that appear as stuck on opening the app again
*/
@SuppressLint("CheckResult")
private void checkAndResumeStuckUploads() {
@ -294,9 +302,10 @@ public class MainActivity extends BaseActivity
.subscribeOn(Schedulers.io())
.blockingGet();
Timber.d("Resuming " + stuckUploads.size() + " uploads...");
if(!stuckUploads.isEmpty()) {
for(Contribution contribution: stuckUploads) {
if (!stuckUploads.isEmpty()) {
for (Contribution contribution : stuckUploads) {
contribution.setState(Contribution.STATE_QUEUED);
contribution.setDateUploadStarted(Calendar.getInstance().getTime());
Completable.fromAction(() -> contributionDao.saveSynchronous(contribution))
.subscribeOn(Schedulers.io())
.subscribe();
@ -323,24 +332,24 @@ public class MainActivity extends BaseActivity
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
String activeFragmentName = savedInstanceState.getString("activeFragment");
if(activeFragmentName != null) {
if (activeFragmentName != null) {
restoreActiveFragment(activeFragmentName);
}
}
private void restoreActiveFragment(@NonNull String fragmentName) {
if(fragmentName.equals(ActiveFragment.CONTRIBUTIONS.name())) {
if (fragmentName.equals(ActiveFragment.CONTRIBUTIONS.name())) {
setTitle(getString(R.string.contributions_fragment));
loadFragment(ContributionsFragment.newInstance(),false);
}else if(fragmentName.equals(ActiveFragment.NEARBY.name())) {
loadFragment(ContributionsFragment.newInstance(), false);
} else if (fragmentName.equals(ActiveFragment.NEARBY.name())) {
setTitle(getString(R.string.nearby_fragment));
loadFragment(NearbyParentFragment.newInstance(),false);
}else if(fragmentName.equals(ActiveFragment.EXPLORE.name())) {
loadFragment(NearbyParentFragment.newInstance(), false);
} else if (fragmentName.equals(ActiveFragment.EXPLORE.name())) {
setTitle(getString(R.string.navigation_item_explore));
loadFragment(ExploreFragment.newInstance(),false);
}else if(fragmentName.equals(ActiveFragment.BOOKMARK.name())) {
loadFragment(ExploreFragment.newInstance(), false);
} else if (fragmentName.equals(ActiveFragment.BOOKMARK.name())) {
setTitle(getString(R.string.bookmarks));
loadFragment(BookmarkFragment.newInstance(),false);
loadFragment(BookmarkFragment.newInstance(), false);
}
}
@ -356,8 +365,9 @@ public class MainActivity extends BaseActivity
// Means that nearby fragment is visible
/* If function nearbyParentFragment.backButtonClick() returns false, it means that the bottomsheet is
not expanded. So if the back button is pressed, then go back to the Contributions tab */
if(!nearbyParentFragment.backButtonClicked()){
getSupportFragmentManager().beginTransaction().remove(nearbyParentFragment).commit();
if (!nearbyParentFragment.backButtonClicked()) {
getSupportFragmentManager().beginTransaction().remove(nearbyParentFragment)
.commit();
setSelectedItemId(NavTab.CONTRIBUTIONS.code());
}
} else if (exploreFragment != null && activeFragment == ActiveFragment.EXPLORE) {
@ -382,18 +392,6 @@ public class MainActivity extends BaseActivity
//initBackButton();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.notifications:
// Starts notification activity on click to notification icon
NotificationActivity.startYourself(this, "unread");
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* Retry all failed uploads as soon as the user returns to the app
*/
@ -403,31 +401,35 @@ public class MainActivity extends BaseActivity
getContribution(Collections.singletonList(Contribution.STATE_FAILED))
.subscribeOn(Schedulers.io())
.subscribe(failedUploads -> {
for (Contribution contribution: failedUploads) {
for (Contribution contribution : failedUploads) {
contributionsFragment.retryUpload(contribution);
}
});
}
public void toggleLimitedConnectionMode() {
defaultKvStore.putBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
!defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false));
if (defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
viewUtilWrapper
.showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled));
} else {
WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(),
ExistingWorkPolicy.APPEND_OR_REPLACE);
viewUtilWrapper
.showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled));
/**
* Handles item selection in the options menu. This method is called when a user interacts with
* the options menu in the Top Bar.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.upload_tab:
startActivity(new Intent(this, UploadProgressActivity.class));
return true;
case R.id.notifications:
// Starts notification activity on click to notification icon
NotificationActivity.startYourself(this, "unread");
return true;
default:
return super.onOptionsItemSelected(item);
}
}
public void centerMapToPlace(Place place) {
setSelectedItemId(NavTab.NEARBY.code());
nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback(new NearbyParentFragmentInstanceReadyCallback() {
nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback(
new NearbyParentFragmentInstanceReadyCallback() {
@Override
public void onReady() {
nearbyParentFragment.centerMapToPlace(place);
@ -437,7 +439,7 @@ public class MainActivity extends BaseActivity
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
Timber.d(data!=null?data.toString():"onActivityResult data is null");
Timber.d(data != null ? data.toString() : "onActivityResult data is null");
super.onActivityResult(requestCode, resultCode, data);
controller.handleActivityResult(this, requestCode, resultCode, data);
}
@ -482,14 +484,15 @@ public class MainActivity extends BaseActivity
/**
* Load default language in onCreate from SharedPreferences
*/
private void loadLocale(){
final SharedPreferences preferences = getSharedPreferences("Settings", Activity.MODE_PRIVATE);
private void loadLocale() {
final SharedPreferences preferences = getSharedPreferences("Settings",
Activity.MODE_PRIVATE);
final String language = preferences.getString("language", "");
final SettingsFragment settingsFragment = new SettingsFragment();
settingsFragment.setLocale(this, language);
}
public NavTabLayout.OnNavigationItemSelectedListener getNavListener(){
public NavTabLayout.OnNavigationItemSelectedListener getNavListener() {
return navListener;
}
}

View file

@ -20,6 +20,7 @@ import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.review.ReviewActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
import fr.free.nrw.commons.upload.UploadActivity;
import fr.free.nrw.commons.upload.UploadProgressActivity;
/**
* This Class handles the dependency injection (using dagger)
@ -81,6 +82,9 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract ZoomableActivity bindZoomableActivity();
@ContributesAndroidInjector
abstract UploadProgressActivity bindUploadProgressActivity();
@ContributesAndroidInjector
abstract WikidataFeedback bindWikiFeedback();
}

View file

@ -34,6 +34,8 @@ import fr.free.nrw.commons.profile.achievements.AchievementsFragment;
import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment;
import fr.free.nrw.commons.review.ReviewImageFragment;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.upload.FailedUploadsFragment;
import fr.free.nrw.commons.upload.PendingUploadsFragment;
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
import fr.free.nrw.commons.upload.depicts.DepictsFragment;
import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
@ -155,4 +157,10 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract LeaderboardFragment bindLeaderboardFragment();
@ContributesAndroidInjector
abstract PendingUploadsFragment bindPendingUploadsFragment();
@ContributesAndroidInjector
abstract FailedUploadsFragment bindFailedUploadsFragment();
}

View file

@ -306,7 +306,6 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
if (uploadCount==0){
setZeroAchievements();
}else {
binding.imagesUploadedProgressbar.setVisibility(View.VISIBLE);
binding.imagesUploadedProgressbar.setProgress
(100*uploadCount/levelInfo.getMaxUploadCount());
@ -326,9 +325,9 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
getString(R.string.ok),
() -> {},
true);
binding.imagesUploadedProgressbar.setVisibility(View.INVISIBLE);
binding.imageRevertsProgressbar.setVisibility(View.INVISIBLE);
binding.imagesUsedByWikiProgressBar.setVisibility(View.INVISIBLE);
// binding.imagesUploadedProgressbar.setVisibility(View.INVISIBLE);
// binding.imageRevertsProgressbar.setVisibility(View.INVISIBLE);
// binding.imagesUsedByWikiProgressBar.setVisibility(View.INVISIBLE);
binding.achievementBadgeImage.setVisibility(View.INVISIBLE);
binding.imagesUsedByWikiText.setText(R.string.no_image);
binding.imagesRevertedText.setText(R.string.no_image_reverted);
@ -354,7 +353,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment {
* @param achievements
*/
private void inflateAchievements(Achievements achievements) {
binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE);
// binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE);
binding.thanksReceived.setText(String.valueOf(achievements.getThanksReceived()));
binding.imagesUsedByWikiProgressBar.setProgress
(100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages());

View file

@ -203,6 +203,16 @@ public class UploadRepository {
return uploadModel.getImageQuality(uploadItem, location);
}
/**
* query the RemoteDataSource for image duplicity check
*
* @param filePath file to be checked
* @return IMAGE_DUPLICATE or IMAGE_OK
*/
public Single<Integer> checkDuplicateImage(String filePath) {
return uploadModel.checkDuplicateImage(filePath);
}
/**
* query the RemoteDataSource for caption quality
*

View file

@ -0,0 +1,139 @@
package fr.free.nrw.commons.upload
import android.net.Uri
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.URLUtil
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.facebook.imagepipeline.request.ImageRequest
import fr.free.nrw.commons.R
import fr.free.nrw.commons.contributions.Contribution
import java.io.File
/**
* Adapter for displaying failed uploads in a paginated list in FailedUploadsFragment. This adapter
* binds the data from [Contribution] objects to the item views in the RecyclerView, allowing users to view
* details of failed uploads, retry them, or delete them.
*
* @param callback The callback to handle user actions such as Delete Uploads and Restart Uploads
* on failed uploads.
*/
class FailedUploadsAdapter(callback: Callback) :
PagedListAdapter<Contribution, FailedUploadsAdapter.ViewHolder>(ContributionDiffCallback()) {
private var callback: Callback = callback
/**
* Creates a new ViewHolder instance. Inflates the layout for each item in the list.
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view: View =
LayoutInflater.from(parent.context).inflate(R.layout.item_failed_upload, parent, false)
return ViewHolder(view)
}
/**
* Binds data to the provided ViewHolder. Sets up the item view with data from the
* contribution at the specified position.
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item: Contribution? = getItem(position)
if (item != null) {
holder.titleTextView.setText(item.media.displayTitle)
}
var imageRequest: ImageRequest? = null
val imageSource: String = item?.localUri.toString()
if (!TextUtils.isEmpty(imageSource)) {
if (URLUtil.isFileUrl(imageSource)) {
imageRequest = ImageRequest.fromUri(Uri.parse(imageSource))!!
} else if (imageSource != null) {
val file = File(imageSource)
imageRequest = ImageRequest.fromFile(file)!!
}
if (imageRequest != null) {
holder.itemImage.setImageRequest(imageRequest)
}
}
if (item != null) {
if (item.state == Contribution.STATE_FAILED) {
if (item.errorInfo != null) {
holder.errorTextView.setText(item.errorInfo)
} else {
holder.errorTextView.setText("Failed")
}
holder.errorTextView.visibility = View.VISIBLE
holder.itemProgress.visibility = View.GONE
}
}
holder.deleteButton.setOnClickListener {
callback.deleteUpload(item)
}
holder.retryButton.setOnClickListener {
callback.restartUpload(position)
}
holder.itemImage.setImageRequest(imageRequest)
}
/**
* ViewHolder for the failed upload item. Holds references to the views for each item.
*/
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var itemImage: com.facebook.drawee.view.SimpleDraweeView =
itemView.findViewById(R.id.itemImage)
var titleTextView: TextView = itemView.findViewById<TextView>(R.id.titleTextView)
var itemProgress: ProgressBar = itemView.findViewById<ProgressBar>(R.id.itemProgress)
var errorTextView: TextView = itemView.findViewById<TextView>(R.id.errorTextView)
var deleteButton: ImageView = itemView.findViewById(R.id.deleteButton)
var retryButton: ImageView = itemView.findViewById(R.id.retryButton)
}
/**
* Returns the ID of the item at the specified position. Uses the pageId of the contribution
* for unique identification.
*/
override fun getItemId(position: Int): Long {
return getItem(position)?.pageId?.hashCode()?.toLong() ?: position.toLong()
}
/**
* Uses DiffUtil to calculate the changes in the list
* It has methods that check pageId and the content of the items to determine if its a new item
*/
class ContributionDiffCallback : DiffUtil.ItemCallback<Contribution>() {
override fun areItemsTheSame(oldItem: Contribution, newItem: Contribution): Boolean {
return oldItem.pageId.hashCode() == newItem.pageId.hashCode()
}
override fun areContentsTheSame(oldItem: Contribution, newItem: Contribution): Boolean {
return oldItem.transferred == newItem.transferred
}
}
/**
* Callback interface for handling actions related to failed uploads.
*/
interface Callback {
/**
* Deletes the failed upload item.
*
* @param contribution to be deleted.
*/
fun deleteUpload(contribution: Contribution?)
/**
* Restarts the upload for the item at the specified index.
*
* @param index The position of the item in the list.
*/
fun restartUpload(index: Int)
}
}

View file

@ -0,0 +1,201 @@
package fr.free.nrw.commons.upload
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.databinding.FragmentFailedUploadsBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.profile.ProfileActivity
import fr.free.nrw.commons.utils.DialogUtil
import fr.free.nrw.commons.utils.ViewUtil
import org.apache.commons.lang3.StringUtils
import java.util.Locale
import javax.inject.Inject
/**
* Fragment for displaying a list of failed uploads in Upload Progress Activity. This fragment provides
* functionality for the user to retry or cancel failed uploads.
*/
class FailedUploadsFragment : CommonsDaggerSupportFragment(), PendingUploadsContract.View,
FailedUploadsAdapter.Callback {
@Inject
lateinit var pendingUploadsPresenter: PendingUploadsPresenter
@Inject
lateinit var mediaClient: MediaClient
@Inject
lateinit var sessionManager: SessionManager
private var userName: String? = null
lateinit var binding: FragmentFailedUploadsBinding
private lateinit var adapter: FailedUploadsAdapter
var contributionsList = ArrayList<Contribution>()
private lateinit var uploadProgressActivity: UploadProgressActivity
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is UploadProgressActivity) {
uploadProgressActivity = context
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//Now that we are allowing this fragment to be started for
// any userName- we expect it to be passed as an argument
if (arguments != null) {
userName = requireArguments().getString(ProfileActivity.KEY_USERNAME)
}
if (StringUtils.isEmpty(userName)) {
userName = sessionManager!!.getUserName()
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentFailedUploadsBinding.inflate(layoutInflater)
pendingUploadsPresenter.onAttachView(this)
initAdapter()
return binding.root
}
fun initAdapter() {
adapter = FailedUploadsAdapter(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initRecyclerView()
}
/**
* Initializes the recycler view.
*/
fun initRecyclerView() {
binding.failedUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context))
binding.failedUploadsRecyclerView.adapter = adapter
pendingUploadsPresenter!!.getFailedContributions()
pendingUploadsPresenter!!.failedContributionList.observe(
viewLifecycleOwner
) { list: PagedList<Contribution?> ->
adapter.submitList(list)
contributionsList = ArrayList()
list.forEach {
if (it != null) {
contributionsList.add(it)
}
}
if (list.size == 0) {
uploadProgressActivity.setErrorIconsVisibility(false)
binding.nofailedTextView.visibility = View.VISIBLE
binding.failedUplaodsLl.visibility = View.GONE
} else {
uploadProgressActivity.setErrorIconsVisibility(true)
binding.nofailedTextView.visibility = View.GONE
binding.failedUplaodsLl.visibility = View.VISIBLE
binding.failedUploadsRecyclerView.setAdapter(adapter)
}
}
}
/**
* Restarts all the failed uploads.
*/
fun restartUploads() {
if (contributionsList != null) {
pendingUploadsPresenter.restartUploads(
contributionsList,
0,
this.requireContext().applicationContext
)
}
}
/**
* Restarts a specific upload.
*/
override fun restartUpload(index: Int) {
if (contributionsList != null) {
pendingUploadsPresenter.restartUpload(
contributionsList,
index,
this.requireContext().applicationContext
)
}
}
/**
* Deletes a specific upload after getting a confirmation from the user using Dialog.
*/
override fun deleteUpload(contribution: Contribution?) {
DialogUtil.showAlertDialog(
requireActivity(),
String.format(
Locale.getDefault(),
requireActivity().getString(R.string.cancelling_upload)
),
String.format(
Locale.getDefault(),
requireActivity().getString(R.string.cancel_upload_dialog)
),
String.format(Locale.getDefault(), requireActivity().getString(R.string.yes)),
String.format(Locale.getDefault(), requireActivity().getString(R.string.no)),
{
ViewUtil.showShortToast(context, R.string.cancelling_upload)
pendingUploadsPresenter.deleteUpload(
contribution,
this.requireContext().applicationContext
)
},
{}
)
}
/**
* Deletes all the uploads after getting a confirmation from the user using Dialog.
*/
fun deleteUploads() {
if (contributionsList != null) {
DialogUtil.showAlertDialog(
requireActivity(),
String.format(
Locale.getDefault(),
requireActivity().getString(R.string.cancelling_all_the_uploads)
),
String.format(
Locale.getDefault(),
requireActivity().getString(R.string.are_you_sure_that_you_want_cancel_all_the_uploads)
),
String.format(Locale.getDefault(), requireActivity().getString(R.string.yes)),
String.format(Locale.getDefault(), requireActivity().getString(R.string.no)),
{
ViewUtil.showShortToast(context, R.string.cancelling_upload)
uploadProgressActivity.hidePendingIcons()
pendingUploadsPresenter.deleteUploads(
listOf(Contribution.STATE_FAILED)
)
},
{}
)
}
}
}

View file

@ -140,7 +140,7 @@ public class ImageProcessingService {
* @param filePath file to be checked
* @return IMAGE_DUPLICATE or IMAGE_OK
*/
private Single<Integer> checkDuplicateImage(String filePath) {
Single<Integer> checkDuplicateImage(String filePath) {
Timber.d("Checking for duplicate image %s", filePath);
return Single.fromCallable(() -> fileUtilsWrapper.getFileInputStream(filePath))
.map(fileUtilsWrapper::getSHA1)

View file

@ -0,0 +1,229 @@
package fr.free.nrw.commons.upload
import android.net.Uri
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.URLUtil
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.facebook.imagepipeline.request.ImageRequest
import fr.free.nrw.commons.R
import fr.free.nrw.commons.contributions.Contribution
import timber.log.Timber
import java.io.File
/**
* Adapter for displaying pending uploads in a paginated list in PendingUploadsFragment. This adapter
* binds data from [Contribution] objects to the item views in the RecyclerView, allowing users to
* view details of pending uploads and perform actions such as deleting them.
*
* @param callback The callback to handle user actions such as Delete Uploads on pending uploads.
*/
class PendingUploadsAdapter(private val callback: Callback) :
PagedListAdapter<Contribution, PendingUploadsAdapter.ViewHolder>(ContributionDiffCallback()) {
/**
* Creates a new ViewHolder instance. Inflates the layout for each item in the list.
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view: View = LayoutInflater.from(parent.context)
.inflate(R.layout.item_pending_upload, parent, false)
return ViewHolder(view)
}
/**
* Binds data to the provided ViewHolder. Sets up the item view with data from the
* contribution at the specified position utilizing payloads.
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.isNotEmpty()) {
when (val latestPayload = payloads.lastOrNull()) {
is ContributionChangePayload.Progress -> holder.bindProgress(
latestPayload.transferred,
latestPayload.total,
getItem(position)!!.state
)
is ContributionChangePayload.State -> holder.bindState(latestPayload.state)
else -> onBindViewHolder(holder, position)
}
} else {
onBindViewHolder(holder, position)
}
}
/**
* Binds data to the provided ViewHolder. Sets up the item view with data from the
* contribution at the specified position.
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val contribution = getItem(position)
contribution?.let {
holder.bind(it)
holder.deleteButton.setOnClickListener {
callback.deleteUpload(contribution)
}
}
}
/**
* ViewHolder class for holding and binding item views.
*/
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var itemImage: com.facebook.drawee.view.SimpleDraweeView =
itemView.findViewById(R.id.itemImage)
var titleTextView: TextView = itemView.findViewById(R.id.titleTextView)
var itemProgress: ProgressBar = itemView.findViewById(R.id.itemProgress)
var errorTextView: TextView = itemView.findViewById(R.id.errorTextView)
var deleteButton: ImageView = itemView.findViewById(R.id.deleteButton)
fun bind(contribution: Contribution) {
titleTextView.text = contribution.media.displayTitle
val imageSource: String = contribution.localUri.toString()
var imageRequest: ImageRequest? = null
if (!TextUtils.isEmpty(imageSource)) {
if (URLUtil.isFileUrl(imageSource)) {
imageRequest = ImageRequest.fromUri(Uri.parse(imageSource))
} else {
val file = File(imageSource)
imageRequest = ImageRequest.fromFile(file)
}
}
if (imageRequest != null) {
itemImage.setImageRequest(imageRequest)
}
bindState(contribution.state)
bindProgress(contribution.transferred, contribution.dataLength, contribution.state)
}
fun bindState(state: Int) {
if (state == Contribution.STATE_QUEUED || state == Contribution.STATE_PAUSED) {
errorTextView.text = "Queued"
errorTextView.visibility = View.VISIBLE
itemProgress.visibility = View.GONE
} else {
errorTextView.visibility = View.GONE
itemProgress.visibility = View.VISIBLE
}
}
fun bindProgress(transferred: Long, total: Long, state: Int) {
if (transferred == 0L) {
errorTextView.text = "Queued"
errorTextView.visibility = View.VISIBLE
itemProgress.visibility = View.GONE
} else {
if (state == Contribution.STATE_QUEUED || state == Contribution.STATE_PAUSED) {
errorTextView.text = "Queued"
errorTextView.visibility = View.VISIBLE
itemProgress.visibility = View.GONE
} else {
errorTextView.visibility = View.GONE
itemProgress.visibility = View.VISIBLE
if (transferred >= total) {
itemProgress.isIndeterminate = true
} else {
itemProgress.isIndeterminate = false
itemProgress.progress =
((transferred.toDouble() / total.toDouble()) * 100).toInt()
}
}
}
}
}
/**
* Callback interface for handling actions related to failed uploads.
*/
interface Callback {
/**
* Deletes the failed upload item.
*
* @param contribution to be deleted.
*/
fun deleteUpload(contribution: Contribution?)
}
/**
* Uses DiffUtil and payloads to calculate the changes in the list
* It has methods that check pageId and the content of the items to determine if its a new item
*/
class ContributionDiffCallback : DiffUtil.ItemCallback<Contribution>() {
/**
* Checks if two items represent the same contribution.
* @param oldItem The old contribution item.
* @param newItem The new contribution item.
* @return True if the items are the same, false otherwise.
*/
override fun areItemsTheSame(oldItem: Contribution, newItem: Contribution): Boolean {
return oldItem.pageId.hashCode() == newItem.pageId.hashCode()
}
/**
* Checks if the content of two items is the same.
* @param oldItem The old contribution item.
* @param newItem The new contribution item.
* @return True if the contents are the same, false otherwise.
*/
override fun areContentsTheSame(oldItem: Contribution, newItem: Contribution): Boolean {
return oldItem.transferred == newItem.transferred
}
/**
* Returns a payload representing the change between the old and new items.
* @param oldItem The old contribution item.
* @param newItem The new contribution item.
* @return An object representing the change, or null if there are no changes.
*/
override fun getChangePayload(oldItem: Contribution, newItem: Contribution): Any? {
return when {
oldItem.transferred != newItem.transferred -> {
ContributionChangePayload.Progress(newItem.transferred, newItem.dataLength)
}
oldItem.state != newItem.state -> {
ContributionChangePayload.State(newItem.state)
}
else -> super.getChangePayload(oldItem, newItem)
}
}
}
/**
* Returns the unique item ID for the contribution at the specified position.
* @param position The position of the item.
* @return The unique item ID.
*/
override fun getItemId(position: Int): Long {
return getItem(position)?.pageId?.hashCode()?.toLong() ?: position.toLong()
}
/**
* Sealed interface representing different types of changes to a contribution.
*/
private sealed interface ContributionChangePayload {
/**
* Represents a change in the progress of a contribution.
* @param transferred The amount of data transferred.
* @param total The total amount of data.
*/
data class Progress(val transferred: Long, val total: Long) : ContributionChangePayload
/**
* Represents a change in the state of a contribution.
* @param state The state of the contribution.
*/
data class State(val state: Int) : ContributionChangePayload
}
}

View file

@ -0,0 +1,31 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract.View;
/**
* The contract using which the PendingUploadsFragment or FailedUploadsFragment would communicate
* with its PendingUploadsPresenter
*/
public class PendingUploadsContract {
/**
* Interface representing the view for uploads.
*/
public interface View { }
/**
* Interface representing the user actions related to uploads.
*/
public interface UserActionListener extends
BasePresenter<fr.free.nrw.commons.upload.PendingUploadsContract.View> {
/**
* Deletes a upload.
*/
void deleteUpload(Contribution contribution, Context context);
}
}

View file

@ -0,0 +1,200 @@
package fr.free.nrw.commons.upload
import android.content.Context
import android.os.AsyncTask
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.paging.PagedList
import androidx.paging.PositionalDataSource
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.databinding.FragmentPendingUploadsBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.profile.ProfileActivity
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
import fr.free.nrw.commons.utils.ViewUtil
import org.apache.commons.lang3.StringUtils
import timber.log.Timber
import java.util.Locale
import javax.inject.Inject
/**
* Fragment for showing pending uploads in Upload Progress Activity. This fragment provides
* functionality for the user to pause uploads.
*/
class PendingUploadsFragment : CommonsDaggerSupportFragment(), PendingUploadsContract.View,
PendingUploadsAdapter.Callback {
@Inject
lateinit var pendingUploadsPresenter: PendingUploadsPresenter
private lateinit var binding: FragmentPendingUploadsBinding
private lateinit var uploadProgressActivity: UploadProgressActivity
private lateinit var adapter: PendingUploadsAdapter
private var contributionsSize = 0
var contributionsList = ArrayList<Contribution>()
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is UploadProgressActivity) {
uploadProgressActivity = context
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreate(savedInstanceState)
binding = FragmentPendingUploadsBinding.inflate(inflater, container, false)
pendingUploadsPresenter.onAttachView(this)
initAdapter()
return binding.root
}
fun initAdapter() {
adapter = PendingUploadsAdapter(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initRecyclerView()
}
/**
* Initializes the recycler view.
*/
fun initRecyclerView() {
binding.pendingUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context))
binding.pendingUploadsRecyclerView.adapter = adapter
pendingUploadsPresenter!!.setup()
pendingUploadsPresenter!!.totalContributionList.observe(
viewLifecycleOwner
) { list: PagedList<Contribution?> ->
contributionsSize = list.size
contributionsList = ArrayList()
var pausedOrQueuedUploads = 0
list.forEach {
if (it != null) {
if (it.state == Contribution.STATE_PAUSED
|| it.state == Contribution.STATE_QUEUED
|| it.state == Contribution.STATE_IN_PROGRESS
) {
contributionsList.add(it)
}
if (it.state == Contribution.STATE_PAUSED
|| it.state == Contribution.STATE_QUEUED
) {
pausedOrQueuedUploads++
}
}
}
if (contributionsSize == 0) {
binding.nopendingTextView.visibility = View.VISIBLE
binding.pendingUplaodsLl.visibility = View.GONE
uploadProgressActivity.hidePendingIcons()
} else {
binding.nopendingTextView.visibility = View.GONE
binding.pendingUplaodsLl.visibility = View.VISIBLE
adapter.submitList(list)
binding.progressTextView.setText(contributionsSize.toString() + " uploads left")
if ((pausedOrQueuedUploads == contributionsSize) || CommonsApplication.isPaused) {
uploadProgressActivity.setPausedIcon(true)
} else {
uploadProgressActivity.setPausedIcon(false)
}
}
}
}
/**
* Cancels a specific upload after getting a confirmation from the user using Dialog.
*/
override fun deleteUpload(contribution: Contribution?) {
showAlertDialog(
requireActivity(),
String.format(
Locale.getDefault(),
requireActivity().getString(R.string.cancelling_upload)
),
String.format(
Locale.getDefault(),
requireActivity().getString(R.string.cancel_upload_dialog)
),
String.format(Locale.getDefault(), requireActivity().getString(R.string.yes)),
String.format(Locale.getDefault(), requireActivity().getString(R.string.no)),
{
ViewUtil.showShortToast(context, R.string.cancelling_upload)
pendingUploadsPresenter.deleteUpload(
contribution,
this.requireContext().applicationContext
)
},
{}
)
}
/**
* Restarts all the paused uploads.
*/
fun restartUploads() {
if (contributionsList != null) {
pendingUploadsPresenter.restartUploads(
contributionsList,
0,
this.requireContext().applicationContext
)
}
}
/**
* Pauses all the ongoing uploads.
*/
fun pauseUploads() {
pendingUploadsPresenter.pauseUploads()
}
/**
* Cancels all the uploads after getting a confirmation from the user using Dialog.
*/
fun deleteUploads() {
showAlertDialog(
requireActivity(),
String.format(
Locale.getDefault(),
requireActivity().getString(R.string.cancelling_all_the_uploads)
),
String.format(
Locale.getDefault(),
requireActivity().getString(R.string.are_you_sure_that_you_want_cancel_all_the_uploads)
),
String.format(Locale.getDefault(), requireActivity().getString(R.string.yes)),
String.format(Locale.getDefault(), requireActivity().getString(R.string.no)),
{
ViewUtil.showShortToast(context, R.string.cancelling_upload)
uploadProgressActivity.hidePendingIcons()
pendingUploadsPresenter.deleteUploads(
listOf(
Contribution.STATE_QUEUED,
Contribution.STATE_IN_PROGRESS,
Contribution.STATE_PAUSED
)
)
},
{}
)
}
}

View file

@ -0,0 +1,262 @@
package fr.free.nrw.commons.upload;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.paging.DataSource.Factory;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import androidx.work.ExistingWorkPolicy;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionBoundaryCallback;
import fr.free.nrw.commons.contributions.ContributionsRemoteDataSource;
import fr.free.nrw.commons.contributions.ContributionsRepository;
import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.repository.UploadRepository;
import fr.free.nrw.commons.upload.PendingUploadsContract.UserActionListener;
import fr.free.nrw.commons.upload.PendingUploadsContract.View;
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
/**
* The presenter class for PendingUploadsFragment and FailedUploadsFragment
*/
public class PendingUploadsPresenter implements UserActionListener {
private final ContributionBoundaryCallback contributionBoundaryCallback;
private final ContributionsRepository contributionsRepository;
private final UploadRepository uploadRepository;
private final Scheduler ioThreadScheduler;
private final CompositeDisposable compositeDisposable;
private final ContributionsRemoteDataSource contributionsRemoteDataSource;
LiveData<PagedList<Contribution>> totalContributionList;
LiveData<PagedList<Contribution>> failedContributionList;
@Inject
PendingUploadsPresenter(
final ContributionBoundaryCallback contributionBoundaryCallback,
final ContributionsRemoteDataSource contributionsRemoteDataSource,
final ContributionsRepository contributionsRepository,
final UploadRepository uploadRepository,
@Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) {
this.contributionBoundaryCallback = contributionBoundaryCallback;
this.contributionsRepository = contributionsRepository;
this.uploadRepository = uploadRepository;
this.ioThreadScheduler = ioThreadScheduler;
this.contributionsRemoteDataSource = contributionsRemoteDataSource;
compositeDisposable = new CompositeDisposable();
}
/**
* Setups the paged list of Pending Uploads. This method sets the configuration for paged list
* and ties it up with the live data object. This method can be tweaked to update the lazy
* loading behavior of the contributions list
*/
void setup() {
final PagedList.Config pagedListConfig =
(new PagedList.Config.Builder())
.setPrefetchDistance(50)
.setPageSize(10).build();
Factory<Integer, Contribution> factory;
factory = contributionsRepository.fetchContributionsWithStatesSortedByDateUploadStarted(
Arrays.asList(Contribution.STATE_QUEUED, Contribution.STATE_IN_PROGRESS,
Contribution.STATE_PAUSED));
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
pagedListConfig);
totalContributionList = livePagedListBuilder.build();
}
/**
* Setups the paged list of Failed Uploads. This method sets the configuration for paged list
* and ties it up with the live data object. This method can be tweaked to update the lazy
* loading behavior of the contributions list
*/
void getFailedContributions() {
final PagedList.Config pagedListConfig =
(new PagedList.Config.Builder())
.setPrefetchDistance(50)
.setPageSize(10).build();
Factory<Integer, Contribution> factory;
factory = contributionsRepository.fetchContributionsWithStatesSortedByDateUploadStarted(
Collections.singletonList(Contribution.STATE_FAILED));
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
pagedListConfig);
failedContributionList = livePagedListBuilder.build();
}
@Override
public void onAttachView(@NonNull View view) {
}
@Override
public void onDetachView() {
compositeDisposable.clear();
contributionsRemoteDataSource.dispose();
contributionBoundaryCallback.dispose();
}
/**
* Deletes the specified upload (contribution) from the database.
*
* @param contribution The contribution object representing the upload to be deleted.
* @param context The context in which the operation is being performed.
*/
@Override
public void deleteUpload(final Contribution contribution, Context context) {
compositeDisposable.add(contributionsRepository
.deleteContributionFromDB(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
/**
* Pauses all the uploads by changing the state of contributions from STATE_QUEUED and
* STATE_IN_PROGRESS to STATE_PAUSED in the database.
*/
public void pauseUploads() {
CommonsApplication.isPaused = true;
compositeDisposable.add(contributionsRepository
.updateContributionsWithStates(
List.of(Contribution.STATE_QUEUED, Contribution.STATE_IN_PROGRESS),
Contribution.STATE_PAUSED)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
/**
* Deletes contributions from the database that match the specified states.
*
* @param states A list of integers representing the states of the contributions to be deleted.
*/
public void deleteUploads(List<Integer> states) {
compositeDisposable.add(contributionsRepository
.deleteContributionsFromDBWithStates(states)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
/**
* Restarts the uploads for the specified list of contributions starting from the given index.
*
* @param contributionList The list of contributions to be restarted.
* @param index The starting index in the list from which to restart uploads.
* @param context The context in which the operation is being performed.
*/
public void restartUploads(List<Contribution> contributionList, int index, Context context) {
CommonsApplication.isPaused = false;
if (index >= contributionList.size()) {
return;
}
Contribution it = contributionList.get(index);
if (it.getState() == Contribution.STATE_FAILED) {
it.setDateUploadStarted(Calendar.getInstance().getTime());
if (it.getErrorInfo() == null) {
it.setChunkInfo(null);
it.setTransferred(0);
}
compositeDisposable.add(uploadRepository
.checkDuplicateImage(it.getLocalUriPath().getPath())
.subscribeOn(ioThreadScheduler)
.subscribe(imageCheckResult -> {
if (imageCheckResult == IMAGE_OK) {
it.setState(Contribution.STATE_QUEUED);
compositeDisposable.add(contributionsRepository
.save(it)
.subscribeOn(ioThreadScheduler)
.doOnComplete(() -> {
restartUploads(contributionList, index + 1, context);
})
.subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest(
context, ExistingWorkPolicy.KEEP)));
} else {
Timber.e("Contribution already exists");
compositeDisposable.add(contributionsRepository
.deleteContributionFromDB(it)
.subscribeOn(ioThreadScheduler).doOnComplete(() -> {
restartUploads(contributionList, index + 1, context);
})
.subscribe());
}
}, throwable -> {
Timber.e(throwable);
restartUploads(contributionList, index + 1, context);
}));
} else {
it.setState(Contribution.STATE_QUEUED);
compositeDisposable.add(contributionsRepository
.save(it)
.subscribeOn(ioThreadScheduler)
.doOnComplete(() -> {
restartUploads(contributionList, index + 1, context);
})
.subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest(
context, ExistingWorkPolicy.KEEP)));
}
}
/**
* Restarts the upload for the specified list of contributions for the given index.
*
* @param contributionList The list of contributions.
* @param index The index in the list which to be restarted.
* @param context The context in which the operation is being performed.
*/
public void restartUpload(List<Contribution> contributionList, int index, Context context) {
CommonsApplication.isPaused = false;
if (index >= contributionList.size()) {
return;
}
Contribution it = contributionList.get(index);
if (it.getState() == Contribution.STATE_FAILED) {
it.setDateUploadStarted(Calendar.getInstance().getTime());
if (it.getErrorInfo() == null) {
it.setChunkInfo(null);
it.setTransferred(0);
}
compositeDisposable.add(uploadRepository
.checkDuplicateImage(it.getLocalUriPath().getPath())
.subscribeOn(ioThreadScheduler)
.subscribe(imageCheckResult -> {
if (imageCheckResult == IMAGE_OK) {
it.setState(Contribution.STATE_QUEUED);
compositeDisposable.add(contributionsRepository
.save(it)
.subscribeOn(ioThreadScheduler)
.subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest(
context, ExistingWorkPolicy.KEEP)));
} else {
Timber.e("Contribution already exists");
compositeDisposable.add(contributionsRepository
.deleteContributionFromDB(it)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
}));
} else {
it.setState(Contribution.STATE_QUEUED);
compositeDisposable.add(contributionsRepository
.save(it)
.subscribeOn(ioThreadScheduler)
.subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest(
context, ExistingWorkPolicy.KEEP)));
}
}
}

View file

@ -9,5 +9,6 @@ data class StashUploadResult(
enum class StashUploadState {
SUCCESS,
PAUSED,
FAILED
FAILED,
CANCELLED
}

View file

@ -6,6 +6,7 @@ import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import fr.free.nrw.commons.contributions.ChunkInfo
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.contributions.ContributionDao
import fr.free.nrw.commons.upload.worker.UploadWorker.NotificationUpdateProgressListener
import fr.free.nrw.commons.wikidata.mwapi.MwException
import io.reactivex.Observable
@ -33,7 +34,8 @@ class UploadClient @Inject constructor(
private val csrfTokenClient: CsrfTokenClient,
private val pageContentsCreator: PageContentsCreator,
private val fileUtilsWrapper: FileUtilsWrapper,
private val gson: Gson, private val timeProvider: TimeProvider
private val gson: Gson, private val timeProvider: TimeProvider,
private val contributionDao: ContributionDao
) {
private val CHUNK_SIZE = 512 * 1024 // 512 KB
@ -58,8 +60,6 @@ class UploadClient @Inject constructor(
)
}
contribution.unpause()
val file = contribution.localUriPath
val fileChunks = fileUtilsWrapper.getFileChunks(file, CHUNK_SIZE)
val mediaType = fileUtilsWrapper.getMimeType(file).toMediaTypeOrNull()
@ -79,17 +79,35 @@ class UploadClient @Inject constructor(
val errorMessage = AtomicReference<String>()
compositeDisposable.add(
Observable.fromIterable(fileChunks).forEach { chunkFile: File ->
if (canProcess(contribution, failures)) {
if (canProcess(contributionDao, contribution, failures)) {
if (contributionDao.getContribution(contribution.pageId) == null) {
compositeDisposable.clear()
return@forEach
} else {
processChunk(
filename, contribution, notificationUpdater, chunkFile,
failures, chunkInfo, index, errorMessage, mediaType!!, file!!, fileChunks.size
filename,
contribution,
notificationUpdater,
chunkFile,
failures,
chunkInfo,
index,
errorMessage,
mediaType!!,
file!!,
fileChunks.size
)
}
}
}
)
return when {
contribution.isPaused() -> {
contributionDao.getContribution(contribution.pageId) == null -> {
return Observable.just(StashUploadResult(StashUploadState.CANCELLED, null, "Upload cancelled"))
}
contributionDao.getContribution(contribution.pageId).state == Contribution.STATE_PAUSED
|| CommonsApplication.isPaused -> {
Timber.d("Upload stash paused %s", contribution.pageId)
Observable.just(StashUploadResult(StashUploadState.PAUSED, null, null))
}
@ -248,10 +266,15 @@ class UploadClient @Inject constructor(
}
}
private fun canProcess(contribution: Contribution, failures: AtomicBoolean): Boolean {
private fun canProcess(
contributionDao: ContributionDao,
contribution: Contribution,
failures: AtomicBoolean
): Boolean {
// As long as the contribution hasn't been paused and there are no errors,
// we can process the current chunk.
return !(contribution.isPaused() || failures.get())
return !(contributionDao.getContribution(contribution.pageId).state == Contribution.STATE_PAUSED
|| failures.get() || CommonsApplication.isPaused)
}
private fun shouldSkip(

View file

@ -103,6 +103,16 @@ public class UploadModel {
return imageProcessingService.validateImage(uploadItem, inAppPictureLocation);
}
/**
* Calls checkDuplicateImage() of ImageProcessingService to check if image is duplicate
*
* @param filePath file to be checked
* @return IMAGE_DUPLICATE or IMAGE_OK
*/
public Single<Integer> checkDuplicateImage(String filePath){
return imageProcessingService.checkDuplicateImage(filePath);
}
/**
* Calls validateCaption() of ImageProcessingService to check caption of image
*

View file

@ -5,6 +5,7 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.di.NetworkingModule;
import fr.free.nrw.commons.upload.categories.CategoriesContract;
import fr.free.nrw.commons.upload.categories.CategoriesPresenter;
@ -50,8 +51,8 @@ public abstract class UploadModule {
public static UploadClient provideUploadClient(final UploadInterface uploadInterface,
@Named(NetworkingModule.NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient,
final PageContentsCreator pageContentsCreator, final FileUtilsWrapper fileUtilsWrapper,
final Gson gson) {
final Gson gson, final ContributionDao contributionDao) {
return new UploadClient(uploadInterface, csrfTokenClient, pageContentsCreator,
fileUtilsWrapper, gson, System::currentTimeMillis);
fileUtilsWrapper, gson, System::currentTimeMillis, contributionDao);
}
}

View file

@ -0,0 +1,223 @@
package fr.free.nrw.commons.upload
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.fragment.app.Fragment
import androidx.viewpager.widget.ViewPager
import fr.free.nrw.commons.R
import fr.free.nrw.commons.ViewPagerAdapter
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.contributions.ContributionDao
import fr.free.nrw.commons.databinding.ActivityUploadProgressBinding
import fr.free.nrw.commons.theme.BaseActivity
import io.reactivex.functions.Consumer
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import javax.inject.Inject
/**
* Activity to manage the progress of uploads. It includes tabs to show pending and failed uploads,
* and provides menu options to pause, resume, cancel, and retry uploads. Also, it contains ViewPager
* which holds Pending Uploads Fragment and Failed Uploads Fragment to show list of pending and
* failed uploads respectively.
*/
class UploadProgressActivity : BaseActivity() {
private lateinit var binding: ActivityUploadProgressBinding
private var pendingUploadsFragment: PendingUploadsFragment? = null
private var failedUploadsFragment: FailedUploadsFragment? = null
var viewPagerAdapter: ViewPagerAdapter? = null
var menu: Menu? = null
@Inject
lateinit var contributionDao: ContributionDao
val fragmentList: MutableList<Fragment> = ArrayList()
val titleList: MutableList<String> = ArrayList()
var isPaused = true
var isPendingIconsVisible = true
var isErrorIconsVisisble = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityUploadProgressBinding.inflate(layoutInflater)
setContentView(binding.root)
viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
binding.uploadProgressViewPager.setAdapter(viewPagerAdapter)
binding.uploadProgressViewPager.setId(R.id.upload_progress_view_pager)
binding.uploadProgressTabLayout.setupWithViewPager(binding.uploadProgressViewPager)
binding.toolbarBinding.toolbar.title = getString(R.string.uploads)
setSupportActionBar(binding.toolbarBinding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.uploadProgressViewPager.addOnPageChangeListener(object :
ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int, positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
updateMenuItems(position)
if (position == 2) {
binding.uploadProgressViewPager.setCanScroll(false)
} else {
binding.uploadProgressViewPager.setCanScroll(true)
}
}
override fun onPageScrollStateChanged(state: Int) {
}
})
setTabs()
}
/**
* Initializes and sets up the tabs data by creating instances of `PendingUploadsFragment`
* and `FailedUploadsFragment`, adds them to the `fragmentList`, and assigns corresponding
* titles from resources to the `titleList`.
*/
fun setTabs() {
pendingUploadsFragment = PendingUploadsFragment()
failedUploadsFragment = FailedUploadsFragment()
fragmentList.add(pendingUploadsFragment!!)
titleList.add(getString(R.string.pending))
fragmentList.add(failedUploadsFragment!!)
titleList.add(getString(R.string.failed))
viewPagerAdapter!!.setTabData(fragmentList, titleList)
viewPagerAdapter!!.notifyDataSetChanged()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_uploads, menu)
this.menu = menu
updateMenuItems(0)
return super.onCreateOptionsMenu(menu)
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
/**
* Updates the menu items based on the current position in the view pager and the visibility
* of icons related to pending or failed uploads. This function dynamically modifies the menu
* to display pause, resume, retry, and cancel options depending on the state of the uploads.
*
* @param currentPosition The current position in the view pager. A value of `0` indicates
* pending uploads, while `1` indicates failed uploads.
*/
fun updateMenuItems(currentPosition: Int) {
if (menu != null) {
menu!!.clear()
if (currentPosition == 0) {
if (isPendingIconsVisible) {
if (!isPaused) {
if (menu!!.findItem(R.id.pause_icon) == null) {
menu!!.add(
Menu.NONE,
R.id.pause_icon,
Menu.NONE,
getString(R.string.pause)
)
.setIcon(R.drawable.pause_icon)
.setOnMenuItemClickListener {
pendingUploadsFragment!!.pauseUploads()
setPausedIcon(true)
true
}
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
if (menu!!.findItem(R.id.cancel_icon) == null) {
menu!!.add(
Menu.NONE,
R.id.cancel_icon,
Menu.NONE,
getString(R.string.cancel)
)
.setIcon(R.drawable.ic_cancel_upload)
.setOnMenuItemClickListener {
pendingUploadsFragment!!.deleteUploads()
true
}
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
} else {
if (menu!!.findItem(R.id.resume_icon) == null) {
menu!!.add(
Menu.NONE,
R.id.resume_icon,
Menu.NONE,
getString(R.string.resume)
)
.setIcon(R.drawable.play_icon)
.setOnMenuItemClickListener {
pendingUploadsFragment!!.restartUploads()
setPausedIcon(false)
true
}
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
}
}
} else if (currentPosition == 1) {
if (isErrorIconsVisisble) {
if (menu!!.findItem(R.id.retry_icon) == null) {
menu!!.add(Menu.NONE, R.id.retry_icon, Menu.NONE, getString(R.string.retry))
.setIcon(R.drawable.ic_refresh_24dp).setOnMenuItemClickListener {
failedUploadsFragment!!.restartUploads()
true
}
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
if (menu!!.findItem(R.id.cancel_icon) == null) {
menu!!.add(
Menu.NONE,
R.id.cancel_icon,
Menu.NONE,
getString(R.string.cancel)
)
.setIcon(R.drawable.ic_cancel_upload)
.setOnMenuItemClickListener {
failedUploadsFragment!!.deleteUploads()
true
}
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
}
}
}
}
/**
* Hides the menu icons related to pending uploads.
*/
fun hidePendingIcons() {
isPendingIconsVisible = false
updateMenuItems(binding.uploadProgressViewPager.currentItem)
}
/**
* Sets the paused state and updates the menu items accordingly.
* @param paused A boolean indicating whether all the uploads are paused.
*/
fun setPausedIcon(paused: Boolean) {
isPaused = paused
updateMenuItems(binding.uploadProgressViewPager.currentItem)
}
/**
* Sets the visibility of the menu icons related to failed uploads.
* @param visible A boolean indicating whether the error icons should be visible.
*/
fun setErrorIconsVisibility(visible: Boolean) {
isErrorIconsVisisble = visible
updateMenuItems(binding.uploadProgressViewPager.currentItem)
}
}

View file

@ -34,13 +34,11 @@ import fr.free.nrw.commons.upload.FileUtilsWrapper
import fr.free.nrw.commons.upload.StashUploadResult
import fr.free.nrw.commons.upload.StashUploadState
import fr.free.nrw.commons.upload.UploadClient
import fr.free.nrw.commons.upload.UploadProgressActivity
import fr.free.nrw.commons.upload.UploadResult
import fr.free.nrw.commons.wikidata.WikidataEditService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
@ -106,7 +104,6 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)!!
statesToProcess.add(Contribution.STATE_QUEUED)
statesToProcess.add(Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE)
}
@dagger.Module
@ -166,7 +163,8 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
}
override suspend fun doWork(): Result {
var countUpload = 0
try {
var totalUploadsStarted = 0
// Start a foreground service
setForeground(createForegroundInfo())
notificationManager = NotificationManagerCompat.from(appContext)
@ -174,6 +172,13 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL
)!!
withContext(Dispatchers.IO) {
while (contributionDao.getContribution(statesToProcess)
.blockingGet().size > 0 && contributionDao.getContribution(
arrayListOf(
Contribution.STATE_IN_PROGRESS
)
).blockingGet().size == 0
) {
/*
queuedContributions receives the results from a one-shot query.
This means that once the list has been fetched from the database,
@ -188,8 +193,6 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
.blockingGet()
//Showing initial notification for the number of uploads being processed
Timber.e("Queued Contributions: " + queuedContributions.size)
processingUploads.setContentTitle(appContext.getString(R.string.starting_uploads))
processingUploads.setContentText(
appContext.resources.getQuantityString(
@ -204,45 +207,20 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
processingUploads.build()
)
/**
* To avoid race condition when multiple of these workers are working, assign this state
so that the next one does not process these contribution again
*/
queuedContributions.forEach {
it.state = Contribution.STATE_IN_PROGRESS
contributionDao.saveSynchronous(it)
}
val sortedQueuedContributionsList: List<Contribution> =
queuedContributions.sortedBy { it.dateUploadStartedInMillis() }
queuedContributions.asFlow().map { contribution ->
// Upload the contribution if it has not been cancelled by the user
if (!CommonsApplication.cancelledUploads.contains(contribution.pageId)) {
/**
* If the limited connection mode is on, lets iterate through the queued
* contributions
* and set the state as STATE_QUEUED_LIMITED_CONNECTION_MODE ,
* otherwise proceed with the upload
*/
if (isLimitedConnectionModeEnabled()) {
if (contribution.state == Contribution.STATE_QUEUED) {
contribution.state = Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE
contributionDao.saveSynchronous(contribution)
}
} else {
var contribution = sortedQueuedContributionsList.first()
if (contributionDao.getContribution(contribution.pageId) != null) {
contribution.transferred = 0
contribution.state = Contribution.STATE_IN_PROGRESS
contributionDao.saveSynchronous(contribution)
setProgressAsync(Data.Builder().putInt("progress", countUpload).build())
countUpload++
setProgressAsync(Data.Builder().putInt("progress", totalUploadsStarted).build())
totalUploadsStarted++
uploadContribution(contribution = contribution)
}
} else {
/* We can remove the cancelled upload from the hashset
as this contribution will not be processed again
*/
removeUploadFromInMemoryHashSet(contribution)
}
}.collect()
//Dismiss the global notification
notificationManager?.cancel(
PROCESSING_UPLOADS_NOTIFICATION_TAG,
@ -258,13 +236,12 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
}
return Result.success()
} catch (e: Exception) {
Timber.e(e, "UploadWorker encountered an error.")
return Result.failure()
} finally {
WorkRequestHelper.markUploadWorkerAsStopped()
}
/**
* Removes the processed contribution from the cancelledUploads in-memory hashset
*/
private fun removeUploadFromInMemoryHashSet(contribution: Contribution) {
CommonsApplication.cancelledUploads.remove(contribution.pageId)
}
/**
@ -287,12 +264,6 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
.setContentTitle(appContext.getString(R.string.upload_in_progress))
.build()
}
/**
* Returns true is the limited connection mode is enabled
*/
private fun isLimitedConnectionModeEnabled(): Boolean {
return sessionManager.getPreference(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED)
}
/**
* Upload the contribution
@ -343,7 +314,6 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
).onErrorReturn{
return@onErrorReturn StashUploadResult(StashUploadState.FAILED,fileKey = null,errorMessage = it.message)
}.blockingSingle()
when (stashUploadResult.state) {
StashUploadState.SUCCESS -> {
//If the stash upload succeeds
@ -403,14 +373,19 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
contribution.state = Contribution.STATE_PAUSED
contributionDao.saveSynchronous(contribution)
}
StashUploadState.CANCELLED -> {
showCancelledNotification(contribution)
}
else -> {
Timber.e("""upload file to stash failed with status: ${stashUploadResult.state}""")
showInvalidLoginNotification(contribution)
contribution.state = Contribution.STATE_FAILED
contribution.chunkInfo = null
contribution.errorInfo = stashUploadResult.errorMessage
showErrorNotification(contribution)
contributionDao.saveSynchronous(contribution)
if (stashUploadResult.errorMessage.equals(CsrfTokenClient.INVALID_TOKEN_ERROR_MESSAGE)) {
Timber.e("Invalid Login, logging out")
showInvalidLoginNotification(contribution)
val username = sessionManager.userName
var logoutListener = CommonsApplication.BaseLogoutListener(
appContext,
@ -426,6 +401,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
Timber.e(exception)
Timber.e("Stash upload failed for contribution: $filename")
showFailedNotification(contribution)
contribution.errorInfo=exception.message
contribution.state=Contribution.STATE_FAILED
clearChunks(contribution)
}
@ -543,6 +519,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
private fun showSuccessNotification(contribution: Contribution) {
val displayTitle = contribution.media.displayTitle
contribution.state=Contribution.STATE_COMPLETED
curentNotification.setContentIntent(getPendingIntent(MainActivity::class.java))
curentNotification.setContentTitle(
appContext.getString(
R.string.upload_completed_notification_title,
@ -565,7 +542,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
@SuppressLint("StringFormatInvalid")
private fun showFailedNotification(contribution: Contribution) {
val displayTitle = contribution.media.displayTitle
curentNotification.setContentIntent(getPendingIntent(MainActivity::class.java))
curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
curentNotification.setContentTitle(
appContext.getString(
R.string.upload_failed_notification_title,
@ -598,12 +575,34 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
)
}
/**
* Shows a notification for a failed contribution upload.
*/
@SuppressLint("StringFormatInvalid")
private fun showErrorNotification(contribution: Contribution) {
val displayTitle = contribution.media.displayTitle
curentNotification.setContentTitle(
appContext.getString(
R.string.upload_failed_notification_title,
displayTitle
)
)
.setContentText(contribution.errorInfo)
.setProgress(0, 0, false)
.setOngoing(false)
notificationManager?.notify(
currentNotificationTag, currentNotificationID,
curentNotification.build()
)
}
/**
* Notify that the current upload is paused
* @param contribution
*/
private fun showPausedNotification(contribution: Contribution) {
val displayTitle = contribution.media.displayTitle
curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
curentNotification.setContentTitle(
appContext.getString(
R.string.upload_paused_notification_title,
@ -619,6 +618,25 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
)
}
/**
* Notify that the current upload is cancelled
* @param contribution
*/
private fun showCancelledNotification(contribution: Contribution) {
val displayTitle = contribution.media.displayTitle
curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
curentNotification.setContentTitle(
displayTitle
)
.setContentText("Upload has been cancelled!")
.setProgress(0, 0, false)
.setOngoing(false)
notificationManager!!.notify(
currentNotificationTag, currentNotificationID,
curentNotification.build()
)
}
/**
* Method used to get Pending intent for opening different screen after clicking on notification
* @param toClass

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.upload.worker
import android.content.Context
import androidx.work.*
import androidx.work.WorkRequest.Companion.MIN_BACKOFF_MILLIS
import timber.log.Timber
import java.util.concurrent.TimeUnit
/**
@ -11,7 +12,22 @@ import java.util.concurrent.TimeUnit
class WorkRequestHelper {
companion object {
private var isUploadWorkerRunning = false
private val lock = Object()
fun makeOneTimeWorkRequest(context: Context, existingWorkPolicy: ExistingWorkPolicy) {
synchronized(lock) {
if (isUploadWorkerRunning) {
Timber.e("UploadWorker is already running. Cannot start another instance.")
return
} else {
Timber.e("Setting isUploadWorkerRunning to true")
isUploadWorkerRunning = true
}
}
/* Set backoff criteria for the work request
The default backoff policy is EXPONENTIAL, but while testing we found that it
too long for the uploads to finish. So, set the backoff policy as LINEAR with the
@ -35,7 +51,17 @@ class WorkRequestHelper {
WorkManager.getInstance(context).enqueueUniqueWork(
UploadWorker::class.java.simpleName, existingWorkPolicy, uploadRequest
)
}
}
/**
* Sets the flag isUploadWorkerRunning to`false` allowing new worker to be started.
*/
fun markUploadWorkerAsStopped() {
synchronized(lock) {
isUploadWorkerRunning = false
}
}
}
}

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/mediaDetailsHeadingText"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<group
android:scaleX="1.44427"
android:scaleY="1.44427"
android:translateX="-5.33124"
android:translateY="-5.33124">
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</group>
</vector>

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="@dimen/half_standard_height"
android:height="@dimen/half_standard_height"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<group android:scaleX="1.44427"
android:scaleY="1.44427"
android:translateX="-5.33124"
android:translateY="-5.33124">
<path
android:fillColor="?attr/mediaDetailsHeadingText"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</group>
</vector>

View file

@ -5,7 +5,7 @@
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:fillColor="@color/white"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/primaryDarkColor"
android:pathData="M5,20h14v-2H5V20zM5,8h4v8h6v-8h4l-7,-7L5,8z" />
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M5,20h14v-2H5V20zM5,8h4v8h6v-8h4l-7,-7L5,8z" />
</vector>

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".upload.UploadProgressActivity">
<include
android:id="@+id/toolbarBinding"
layout="@layout/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/upload_progress_toolbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/card_light_grey">
<com.google.android.material.tabs.TabLayout
android:id="@+id/upload_progress_tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar"
android:background="?attr/tabBackground"
app:tabIndicatorColor="?attr/tabIndicatorColor"
app:tabMode="fixed"
app:tabSelectedTextColor="?attr/tabSelectedTextColor"
app:tabTextColor="?attr/tabTextColor" />
</com.google.android.material.appbar.AppBarLayout>
<fr.free.nrw.commons.explore.ParentViewPager
android:id="@+id/upload_progress_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/upload_progress_toolbar_layout"
android:background="?attr/mainBackground" />
</RelativeLayout>
</LinearLayout>

View file

@ -18,36 +18,6 @@
android:layout_marginTop="@dimen/miniscule_margin"
android:layout_margin="@dimen/very_tiny_gap"/>
<LinearLayout
android:id="@+id/limited_connection_enabled_layout"
android:animateLayoutChanges="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/miniscule_margin"
android:padding="@dimen/standard_gap"
android:orientation="vertical"
android:clickable="true"
android:focusable="true"
android:background="@color/wikimedia_green">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="5dp"
android:textColor="@android:color/white"
android:layout_marginBottom="@dimen/tiny_gap"
android:textSize="@dimen/subheading_text_size"
android:text="@string/limited_connection_is_on"
app:drawableTint="@color/white"
app:drawableStartCompat="@drawable/ic_baseline_cloud_off_24"/>
<TextView
android:id="@+id/limited_connection_description_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="@dimen/description_text_size"
android:text="@string/limited_connection_explanation"/>
</LinearLayout>
<FrameLayout
android:id="@+id/explore_container"
android:layout_width="match_parent"

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".upload.FailedUploadsFragment">
<TextView
android:id="@+id/nofailedTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="You do not have any failed Uploads!" />
<LinearLayout
android:id="@+id/failedUplaodsLl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:orientation="vertical"
android:visibility="gone">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/failed_uploads_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="10dp" />
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".upload.PendingUploadsFragment">
<TextView
android:id="@+id/nopendingTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="You do not have any pending Uploads!"
android:visibility="gone" />
<LinearLayout
android:id="@+id/pendingUplaodsLl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="Progress:"
android:textSize="22sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/progress_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right"
android:text=""
android:textSize="21sp" />
</LinearLayout>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/pending_uploads_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="10dp" />
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fresco="http://schemas.android.com/tools"
android:paddingBottom="8dp"
android:gravity="center"
android:orientation="horizontal">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/itemImage"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="?attr/mainBackground"
app:actualImageScaleType="centerCrop"
fresco:placeholderImage="@drawable/ic_image_black_24dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="6dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/titleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp"/>
<ProgressBar
android:id="@+id/itemProgress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/errorTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Queued"
android:visibility="gone" />
</LinearLayout>
<ImageView
android:id="@+id/retryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dimen_10"
android:src="@drawable/ic_refresh_24dp" />
<ImageView
android:id="@+id/deleteButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_cancel_upload" />
</LinearLayout>

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fresco="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:paddingBottom="8dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/itemImage"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginBottom="8dp"
android:background="?attr/mainBackground"
app:actualImageScaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
fresco:placeholderImage="@drawable/ic_image_black_24dp" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical"
android:paddingHorizontal="6dp"
app:layout_constraintEnd_toStartOf="@+id/deleteButton"
app:layout_constraintStart_toEndOf="@+id/itemImage">
<TextView
android:id="@+id/titleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp" />
<ProgressBar
android:id="@+id/itemProgress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible" />
<TextView
android:id="@+id/errorTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Queued"
android:visibility="gone" />
</LinearLayout>
<ImageView
android:id="@+id/deleteButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_cancel_upload"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -104,40 +104,6 @@
android:paddingTop="@dimen/standard_gap"
android:visibility="visible">
<ImageButton
android:id="@+id/pauseResumeButton"
android:layout_width="@dimen/dimen_40"
android:layout_height="@dimen/dimen_40"
android:layout_marginEnd="@dimen/tiny_padding"
android:layout_toStartOf="@id/cancelButton"
android:background="@android:color/transparent"
android:tag="@string/pause"
app:srcCompat="@drawable/pause_icon" />
<ImageButton
android:id="@+id/cancelButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="@dimen/tiny_padding"
android:layout_toStartOf="@id/retryButton"
android:background="@android:color/transparent"
android:padding="@dimen/activity_margin_horizontal"
android:src="@drawable/ic_cancel_white"
android:tint="?attr/contributionsListTextSecondary"
android:text="@string/menu_cancel_upload" />
<ImageButton
android:id="@+id/retryButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="@dimen/tiny_padding"
android:layout_toStartOf="@id/wikipediaButton"
android:background="@android:color/transparent"
android:padding="@dimen/activity_margin_horizontal"
android:src="@drawable/ic_retry_white"
android:tint="?attr/contributionsListTextSecondary"
android:text="@string/menu_retry_upload" />
<ImageButton
android:id="@+id/wikipediaButton"
android:layout_width="48dp"

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:clickable="true"
android:focusable="true"
android:gravity="center"
tools:background="@color/black">
<ImageView
android:id="@+id/pending_uploads_image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:gravity="center"
app:srcCompat="?attr/upload_icon_drawable" />
<TextView
android:id="@+id/pending_uploads_count_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/pending_uploads_image_view"
android:layout_alignEnd="@id/pending_uploads_image_view"
android:layout_alignRight="@id/pending_uploads_image_view"
android:background="@drawable/notification_badge"
android:backgroundTint="@color/button_blue"
android:gravity="center"
android:padding="@dimen/miniscule_margin"
android:textColor="@color/white"
android:textSize="7sp"
android:textStyle="bold"
android:visibility="gone"
tools:text="9+"
tools:visibility="visible" />
<TextView
android:id="@+id/uploads_error_count_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/pending_uploads_count_badge"
android:layout_alignEnd="@id/pending_uploads_image_view"
android:layout_alignRight="@id/pending_uploads_image_view"
android:layout_marginTop="1dp"
android:layout_marginEnd="0dp"
android:layout_marginRight="0dp"
android:background="@drawable/notification_badge"
android:gravity="center"
android:padding="@dimen/miniscule_margin"
android:textColor="?attr/notification_icon_text_color"
android:textSize="7sp"
android:textStyle="bold"
android:visibility="gone"
tools:text="9+"
tools:visibility="visible" />
</RelativeLayout>

View file

@ -1,16 +1,14 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/toggle_limited_connection_mode"
android:title="@string/limited_connection_mode"
app:showAsAction="always"
android:checkable="true"
android:icon="@drawable/ic_baseline_cloud_queue_24"
/>
<item android:id="@+id/notifications"
android:title="@string/notifications"
app:showAsAction="ifRoom|withText"
<item
android:id="@+id/upload_tab"
android:title="Upload"
app:actionLayout="@layout/pending_uploads_icon"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/notifications"
android:menuCategory="secondary"
android:title="@string/notifications"
app:actionLayout="@layout/notification_icon"
/>
app:showAsAction="ifRoom|withText" />
</menu>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".upload.UploadProgressActivity"
>
<item
android:id="@+id/resume_icon"
android:title="Resume"
android:icon="@drawable/play_icon"
android:orderInCategory="1"
app:showAsAction="ifRoom"
/>
<item
android:id="@+id/pause_icon"
android:title="Pause"
android:icon="@drawable/pause_icon"
android:orderInCategory="1"
app:showAsAction="ifRoom"
/>
<item
android:id="@+id/retry_icon"
android:title="Retry"
android:icon="@drawable/ic_refresh_24dp"
android:orderInCategory="1"
app:showAsAction="ifRoom"
/>
<item
android:id="@+id/cancel_icon"
android:title="Cancel"
android:icon="@drawable/ic_remove"
android:orderInCategory="1"
app:showAsAction="ifRoom"
/>
</menu>

View file

@ -783,5 +783,10 @@
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\'%1$s\' ligger et andet sted. Angiv venligst det korrekte sted nedenfor, og skriv om muligt den korrekte bredde- og længdegrad.</string>
<string name="other_problem_or_information_please_explain_below">Andet problem eller anden information (forklar venligst nedenfor).</string>
<string name="feedback_destination_note">Din feedback bliver slået op på følgende wiki-side: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Mobile app/Feedback&lt;/a&gt;</string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">Er du sikker på, at du vil annullere alle uploads?</string>
<string name="cancelling_all_the_uploads">Annullerer alle uploads...</string>
<string name="uploads">Uploads</string>
<string name="pending">Afventer</string>
<string name="failed">Mislykkedes</string>
<string name="could_not_load_place_data">Kunne ikke indlæse steddata</string>
</resources>

View file

@ -403,57 +403,57 @@
<string name="notifications">Ενημερώσεις</string>
<string name="read_notifications">Ειδοποιήσεις (ανάγνωση)</string>
<string name="display_nearby_notification">Εμφάνιση ειδοποίησης σε κοντινή απόσταση</string>
<string name="display_nearby_notification_summary">Πατήστε εδώ για να δείτε την πιο κοντινή θέση που χρειάζεται εικόνες</string>
<string name="list_sheet">Λίστα</string>
<string name="storage_permission">Άδεια Αποθήκευσης</string>
<string name="display_nearby_notification_summary">Εμφάνιση ειδοποίησης εντός της εφαρμογής για το πλησιέστερο μέρος που χρειάζεται φωτογραφίες</string>
<string name="list_sheet">Κατάλογος</string>
<string name="storage_permission">Άδεια αποθήκευσης</string>
<string name="write_storage_permission_rationale_for_image_share">Χρειαζόμαστε την άδειά σας για πρόσβαση στον εξωτερικό χώρο αποθήκευσης της συσκευής σας προκειμένου να ανεβάσουμε εικόνες.</string>
<string name="nearby_notification_dismiss_message">Δεν θα δείτε την πιο κοντινή τοποθεσία που χρειάζεται επιπλέον εικόνες. Ωστόσο, μπορείτε να ενεργοποιήσετε ξανά αυτή την ειδοποίηση στις Ρυθμίσεις αν θέλετε.</string>
<string name="nearby_notification_dismiss_message">Δε θα βλέπετε πλέον το πλησιέστερο μέρος που χρειάζεται φωτογραφίες. Ωστόσο, μπορείτε να ενεργοποιήσετε ξανά αυτή την ειδοποίηση στις Ρυθμίσεις, αν το επιθυμείτε.</string>
<string name="step_count">Βήμα %1$d από %2$d: %3$s</string>
<string name="next">Επόμενο</string>
<string name="previous">Προηγούμενο</string>
<string name="upload_title_duplicate">Υπάρχει ήδη αρχείο με όνομα %1$s. Είστε σίγουροι πως θέλετε να προχωρήσετε;\n\nΣημείωση: θα προστεθεί αυτόματα μια διόρθωση όνομα αρχείου.</string>
<string name="map_application_missing">Καμία εφαρμογή χάρτη δεν βρέθηκε στον υπολογιστή. Παρακαλώ εγκαταστήστε εφαρμογή χάρτη για να χρησιμοποιήσετε αυτήν την ιδιότητα.</string>
<string name="title_page_bookmarks_pictures">εικόνες</string>
<string name="upload_title_duplicate">Υπάρχει ήδη αρχείο με το όνομα %1$s. Είστε σίγουροι πως θέλετε να προχωρήσετε;\n\nΣημείωση: Ένα κατάλληλο επίθημα θα προστεθεί αυτόματα στο όνομα του αρχείου.</string>
<string name="map_application_missing">Δε βρέθηκε καμία συμβατή εφαρμογή χάρτη στη συσκευή σας. Εγκαταστήστε μια εφαρμογή χάρτη για να χρησιμοποιήσετε αυτήν τη δυνατότητα.</string>
<string name="title_page_bookmarks_pictures">Φωτογραφίες</string>
<string name="title_page_bookmarks_locations">Τοποθεσίες</string>
<string name="menu_bookmark">Προσθήκη/Κατάργηση σε σελιδοδείκτες</string>
<string name="provider_bookmarks">Σελιδοδείκτες</string>
<string name="bookmark_empty">Δεν έχετε προσθέσει σελιδοδείκτες</string>
<string name="provider_bookmarks_location">Σελιδοδείκτες</string>
<string name="log_collection_started">Η συλλογή αρχείων καταγραφής ξεκίνησε. ΕΠΑΝΑΚΙΝΗΣΤΕ την εφαρμογή, εκτελέστε την ενέργεια που θέλετε να καταγράψετε και, στη συνέχεια, πατήστε ξανά \"Αποστολή αρχείου καταγραφής\"</string>
<string name="deletion_reason_uploaded_by_mistake">Το ανέβασα κατά λάθος</string>
<string name="deletion_reason_publicly_visible">Δεν ήξερα ότι θα δημοσιευόταν</string>
<string name="deletion_reason_bad_for_my_privacy">Κατάλαβα πως δεν προστατεύονται τα ατομικά μου στοιχεία</string>
<string name="deletion_reason_no_longer_want_public">Άλλαξα γνώμη, δεν θέλω να προβάλλεται πλέον δημόσια</string>
<string name="deletion_reason_not_interesting">Λυπάμαι αυτή η εικόνα δεν έχει ενδιαφέρον για εγκυκλοπαίδεια</string>
<string name="uploaded_by_myself">Ανέβηκε από εμένα στο %1$s, χρησιμοποιήθηκε σε %2$d άρθρο(α)</string>
<string name="no_uploads">Καλώς ήρθατε στα Commons!\n\nΑνεβάστε τα πρώτα σας πολυμέσα πατώντας το κουμπί προσθήκης.</string>
<string name="log_collection_started">Η συλλογή αρχείων καταγραφής ξεκίνησε. ΕΠΑΝΕΚΚΙΝΗΣΤΕ την εφαρμογή, εκτελέστε την ενέργεια που επιθυμείτε να καταγράψετε και, στη συνέχεια, πατήστε ξανά «Αποστολή αρχείου καταγραφής»</string>
<string name="deletion_reason_uploaded_by_mistake">Το μεταφόρτωσα κατά λάθος</string>
<string name="deletion_reason_publicly_visible">Δεν ήξερα ότι θα ήταν δημόσια ορατό</string>
<string name="deletion_reason_bad_for_my_privacy">Συνειδητοποίησα ότι είναι κακό για την ιδιωτικότητά μου</string>
<string name="deletion_reason_no_longer_want_public">Άλλαξα γνώμη, δε θέλω να προβάλλεται πλέον δημόσια</string>
<string name="deletion_reason_not_interesting">Συγγνώμη, αυτή η φωτογραφία δεν είναι ενδιαφέρουσα για μια εγκυκλοπαίδεια</string>
<string name="uploaded_by_myself">Ανέβηκε από εμένα στο %1$s, χρησιμοποιήθηκε σε %2$d άρθρο/α</string>
<string name="no_uploads">Καλώς ήρθατε στα Commons!\n\nΑνεβάστε τα πρώτα σας πολυμέσα πατώντας το κουμπί της προσθήκης.</string>
<string name="no_categories_selected">Δεν επιλέχθηκαν κατηγορίες</string>
<string name="no_categories_selected_warning_desc">Εικόνες χωρίς κατηγορίες χρησιμοποιούνται σπάνια. Θέλετε πράγματι να συνεχίσετε δίχως να επιλέξετε κατηγορίες?</string>
<string name="no_depictions_selected">Δεν έχουν επιλεγεί αποτυπώσεις</string>
<string name="no_depictions_selected_warning_desc">Οι εικόνες με απεικονίσεις βρίσκονται πιο εύκολα και πιο πιθανό να χρησιμοποιηθούν. Είστε βέβαιοι ότι θέλετε να συνεχίσετε χωρίς να επιλέξετε απεικονίσεις;</string>
<string name="no_categories_selected_warning_desc">Οι εικόνες χωρίς κατηγορίες χρησιμοποιούνται σπάνια. Θέλετε πράγματι να συνεχίσετε δίχως να επιλέξετε κατηγορίες;</string>
<string name="no_depictions_selected">Δεν έχουν επιλεγεί απεικονίσεις</string>
<string name="no_depictions_selected_warning_desc">Οι εικόνες με απεικονίσεις είναι πιο εύκολα ανιχνεύσιμες και πιο πιθανό να χρησιμοποιηθούν. Θέλετε σίγουρα να συνεχίσετε χωρίς να επιλέξετε απεικονίσεις;</string>
<string name="back_button_warning">Ακύρωση Μεταφόρτωσης</string>
<string name="back_button_warning_desc">Η χρήση του κουμπιού \"πίσω\" θα ακυρώσει αυτήν τη μεταφόρτωση και θα χάσετε την πρόοδό σας</string>
<string name="back_button_warning_desc">Χρησιμοποιώντας το κουμπί επιστροφής θα ακυρώσετε αυτή τη μεταφόρτωση και θα χάσετε την πρόοδό σας</string>
<string name="back_button_continue">Συνέχιση Μεταφόρτωσης</string>
<string name="upload_flow_all_images_in_set">(Για όλες τις εικόνες στο σετ)</string>
<string name="upload_flow_all_images_in_set">(Για όλες τις εικόνες στο σύνολο)</string>
<string name="search_this_area">Αναζήτηση στην περιοχή</string>
<string name="nearby_card_permission_title">Αίτημα Άδειας</string>
<string name="nearby_card_permission_explanation">Θα θέλατε να χρησιμοποιήσουμε την τρέχουσα τοποθεσία σας για να εμφανίσουμε το πλησιέστερο μέρος που χρειάζεται φωτογραφίες;</string>
<string name="unable_to_display_nearest_place">Δεν είναι δυνατή η εμφάνιση του πλησιέστερου μέρους που χρειάζεται φωτογραφίες χωρίς δικαιώματα τοποθεσίας</string>
<string name="never_ask_again">Μην το ρωτήσετε ξανά αυτό</string>
<string name="never_ask_again">Μη με ξαναρωτήσετε</string>
<string name="display_location_permission_title">Ζητήστε άδεια τοποθεσίας</string>
<string name="display_location_permission_explanation">Ζητήστε άδεια τοποθεσίας όταν χρειάζεται για τη λειτουργία προβολής κοντινής κάρτας ειδοποιήσεων.</string>
<string name="achievements_fetch_failed">Κάτι πήγε στραβά. Δεν μπορέσαμε να ανακτήσουμε επιτεύγματα</string>
<string name="achievements_fetch_failed_ultimate_achievement">Έχετε κάνει τόσες πολλές συνεισφορές που δεν μπορεί να αντεπεξέλθει το σύστημα υπολογισμού των επιτευγμάτων μας. Αυτό είναι το απόλυτο επίτευγμα.</string>
<string name="ends_on">Τελειώνει σε:</string>
<string name="display_campaigns">Προβολή καμπανιών</string>
<string name="display_campaigns_explanation">Δείτε τις τρέχουσες καμπάνιες</string>
<string name="achievements_fetch_failed_ultimate_achievement">Έχετε κάνει τόσες πολλές συνεισφορές που δεν μπορεί να αντεπεξέλθει το σύστημα υπολογισμού επιτευγμάτων μας. Αυτό είναι το απόλυτο επίτευγμα.</string>
<string name="ends_on">Λήγει στις:</string>
<string name="display_campaigns">Προβολή εκστρατειών</string>
<string name="display_campaigns_explanation">Δείτε τις τρέχουσες εκστρατείες</string>
<string name="in_app_camera_location_access_explanation">Επιτρέψτε στην εφαρμογή να ανακτήσει τοποθεσία σε περίπτωση που η κάμερα δεν την καταγράψει. Ορισμένες κάμερες συσκευών δεν καταγράφουν τοποθεσία. Σε τέτοιες περιπτώσεις, το να αφήσετε την εφαρμογή να ανακτήσει και να επισυνάψει τοποθεσία καθιστά τη συνεισφορά σας πιο χρήσιμη. Μπορείτε να το αλλάξετε ανά πάσα στιγμή από τις Ρυθμίσεις</string>
<string name="option_allow">Επιτρέψτε</string>
<string name="option_allow">Αποδοχή</string>
<string name="option_dismiss">Απόρριψη</string>
<string name="in_app_camera_needs_location">Ενεργοποιήστε την πρόσβαση τοποθεσίας από τις Ρυθμίσεις και δοκιμάστε ξανά. \n\nΣημείωση: Η μεταφόρτωση ενδέχεται να μην έχει τοποθεσία, εάν η εφαρμογή δεν μπορεί να ανακτήσει την τοποθεσία από τη συσκευή σε σύντομο χρονικό διάστημα.</string>
<string name="in_app_camera_needs_location">Ενεργοποιήστε την πρόσβαση τοποθεσίας από τις Ρυθμίσεις και δοκιμάστε ξανά.\n\nΣημείωση: Η μεταφόρτωση ενδέχεται να μην έχει τοποθεσία, εάν η εφαρμογή δεν μπορεί να ανακτήσει την τοποθεσία από τη συσκευή σε σύντομο χρονικό διάστημα.</string>
<string name="in_app_camera_location_permission_rationale">Η κάμερα εντός εφαρμογής χρειάζεται άδεια τοποθεσίας για να την επισυνάψει στις εικόνες σας σε περίπτωση που η τοποθεσία δεν είναι διαθέσιμη στο EXIF. Επιτρέψτε στην εφαρμογή να αποκτήσει πρόσβαση στην τοποθεσία σας και δοκιμάστε ξανά.\n\nΣημείωση: Η μεταφόρτωση ενδέχεται να μην έχει τοποθεσία εάν η εφαρμογή δεν μπορεί να ανακτήσει την τοποθεσία από τη συσκευή σε σύντομο χρονικό διάστημα.</string>
<string name="in_app_camera_location_permission_denied">Η εφαρμογή δεν θα καταγράψει την τοποθεσία μαζί με τις φωτογραφίες λόγω έλλειψης άδειας τοποθεσίας</string>
<string name="in_app_camera_location_unavailable">Η εφαρμογή δεν θα καταγράψει την τοποθεσία μαζί με τις φωτογραφίες καθώς το GPS είναι απενεργοποιημένο</string>
<string name="in_app_camera_location_permission_denied">Η εφαρμογή δε θα καταγράψει την τοποθεσία μαζί με τις φωτογραφίες λόγω έλλειψης άδειας τοποθεσίας</string>
<string name="in_app_camera_location_unavailable">Η εφαρμογή δε θα καταγράψει την τοποθεσία μαζί με τις φωτογραφίες καθώς το GPS είναι απενεργοποιημένο</string>
<string name="open_document_photo_picker_title">Χρησιμοποιήστε εργαλείο επιλογής φωτογραφιών βάσει εγγράφων</string>
<string name="open_document_photo_picker_explanation">Το νέο εργαλείο επιλογής φωτογραφιών Android κινδυνεύει να χάσει τις πληροφορίες τοποθεσίας. Ενεργοποιήστε εάν φαίνεται ότι το χρησιμοποιείτε.</string>
<string name="location_loss_warning">Παρακαλώ σιγουρευτείτε ότι αύτος ο κανούριος επιλογέας Android δεν αφαιρεί την τοποθεσία από τις εικόνες.\n\nΠατήστε στο \'Διαβάστε περισσότερα\' για περισσότερες πληροφορίες.</string>
@ -797,5 +797,10 @@
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">Το \'%1$s\' βρίσκεται σε διαφορετική θέση. Παρακαλούμε προσδιορίστε τη σωστή θέση παρακαλώ, και αν είναι εφικτό, γράψτε το σωστό γεωγραφικό πλάτος και μήκος.</string>
<string name="other_problem_or_information_please_explain_below">Άλλο πρόβλημα ή πληροφορίες (παρακαλούμε εξηγήστε παρακάτω).</string>
<string name="feedback_destination_note">Τα σχόλιά σας δημοσιεύονται στην ακόλουθη σελίδα wiki: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Εφαρμογή για κινητά/Σχόλια&lt;/a&gt;</string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">Είστε βέβαιοι ότι θέλετε να ακυρώσετε όλες τις μεταφορτώσεις;</string>
<string name="cancelling_all_the_uploads">Ακύρωση όλων των μεταφορτώσεων...</string>
<string name="uploads">Μεταφορτώσεις</string>
<string name="pending">Σε εκκρεμότητα</string>
<string name="failed">Απέτυχε</string>
<string name="could_not_load_place_data">Δεν ήταν δυνατή η φόρτωση δεδομένων της θέσης</string>
</resources>

View file

@ -32,6 +32,7 @@
* Juanman
* Keneth Urrutia
* Ktranz
* Laquin
* Luisangelrg
* Macofe
* Madamebiblio
@ -788,6 +789,9 @@
<string name="storage_permissions_denied">Se denegaron los permisos de almacenamiento</string>
<string name="unable_to_share_upload_item">No se puede compartir este elemento</string>
<string name="permissions_are_required_for_functionality">Se requieren permisos para la funcionalidad</string>
<string name="learn_how_to_write_a_useful_description">Aprenda a escribir una descripción útil</string>
<string name="learn_how_to_write_a_useful_caption">Aprenda a escribir una leyenda útil</string>
<string name="see_your_achievements">Ver sus logros</string>
<string name="edit_image">Editar Imagen</string>
<string name="edit_location">Editar Ubicación</string>
<string name="location_updated">¡Ubicación actualizada!</string>
@ -795,6 +799,8 @@
<string name="remove_location_warning_title">Eliminar el aviso de ubicación</string>
<string name="remove_location_warning_desc">La ubicación hace que las imágenes sean más útiles y accesibles. ¿De verdad quieres eliminar la ubicación de esta foto?</string>
<string name="location_removed">¡Ubicación eliminada!</string>
<string name="send_thanks_to_author">Agradecer al autor</string>
<string name="error_sending_thanks">Error al enviar gracias al autor.</string>
<string name="invalid_login_message">Su sesión ha caducado. Inicie sesión de nuevo.</string>
<string name="no_application_available_to_open_gpx_files">No hay ninguna aplicación disponible para abrir archivos GPX</string>
<string name="file_saved_successfully">Guardado correctamente</string>
@ -810,4 +816,12 @@
</plurals>
<string name="multiple_files_depiction">Recuerde que todas las imágenes en una carga múltiple tienen la misma categoría y representación. Si las imágenes no comparten representación y categoría, haga varias cargas por separado.</string>
<string name="multiple_files_depiction_header">Nota sobre cargas múltiples</string>
<string name="nearby_wikitalk">Informar a Wikidata sobre un problema relacionado con este elemento</string>
<string name="please_enter_some_comments">Por favor, escriba algunos comentarios.</string>
<string name="talk">Discusión</string>
<string name="write_something_about_the_item">Escriba algo sobre el elemento \'%1$s\'. Será visible públicamente.</string>
<string name="cancelling_all_the_uploads">Cancelando todas las subidas...</string>
<string name="uploads">Subidas</string>
<string name="pending">Pendiente</string>
<string name="failed">Falló</string>
</resources>

View file

@ -7,6 +7,7 @@
* Fitoschido
* Iñaki LL
* Joseba
* Laquin
* Mikel Ibaiba
* Sator
* Subi
@ -24,6 +25,11 @@
<string name="add_new_contribution">Ekarpen berria gehitu</string>
<string name="add_contribution_from_camera">Gehitu ekarpena kamaratik</string>
<string name="add_contribution_from_photos">Gehitu ekarpena argazkietatik</string>
<string name="add_contribution_from_contributions_gallery">Gehitu ekarpena aurreko ekarpen-galeriatik</string>
<string name="show_captions">Irudi-oineko testuak</string>
<string name="row_item_language_description">Hizkuntzaren deskribapena</string>
<string name="row_item_caption">Irudi-oineko testua</string>
<string name="nearby_row_image">Irudia</string>
<string name="appwidget_img">Eguneko argazkia</string>
<plurals name="uploads_pending_notification_indicator">
<item quantity="one">Fitxategi %1$d kargatzen</item>
@ -67,6 +73,7 @@
<string name="signup">Eman izena</string>
<string name="logging_in_title">Saioa hasten</string>
<string name="logging_in_message">Mesedez itxaron…</string>
<string name="updating_caption_message">Itxaron mesedez…</string>
<string name="login_success">Sarrera arrakastatsua!</string>
<string name="login_failed">Saio hasieran akatsa!</string>
<string name="upload_failed">Fitxategia ez da aurkitu. Mesedez saiatu beste batekin.</string>
@ -79,6 +86,7 @@
<string name="upload_progress_notification_title_finishing">%1$s igotzen bukatzen</string>
<string name="upload_failed_notification_title">%1$s igotzean akatsa</string>
<string name="upload_failed_notification_subtitle">Ukitu ikusteko</string>
<string name="upload_paused_notification_subtitle">Ukitu ikusteko</string>
<string name="title_activity_contributions">Nire azken igoerak</string>
<string name="contribution_state_queued">Itxoite-zerrendan</string>
<string name="contribution_state_failed">Hutseginda</string>
@ -97,7 +105,7 @@
<string name="login_failed_throttled">Sartzeko saiakera txar gehiegi. Mesedez saiatu zaitez minutu batzuk barru.</string>
<string name="login_failed_blocked">Barka, baina erabiltzaile hau blokeatuta dago Commonsen</string>
<string name="login_failed_2fa_needed">Zure bi faktoreko autentifikazio kodea eman behar duzu.</string>
<string name="login_failed_generic" fuzzy="true">Saio hasieran akatsa</string>
<string name="login_failed_generic">Saio hasieran akatsa</string>
<string name="share_upload_button">Igo</string>
<string name="multiple_share_base_title">Izena eman bilduma honi</string>
<string name="provider_modifications">Aldaketak</string>
@ -114,6 +122,7 @@
<string name="title_activity_signup">Eman izena</string>
<string name="title_activity_featured_images">Nabarmendutako irudiak</string>
<string name="title_activity_category_details">Kategoria</string>
<string name="title_activity_review">Parekoen Ebaluazioa</string>
<string name="menu_about">Honi buruz</string>
<string name="about_license">Wikimedia Commons iturri-irekiko aplikazioa da Wikimedia komunitateko bolondresek sortu eta mantendutakoa. Wikimedia Fundazioa ez dago aplikazioaren sorreran, garapenean, edota mantenuan ibili.</string>
<string name="about_improve">&lt;a href=\"%1$s\"&gt;GitHub-eko gai&lt;/a&gt; berria sortu errore eta iradokizunen berri emateko.</string>
@ -150,6 +159,7 @@
<string name="tutorial_3_text">Mesedez EZ igo:</string>
<string name="tutorial_3_subtext_1">Autorretratuak edo zure lagunen argazkiak</string>
<string name="tutorial_3_subtext_2">Internetetik jaitsitako irudiak</string>
<string name="tutorial_3_subtext_3">Aplikazio jabedunen pantaila-irudiak</string>
<string name="tutorial_4_text">Igoera adibidea:</string>
<string name="tutorial_4_subtext_1">Izenburua: Sydney Opera House</string>
<string name="tutorial_4_subtext_2">Deskribapena: Sydney Opera House badiaren beste aldetik ikusita</string>
@ -206,6 +216,7 @@
<string name="navigation_item_about">Honi buruz</string>
<string name="navigation_item_settings">Ezarpenak</string>
<string name="navigation_item_feedback">Feedback</string>
<string name="navigation_item_feedback_github">Github-en bidez berrelikatu</string>
<string name="navigation_item_logout">Saioa itxi</string>
<string name="navigation_item_info">Tutoriala</string>
<string name="navigation_item_notification">Jakinarazpenak</string>
@ -215,9 +226,12 @@
<string name="nearby_info_menu_wikidata_article">Wikidata itema</string>
<string name="nearby_info_menu_wikipedia_article">Wikipediako artikulua</string>
<string name="description_info">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 ordutan hartu den. Multimediak zerbait berezia erakusten badu, mesedez azaldu zerk egiten duen berezia.</string>
<string name="upload_problem_exist">Irudi honen arazo potentzialak:</string>
<string name="upload_problem_image_dark">Irudia ilunegia da.</string>
<string name="upload_problem_image_blurry">Argazkia lausoa da.</string>
<string name="upload_problem_image_duplicate">Irudia Commonsen badago.</string>
<string name="upload_problem_different_geolocation">Irudi hau beste leku batean hartu da.</string>
<string name="upload_problem_do_you_continue">Oraindik igo nahi al duzu argazki hau?</string>
<string name="upload_connection_error_alert_title">Konektatzeko Errorea</string>
<string name="upload_problem_image">Arazoak aurkitu dira irudian</string>
<string name="use_external_storage">Irudiak aplikazioan gorde</string>
@ -273,6 +287,7 @@
<string name="search_tab_title_depictions">Elementuak</string>
<string name="explore_tab_title_featured">Nabarmendua</string>
<string name="explore_tab_title_mobile">Mugikorretik igota</string>
<string name="explore_tab_title_map">Mapa</string>
<string name="successful_wikidata_edit">Irudia gehitu da %1$s-(e)ra Wikidatan!</string>
<string name="wikidata_edit_failure">Ezin izan da dagokion Wikidata entitatea eguneratu!</string>
<string name="menu_set_wallpaper">Horma-paper gisa ezarri</string>
@ -328,6 +343,7 @@
<string name="provider_bookmarks">Lastermarkak</string>
<string name="provider_bookmarks_location">Lastermarkak</string>
<string name="search_this_area">Leku honetan bilatu</string>
<string name="achievements_fetch_failed_ultimate_achievement">Hainbeste ekarpen egin dituzu, non gure lorpenetarako kalkulu-sistema ez den iristen. Hau da lorpen handiena.</string>
<string name="nominate_for_deletion_done">Egina</string>
<string name="review_thanks_yes_button_text">Hurrengo orria</string>
<string name="review_thanks_no_button_text">Bai, zergatik ez</string>
@ -344,6 +360,8 @@
<string name="delete_helper_ask_reason_copyright_press_photo">Prentsarako argazkia</string>
<string name="delete_helper_ask_reason_copyright_logo">Logo</string>
<string name="category_edit_helper_show_edit_title_success">Arrakasta</string>
<string name="you_have_no_achievements_yet">Oraindik ez duzu ekarpenik egin</string>
<string name="no_achievements_yet">%s(r)ek oraindik ez du ekarpenik egin</string>
<string name="title_app_shortcut_setting">Hobespenak</string>
<string name="theme_dark_name">Iluna</string>
<string name="theme_light_name">Argia</string>
@ -358,4 +376,5 @@
<string name="leaderboard_column_count">Zenbaketa</string>
<string name="leaderboard_upload">Igo</string>
<string name="leaderboard_nearby">Hurbilekoak</string>
<string name="contributions_of_user">Erabiltzailearen ekarpenak: %s</string>
</resources>

View file

@ -10,13 +10,27 @@
* Mguix
* Toliño
* Vivaelcelta
* Xosecalvo
-->
<resources>
<string name="commons_facebook">Páxina de Commons en Facebook</string>
<string name="commons_github">Código fonte de Commons en Github</string>
<string name="commons_logo">Logo de Commons</string>
<string name="commons_website">Sitio web de Commons</string>
<string name="exit_location_picker">Saír do selector de localización</string>
<string name="submit">Enviar</string>
<string name="add_another_description">Engadir outra descrición</string>
<string name="add_new_contribution">Engadir unha nova achega</string>
<string name="add_contribution_from_camera">Engadir achega desde cámara</string>
<string name="add_contribution_from_photos">Engadir achega desde Photos</string>
<string name="add_contribution_from_contributions_gallery">Engadir achega desde galería de achegas previas</string>
<string name="show_captions">Lendas</string>
<string name="row_item_language_description">Descrición da lingua</string>
<string name="row_item_caption">Lenda</string>
<string name="show_captions_description">Descrición</string>
<string name="nearby_row_image">Imaxe</string>
<string name="nearby_all">Todo</string>
<string name="nearby_filter_search">Vista de busca</string>
<string name="appwidget_img">Imaxe do día</string>
<plurals name="uploads_pending_notification_indicator">
<item quantity="one">Cargando %1$d ficheiro</item>
@ -55,6 +69,7 @@
<string name="bullet"></string>
<string name="menu_settings">Configuracións</string>
<string name="intent_share_upload_label">Cargar en Commons</string>
<string name="upload_in_progress">Envío en curso</string>
<string name="username">Nome de usuario</string>
<string name="password">Contrasinal</string>
<string name="login_credential">Acceda á súa conta de Commons Beta</string>
@ -63,18 +78,25 @@
<string name="signup">Rexistrarse</string>
<string name="logging_in_title">Accedendo ao sistema</string>
<string name="logging_in_message">Por favor, agarde…</string>
<string name="login_success" fuzzy="true">Accedeu correctamente!</string>
<string name="login_failed" fuzzy="true">Erro durante o inición de sesión!</string>
<string name="updating_caption_title">A actualizar lendas e descricións</string>
<string name="updating_caption_message">Agarde un chisco…</string>
<string name="login_success">Accedeu correctamente!</string>
<string name="login_failed">Erro durante o inició de sesión!</string>
<string name="upload_failed">Ficheiro non atopado. Por favor, probe con outro.</string>
<string name="authentication_failed" fuzzy="true">Erro de autenticación, por favor inicia unha nova sesión</string>
<string name="retry_limit_reached">Alcanzouse o límite máximo de reintentos! Cancele o envío e ténteo de novo</string>
<string name="unrestricted_battery_mode">Desactivar a optimización da batería?</string>
<string name="authentication_failed">Fallou a autenticación. Inicie sesión de novo.</string>
<string name="uploading_started">A carga comezou!</string>
<string name="uploading_queued">Envío en cola (modo de conexión limitado activado)</string>
<string name="upload_completed_notification_title">Cargouse \"%1$s\"!</string>
<string name="upload_completed_notification_text">Prema para ollar a súa carga</string>
<string name="upload_progress_notification_title_start" fuzzy="true">Comezando a carga de \"%1$s\"</string>
<string name="upload_progress_notification_title_start">A enviar ficheiro: %s</string>
<string name="upload_progress_notification_title_in_progress">Cargando \"%1$s\"</string>
<string name="upload_progress_notification_title_finishing">Rematando a carga de \"%1$s\"</string>
<string name="upload_failed_notification_title" fuzzy="true">Erro ao cargar \"%1$s\"</string>
<string name="upload_failed_notification_title">Produciuse un erro ao enviar %1$s</string>
<string name="upload_paused_notification_title">Deteuse o envío de %1$s</string>
<string name="upload_failed_notification_subtitle">Prema para amosalo</string>
<string name="upload_paused_notification_subtitle">Toque para ver</string>
<string name="title_activity_contributions">As miñas subas recentes</string>
<string name="contribution_state_queued">Na cola</string>
<string name="contribution_state_failed">Erróneo</string>
@ -85,13 +107,15 @@
<string name="menu_nearby">Preto</string>
<string name="provider_contributions">As miñas subidas</string>
<string name="menu_share">Compartir</string>
<string name="menu_view_file_page">Ver a páxina do ficheiro</string>
<string name="share_title_hint">Lenda (Obrigatoria)</string>
<string name="share_description_hint">Descrición</string>
<string name="login_failed_network" fuzzy="true">Erro ao acceder ao sistema: Fallou a rede</string>
<string name="share_caption_hint">Lenda</string>
<string name="login_failed_network">Non foi posíbel acceder ao sistema - fallou a rede</string>
<string name="login_failed_throttled">Demasiados intentos incorrectos. Inténteo de novo nuns minutos.</string>
<string name="login_failed_blocked">Sentímolo, este usuario está bloqueado en Commons</string>
<string name="login_failed_2fa_needed">Debe proporcionar o seu código de autenticación de dous factores.</string>
<string name="login_failed_generic" fuzzy="true">Erro durante o inición de sesión</string>
<string name="login_failed_generic">Fallou o inicio de sesión</string>
<string name="share_upload_button">Subir</string>
<string name="multiple_share_base_title">Dea un nome a este conxunto</string>
<string name="provider_modifications">Modificacións</string>
@ -102,11 +126,13 @@
<string name="display_list_button">Lista</string>
<string name="contributions_subtitle_zero">(Aínda non hai subas)</string>
<string name="categories_not_found">Non se atopou ningunha categoría que coincidise con \"%1$s\"</string>
<string name="depictions_not_found">Non se atopou ningún elemento de Wikidata que coincida con %1$s</string>
<string name="categories_skip_explanation">Engada categorías para facer máis accesibles as súas imaxes na Wikimedia Commons.\nComece a escribir para engadir categorías.</string>
<string name="categories_activity_title">Categorías</string>
<string name="title_activity_settings">Configuracións</string>
<string name="title_activity_signup">Rexistrarse</string>
<string name="title_activity_featured_images">Imaxes destacadas</string>
<string name="title_activity_custom_selector">Selector personalizado</string>
<string name="title_activity_category_details">Categoría</string>
<string name="title_activity_review">Revisión por pares</string>
<string name="menu_about">Acerca de</string>
@ -160,6 +186,7 @@
<string name="detail_panel_cats_label">Categorías</string>
<string name="detail_panel_cats_loading">Cargando…</string>
<string name="detail_panel_cats_none">Ningunha seleccionada</string>
<string name="detail_caption_empty">Sen lenda</string>
<string name="detail_description_empty">Sen descrición</string>
<string name="detail_discussion_empty">Sen conversas</string>
<string name="detail_license_empty">Licenza descoñecida</string>
@ -170,8 +197,11 @@
<string name="location_permission_title">Pedindo Permiso de Localización</string>
<string name="ok">Aceptar</string>
<string name="warning">Aviso</string>
<string name="duplicate_file_name">Atopouse un nome de ficheiro duplicado</string>
<string name="upload">Enviar</string>
<string name="yes">Si</string>
<string name="no">Non</string>
<string name="media_detail_caption">Lenda</string>
<string name="media_detail_title">Título</string>
<string name="media_detail_description">Descrición</string>
<string name="media_detail_discussion">Conversa</string>
@ -219,6 +249,8 @@
<string name="upload_problem_different_geolocation">Esta imaxe foi realizada nunha localización diferente.</string>
<string name="upload_problem_fbmd">Por favor sube so fotografías feitas por ti mesmo. Non subas imaxes ou fotografías que atopes nas contas de Facebook de outros.</string>
<string name="upload_problem_do_you_continue">Aínda quere subir esta imaxe?</string>
<string name="upload_connection_error_alert_title">Erro de conexión</string>
<string name="upload_connection_error_alert_detail">O proceso de envío require acceso activo a Internet. Comprobe a súa conexión de rede.</string>
<string name="internet_downloaded">Por favor suba so fotografías feitas por vostede mesmo. Non suba imaxes ou fotografías que descargara da Internet.</string>
<string name="use_external_storage">Gardar fotos tiradas na aplicación</string>
<string name="use_external_storage_summary">Gardar ao almacenamento interno as fotografías tiradas na aplicación</string>
@ -232,8 +264,8 @@
<string name="nominated_see_more">Ver páxina web para máis detalles</string>
<string name="skip_login">Omitir</string>
<string name="navigation_item_login">Acceder ao sistema</string>
<string name="skip_login_title" fuzzy="true">Realmente quere saltar o inicio de sesión?</string>
<string name="skip_login_message" fuzzy="true">Terá que iniciar sesión para subir imaxes no futuro.</string>
<string name="skip_login_title">Confirma quequere saltar o inicio de sesión?</string>
<string name="skip_login_message">Terá que iniciar sesión para enviar imaxes no futuro.</string>
<string name="login_alert_message">Por favor, inicie a sesión para usar esta funcionalidade</string>
<string name="copy_wikicode">Copiar o texto wiki ó portapapeis</string>
<string name="wikicode_copied">Texto wiki copiado ó portapapeis</string>
@ -245,6 +277,7 @@
<string name="nearby_commons">COMMONS</string>
<string name="about_rate_us">Avalíenos</string>
<string name="about_faq">FAQ</string>
<string name="user_guide">Guía de uso</string>
<string name="welcome_skip_button">Saltar titorial</string>
<string name="no_internet">Internet non dispoñible</string>
<string name="error_notifications">Erro ó recuperar as notificacións</string>
@ -257,6 +290,9 @@
<string name="about_translate_cancel">Cancelar</string>
<string name="retry">Reintentar</string>
<string name="showcase_view_whole_nearby_activity">Hai sitios preto de vostede que precisan fotos para ilustrar os seus artigos de Wikipedia</string>
<string name="showcase_view_needs_photo">Este lugar precisa dunha foto.</string>
<string name="showcase_view_has_photo">Este lugar xa ten unha foto.</string>
<string name="showcase_view_no_longer_exists">Este lugar xa non existe.</string>
<string name="no_images_found">Non se atopou ningunha imaxeǃ</string>
<string name="error_loading_images">Houbo un erro ó subir as imaxes.</string>
<string name="image_uploaded_by">Subida porː %1$s</string>
@ -271,8 +307,10 @@
<string name="error_loading_categories">Houbo un erro ó cargar categorías.</string>
<string name="search_tab_title_media">Multimedia</string>
<string name="search_tab_title_categories">Categorías</string>
<string name="search_tab_title_depictions">Elementos</string>
<string name="explore_tab_title_featured">Destacadas</string>
<string name="explore_tab_title_mobile">Cargada vía móbil</string>
<string name="explore_tab_title_map">Mapa</string>
<string name="successful_wikidata_edit">A imaxe engadiuse a %1$s en Wikidata!</string>
<string name="wikidata_edit_failure">Fallou a actualización da entidade do Wikidata correspondente!</string>
<string name="menu_set_wallpaper">Poñer como imaxe de fondo</string>
@ -290,21 +328,27 @@
<string name="construction_event_answer">Fotografiás que amosen tecnoloxía ou cultura son moi benvidas en Commons</string>
<string name="congratulatory_message_quiz">Acadou un %1$s de respostas correctas. Parabéns!</string>
<string name="warning_for_no_answer">Escolla unha das dúas opcións para contestar a pregunta</string>
<string name="user_not_logged_in" fuzzy="true">A sesión caducou, por favor inicia unha nova sesión.</string>
<string name="user_not_logged_in">O inicio de sesión caducou. Inicie sesión de novo.</string>
<string name="quiz_result_share_message">Comparta o seu cuestionario cos seus amigos!</string>
<string name="continue_message">Continuar</string>
<string name="correct">Resposta correcta</string>
<string name="wrong">Resposta incorrecta</string>
<string name="quiz_screenshot_question">Pódese subir esta captura de pantalla?</string>
<string name="share_app_title">Compartir a aplicación</string>
<string name="error_fetching_nearby_places" fuzzy="true">Erro ó procurar os lugares próximos.</string>
<string name="rotate">Xirar</string>
<string name="error_fetching_nearby_places">Non foi posíbel cargar lugares próximos</string>
<string name="no_pictures_in_this_area">Non hai imaxes nesta zona</string>
<string name="no_nearby_places_around">Non hai lugares próximos</string>
<string name="error_fetching_nearby_monuments">Produciuse un erro ao buscar monumentos próximos.</string>
<string name="no_recent_searches">Non hai procuras recentes</string>
<string name="delete_recent_searches_dialog">Está seguro de querer borrar o seu historial de procuras?</string>
<string name="cancel_upload_dialog">Confirma que quere cancelar este envío?</string>
<string name="delete_search_dialog">Queres borrar esta procura?</string>
<string name="search_history_deleted">Eliminouse o historial de procuras</string>
<string name="nominate_delete">Nomear para borrado</string>
<string name="delete">Borrar</string>
<string name="Achievements">Logros</string>
<string name="Profile">Perfil</string>
<string name="statistics">Estatísticas</string>
<string name="statistics_thanks">Agradecementos recibidos</string>
<string name="statistics_featured">Imaxes destacadas</string>
@ -355,18 +399,22 @@
<string name="no_uploads">Dámoslle a benvida ó Commonsǃ\n\nCargue o seu primeiro ficheiro premendo no botón Engadir.</string>
<string name="no_categories_selected">Non hai categorías seleccionadas</string>
<string name="no_categories_selected_warning_desc">As imaxes sen categorías só son utilizables en contadas ocasións. Está seguro de que quere continuar sen seleccionar categorías?</string>
<string name="upload_flow_all_images_in_set" fuzzy="true">(Para tódalas imaxes no conxunto)</string>
<string name="back_button_warning">Cancelar envío</string>
<string name="back_button_continue">Continuar co envío</string>
<string name="upload_flow_all_images_in_set">(Para tódalas imaxes do conxunto)</string>
<string name="search_this_area">Procurar nesta área</string>
<string name="nearby_card_permission_title">Solicitude de permisos</string>
<string name="nearby_card_permission_explanation">Desexa que usemos a súa localizacións actual para amosarlle o lugar máis preto que precisa imaxes?</string>
<string name="unable_to_display_nearest_place">Imposible amosar o sitio máis achegado que precisa fotos sen ter permisos de localización</string>
<string name="never_ask_again">Non volver a preguntar isto nunca</string>
<string name="display_location_permission_title" fuzzy="true">Amosar permiso de localización</string>
<string name="display_location_permission_title">Solicitar permiso de localización</string>
<string name="display_location_permission_explanation">Pedir permisos de localización cando sexa necesario para a funcionalidade de notificación de proximidade.</string>
<string name="achievements_fetch_failed" fuzzy="true">Algo foi mal, non puidemos obter as túas achegas</string>
<string name="ends_on">Finaliza o:</string>
<string name="display_campaigns">Amosar campañas</string>
<string name="display_campaigns_explanation">Ver as campañas en curso</string>
<string name="option_allow">Permitir</string>
<string name="option_dismiss">Descartar</string>
<string name="nearby_campaign_dismiss_message">Xa non verá as campañas. Porén, pode volver habilitar esta notificación na configuración.</string>
<string name="this_function_needs_network_connection" fuzzy="true">Esta función require conexión de rede, verifique a súa configuración de conexión.</string>
<string name="error_processing_image">Houbo un erro ó procesar a imaxe. Por favor, ténteo de novoǃ</string>
@ -383,8 +431,8 @@
<string name="send_thank_success_title">Enviando agradecementos: Éxito</string>
<string name="send_thank_success_message">Enviado correctamente o agradecemento a %1$s</string>
<string name="send_thank_toast">Enviando agradecementos por %1$s</string>
<string name="review_thanks_yes_button_text" fuzzy="true">Si, por que non</string>
<string name="review_thanks_no_button_text" fuzzy="true">Seguinte imaxe</string>
<string name="review_thanks_yes_button_text">Imaxe seguinte</string>
<string name="review_thanks_no_button_text">Si, por que non</string>
<string name="no_image">Ningunha imaxe usada</string>
<string name="no_image_reverted">Ningunha imaxe revertida</string>
<string name="no_image_uploaded">Ningunha imaxe subida</string>
@ -396,6 +444,8 @@
<string name="error_occurred_in_picking_images">Houbo un erro ó escoller as imaxes</string>
<string name="please_wait">Por favor, agarde…</string>
<string name="skip_image">Saltar esta imaxe</string>
<string name="manage_exif_tags">Xestionar etiquetas EXIF</string>
<string name="manage_exif_tags_summary">Seleccione que etiquetas EXIF manter nos envíos</string>
<string name="exif_tag_name_author">Autor</string>
<string name="exif_tag_name_copyright">Dereitos de autoría</string>
<string name="exif_tag_name_location">Localización</string>
@ -407,16 +457,43 @@
<string name="image_info">Información da imaxe</string>
<string name="no_categories_found">Non se atoparon categorías</string>
<string name="upload_cancelled">Cancelouse a carga</string>
<string name="default_description_language">Lingua de descrición predeterminada</string>
<string name="delete_helper_show_deletion_title">Nomeando para borrado</string>
<string name="delete_helper_show_deletion_title_success">Todo correcto</string>
<string name="delete_helper_show_deletion_title_failed">Fallou</string>
<string name="delete_helper_ask_spam_selfie" fuzzy="true">Un autorretrato</string>
<string name="delete_helper_show_deletion_message_else">Non foi posíbel solicitar a eliminación.</string>
<string name="delete_helper_ask_spam_selfie">Un autorretrato que non se emprega en ningún artigo</string>
<string name="delete_helper_ask_spam_blurry" fuzzy="true">Borrosa</string>
<string name="delete_helper_ask_spam_nonsense" fuzzy="true">Sen sentido</string>
<string name="delete_helper_ask_reason_copyright_press_photo">Foto de prensa</string>
<string name="delete_helper_ask_reason_copyright_internet_photo">Foto aleatoria de internet</string>
<string name="delete_helper_ask_reason_copyright_logo">Logo</string>
<string name="delete_helper_ask_alert_set_positive_button_reason">Porque é</string>
<string name="category_edit_helper_show_edit_title_success">Todo correcto</string>
<plurals name="category_edit_helper_show_edit_message_if">
<item quantity="one">Engádese a categoría %1$s .</item>
<item quantity="other">Engádense as categorías %1$s .</item>
</plurals>
<string name="category_edit_helper_edit_message_else">Non foi posíbel engadir categorías.</string>
<string name="category_edit_button_text">Actualizar categorías</string>
<string name="depictions_edit_helper_make_edit_toast">A tentar actualizar representacións.</string>
<string name="depictions_edit_helper_show_edit_title">Editar representacións</string>
<plurals name="depictions_edit_helper_show_edit_message_if">
<item quantity="one">Engádese a representación %1$s .</item>
<item quantity="other">Engádense as representacións %1$s .</item>
</plurals>
<string name="depictions_edit_helper_edit_message_else">Non foi posíbel engadir representacións.</string>
<string name="coordinates_edit_helper_make_edit_toast">A tentar actualizar coordenadas.</string>
<string name="coordinates_edit_helper_show_edit_title">Actualización de coordenadas</string>
<string name="description_edit_helper_show_edit_title">Actualización da descrición</string>
<string name="caption_edit_helper_show_edit_title">Actualización da lenda</string>
<string name="coordinates_edit_helper_show_edit_title_success">Todo correcto</string>
<string name="coordinates_edit_helper_show_edit_message">Engádense as coordenadas %1$s .</string>
<string name="description_edit_helper_show_edit_message">Engádense as descricións.</string>
<string name="caption_edit_helper_show_edit_message">Engádese a lenda.</string>
<string name="coordinates_edit_helper_edit_message_else">Non foi posíbel engadir as coordenadas.</string>
<string name="description_edit_helper_edit_message_else">Non foi posíbel engadir descricións.</string>
<string name="caption_edit_helper_edit_message_else">Non foi posíbel engadir lenda.</string>
<string name="share_image_via">Compartir imaxe vía</string>
<string name="account_created">Conta creada!</string>
<string name="place_state_exists">Existe</string>
@ -429,4 +506,28 @@
<string name="setting_wallpaper_dialog_title">Definir como fondo de pantalla</string>
<string name="theme_dark_name">Escuro</string>
<string name="theme_light_name">Claro</string>
<string name="todo_improve">Melloras suxeridas:</string>
<string name="missing_category">- Engadir categorías a esta imaxe para mellorar a usabilidade.</string>
<string name="missing_article">- Engade esta imaxe ao artigo asociado da Wikipedia que non ten imaxes.</string>
<string name="add_picture_to_wikipedia_article_title">Engadir imaxe á Wikipedia</string>
<string name="confirm">Confirmar</string>
<string name="instructions_title">Instrucións</string>
<string name="wikipedia_instructions_step_7">7. Publicar o artigo</string>
<string name="pause">pausar</string>
<string name="resume">continuar</string>
<string name="paused">En pausa</string>
<string name="more">Máis</string>
<string name="bookmarks">Marcadores</string>
<string name="achievements_tab_title">Logros</string>
<string name="leaderboard_tab_title">Tǃboa de maior actividade</string>
<string name="rank_prefix">Clasificaciónː</string>
<string name="count_prefix">Número:</string>
<string name="leaderboard_column_rank">Clasificación</string>
<string name="menu_set_avatar">Establecer como avatar</string>
<string name="leaderboard_yearly">Anualmente</string>
<string name="leaderboard_weekly">Semanalmente</string>
<string name="leaderboard_all_time">Todo o tempo</string>
<string name="leaderboard_upload">Enviar</string>
<string name="leaderboard_my_rank_button_text">A miña clasificación</string>
<string name="limited_connection_enabled">Activouse o modo de conexión limitadoǃ</string>
</resources>

View file

@ -4,6 +4,7 @@
* Abijeet Patro
* Anamdas
* Anandra
* AnupamM
* Bhatakati aatma
* Gopalindians
* Nilesh shukla
@ -349,4 +350,9 @@
<string name="leaderboard_column_count">गणना</string>
<string name="custom_selector_dismiss_limit_warning_button_text">रद्द करें</string>
<string name="talk">वार्ता</string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">क्या आप वाकई सभी अपलोड रद्द करना चाहते हैं?</string>
<string name="cancelling_all_the_uploads">सभी अपलोड रद्द किये जा रहे हैं...</string>
<string name="uploads">अपलोड</string>
<string name="pending">लंबित</string>
<string name="failed">विफल हुआ</string>
</resources>

View file

@ -5,6 +5,7 @@
* Arifin.wijaya
* DARMAS BUDI SANTOSO
* Daud I.F. Argana
* Fafau06
* Farras
* Gombang
* Hidayatsrf
@ -240,6 +241,7 @@
<string name="navigation_item_about">Perihal</string>
<string name="navigation_item_settings">Pengaturan</string>
<string name="navigation_item_feedback">Umpan balik</string>
<string name="navigation_item_feedback_github">Ulasan melalui GitHub</string>
<string name="navigation_item_logout">Keluar</string>
<string name="navigation_item_info">Tutorial</string>
<string name="navigation_item_notification">Pemberitahuan</string>
@ -349,6 +351,7 @@
<string name="share_app_title">Bagikan Aplikasi</string>
<string name="rotate">Putar</string>
<string name="error_fetching_nearby_places" fuzzy="true">Galat saat mengambil tempat terdekat.</string>
<string name="no_pictures_in_this_area">Tidak ada gambar di area ini</string>
<string name="no_nearby_places_around">Tidak ditemukan tempat yang dekat</string>
<string name="error_fetching_nearby_monuments">Galat saat mengambil monumen terdekat.</string>
<string name="no_recent_searches">Tidak ada pencarian terbaru</string>
@ -729,7 +732,19 @@
<string name="learn_how_to_write_a_useful_description">Pelajari cara menulis deskripsi yang berguna</string>
<string name="learn_how_to_write_a_useful_caption">Pelajari cara menulis takarir yang berguna</string>
<string name="see_your_achievements">Lihat pencapaian Anda</string>
<string name="edit_image">Edit Gambar</string>
<string name="edit_location">Edit Lokasi</string>
<string name="location_updated">Lokasi diperbarui!</string>
<string name="remove_location">Hapus Lokasi</string>
<string name="remove_location_warning_title">Hapus Peringatan Lokasi</string>
<string name="remove_location_warning_desc">Lokasi membuat gambar lebih berguna dan mudah ditemukan. Apakah Anda benar-benar ingin menghapus lokasi dari gambar ini?</string>
<string name="location_removed">Lokasi dihapus!</string>
<plurals name="custom_picker_images_selected_title_appendix">
<item quantity="other">%d gambar dipilih</item>
</plurals>
<string name="talk">Bicara</string>
<string name="cancelling_all_the_uploads">Membatalkan semua unggahan...</string>
<string name="uploads">Unggah</string>
<string name="pending">Menunggu</string>
<string name="failed">Gagal</string>
</resources>

View file

@ -8,6 +8,7 @@
* Black Sky83
* Champ0999
* Davio
* Dream Indigo
* Gianfranco
* Lorelai87
* Lorem Ipsum
@ -127,7 +128,7 @@
<string name="share_caption_hint">Didascalia</string>
<string name="login_failed_network">Impossibile accedere: errore di rete</string>
<string name="login_failed_throttled">Troppi tentativi falliti. Riprova tra alcuni minuti.</string>
<string name="login_failed_blocked">Spiacente, questo utente è stato bloccato su Commons</string>
<string name="login_failed_blocked">Spiacente, quest\'utente è stato/a bloccato/a su Commons</string>
<string name="login_failed_2fa_needed">Devi fornire il tuo codice di autenticazione a due fattori.</string>
<string name="login_failed_generic">Accesso non riuscito</string>
<string name="share_upload_button">Carica</string>
@ -737,9 +738,9 @@
<string name="menu_view_set_white_background">Imposta lo sfondo bianco</string>
<string name="menu_view_set_black_background">Imposta lo sfondo nero</string>
<string name="report_violation">Segnala violazione</string>
<string name="report_user">Segnala questo utente</string>
<string name="report_user">Segnala quest\'utente</string>
<string name="report_content">Segnala questo contenuto</string>
<string name="request_user_block">Richiedi di bloccare questo utente</string>
<string name="request_user_block">Richiedi di bloccare quest\'utente</string>
<string name="welcome_to_full_screen_mode_text">Benvenuto nella modalità di selezione a schermo intero</string>
<string name="full_screen_mode_zoom_info">Usa due dita per ingrandire e rimpicciolire.</string>
<string name="full_screen_mode_features_info">Scorri velocemente e a lungo per eseguire queste azioni: \n- Sinistra/destra: vai al precedente/successivo \n- Su: seleziona\n- Giù: contrassegna come da non caricare.</string>

View file

@ -815,5 +815,10 @@
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\"%1$s\" נמצא במקום אחר. נא לציין את המקום הנכון למטה, ואם אפשר, לכתוב את קו הרוחב ואת קו האורך הנכונים.</string>
<string name="other_problem_or_information_please_explain_below">בעיה אחרת או מידע אחר (נא להסביר הלאה).</string>
<string name="feedback_destination_note">המשוב שלך מתפרסם בדף הוויקי הבא: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Mobile app/Feedback&lt;/a&gt;</string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">האם ברצונך באמת לבטל את כל ההעלאות?</string>
<string name="cancelling_all_the_uploads">ביטול כל ההעלאות...</string>
<string name="uploads">העלאות</string>
<string name="pending">ממתינות</string>
<string name="failed">נכשלו</string>
<string name="could_not_load_place_data">לא היה אפשר לטעון את נתוני המקום</string>
</resources>

View file

@ -779,5 +779,10 @@
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">„%1$s“ се наоѓа на друго место. Подолу укажете го исправното место и, ако е можно, ставете исправна географска ширина и должина.</string>
<string name="other_problem_or_information_please_explain_below">Друг проблем или информација (објаснете подолу).</string>
<string name="feedback_destination_note">Вашите мислења се објавуваат на следнава викистраница: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Mobile app/Feedback&lt;/a&gt;</string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">Дали сигурно сакате да ги откажете сите подигања?</string>
<string name="cancelling_all_the_uploads">Ги откажувам сите подигања...</string>
<string name="uploads">Подигања</string>
<string name="pending">Во исчекување</string>
<string name="failed">Неуспешно</string>
<string name="could_not_load_place_data">Не можев да ги вчитам податоците за место</string>
</resources>

View file

@ -50,7 +50,7 @@
<string name="login_success" fuzzy="true">လုက်အေန် အာစိုပ်ဒတုဲ!</string>
<string name="login_failed" fuzzy="true">လံက်အေန် လီုလာ်!</string>
<string name="upload_failed">ဝှာင် ဟွံဂွံဆဵု၊ ပဂုန်တုဲ ဂၠာဲ ဝှာင်တၞဟ်။</string>
<string name="authentication_failed" fuzzy="true">ပွမစၟဳစၟတ်ဂှ် ဟွံအံင်ဇၞး။ ပဂုန်တုဲ လံက်အေန် မွဲဝါပၠန်</string>
<string name="authentication_failed">ပွမစၟဳစၟတ်ဂှ် ဟွံအံင်ဇၞး။ ပဂုန်တုဲ လံက်အေန် မွဲဝါပၠန်</string>
<string name="uploading_started">ပတိုန်ဝှာင် စဒၟံင်ရ!</string>
<string name="upload_completed_notification_title">%1$s ပတိုန်ပၠုပ်တုဲ!</string>
<string name="upload_completed_notification_text">ဒၞာဲမကလေင်ရံင် ဝှာင်ပတိုန်ပၠုပ် မၞး</string>
@ -69,11 +69,12 @@
<string name="menu_nearby">ဗဒဲါဒၞာဲဏအ်</string>
<string name="provider_contributions">ပတိုန်ပၠုပ် ဇကုဂမၠိုင်</string>
<string name="menu_share">ပါ်ပရအ်</string>
<string name="menu_view_file_page">ဗဵု မုက်လိက် ဝှာင်</string>
<string name="share_title_hint">က္ဍိုပ်လိက် (အာတ်မိက်ဒၟံင်)</string>
<string name="add_caption_toast">ပဂုန်တုဲ ကဵု က္ဍိုပ်လိက် ဝှာင်ဏအ်ညိ</string>
<string name="share_description_hint">မဗမံက်ထ္ၜး</string>
<string name="share_caption_hint" fuzzy="true">က္ဍိုပ်လိက် (ပိုင်ခြာလဝ် လၟိဟ်မလိက် ၂၅၅)</string>
<string name="login_failed_network" fuzzy="true">လုပ်လံက်အေန် ဟွံဂွံ - ဇာဇၞိက် ဗၠေတ်</string>
<string name="share_caption_hint">က္ဍိုပ်လိက်</string>
<string name="login_failed_network">လုပ်လံက်အေန် ဟွံဂွံ - ဇာဇၞိက် ဗၠေတ်</string>
<string name="login_failed_throttled">ပရေင်ဂိုတ်ဂစာန် ဟွံအံင်ဇၞး ဂၠိုင်လောန်အာရ။ ပဂုန်တုဲ မိနေတ်ညိညပၠန် ကလေင်စမ်ပၠန်။</string>
<string name="login_failed_blocked">သၠးအခေါင်၊ ညးလွပ်ဏအ် ဒးဒုင်ကၟာတ်လဒဵုလဝ် ပ္ဍဲ ကောမ်မောန်</string>
<string name="login_failed_2fa_needed">ကုစၟဳစၟတ်မၞးၜါဂှ် သ္ဒးပါ်လဝ် ဗွဲတၞဟ်ခြာရောင်။</string>
@ -85,7 +86,7 @@
<string name="categories_search_text_hint">ဂၠာဲ ကဏ္ဍဂမၠိုၚ်</string>
<string name="depicts_search_text_hint">ဂၠာဲ တင်ဂၞင် မၞိဟ်မဗၟံက်ထ္ၜး (မပတံ ဒဵု၊ ဍုင်လ္ဂုင်)</string>
<string name="menu_save_categories">ဂိုင်သိပ်</string>
<string name="refresh_button">ကလေင်မၚုဟ်</string>
<string name="refresh_button">ကလေင်မင္ၚုဟ်</string>
<string name="display_list_button">စရၚ်</string>
<string name="contributions_subtitle_zero" fuzzy="true">ဟွံဂွံ ပတိုန်ပၠုပ်ဏီ</string>
<string name="categories_not_found">ကဏ္ဍ မကိတ်ညဳ ကု %1$s ဟွံဆဵု</string>

View file

@ -800,5 +800,10 @@
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">%1$s bevindt zich ergens anders. Geef hieronder de juiste plaats aan en noteer, indien mogelijk, de juiste breedte- en lengtegraad.</string>
<string name="other_problem_or_information_please_explain_below">Ander probleem of andere informatie (verklaar hieronder).</string>
<string name="feedback_destination_note">Uw feedback wordt op de volgende wikipagina geplaatst: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Mobile app/Feedback&lt;/a&gt;</string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">Weet u zeker dat u alle uploads wilt annuleren?</string>
<string name="cancelling_all_the_uploads">Alle uploads worden geannuleerd…</string>
<string name="uploads">Uploads</string>
<string name="pending">In behandeling</string>
<string name="failed">Mislukt</string>
<string name="could_not_load_place_data">Plaatsgegevens konden niet geladen worden</string>
</resources>

View file

@ -775,5 +775,10 @@
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\'%1$s\' a l\'é ant un pòst diferent. Për piasì, ch\'a spessìfica ël pòst giust sì-sota e, si possìbil, ch\'a scriva latitùdin e longitùdin giuste.</string>
<string name="other_problem_or_information_please_explain_below">Àutr problema o anformassion (për piasì, ch\'a spiega sì-sota).</string>
<string name="feedback_destination_note">Ij sò sugeriment a saran giontà a coste pàgine wiki: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Mobile app/Feedback&lt;/a&gt;</string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">É-lo sigur ëd vorèj anulé tuti ij cariament?</string>
<string name="cancelling_all_the_uploads">Anulament ëd tuti ij cariament...</string>
<string name="uploads">Cariament</string>
<string name="pending">An atèisa</string>
<string name="failed">Falì</string>
<string name="could_not_load_place_data">Impossìbil carié ij dàit dël pòst</string>
</resources>

View file

@ -18,6 +18,7 @@
* Jesusmc
* Kaganer
* Kareyac
* Lutece398
* MaxBioHazard
* McDutchie
* Megakott
@ -839,5 +840,8 @@
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\'%1$s\' находится в другом месте. Пожалуйста, укажите правильное место ниже и, если возможно, напишите правильную широту и долготу.</string>
<string name="other_problem_or_information_please_explain_below">Другая проблема или информация (пожалуйста, объясните ниже).</string>
<string name="feedback_destination_note">Ваш отзыв будет опубликован на следующей вики-странице: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Mobile app/Feedback&lt;/a&gt;</string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">Вы уверены, что хотите отменить все загрузки?</string>
<string name="cancelling_all_the_uploads">Отмена всех загрузок...</string>
<string name="uploads">Загрузки</string>
<string name="could_not_load_place_data">Не удалось загрузить данные о месте</string>
</resources>

View file

@ -270,4 +270,7 @@
<string name="menu_view_report">رپورٹ</string>
<string name="error_sending_thanks">مصنف کوں شکریہ بھیڄݨ وچ خرابی۔</string>
<string name="talk">ڳالھ مہاڑ</string>
<string name="uploads">اپلوڈاں</string>
<string name="pending">وچار ہیٹھ</string>
<string name="failed">ناکام تھیا</string>
</resources>

View file

@ -735,4 +735,7 @@
<string name="please_enter_some_comments">Унесите коментар</string>
<string name="talk">Разговор</string>
<string name="does_not_exist_anymore_no_picture_can_ever_be_taken_of_it">„%1$s” не постоји више, и није га могуће више сликати.</string>
<string name="uploads">Отпремања</string>
<string name="pending">На чекању</string>
<string name="failed">Није успело</string>
</resources>

View file

@ -783,5 +783,10 @@
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\"%1$s\" är på en annan plats. Ange den korrekta platsen nedan samt ange latitud och longitud om det är möjligt.</string>
<string name="other_problem_or_information_please_explain_below">Andra problem eller information (ange nedan).</string>
<string name="feedback_destination_note">Din återkoppling kommer att skickas till följande wikisida: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Mobilapp/Återkoppling&lt;/a&gt;</string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">Är du säker på att du vill avbryta alla uppladdningar?</string>
<string name="cancelling_all_the_uploads">Avbryter alla uppladdningar...</string>
<string name="uploads">Uppladdningar</string>
<string name="pending">Pågår</string>
<string name="failed">Misslyckades</string>
<string name="could_not_load_place_data">Kunde inte läsa in platsdata</string>
</resources>

View file

@ -800,5 +800,10 @@
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">「%1$s」位於不同的位置。請在下面指定正確的位置可以的話請填寫正確的經緯度。</string>
<string name="other_problem_or_information_please_explain_below">其他問題或資訊(請在下方解釋)。</string>
<string name="feedback_destination_note">您的回饋已發布到以下 wiki 頁面:&lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Mobile app/Feedback&lt;/a&gt;</string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">您確定要取消所有上傳嗎?</string>
<string name="cancelling_all_the_uploads">正在取消所有上傳…</string>
<string name="uploads">上傳</string>
<string name="pending">待處理</string>
<string name="failed">失敗</string>
<string name="could_not_load_place_data">無法載入地點資料</string>
</resources>

View file

@ -42,6 +42,7 @@
<attr name="more_bottom_sheet_drawable_color" format="reference"/>
<attr name="card_item_color" format="reference"/>
<attr name="notification_icon_drawable" format="reference"/>
<attr name="upload_icon_drawable" format="reference"/>
<attr name="notification_icon_text_color" format="reference"/>
<attr name="toggle_theme" format="reference"/>
<attr name="contributionsListTextSecondary" format="reference"/>

View file

@ -826,5 +826,10 @@ Upload your first media by tapping on the add button.</string>
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\'%1$s\' is at a different place. Please specify the correct place below, and if possible, write the correct latitude and longitude.</string>
<string name="other_problem_or_information_please_explain_below">Other problem or information (please explain below).</string>
<string name="feedback_destination_note">Your feedback gets posted to the following wiki page: <![CDATA[ <a href="https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback">Commons:Mobile app/Feedback</a> ]]></string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">Are you sure that you want cancel all the uploads?</string>
<string name="cancelling_all_the_uploads">Cancelling all the uploads...</string>
<string name="uploads">Uploads</string>
<string name="pending">Pending</string>
<string name="failed">Failed</string>
<string name="could_not_load_place_data">Could not load place data</string>
</resources>

View file

@ -48,6 +48,7 @@
<item name="more_bottom_sheet_style">@style/DarkMoreBottomSheetStyle</item>
<item name="more_bottom_sheet_drawable_color">@color/white</item>
<item name="card_item_color">@color/white</item>
<item name="upload_icon_drawable">@drawable/ic_upload_white_24dp</item>
<item name="notification_icon_drawable">@drawable/ic_notifications_white_24dp</item>
<item name="notification_icon_text_color">@color/white</item>
<item name="toggle_theme">@style/SwitchThemeDark</item>
@ -108,6 +109,7 @@
<item name="more_bottom_sheet_style">@style/LightMoreBottomSheetStyle</item>
<item name="more_bottom_sheet_drawable_color">@color/black</item>
<item name="card_item_color">@color/primaryDarkColor</item>
<item name="upload_icon_drawable">@drawable/ic_upload_blue_24dp</item>
<item name="notification_icon_drawable">@drawable/ic_notifications_blue_24dp</item>
<item name="notification_icon_text_color">@color/primaryDarkColor</item>
<item name="toggle_theme">@style/SwitchThemeLight</item>

View file

@ -92,63 +92,6 @@ class ContributionViewHolderUnitTests {
Assert.assertNotNull(contributionViewHolder)
}
@Test
@Throws(Exception::class)
fun testSetResume() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
val method: Method = ContributionViewHolder::class.java.getDeclaredMethod(
"setResume"
)
method.isAccessible = true
method.invoke(contributionViewHolder)
}
@Test
@Throws(Exception::class)
fun testSetPaused() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
val method: Method = ContributionViewHolder::class.java.getDeclaredMethod(
"setPaused"
)
method.isAccessible = true
method.invoke(contributionViewHolder)
}
@Test
@Throws(Exception::class)
fun testPause() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
val method: Method = ContributionViewHolder::class.java.getDeclaredMethod(
"pause"
)
method.isAccessible = true
method.invoke(contributionViewHolder)
}
@Test
@Throws(Exception::class)
fun testResume() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
val method: Method = ContributionViewHolder::class.java.getDeclaredMethod(
"resume"
)
method.isAccessible = true
method.invoke(contributionViewHolder)
}
@Test
@Throws(Exception::class)
fun testOnPauseResumeButtonClickedCaseTrue() {
contributionViewHolder.onPauseResumeButtonClicked()
}
@Test
@Throws(Exception::class)
fun testOnPauseResumeButtonClickedCaseFalse() {
bindind.pauseResumeButton.tag = ""
contributionViewHolder.onPauseResumeButtonClicked()
}
@Test
@Throws(Exception::class)
fun testWikipediaButtonClicked() {
@ -161,18 +104,6 @@ class ContributionViewHolderUnitTests {
contributionViewHolder.imageClicked()
}
@Test
@Throws(Exception::class)
fun testDeleteUpload() {
contributionViewHolder.deleteUpload()
}
@Test
@Throws(Exception::class)
fun testRetryUpload() {
contributionViewHolder.retryUpload()
}
@Test
@Throws(Exception::class)
fun testChooseImageSource() {
@ -240,17 +171,6 @@ class ContributionViewHolderUnitTests {
contributionViewHolder.init(0, contribution)
}
@Test
@Throws(Exception::class)
fun testInitCaseNonNull_STATE_QUEUED_LIMITED_CONNECTION_MODE() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
`when`(contribution.state).thenReturn(Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE)
`when`(contribution.media).thenReturn(media)
`when`(media.mostRelevantCaption).thenReturn("")
`when`(media.author).thenReturn("")
contributionViewHolder.init(0, contribution)
}
@Test
@Throws(Exception::class)
fun testInitCaseNonNull_STATE_IN_PROGRESS() {

View file

@ -205,7 +205,6 @@ class ContributionsFragmentUnitTests {
`when`(menu.findItem(anyInt())).thenReturn(menuItem)
`when`(menuItem.actionView).thenReturn(notification)
`when`(store.getBoolean(anyString(), anyBoolean())).thenReturn(true)
fragment.updateLimitedConnectionToggle(menu)
}
@Test

View file

@ -137,20 +137,6 @@ class ContributionsListFragmentUnitTests {
method.invoke(fragment, contribution)
}
@Test
@Throws(Exception::class)
fun testResumeUpload() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.resumeUpload(contribution)
}
@Test
@Throws(Exception::class)
fun testPauseUpload() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.pauseUpload(contribution)
}
@Test
@Throws(Exception::class)
fun testAddImageToWikipedia() {
@ -165,20 +151,6 @@ class ContributionsListFragmentUnitTests {
fragment.openMediaDetail(0, true)
}
@Test
@Throws(Exception::class)
fun testDeleteUpload() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.deleteUpload(contribution)
}
@Test
@Throws(Exception::class)
fun testRetryUpload() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.retryUpload(contribution)
}
@Test
@Throws(Exception::class)
fun testOnViewStateRestored() {

View file

@ -54,12 +54,4 @@ class ContributionsListPresenterTest {
);
}
@Test
fun testDeleteUpload() {
whenever(repository.deleteContributionFromDB(any<Contribution>()))
.thenReturn(Completable.complete())
contributionsListPresenter.deleteUpload(mock(Contribution::class.java))
verify(repository, times(1))
.deleteContributionFromDB(ArgumentMatchers.any(Contribution::class.java));
}
}

View file

@ -8,6 +8,7 @@ import androidx.loader.content.CursorLoader
import androidx.loader.content.Loader
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.repository.UploadRepository
import io.reactivex.Completable
import io.reactivex.schedulers.TestScheduler
import org.junit.Before
@ -24,6 +25,10 @@ import org.mockito.MockitoAnnotations
class ContributionsPresenterTest {
@Mock
internal lateinit var repository: ContributionsRepository
@Mock
internal lateinit var uploadRepository: UploadRepository
@Mock
internal lateinit var view: ContributionsContract.View
@ -37,9 +42,11 @@ class ContributionsPresenterTest {
lateinit var liveData: LiveData<List<Contribution>>
@Rule @JvmField var instantTaskExecutorRule = InstantTaskExecutorRule()
@Rule
@JvmField
var instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var scheduler : TestScheduler
lateinit var scheduler: TestScheduler
/**
* initial setup
@ -48,35 +55,23 @@ class ContributionsPresenterTest {
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
scheduler=TestScheduler()
scheduler = TestScheduler()
cursor = Mockito.mock(Cursor::class.java)
contribution = Mockito.mock(Contribution::class.java)
contributionsPresenter = ContributionsPresenter(repository, scheduler)
contributionsPresenter = ContributionsPresenter(repository, uploadRepository, scheduler)
loader = Mockito.mock(CursorLoader::class.java)
contributionsPresenter.onAttachView(view)
liveData=MutableLiveData()
}
/**
* Test presenter actions onDeleteContribution
*/
@Test
fun testDeleteContribution() {
whenever(repository.deleteContributionFromDB(ArgumentMatchers.any<Contribution>()))
.thenReturn(Completable.complete())
contributionsPresenter.deleteUpload(contribution)
verify(repository).deleteContributionFromDB(contribution)
liveData = MutableLiveData()
}
/**
* Test fetch contribution with filename
*/
@Test
fun testGetContributionWithFileName(){
fun testGetContributionWithFileName() {
contributionsPresenter.getContributionsWithTitle("ashish")
verify(repository).getContributionWithFileName("ashish")
}
}

View file

@ -195,25 +195,6 @@ class MainActivityUnitTests {
MainActivity.startYourself(mockContext)
}
@Test
@Throws(Exception::class)
fun testToggleLimitedConnectionModeCaseDefault() {
activity.toggleLimitedConnectionMode()
}
@Test
@Throws(Exception::class)
fun testToggleLimitedConnectionMode() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
`when`(
defaultKvStore.getBoolean(
CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false
)
)
.thenReturn(false)
activity.toggleLimitedConnectionMode()
}
@Test
@Throws(Exception::class)
fun testSetUpPager() {

View file

@ -15,19 +15,25 @@ import fr.free.nrw.commons.CommonsApplication.DEFAULT_EDIT_SUMMARY
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import fr.free.nrw.commons.contributions.ChunkInfo
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.contributions.ContributionDao
import fr.free.nrw.commons.upload.UploadClient.TimeProvider
import fr.free.nrw.commons.wikidata.mwapi.MwException
import fr.free.nrw.commons.wikidata.mwapi.MwServiceError
import io.reactivex.Observable
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertSame
import kotlinx.coroutines.runBlocking
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okio.Buffer
import org.junit.Assert
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.jupiter.api.assertThrows
import org.junit.platform.commons.annotation.Testable
import java.io.File
import java.util.Date
@ -41,14 +47,24 @@ class UploadClientTest {
private val pageContentsCreator = mock<PageContentsCreator>()
private val fileUtilsWrapper = mock<FileUtilsWrapper>()
private val gson = mock<Gson>()
private val contributionDao = mock<ContributionDao> { }
private val timeProvider = mock<TimeProvider>()
private val uploadClient = UploadClient(uploadInterface, csrfTokenClient, pageContentsCreator, fileUtilsWrapper, gson, timeProvider)
private val uploadClient = UploadClient(
uploadInterface,
csrfTokenClient,
pageContentsCreator,
fileUtilsWrapper,
gson,
timeProvider,
contributionDao
)
private val expectedChunkSize = 512 * 1024
private val testToken = "test-token"
private val createdContent = "content"
private val filename = "test.jpg"
private val filekey = "the-key"
private val pageId = "page-id"
private val errorCode = "the-code"
private val uploadJson = Gson().fromJson("{\"foo\" = 1}", JsonObject::class.java)
@ -64,7 +80,15 @@ class UploadClientTest {
@Test
fun testUploadFileFromStash_NoErrors() {
whenever(gson.fromJson(uploadJson, UploadResponse::class.java)).thenReturn(uploadResponse)
whenever(uploadInterface.uploadFileFromStash(testToken, createdContent, DEFAULT_EDIT_SUMMARY, filename, filekey)).thenReturn(Observable.just(uploadJson))
whenever(
uploadInterface.uploadFileFromStash(
testToken,
createdContent,
DEFAULT_EDIT_SUMMARY,
filename,
filekey
)
).thenReturn(Observable.just(uploadJson))
val result = uploadClient.uploadFileFromStash(contribution, filename, filekey).test()
@ -80,7 +104,15 @@ class UploadClientTest {
whenever(gson.fromJson(uploadJson, UploadResponse::class.java)).thenReturn(errorResponse)
whenever(gson.fromJson(uploadJson, MwException::class.java)).thenReturn(uploadException)
whenever(uploadInterface.uploadFileFromStash(testToken, createdContent, DEFAULT_EDIT_SUMMARY, filename, filekey)).thenReturn(Observable.just(uploadJson))
whenever(
uploadInterface.uploadFileFromStash(
testToken,
createdContent,
DEFAULT_EDIT_SUMMARY,
filename,
filekey
)
).thenReturn(Observable.just(uploadJson))
val result = uploadClient.uploadFileFromStash(contribution, filename, filekey).test()
@ -91,7 +123,15 @@ class UploadClientTest {
@Test
fun testUploadFileFromStash_Failure() {
val exception = Exception("test")
whenever(uploadInterface.uploadFileFromStash(testToken, createdContent, DEFAULT_EDIT_SUMMARY, filename, filekey))
whenever(
uploadInterface.uploadFileFromStash(
testToken,
createdContent,
DEFAULT_EDIT_SUMMARY,
filename,
filekey
)
)
.thenReturn(Observable.error(exception))
val result = uploadClient.uploadFileFromStash(contribution, filename, filekey).test()
@ -104,7 +144,8 @@ class UploadClientTest {
fun testUploadChunkToStash_Success() {
val fileContent = "content"
val requestBody: RequestBody = fileContent.toRequestBody("text/plain".toMediaType())
val countingRequestBody = CountingRequestBody(requestBody, mock(), 0, fileContent.length.toLong())
val countingRequestBody =
CountingRequestBody(requestBody, mock(), 0, fileContent.length.toLong())
val filenameCaptor: KArgumentCaptor<RequestBody> = argumentCaptor<RequestBody>()
val totalFileSizeCaptor = argumentCaptor<RequestBody>()
@ -113,12 +154,15 @@ class UploadClientTest {
val tokenCaptor = argumentCaptor<RequestBody>()
val fileCaptor = argumentCaptor<MultipartBody.Part>()
whenever(uploadInterface.uploadFileToStash(
whenever(
uploadInterface.uploadFileToStash(
filenameCaptor.capture(), totalFileSizeCaptor.capture(), offsetCaptor.capture(),
fileKeyCaptor.capture(), tokenCaptor.capture(), fileCaptor.capture()
)).thenReturn(Observable.just(uploadResponse))
)
).thenReturn(Observable.just(uploadResponse))
val result = uploadClient.uploadChunkToStash(filename, 100, 10, filekey, countingRequestBody).test()
val result =
uploadClient.uploadChunkToStash(filename, 100, 10, filekey, countingRequestBody).test()
result.assertNoErrors()
assertSame(uploadResult, result.values()[0])
@ -156,28 +200,18 @@ class UploadClientTest {
assertEquals(StashUploadState.SUCCESS, stashResult.state)
}
@Test
fun uploadFileToStash_contributionIsUnpaused() {
whenever(contribution.isCompleted()).thenReturn(false)
whenever(contribution.fileKey).thenReturn(filekey)
whenever(fileUtilsWrapper.getMimeType(anyOrNull<File>())).thenReturn("image/png")
whenever(fileUtilsWrapper.getFileChunks(anyOrNull<File>(), eq(expectedChunkSize))).thenReturn(emptyList())
val result = uploadClient.uploadFileToStash(filename, contribution, mock()).test()
result.assertNoErrors()
verify(contribution, times(1)).unpause()
}
@Test
fun uploadFileToStash_returnsFailureIfNothingToUpload() {
val tempFile = File.createTempFile("tempFile", ".tmp")
tempFile.deleteOnExit()
whenever(contribution.isCompleted()).thenReturn(false)
whenever(contribution.fileKey).thenReturn(filekey)
whenever(contribution.pageId).thenReturn(pageId)
whenever(contributionDao.getContribution(pageId)).thenReturn(contribution)
whenever(contribution.localUriPath).thenReturn(tempFile)
whenever(fileUtilsWrapper.getMimeType(anyOrNull<File>())).thenReturn("image/png")
whenever(fileUtilsWrapper.getFileChunks(anyOrNull<File>(), eq(expectedChunkSize))).thenReturn(emptyList())
val result = uploadClient.uploadFileToStash(filename, contribution, mock()).test()
val result = uploadClient.uploadFileToStash(filename, contribution, mock() ).test()
result.assertNoErrors()
assertEquals(StashUploadState.FAILED, result.values()[0].state)
}
@ -188,10 +222,26 @@ class UploadClientTest {
whenever(mockFile.length()).thenReturn(1)
whenever(contribution.localUriPath).thenReturn(mockFile)
whenever(contribution.isCompleted()).thenReturn(false)
whenever(contribution.pageId).thenReturn(pageId)
whenever(contributionDao.getContribution(pageId)).thenReturn(contribution)
whenever(contribution.fileKey).thenReturn(filekey)
whenever(fileUtilsWrapper.getMimeType(anyOrNull<File>())).thenReturn("image/png")
whenever(fileUtilsWrapper.getFileChunks(anyOrNull<File>(), eq(expectedChunkSize))).thenReturn(listOf(mockFile))
whenever(uploadInterface.uploadFileToStash(any(), any(), any(), any(), any(), any())).thenReturn(Observable.just(uploadResponse))
whenever(
fileUtilsWrapper.getFileChunks(
anyOrNull<File>(),
eq(expectedChunkSize)
)
).thenReturn(listOf(mockFile))
whenever(
uploadInterface.uploadFileToStash(
any(),
any(),
any(),
any(),
any(),
any()
)
).thenReturn(Observable.just(uploadResponse))
val result = uploadClient.uploadFileToStash(filename, contribution, mock()).test()
@ -215,12 +265,23 @@ class UploadClientTest {
whenever(contribution.dateModified).thenReturn(Date(100))
whenever(timeProvider.currentTimeMillis()).thenReturn(200)
whenever(contribution.fileKey).thenReturn(filekey)
whenever(contribution.pageId).thenReturn(pageId)
whenever(contributionDao.getContribution(pageId)).thenReturn(contribution)
whenever(fileUtilsWrapper.getMimeType(anyOrNull<File>())).thenReturn("image/png")
whenever(fileUtilsWrapper.getFileChunks(anyOrNull<File>(), eq(expectedChunkSize))).thenReturn(listOf(mockFile))
whenever(
fileUtilsWrapper.getFileChunks(
anyOrNull<File>(),
eq(expectedChunkSize)
)
).thenReturn(listOf(mockFile))
whenever(uploadInterface.uploadFileToStash(anyOrNull(), anyOrNull(), anyOrNull(),
anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(Observable.just(uploadResponse))
whenever(
uploadInterface.uploadFileToStash(
anyOrNull(), anyOrNull(), anyOrNull(),
anyOrNull(), anyOrNull(), anyOrNull()
)
).thenReturn(Observable.just(uploadResponse))
val result = uploadClient.uploadFileToStash(filename, contribution, mock()).test()

View file

@ -8,6 +8,7 @@ import fr.free.nrw.commons.Media
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.kvstore.JsonKvStore
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.mockito.InjectMocks
import org.mockito.Mock
@ -32,6 +33,7 @@ class UploadControllerTest {
MockitoAnnotations.openMocks(this)
}
@Ignore
@Test
fun startUpload() {
val contribution = mock(Contribution::class.java)

View file

@ -6,6 +6,7 @@ import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import media
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
@ -28,6 +29,7 @@ class UploadModelUnitTest {
)
}
@Ignore
@Test
fun `Test onDepictItemClicked when DepictedItem is selected`(){
uploadModel.onDepictItemClicked(
@ -42,6 +44,7 @@ class UploadModelUnitTest {
), media(filename = "File:Example.jpg"))
}
@Ignore
@Test
fun `Test onDepictItemClicked when DepictedItem is not selected`(){
uploadModel.onDepictItemClicked(
@ -57,6 +60,7 @@ class UploadModelUnitTest {
)
}
@Ignore
@Test
fun `Test onDepictItemClicked when DepictedItem is not selected and not included in media`(){
uploadModel.onDepictItemClicked(
@ -72,6 +76,7 @@ class UploadModelUnitTest {
)
}
@Ignore
@Test
fun `Test onDepictItemClicked when media is null and DepictedItem is not selected`(){
uploadModel.onDepictItemClicked(
@ -86,6 +91,7 @@ class UploadModelUnitTest {
), null)
}
@Ignore
@Test
fun `Test onDepictItemClicked when media is not null and DepictedItem is selected`(){
uploadModel.onDepictItemClicked(
@ -100,6 +106,7 @@ class UploadModelUnitTest {
), media(filename = "File:Example.jpg"))
}
@Ignore
@Test
fun `Test onDepictItemClicked when media is null and DepictedItem is selected`(){
uploadModel.onDepictItemClicked(
@ -114,11 +121,13 @@ class UploadModelUnitTest {
), null)
}
@Ignore
@Test
fun testGetSelectedExistingDepictions(){
uploadModel.selectedExistingDepictions
}
@Ignore
@Test
fun testSetSelectedExistingDepictions(){
uploadModel.selectedExistingDepictions = listOf("")

View file

@ -9,6 +9,7 @@ import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.ImageCoordinates
import io.reactivex.Observable
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.InjectMocks
@ -68,6 +69,7 @@ class UploadPresenterTest {
/**
* unit test case for method UploadPresenter.handleSubmit
*/
@Ignore
@Test
fun handleSubmitTestUserLoggedIn() {
`when`(view.isLoggedIn).thenReturn(true)
@ -78,6 +80,7 @@ class UploadPresenterTest {
verify(repository).buildContributions()
}
@Ignore
@Test
fun handleSubmitImagesNoLocationWithConsecutiveNoLocationUploads() {
`when`(imageCoords.imageCoordsExists).thenReturn(false)
@ -102,6 +105,7 @@ class UploadPresenterTest {
verify(view).showAlertDialog(ArgumentMatchers.anyInt(), ArgumentMatchers.any<Runnable>())
}
@Ignore
@Test
fun handleSubmitImagesWithLocationWithConsecutiveNoLocationUploads() {
`when`(
@ -117,6 +121,7 @@ class UploadPresenterTest {
.showAlertDialog(ArgumentMatchers.anyInt(), ArgumentMatchers.any<Runnable>())
}
@Ignore
@Test
fun handleSubmitTestUserLoggedInAndLimitedConnectionOn() {
`when`(
@ -136,6 +141,7 @@ class UploadPresenterTest {
/**
* unit test case for method UploadPresenter.handleSubmit
*/
@Ignore
@Test
fun handleSubmitTestUserNotLoggedIn() {
`when`(view.isLoggedIn).thenReturn(false)
@ -152,6 +158,7 @@ class UploadPresenterTest {
/**
* Test which asserts If the next fragment to be shown is not one of the MediaDetailsFragment, lets hide the top card
*/
@Ignore
@Test
fun hideTopCardWhenReachedTheLastFile(){
deletePictureBaseTest()
@ -163,6 +170,7 @@ class UploadPresenterTest {
/**
* Test media deletion during single upload
*/
@Ignore
@Test
fun testDeleteWhenSingleUpload(){
deletePictureBaseTest()
@ -176,6 +184,7 @@ class UploadPresenterTest {
/**
* Test media deletion during multiple upload
*/
@Ignore
@Test
fun testDeleteWhenMultipleFilesUpload(){
deletePictureBaseTest()

View file

@ -17,6 +17,7 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.Completable
import io.reactivex.Single
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
import org.mockito.Mock
@ -199,7 +200,6 @@ class UploadRepositoryUnitTest {
)
}
@Test
fun testDeletePicture() {
assertEquals(repository.deletePicture(""), uploadModel.deletePicture(""))

View file

@ -6,6 +6,7 @@ import entity
import entityId
import fr.free.nrw.commons.wikidata.WikidataProperties
import org.junit.Assert
import org.junit.Ignore
import org.junit.Test
import place
import snak

View file

@ -4,6 +4,7 @@ import com.nhaarman.mockitokotlin2.mock
import fr.free.nrw.commons.upload.FileUtils
import fr.free.nrw.commons.upload.FileUtilsWrapper
import org.junit.Assert.assertEquals
import org.junit.Ignore
import org.junit.Test
import java.io.*

View file

@ -13,6 +13,7 @@ import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertSame
import junit.framework.TestCase.assertTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.mockito.Mockito.mock

View file

@ -17,6 +17,7 @@ import org.mockito.MockitoAnnotations
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResult
import fr.free.nrw.commons.wikidata.model.Statement_partial
import org.junit.Ignore
class WikidataClientTest {

View file

@ -20,7 +20,6 @@ allprojects {
gradlePluginPortal() // potential jcenter() replacement
maven { url "https://jitpack.io" }
maven { url "https://maven.google.com" }
jcenter()
}
}
subprojects{