diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java index 0359e8f98..451ea2ad0 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -5,22 +5,10 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; -import android.content.pm.PackageManager; import android.database.Cursor; import android.database.DataSetObserver; -import android.os.Build; import android.os.Bundle; import android.os.IBinder; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; -import androidx.cursoradapter.widget.CursorAdapter; -import androidx.appcompat.app.AlertDialog; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -28,6 +16,16 @@ import android.widget.Adapter; import android.widget.CheckBox; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.cursoradapter.widget.CursorAdapter; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; + import java.util.ArrayList; import javax.inject.Inject; @@ -57,6 +55,7 @@ import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.upload.UploadService; import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.utils.DialogUtil; +import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -66,7 +65,7 @@ import timber.log.Timber; import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS; import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; -import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUEST; +import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION; import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING; import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; @@ -351,35 +350,6 @@ public class ContributionsFragment } - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - Timber.d("onRequestPermissionsResult"); - switch (requestCode) { - case LOCATION_REQUEST: { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Timber.d("Location permission granted, refreshing view"); - // No need to display permission request button anymore - locationManager.registerLocationManager(getActivity()); - } else { - if (store.getBoolean("displayLocationPermissionForCardView", true)) { - // Still ask for permission - DialogUtil.showAlertDialog(getActivity(), - getString(R.string.nearby_card_permission_title), - getString(R.string.nearby_card_permission_explanation), - this::displayYouWontSeeNearbyMessage, - this::enableLocationPermission, - checkBoxView, - false); - } - } - } - break; - - default: - // This is needed to allow the request codes from the Fragments to be routed appropriately - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - } /** * Replace whatever is in the current contributionsFragmentContainer view with * mediaDetailPagerFragment, and preserve previous state in back stack. @@ -496,7 +466,7 @@ public class ContributionsFragment if (store.getBoolean("displayNearbyCardView", true)) { - checkGPS(); + checkPermissionsAndShowNearbyCardView(); if (nearbyNotificationCardView.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) { nearbyNotificationCardView.setVisibility(View.VISIBLE); } @@ -509,77 +479,39 @@ public class ContributionsFragment fetchCampaigns(); } - /** - * Check GPS to decide displaying request permission button or not. - */ - private void checkGPS() { - if (!locationManager.isProviderEnabled()) { - Timber.d("GPS is not enabled"); - nearbyNotificationCardView.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_GPS; - if (store.getBoolean("displayLocationPermissionForCardView", true)) { - DialogUtil.showAlertDialog(getActivity(), - getString(R.string.nearby_card_permission_title), - getString(R.string.nearby_card_permission_explanation), - this::displayYouWontSeeNearbyMessage, - this::enableGPS, - checkBoxView, - false); - } - } else { - Timber.d("GPS is enabled"); - checkLocationPermission(); + private void checkPermissionsAndShowNearbyCardView() { + if (PermissionUtils.hasPermission(getActivity(), Manifest.permission.ACCESS_FINE_LOCATION)) { + onLocationPermissionGranted(); + } else if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) + && store.getBoolean("displayLocationPermissionForCardView", true) + && (((MainActivity) getActivity()).viewPager.getCurrentItem() == CONTRIBUTIONS_TAB_POSITION)) { + nearbyNotificationCardView.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION; + showNearbyCardPermissionRationale(); } } - private void checkLocationPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (locationManager.isLocationPermissionGranted(requireContext())) { - nearbyNotificationCardView.permissionType = NearbyNotificationCardView.PermissionType.NO_PERMISSION_NEEDED; - locationManager.registerLocationManager(getActivity()); - } else { - nearbyNotificationCardView.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION; - // If user didn't selected Don't ask again - if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) - && store.getBoolean("displayLocationPermissionForCardView", true)) { - DialogUtil.showAlertDialog(getActivity(), - getString(R.string.nearby_card_permission_title), - getString(R.string.nearby_card_permission_explanation), - this::displayYouWontSeeNearbyMessage, - this::enableLocationPermission, - checkBoxView, - false); - } - } - } else { - // If device is under Marshmallow, we already checked for GPS - nearbyNotificationCardView.permissionType = NearbyNotificationCardView.PermissionType.NO_PERMISSION_NEEDED; - locationManager.registerLocationManager(getActivity()); - } + private void requestLocationPermission() { + PermissionUtils.checkPermissionsAndPerformAction(getActivity(), + Manifest.permission.ACCESS_FINE_LOCATION, + this::onLocationPermissionGranted, + this::displayYouWontSeeNearbyMessage, + -1, + -1); } - private void enableLocationPermission() { - if (!getActivity().isFinishing()) { - ((MainActivity) getActivity()).locationManager.requestPermissions(getActivity()); - } + private void onLocationPermissionGranted() { + nearbyNotificationCardView.permissionType = NearbyNotificationCardView.PermissionType.NO_PERMISSION_NEEDED; + locationManager.registerLocationManager(getActivity()); } - private void enableGPS() { - new AlertDialog.Builder(getActivity()) - .setMessage(R.string.gps_disabled) - .setCancelable(false) - .setPositiveButton(R.string.enable_gps, - (dialog, id) -> { - Intent callGPSSettingIntent = new Intent( - android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS); - Timber.d("Loaded settings page"); - ((MainActivity) getActivity()).startActivityForResult(callGPSSettingIntent, 1); - }) - .setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> { - dialog.cancel(); - displayYouWontSeeNearbyMessage(); - }) - .create() - .show(); + private void showNearbyCardPermissionRationale() { + DialogUtil.showAlertDialog(getActivity(), + getString(R.string.nearby_card_permission_title), + getString(R.string.nearby_card_permission_explanation), + this::displayYouWontSeeNearbyMessage, + this::requestLocationPermission, + checkBoxView, + false); } private void displayYouWontSeeNearbyMessage() { @@ -589,7 +521,6 @@ public class ContributionsFragment private void updateClosestNearbyCardViewInfo() { curLatLng = locationManager.getLastLocation(); - compositeDisposable.add(Observable.fromCallable(() -> nearbyController .loadAttractionsFromLocation(curLatLng, curLatLng, true, false)) // thanks to boolean, it will only return closest result .subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index de3f011d8..c3c83021c 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -46,7 +46,6 @@ import io.reactivex.schedulers.Schedulers; import timber.log.Timber; import static android.content.ContentResolver.requestSync; -import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUEST; public class MainActivity extends AuthenticatedActivity implements FragmentManager.OnBackStackChangedListener { @@ -69,8 +68,8 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag public boolean isAuthCookieAcquired = false; public ContributionsActivityPagerAdapter contributionsActivityPagerAdapter; - public final int CONTRIBUTIONS_TAB_POSITION = 0; - public final int NEARBY_TAB_POSITION = 1; + public static final int CONTRIBUTIONS_TAB_POSITION = 0; + public static final int NEARBY_TAB_POSITION = 1; public boolean isContributionsFragmentVisible = true; // False means nearby fragment is visible private Menu menu; @@ -362,12 +361,6 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag } } - private boolean deviceHasCamera() { - PackageManager pm = getPackageManager(); - return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) || - pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); - } - public class ContributionsActivityPagerAdapter extends FragmentPagerAdapter { FragmentManager fragmentManager; private boolean isContributionsListFragment = true; // to know what to put in first tab, Contributions of Media Details @@ -451,15 +444,6 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag return "android:switcher:" + viewId + ":" + index; } - /** - * In first tab we can have ContributionsFragment or Media details fragment. This method - * is responsible to update related boolean - * @param isContributionsListFragment true when contribution fragment should be visible, false - * means user clicked to MediaDetails - */ - private void updateContributionFragmentTabContent(boolean isContributionsListFragment) { - this.isContributionsListFragment = isContributionsListFragment; - } } @Override @@ -469,26 +453,6 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag controller.handleActivityResult(this, requestCode, resultCode, data); } - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - if (requestCode == LOCATION_REQUEST) { - // If request is cancelled, the result arrays are empty. - if (grantResults.length > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Timber.d("Location permission given"); - ((ContributionsFragment)contributionsActivityPagerAdapter - .getItem(0)).locationManager.registerLocationManager(this); - } else { - // If nearby fragment is visible and location permission is not given, send user back to contrib fragment - if (!isContributionsFragmentVisible) { - viewPager.setCurrentItem(CONTRIBUTIONS_TAB_POSITION); - // TODO: If contrib fragment is visible and location permission is not given, display permission request button - } - } - } - } - - @Override protected void onResume() { super.onResume(); setNotificationCount(); diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index 903c35ab0..7f0ee4048 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -3,11 +3,12 @@ package fr.free.nrw.commons.di; import android.app.Activity; import android.content.ContentProviderClient; import android.content.Context; -import androidx.collection.LruCache; import android.view.inputmethod.InputMethodManager; import com.google.gson.Gson; +import org.wikipedia.dataclient.WikiSite; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -16,6 +17,7 @@ import java.util.Map; import javax.inject.Named; import javax.inject.Singleton; +import androidx.collection.LruCache; import dagger.Module; import dagger.Provides; import fr.free.nrw.commons.BuildConfig; @@ -24,7 +26,6 @@ import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.upload.UploadController; diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java index 23bf7e3fc..c8664cd9f 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java @@ -4,6 +4,8 @@ import android.content.Context; import com.google.gson.Gson; +import org.wikipedia.dataclient.ServiceFactory; +import org.wikipedia.dataclient.WikiSite; import org.wikipedia.json.GsonUtil; import java.io.File; @@ -20,6 +22,7 @@ import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; +import fr.free.nrw.commons.review.ReviewInterface; import okhttp3.Cache; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; @@ -108,4 +111,16 @@ public class NetworkingModule { return GsonUtil.getDefaultGson(); } + @Provides + @Singleton + @Named("commons-wikisite") + public WikiSite provideCommonsWikiSite() { + return new WikiSite(BuildConfig.COMMONS_URL); + } + + @Provides + @Singleton + public ReviewInterface provideReviewInterface(@Named("commons-wikisite") WikiSite commonsWikiSite) { + return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java index ee795b836..d639d0c73 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java @@ -1,20 +1,13 @@ package fr.free.nrw.commons.location; -import android.Manifest; -import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; -import android.content.pm.PackageManager; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; import android.util.Log; -import androidx.annotation.NonNull; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; - import java.util.HashSet; import java.util.List; import java.util.Set; @@ -45,78 +38,6 @@ public class LocationServiceManager implements LocationListener { this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); } - /** - * Returns the current status of the location provider. - * - * @return true if the location provider is enabled - */ - public boolean isProviderEnabled() { - return (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)); - } - - /** - * Returns whether the location permission is granted. - * @return true if the location permission is granted - */ - public boolean isLocationPermissionGranted(@NonNull Context context) { - return ContextCompat.checkSelfPermission(context, - Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; - } - - /** - * Requests the location permission to be granted. - * - * @param activity the activity - */ - public void requestPermissions(Activity activity) { - if (activity.isFinishing()) { - return; - } - ActivityCompat.requestPermissions(activity, - new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, - LOCATION_REQUEST); - } - - /** - * The permission explanation dialog box is now displayed just once for a particular activity. We are subscribing - * to updates from multiple providers so its important to show the dialog just once. Otherwise it will be displayed - * once for every provider, which in our case currently is 2. - * @param activity - * @return - */ - public boolean isPermissionExplanationRequired(Activity activity) { - if (activity.isFinishing()) { - return false; - } - boolean showRequestPermissionRationale = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_FINE_LOCATION); - if (showRequestPermissionRationale && !locationExplanationDisplayed.contains(activity)) { - locationExplanationDisplayed.add(activity); - return true; - } - return false; - } - - /** - * Gets the last known location in cases where there wasn't time to register a listener - * (e.g. when Location permission just granted) - * @return last known LatLng - */ - @SuppressLint("MissingPermission") - public LatLng getLKL(@NonNull Context context) { - if (isLocationPermissionGranted(context)) { - Location lastKL = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); - if (lastKL == null) { - lastKL = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); - } - if (lastKL == null) { - return null; - } - return LatLng.from(lastKL); - } else { - return null; - } - } - public LatLng getLastLocation() { if (lastLocation == null) { return null; diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java index a2c9e81d8..e829e946f 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.mwapi; import android.text.TextUtils; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -10,8 +11,6 @@ import com.google.gson.reflect.TypeToken; import org.apache.commons.lang3.StringUtils; import org.wikipedia.dataclient.mwapi.MwQueryPage; import org.wikipedia.dataclient.mwapi.MwQueryResponse; -import org.wikipedia.dataclient.mwapi.RecentChange; -import org.wikipedia.util.DateUtil; import java.io.IOException; import java.lang.reflect.Type; @@ -20,7 +19,6 @@ import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Random; import javax.inject.Inject; import javax.inject.Singleton; @@ -86,7 +84,7 @@ public class OkHttpJsonApiClient { public Single getUploadCount(String userName) { HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder(); urlBuilder - .addPathSegments("/uploadsbyuser.py") + .addPathSegments("uploadsbyuser.py") .addQueryParameter("user", userName); if (ConfigUtils.isBetaFlavour()) { @@ -112,7 +110,7 @@ public class OkHttpJsonApiClient { public Single getWikidataEdits(String userName) { HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder(); urlBuilder - .addPathSegments("/wikidataedits.py") + .addPathSegments("wikidataedits.py") .addQueryParameter("user", userName); if (ConfigUtils.isBetaFlavour()) { @@ -132,7 +130,9 @@ public class OkHttpJsonApiClient { return 0; } GetWikidataEditCountResponse countResponse = gson.fromJson(json, GetWikidataEditCountResponse.class); - return countResponse.getWikidataEditCount(); + if (null != countResponse) { + return countResponse.getWikidataEditCount(); + } } return 0; }); @@ -421,80 +421,4 @@ public class OkHttpJsonApiClient { private Map getContinueValues(String keyword) { return defaultKvStore.getJson("query_continue_" + keyword, mapType); } - - /** - * Returns recent changes on commons - * - * @return list of recent changes made - */ - @Nullable - public Single> getRecentFileChanges() { - final int RANDOM_SECONDS = 60 * 60 * 24 * 30; - final String FILE_NAMESPACE = "6"; - Random r = new Random(); - Date now = new Date(); - Date startDate = new Date(now.getTime() - r.nextInt(RANDOM_SECONDS) * 1000L); - - String rcStart = DateUtil.iso8601DateFormat(startDate); - HttpUrl.Builder urlBuilder = HttpUrl - .parse(commonsBaseUrl) - .newBuilder() - .addQueryParameter("action", "query") - .addQueryParameter("format", "json") - .addQueryParameter("formatversion", "2") - .addQueryParameter("list", "recentchanges") - .addQueryParameter("rcstart", rcStart) - .addQueryParameter("rcnamespace", FILE_NAMESPACE) - .addQueryParameter("rcprop", "title|ids") - .addQueryParameter("rctype", "new|log") - .addQueryParameter("rctoponly", "1"); - - Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - return Single.fromCallable(() -> { - Response response = okHttpClient.newCall(request).execute(); - if (response.body() != null && response.isSuccessful()) { - String json = response.body().string(); - MwQueryResponse mwQueryPage = gson.fromJson(json, MwQueryResponse.class); - return mwQueryPage.query().getRecentChanges(); - } - return new ArrayList<>(); - }); - } - - /** - * Returns the first revision of the file - * - * @return Revision object - */ - @Nullable - public Single getFirstRevisionOfFile(String filename) { - HttpUrl.Builder urlBuilder = HttpUrl - .parse(commonsBaseUrl) - .newBuilder() - .addQueryParameter("action", "query") - .addQueryParameter("format", "json") - .addQueryParameter("formatversion", "2") - .addQueryParameter("prop", "revisions") - .addQueryParameter("rvprop", "timestamp|ids|user") - .addQueryParameter("titles", filename) - .addQueryParameter("rvdir", "newer") - .addQueryParameter("rvlimit", "1"); - - Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - return Single.fromCallable(() -> { - Response response = okHttpClient.newCall(request).execute(); - if (response.body() != null && response.isSuccessful()) { - String json = response.body().string(); - MwQueryResponse mwQueryPage = gson.fromJson(json, MwQueryResponse.class); - return mwQueryPage.query().firstPage().revisions().get(0); - } - return null; - }); - } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java index adf217bff..ab43c970d 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java @@ -1,20 +1,11 @@ package fr.free.nrw.commons.nearby; +import android.Manifest; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.os.Build; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.constraintlayout.widget.ConstraintLayout; -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.snackbar.Snackbar; - -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; -import androidx.appcompat.app.AlertDialog; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -22,12 +13,19 @@ import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ProgressBar; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.snackbar.Snackbar; import com.google.gson.Gson; import java.util.List; import javax.inject.Inject; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; @@ -38,6 +36,7 @@ import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.location.LocationUpdateListener; import fr.free.nrw.commons.utils.FragmentUtils; import fr.free.nrw.commons.utils.NetworkUtils; +import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.wikidata.WikidataEditListener; import io.reactivex.Observable; @@ -45,6 +44,8 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; +import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION; +import static fr.free.nrw.commons.contributions.MainActivity.NEARBY_TAB_POSITION; import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED; import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED; @@ -193,8 +194,8 @@ public class NearbyFragment extends CommonsDaggerSupportFragment bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { @Override - public void onStateChanged(View bottomSheet, int newState) { - prepareViewsForSheetPosition(newState); + public void onStateChanged(View bottomSheet, int unusedNewState) { + prepareViewsForSheetPosition(); } @Override @@ -210,9 +211,8 @@ public class NearbyFragment extends CommonsDaggerSupportFragment /** * Sets camera position, zoom level according to sheet positions - * @param bottomSheetState expanded, collapsed or hidden */ - public void prepareViewsForSheetPosition(int bottomSheetState) { + private void prepareViewsForSheetPosition() { // TODO } @@ -246,7 +246,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment * * @param locationChangeType defines if location changed significantly or slightly */ - public void refreshView(LocationServiceManager.LocationChangeType locationChangeType) { + private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) { Timber.d("Refreshing nearby places"); if (lockNearbyView) { return; @@ -324,7 +324,7 @@ public class NearbyFragment extends CommonsDaggerSupportFragment * button. It populates places for custom location. * @param customLatLng Custom area which we will search around */ - public void refreshViewForCustomLocation(LatLng customLatLng, boolean refreshForCurrentLocation) { + void refreshViewForCustomLocation(LatLng customLatLng, boolean refreshForCurrentLocation) { if (customLatLng == null) { // If null, return return; @@ -568,130 +568,8 @@ public class NearbyFragment extends CommonsDaggerSupportFragment * This method first checks if the location permissions has been granted and then register the location manager for updates. */ private void registerLocationUpdates() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (locationManager.isLocationPermissionGranted(requireContext())) { - locationManager.registerLocationManager(getActivity()); - } else { - // Should we show an explanation? - if (locationManager.isPermissionExplanationRequired(getActivity())) { - new AlertDialog.Builder(getActivity()) - .setMessage(getString(R.string.location_permission_rationale_nearby)) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - requestLocationPermissions(); - dialog.dismiss(); - }) - .setNegativeButton(android.R.string.cancel, (dialog, id) -> { - showLocationPermissionDeniedErrorDialog(); - dialog.cancel(); - }) - .create() - .show(); - } else { - // No explanation needed, we can request the permission. - requestLocationPermissions(); - } - } - } else { - locationManager.registerLocationManager(getActivity()); - } - } - - /** - * Requests location permission if activity is not null - */ - private void requestLocationPermissions() { - if (!getActivity().isFinishing()) { - locationManager.requestPermissions(getActivity()); - } - } - - /** - * Will warn user if location is denied - */ - private void showLocationPermissionDeniedErrorDialog() { - new AlertDialog.Builder(getActivity()) - .setMessage(R.string.nearby_needs_permissions) - .setCancelable(false) - .setPositiveButton(R.string.give_permission, (dialog, which) -> { - //will ask for the location permission again - checkGps(); - }) - .setNegativeButton(R.string.cancel, (dialog, which) -> { - //dismiss dialog and send user to contributions tab instead - dialog.cancel(); - ((MainActivity)getActivity()).viewPager.setCurrentItem(((MainActivity)getActivity()).CONTRIBUTIONS_TAB_POSITION); - }) - .create() - .show(); - } - - /** - * Checks device GPS permission first for all API levels - */ - private void checkGps() { - Timber.d("checking GPS"); - if (!locationManager.isProviderEnabled()) { - Timber.d("GPS is not enabled"); - new AlertDialog.Builder(getActivity()) - .setMessage(R.string.gps_disabled) - .setCancelable(false) - .setPositiveButton(R.string.enable_gps, - (dialog, id) -> { - Intent callGPSSettingIntent = new Intent( - android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS); - Timber.d("Loaded settings page"); - startActivityForResult(callGPSSettingIntent, 1); - }) - .setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> { - showLocationPermissionDeniedErrorDialog(); - dialog.cancel(); - }) - .create() - .show(); - } else { - Timber.d("GPS is enabled"); - checkLocationPermission(); - } - } - - /** - * This method ideally should be called from inside of CheckGPS method. If device GPS is enabled - * then we need to control app specific permissions for >=M devices. For other devices, enabled - * GPS is enough for nearby, so directly call refresh view. - */ - private void checkLocationPermission() { - Timber.d("Checking location permission"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (locationManager.isLocationPermissionGranted(requireContext())) { - refreshView(LOCATION_SIGNIFICANTLY_CHANGED); - } else { - // Should we show an explanation? - if (locationManager.isPermissionExplanationRequired(getActivity())) { - // Show an explanation to the user *asynchronously* -- don't block - // this thread waiting for the user's response! After the user - // sees the explanation, try again to request the permission. - new AlertDialog.Builder(getActivity()) - .setMessage(getString(R.string.location_permission_rationale_nearby)) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - requestLocationPermissions(); - dialog.dismiss(); - }) - .setNegativeButton(android.R.string.cancel, (dialog, id) -> { - showLocationPermissionDeniedErrorDialog(); - dialog.cancel(); - }) - .create() - .show(); - - } else { - // No explanation needed, we can request the permission. - requestLocationPermissions(); - } - } - } else { - refreshView(LOCATION_SIGNIFICANTLY_CHANGED); - } + locationManager.registerLocationManager(getActivity()); } private void showErrorMessage(String message) { @@ -748,8 +626,12 @@ public class NearbyFragment extends CommonsDaggerSupportFragment public void onResume() { super.onResume(); // Resume the fragment if exist - resumeFragment(); + if (((MainActivity) getActivity()).viewPager.getCurrentItem() == NEARBY_TAB_POSITION) { + checkPermissionsAndPerformAction(this::resumeFragment); + } else { + resumeFragment(); } + } /** * Perform nearby operations on nearby tab selected @@ -758,8 +640,16 @@ public class NearbyFragment extends CommonsDaggerSupportFragment public void onTabSelected(boolean onOrientationChanged) { Timber.d("On nearby tab selected"); this.onOrientationChanged = onOrientationChanged; - performNearbyOperations(); + checkPermissionsAndPerformAction(this::performNearbyOperations); + } + private void checkPermissionsAndPerformAction(Runnable runnable) { + PermissionUtils.checkPermissionsAndPerformAction(getActivity(), + Manifest.permission.ACCESS_FINE_LOCATION, + runnable, + () -> ((MainActivity) getActivity()).viewPager.setCurrentItem(CONTRIBUTIONS_TAB_POSITION), + R.string.location_permission_title, + R.string.location_permission_rationale_nearby); } /** @@ -769,8 +659,8 @@ public class NearbyFragment extends CommonsDaggerSupportFragment locationManager.addLocationListener(this); registerLocationUpdates(); lockNearbyView = false; - checkGps(); addNetworkBroadcastReceiver(); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/mvp/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/mvp/fragments/NearbyParentFragment.java index ed6f2156d..5c686792d 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/mvp/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/mvp/fragments/NearbyParentFragment.java @@ -298,7 +298,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment @Override public void requestLocationPermissions(LocationServiceManager locationServiceManager) { if (!getActivity().isFinishing()) { - locationServiceManager.requestPermissions(getActivity()); + locationServiceManager.requestPermissions(); } } diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java index 7f029bc6f..cbb9848ca 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java @@ -12,13 +12,20 @@ import android.view.View; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; + import androidx.appcompat.widget.Toolbar; import androidx.drawerlayout.widget.DrawerLayout; -import butterknife.BindView; -import butterknife.ButterKnife; + import com.facebook.drawee.view.SimpleDraweeView; import com.google.android.material.navigation.NavigationView; import com.viewpagerindicator.CirclePageIndicator; + +import java.util.ArrayList; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.AuthenticatedActivity; @@ -29,8 +36,6 @@ import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import javax.inject.Inject; public class ReviewActivity extends AuthenticatedActivity { @@ -139,7 +144,11 @@ public class ReviewActivity extends AuthenticatedActivity { compositeDisposable.add(reviewHelper.getRandomMedia() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::updateImage)); + .subscribe(media -> { + if (media != null) { + updateImage(media); + } + })); return true; } diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java index 7468cc7f6..30903a89a 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.java @@ -1,39 +1,63 @@ package fr.free.nrw.commons.review; + +import org.apache.commons.lang3.StringUtils; import org.wikipedia.dataclient.mwapi.MwQueryPage; import org.wikipedia.dataclient.mwapi.RecentChange; +import org.wikipedia.util.DateUtil; -import java.util.List; +import java.util.Date; import java.util.Random; import javax.inject.Inject; import javax.inject.Singleton; -import androidx.annotation.Nullable; -import androidx.core.util.Pair; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; +import io.reactivex.Observable; import io.reactivex.Single; @Singleton public class ReviewHelper { - private static final int MAX_RANDOM_TRIES = 5; private static final String[] imageExtensions = new String[]{".jpg", ".jpeg", ".png"}; private final OkHttpJsonApiClient okHttpJsonApiClient; private final MediaWikiApi mediaWikiApi; + private final ReviewInterface reviewInterface; @Inject - public ReviewHelper(OkHttpJsonApiClient okHttpJsonApiClient, MediaWikiApi mediaWikiApi) { + public ReviewHelper(OkHttpJsonApiClient okHttpJsonApiClient, + MediaWikiApi mediaWikiApi, + ReviewInterface reviewInterface) { this.okHttpJsonApiClient = okHttpJsonApiClient; this.mediaWikiApi = mediaWikiApi; + this.reviewInterface = reviewInterface; } - Single getRandomMedia() { - return getRandomFileChange() - .flatMap(fileName -> okHttpJsonApiClient.getMedia(fileName, false)); + /** + * Fetches recent changes from MediaWiki API + * Calls the API to get 10 changes in the last 1 hour + * Earlier we were getting changes for the last 30 days but as the API returns just 10 results + * its best to fetch for just last 1 hour. + * @return + */ + private Observable getRecentChanges() { + final int RANDOM_SECONDS = 60 * 60; + Random r = new Random(); + Date now = new Date(); + Date startDate = new Date(now.getTime() - r.nextInt(RANDOM_SECONDS) * 1000L); + + String rcStart = DateUtil.iso8601DateFormat(startDate); + return reviewInterface.getRecentChanges(rcStart) + .map(mwQueryResponse -> mwQueryResponse.query().getRecentChanges()) + .map(recentChanges -> { + //Collections.shuffle(recentChanges); + return recentChanges; + }) + .flatMapIterable(changes -> changes) + .filter(recentChange -> isChangeReviewable(recentChange)); } /** @@ -43,57 +67,62 @@ public class ReviewHelper { * - Checks if the file is nominated for deletion * - Retries upto 5 times for getting a file which is not nominated for deletion * + * @return Random file change + */ + public Single getRandomMedia() { + return getRecentChanges() + .flatMapSingle(change -> getRandomMediaFromRecentChange(change)) + .onExceptionResumeNext(Observable.just(new Media(""))) + .filter(media -> !StringUtils.isBlank(media.getFilename())) + .firstOrError(); + } + + /** + * Returns a proper Media object if the file is not already nominated for deletion + * Else it returns an empty Media object + * @param recentChange * @return */ - private Single getRandomFileChange() { - return okHttpJsonApiClient.getRecentFileChanges() - .map(this::findImageInRecentChanges) - .flatMap(title -> mediaWikiApi.pageExists("Commons:Deletion_requests/" + title) - .map(pageExists -> new Pair<>(title, pageExists))) - .map((Pair pair) -> { - if (!pair.second) { - return pair.first; + private Single getRandomMediaFromRecentChange(RecentChange recentChange) { + return Single.just(recentChange) + .flatMap(change -> mediaWikiApi.pageExists("Commons:Deletion_requests/" + change.getTitle())) + .flatMap(isDeleted -> { + if (isDeleted) { + return Single.just(new Media("")); } - throw new Exception("Already nominated for deletion"); - }).retry(MAX_RANDOM_TRIES); + return okHttpJsonApiClient.getMedia(recentChange.getTitle(), false); + }); + } - Single getFirstRevisionOfFile(String fileName) { - return okHttpJsonApiClient.getFirstRevisionOfFile(fileName); + /** + * Gets the first revision of the file from filename + * @param filename + * @return + */ + Observable getFirstRevisionOfFile(String filename) { + return reviewInterface.getFirstRevisionOfFile(filename) + .map(response -> response.query().firstPage().revisions().get(0)); } - @Nullable - private String findImageInRecentChanges(List recentChanges) { - String imageTitle; - Random r = new Random(); - int count = recentChanges.size(); - // Build a range array - int[] randomIndexes = new int[count]; - for (int i = 0; i < count; i++) { - randomIndexes[i] = i; + /** + * Checks if the change is reviewable or not. + * - checks the type and revisionId of the change + * - checks supported image extensions + * @param recentChange + * @return + */ + private boolean isChangeReviewable(RecentChange recentChange) { + if (recentChange.getType().equals("log") && !(recentChange.getOldRevisionId() == 0)) { + return false; } - // Then shuffle it - for (int i = 0; i < count; i++) { - int swapIndex = r.nextInt(count); - int temp = randomIndexes[i]; - randomIndexes[i] = randomIndexes[swapIndex]; - randomIndexes[swapIndex] = temp; - } - for (int i = 0; i < count; i++) { - int randomIndex = randomIndexes[i]; - RecentChange recentChange = recentChanges.get(randomIndex); - if (recentChange.getType().equals("log") && !(recentChange.getOldRevisionId() == 0)) { - // For log entries, we only want ones where old_revid is zero, indicating a new file - continue; - } - imageTitle = recentChange.getTitle(); - for (String imageExtension : imageExtensions) { - if (imageTitle.toLowerCase().endsWith(imageExtension)) { - return imageTitle; - } + for (String extension : imageExtensions) { + if (recentChange.getTitle().endsWith(extension)) { + return true; } } - return null; + + return false; } } diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewInterface.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewInterface.java new file mode 100644 index 000000000..7dee125fc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewInterface.java @@ -0,0 +1,18 @@ +package fr.free.nrw.commons.review; + +import org.wikipedia.dataclient.mwapi.MwQueryResponse; + +import io.reactivex.Observable; +import retrofit2.http.GET; +import retrofit2.http.Query; + +/** + * Interface class for peer review calls + */ +public interface ReviewInterface { + @GET("w/api.php?action=query&format=json&formatversion=2&list=recentchanges&rcprop=title|ids&rctype=new|log&rctoponly=1&rcnamespace=6") + Observable getRecentChanges(@Query("rcstart") String rcStart); + + @GET("w/api.php?action=query&format=json&formatversion=2&prop=revisions&rvprop=timestamp|ids|user&rvdir=newer&rvlimit=1") + Observable getFirstRevisionOfFile(@Query("titles") String titles); +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java index c4268790c..8d258256a 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java @@ -5,17 +5,17 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.provider.Settings; -import androidx.annotation.StringRes; -import androidx.core.content.ContextCompat; import androidx.annotation.StringRes; import androidx.core.content.ContextCompat; + import com.karumi.dexter.Dexter; import com.karumi.dexter.PermissionToken; import com.karumi.dexter.listener.PermissionDeniedResponse; import com.karumi.dexter.listener.PermissionGrantedResponse; import com.karumi.dexter.listener.PermissionRequest; import com.karumi.dexter.listener.single.BasePermissionListener; + import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; @@ -27,7 +27,7 @@ public class PermissionUtils { It open the app settings from where the user can manually give us the required permission. * @param activity */ - public static void askUserToManuallyEnablePermissionFromSettings(Activity activity) { + private static void askUserToManuallyEnablePermissionFromSettings(Activity activity) { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts("package", activity.getPackageName(), null); intent.setData(uri); @@ -50,6 +50,9 @@ public class PermissionUtils { * Checks for a particular permission and runs the runnable to perform an action when the permission is granted * Also, it shows a rationale if needed * + * rationaleTitle and rationaleMessage can be invalid @StringRes. If the value is -1 then no permission rationale + * will be displayed and permission would be requested + * * Sample usage: * * PermissionUtils.checkPermissionsAndPerformAction(activity, @@ -58,12 +61,20 @@ public class PermissionUtils { * R.string.storage_permission_title, * R.string.write_storage_permission_rationale); * + * If you don't want the permission rationale to be shown then use: + * + * PermissionUtils.checkPermissionsAndPerformAction(activity, + * Manifest.permission.WRITE_EXTERNAL_STORAGE, + * () -> initiateCameraUpload(activity), + * - 1, -1); + * + * * * @param activity activity requesting permissions * @param permission the permission being requests * @param onPermissionGranted the runnable to be executed when the permission is granted - * @param rationaleTitle rationale title to be displayed when permission was denied - * @param rationaleMessage rationale message to be displayed when permission was denied + * @param rationaleTitle rationale title to be displayed when permission was denied. It can be an invalid @StringRes + * @param rationaleMessage rationale message to be displayed when permission was denied. It can be an invalid @StringRes */ public static void checkPermissionsAndPerformAction(Activity activity, String permission, Runnable onPermissionGranted, @StringRes int rationaleTitle, @@ -93,7 +104,6 @@ public class PermissionUtils { * @param rationaleTitle rationale title to be displayed when permission was denied * @param rationaleMessage rationale message to be displayed when permission was denied */ - public static void checkPermissionsAndPerformAction(Activity activity, String permission, Runnable onPermissionGranted, Runnable onPermissionDenied, @StringRes int rationaleTitle, @StringRes int rationaleMessage) { @@ -120,6 +130,10 @@ public class PermissionUtils { @Override public void onPermissionRationaleShouldBeShown(PermissionRequest permission, PermissionToken token) { + if (rationaleTitle == -1 && rationaleMessage == -1) { + token.continuePermissionRequest(); + return; + } DialogUtil.showAlertDialog(activity, activity.getString(rationaleTitle), activity.getString(rationaleMessage), activity.getString(android.R.string.ok), @@ -129,4 +143,5 @@ public class PermissionUtils { }) .check(); } + } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a45de752b..8185aab5b 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -6,6 +6,7 @@ * Azouz.anis * ButterflyOfFire * Claw eg +* Kassem7899 * Meno25 * Mido * Monrokhoury @@ -22,6 +23,7 @@ المظهر عام التعليقات + الخصوصية الموقع كومنز @@ -516,6 +518,15 @@ أمثلة على صور لعدم رفعها تخطي هذه الصورة التنزيل فشل!!. لا يمكننا تنزيل الملف دون إذن تخزين خارجي. + إدارة وسوم EXIF + حدد أية وسوم EXIF ​​لتحتفظ بها في المرفوعات + المؤلف + حقوق نشر + الموقع + طراز الكاميرا + طراز العدسة + الأرقام التسلسلية + برمجية ارفع الصور لويكيميديا ​​كومنز على هاتفك قم بتنزيل تطبيق كومنز: %1$s مشاركة التطبيق عبر... معلومات الصورة diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 9b436a071..0d9714aba 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -322,4 +322,5 @@ হ্যাঁ, জমা দিন না, ফিরে যান অনুগ্রহ করে অপেক্ষা করুন... + অবস্থান diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index acf6d08e2..000091274 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -9,6 +9,8 @@ * FarsiNevis * Fatemi127 * Freshman404 +* Ladsgroup +* Mardetanha * Mehdi * Mjbmr * Omidh @@ -18,6 +20,7 @@ * Vahedi * Yoosef Pooranvary * جواد +* درفش کاویانی --> کاوش @@ -25,6 +28,7 @@ نمایش صفحه عمومی بازخورد + حریم خصوصی مکان ویکی‌انبار @@ -300,9 +304,12 @@ اینترنت در دسترس نیست اینترنت در دسترس است خطا در آوردن اطلاعیه + در تلاش برای بدست‌آوردن تصویر جهت بازبینی خطایی پیش آمد. لطفا برای تلاش مجدد بر روی ریفرش کلیک کنید. + دریافت رده‌های تصویر برای بازبینی به مشکل برخورد کرد. لطفا برای تلاش مجدد صفحه را رفرش کنید. هشداری پیدا نشد <u>ترجمه</u> زبان‌ها + لطفا زبانی که مایلید در آنها ترجمه‌های خود را ثبت کنید انتخاب کنید. ادامه لغو سعى دوباره @@ -317,11 +324,15 @@ جستجوی ویکی‌انبار جستجو جستجوهای اخیر: + کوئری‌های که اخیرا جستجو شده خطا هنگام بار کردن رده ها. + در هنگام بارگیری زیررده‌ها خطایی رخ داد. رسانه دسته بندی برگزیده بارگذاری‌شده با تلفن همراه + تصویر با موفقیت به %1$s در ویکی‌داده افزوده‌شد. + تلاش برای بروزرسانی موجودی ویکی‌دیتای مرتبط شکست خورد انتخاب به عنوان پس‌زمینه تصویر پس زمینه به طور موفقیت آمیز تنظیم شد! امتحان @@ -330,6 +341,7 @@ نتیجه یکی از دو گزینه را انتخاب کنید تا به سوال پاسخ دهید جلسه ورود به سیستم منقضی شد، لطفا دوباره وارد سیستم شوید. + کویز خود را با دوستان خود به اشتراک بگذارید. ادامه جواب درست جواب نادرست @@ -348,6 +360,7 @@ آمارها تشکر دریافت‌شد تصاویر برگزیده + تصاویر بر اساس «مکان‌های اطراف» سطح تصاویر بارگذاری شده تصاویر واگردانی نشده @@ -361,6 +374,9 @@ مشارکت‌ها در نزدیکی آگاه‌سازی‌ها + اعلان‌ها (بایگانی‌شده) + نمایش اعلان اطراف + هیچ‌ مکان نزدیکی به شما یافته نشد فهرست اجازه ذخیره گام %1$d از %2$d @@ -396,25 +412,55 @@ جستجوی این محدوده درخواست اجازه این را دیگر نپرس + کمپین‌های نمایش هنگام پردازش این تصویر خطایی رخ داد. لطفا دوباره سعی کنید! + دریافت توکن برای ویرایش + درخواست بررسی رده + بررسی رده درخواست شده + درخواست بررسی رده کار نکرد + افزودن پیام حذف‌شده به فایل انجام شد + به کاربر در صفحه بحثش خبر بده مطمئن نیستم ارسال تشکر: موفق + تلاش برای فرستادن تشکر شکست خورد %1$s ارسال تشکر: ناموفق ارسال تشکر ارسال تشکر در حال ارسال تشکر برای %1$s آیا این به درستی رده‌بندی شده‌است؟ + آیا این در محدوده قابل قبول است؟ + آیا مایلید که از مشارکت‌ کننده تشکر کنید؟ + عجب، این حتی رده‌بندی هم نشده! + این از محدوده خارج است زیرا که + این فایل ناقض حق تکثیر است به خاطر اینکه خوب به نظر می‌رسد + نه، این از محدوده خارج است خوب به نظر می‌رسد خوب به نظر می‌رسد بله، چرا که نه تصویر بعدی + تصاویر استاده نشده تصویر برگردانده نشد هیچ تصویری بارگذاری نشد + شما هیچ اعلان خوانده‌نشده‌ای ندارید + شما هیچ پیغام بایگانی شده‌ای ندارید نمایش بایگانی‌شده مشاهده خوانده نشده ها انتخاب تصویر برای بارگذاری لطفاً صبر کنید... + از عنوان/توضیحات پیشین استفاده کنید + نمونه تصاویری که برای بازگذاری مناسب نیستند + از این تصویر صرف نظر کن + مدیریت تگ‌های EXIF + تگ‌های موردنظر خود در EXIF را برای آپلود انتخاب کنید + پدیدآور + حق تکثیر + مکان + مدل دوربین + مدل لنز + شماره سریال + نرم‌افزار + اشتراک از طریق... اطلاعات عکس diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d89c6241d..d191a98ad 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -30,6 +30,7 @@ Apparence Général Donner son avis + Confidentialité Emplacement Commons @@ -527,6 +528,15 @@ Exemples d\'images à ne pas téléverser SAUTER CETTE IMAGE Échec du téléchargement ! Nous ne pouvons pas télécharger le fichier sans droit de stockage externe. + Gérer les balises EXIF + Sélectionner quelles balises EXIF à conserver dans les téléchargements + Auteur + Droits d’auteur + Emplacement + Modèle d’appareil photo + Modèle de lentille + Numéros de série + Logiciel Téléverser des photos vers Wikimédia Communs, sur votre téléphone Téléchargez l’application Communs : %1$s Partager l’application via… Informations de l’image diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 26b6bff4b..1228946f3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -469,6 +469,7 @@ Copia titolo/descrizione precedente Clicca per riusare il titolo e la descrizione dell\'immagine precedente e adattarli all\'immagine attuale. SALTA QUESTA IMMAGINE + Autore Condividi applicazione tramite... Informazioni sull\'immagine diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 260931378..9eaf98ffa 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -19,6 +19,7 @@ 보이기 일반 피드백 + 개인정보 위치 공용 @@ -452,6 +453,13 @@ 이전의 제목 및 설명 복사 이 이미지 건너뛰기 다운로드를 실패했습니다!! 외장 스토리지 권한 없이 파일을 다운로드할 수 없습니다. + EXIF 태그 관리 + 만든이 + 저작권 + 위치 + 카메라 모델 + 일련 번호 + 소프트웨어 앱 공유... 이미지 정보 diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index f2f4b39f4..27cfbeda8 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -10,6 +10,7 @@ Изглед Општи Мислења + Лични податоци Место Ризница @@ -507,6 +508,15 @@ Примери — Слики што не се за подигање ПРЕСКОКНИ ЈА ПОРАКАВА Преземањето не успеа!!! Не можеме да ја преземеме податотеката без дозвола од надворешен склад. + Раков. со EXIF-ознаки + Изберете кои EXIF-ознаки да се задржат во подигањата + Автор + Авторски права + Место + Модел на камерата + Модел на објективот + Сериски броеви + Програми Подигајте слики на Ризницата од телефон. Преземете го прилогот на Ризницата: %1$s Сподели преку... Инфо за сликата diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 5f73faab1..0df25a964 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -22,6 +22,7 @@ Aparência Geral Comentário + Privacidade Localização Commons @@ -519,6 +520,15 @@ Exemplos de imagens que não devem ser carregadas PULAR ESTA IMAGEM Falha no Download!!. Não podemos fazer o download do arquivo sem permissão de armazenamento externo. + Gerenciar etiquetas EXIF + Selecione quais etiquetas EXIF para manter nos carregados + Autor + Direitos autorais + Localização + Modelo da câmera + Modelo de lente + Números de série + Software Faça o carregamento de fotos para o Wikimedia Commons no seu telefone ou baixe o aplicativo Commons: %1$s Compartilhar aplicativo via... Informação da imagem diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e2b44d87a..4ec460d30 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -28,6 +28,7 @@ Внешний вид Общие Обратная связь + Конфиденциальность Местоположение Викисклад @@ -526,6 +527,15 @@ Примеры изображений, которые не следует загружать на Викисклад ПРОПУСТИТЬ ЭТО ИЗОБРАЖЕНИЕ Скачивание файла не удалось! Это невозможно выполнить без предоставленного разрешения по использованию внешнего носителя. + Работа с EXIF-тегами + Укажите, какие EXIF-теги следует сохранить при загрузке файлов + Автор + Авторские права + Местоположение + Модель камеры + Модель объектива + Серийный номер + Программное обеспечение Чтобы загружать фото на Викисклад (Wikimedia Commons), скачайте одноимённое приложение «Викисклад» (Commons): %1$s Поделиться приложением с помощью... Информация об изображении diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index f77108521..fe80d2520 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -15,6 +15,7 @@ Utseende Allmänt Återkoppling + Integritet Plats Commons @@ -512,6 +513,15 @@ Exempel på bilder att inte ladda upp HOPPA ÖVER DENNA BILD Nedladdning misslyckades!! Vi kan inte ladda ned filen utan behörighet för extern lagring. + Hantera EXIF-taggar + Välj vilka EXIF-taggar att behålla i uppladdningar + Skapare + Upphovsrätt + Plats + Kameramodell + Linsmodell + Serienummer + Programvara Ladda upp foton till Wikimedia Commons på din telefon Ladda ned Commons-appen: %1$s Dela appen via... Bildinfo diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 5c9e0df05..f03d043d0 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -18,6 +18,7 @@ Зовнішній вигляд Загальні Зворотний зв\'язок + Конфіденційність Розташування Вікісховище @@ -526,6 +527,15 @@ Приклади зображень, які не слід завантажувати ПРОПУСТИТИ ЦЕ ЗОБРАЖЕННЯ Завантаження не вдалося. Ми не змогли завантажити файл без доступу до зовнішнього носія. + Робота з EXIF-тегами + Вкажіть, які EXIF-теги мають бути збережені при завантаженні файлів + Автор + Авторські права + Місцезнаходження + Модель камери + Модель об\'єктиву + Серійний номер + Програмне забезпечення Вивантажуйте фото у Вікісховище зі свого телефона. Завантажте застосунок: %1$s Поділитися програмкою через… Інформація про зображення diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 60b483018..66b74b66d 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -21,6 +21,7 @@ 外觀 一般 意見回饋 + 隱私 位置 維基共享資源 @@ -516,6 +517,15 @@ 未上傳範例圖片 忽略此圖片 下載失敗!因為缺少外部存儲裝置權限緣故,我們無法下載檔案。 + 管理 EXIF 標籤 + 選擇要保持上傳的 EXIF 標籤 + 作者 + 版權 + 位置 + 相機模型 + 透鏡模型 + 序號 + 軟體 在您的手機上更新照片到維基共享資源,下載共享資源應用程式:%1$s 分享應用程式透過… 圖片資訊 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 276f9e2c1..b460f358e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -166,6 +166,7 @@ Requesting Storage Permission Required permission: Read external storage. App cannot access your gallery without this. Required permission: Write external storage. App cannot access your camera/gallery without this. + Requesting Location Permission Optional permission: Get current location for category suggestions OK Nearby Places diff --git a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt index a878fb5ae..d9c808f77 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt @@ -3,7 +3,10 @@ package fr.free.nrw.commons.review import fr.free.nrw.commons.Media import fr.free.nrw.commons.mwapi.MediaWikiApi import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import io.reactivex.Observable import io.reactivex.Single +import junit.framework.Assert.assertNotNull +import junit.framework.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -13,6 +16,8 @@ import org.mockito.Mock import org.mockito.Mockito.* import org.mockito.MockitoAnnotations import org.wikipedia.dataclient.mwapi.MwQueryPage +import org.wikipedia.dataclient.mwapi.MwQueryResponse +import org.wikipedia.dataclient.mwapi.MwQueryResult import org.wikipedia.dataclient.mwapi.RecentChange /** @@ -20,6 +25,8 @@ import org.wikipedia.dataclient.mwapi.RecentChange */ class ReviewHelperTest { + @Mock + internal var reviewInterface: ReviewInterface? = null @Mock internal var okHttpJsonApiClient: OkHttpJsonApiClient? = null @Mock @@ -35,6 +42,31 @@ class ReviewHelperTest { @Throws(Exception::class) fun setUp() { MockitoAnnotations.initMocks(this) + + val mwQueryPage = mock(MwQueryPage::class.java) + val mockRevision = mock(MwQueryPage.Revision::class.java) + `when`(mockRevision.user).thenReturn("TestUser") + `when`(mwQueryPage.revisions()).thenReturn(listOf(mockRevision)) + + val recentChange = getMockRecentChange("test", "File:Test1.jpeg", 0) + val recentChange1 = getMockRecentChange("test", "File:Test2.png", 0) + val recentChange2 = getMockRecentChange("test", "File:Test3.jpg", 0) + val mwQueryResult = mock(MwQueryResult::class.java) + `when`(mwQueryResult.recentChanges).thenReturn(listOf(recentChange, recentChange1, recentChange2)) + `when`(mwQueryResult.firstPage()).thenReturn(mwQueryPage) + `when`(mwQueryResult.pages()).thenReturn(listOf(mwQueryPage)) + val mockResponse = mock(MwQueryResponse::class.java) + `when`(mockResponse.query()).thenReturn(mwQueryResult) + `when`(reviewInterface?.getRecentChanges(ArgumentMatchers.anyString())) + .thenReturn(Observable.just(mockResponse)) + + `when`(reviewInterface?.getFirstRevisionOfFile(ArgumentMatchers.anyString())) + .thenReturn(Observable.just(mockResponse)) + + val media = mock(Media::class.java) + `when`(media.filename).thenReturn("File:Test.jpg") + `when`(okHttpJsonApiClient?.getMedia(ArgumentMatchers.anyString(), ArgumentMatchers.anyBoolean())) + .thenReturn(Single.just(media)) } /** @@ -42,40 +74,48 @@ class ReviewHelperTest { */ @Test fun getRandomMedia() { - val recentChange = getMockRecentChange("test", "File:Test1.jpeg", 0) - val recentChange1 = getMockRecentChange("test", "File:Test2.png", 0) - val recentChange2 = getMockRecentChange("test", "File:Test3.jpg", 0) - `when`(okHttpJsonApiClient?.recentFileChanges) - .thenReturn(Single.just(listOf(recentChange, recentChange1, recentChange2))) - `when`(mediaWikiApi?.pageExists(ArgumentMatchers.anyString())) .thenReturn(Single.just(false)) - `when`(okHttpJsonApiClient?.getMedia(ArgumentMatchers.anyString(), ArgumentMatchers.anyBoolean())) - .thenReturn(Single.just(mock(Media::class.java))) - val randomMedia = reviewHelper?.randomMedia?.blockingGet() + assertNotNull(randomMedia) assertTrue(randomMedia is Media) + verify(reviewInterface, times(1))!!.getRecentChanges(ArgumentMatchers.anyString()) } /** * Test scenario when all media is already nominated for deletion */ - @Test(expected = Exception::class) + @Test(expected = RuntimeException::class) fun getRandomMediaWithWithAllMediaNominatedForDeletion() { - val recentChange = getMockRecentChange("test", "File:Test1.jpeg", 0) - val recentChange1 = getMockRecentChange("test", "File:Test2.png", 0) - val recentChange2 = getMockRecentChange("test", "File:Test3.jpg", 0) - `when`(okHttpJsonApiClient?.recentFileChanges) - .thenReturn(Single.just(listOf(recentChange, recentChange1, recentChange2))) - `when`(mediaWikiApi?.pageExists(ArgumentMatchers.anyString())) .thenReturn(Single.just(true)) - reviewHelper?.randomMedia?.blockingGet() + val media = reviewHelper?.randomMedia?.blockingGet() + assertNull(media) + verify(reviewInterface, times(1))!!.getRecentChanges(ArgumentMatchers.anyString()) } - fun getMockRecentChange(type: String, title: String, oldRevisionId: Long): RecentChange { + /** + * Test scenario when first media is already nominated for deletion + */ + @Test + fun getRandomMediaWithWithOneMediaNominatedForDeletion() { + `when`(mediaWikiApi?.pageExists("Commons:Deletion_requests/File:Test1.jpeg")) + .thenReturn(Single.just(true)) + `when`(mediaWikiApi?.pageExists("Commons:Deletion_requests/File:Test2.png")) + .thenReturn(Single.just(false)) + `when`(mediaWikiApi?.pageExists("Commons:Deletion_requests/File:Test3.jpg")) + .thenReturn(Single.just(true)) + + val media = reviewHelper?.randomMedia?.blockingGet() + + assertNotNull(media) + assertTrue(media is Media) + verify(reviewInterface, times(1))!!.getRecentChanges(ArgumentMatchers.anyString()) + } + + private fun getMockRecentChange(type: String, title: String, oldRevisionId: Long): RecentChange { val recentChange = mock(RecentChange::class.java) `when`(recentChange!!.type).thenReturn(type) `when`(recentChange.title).thenReturn(title) @@ -88,9 +128,7 @@ class ReviewHelperTest { */ @Test fun getFirstRevisionOfFile() { - `when`(okHttpJsonApiClient?.getFirstRevisionOfFile(ArgumentMatchers.anyString())) - .thenReturn(Single.just(mock(MwQueryPage.Revision::class.java))) - val firstRevisionOfFile = reviewHelper?.getFirstRevisionOfFile("Test.jpg")?.blockingGet() + val firstRevisionOfFile = reviewHelper?.getFirstRevisionOfFile("Test.jpg")?.blockingFirst() assertTrue(firstRevisionOfFile is MwQueryPage.Revision) }