Fixes #4437 - Changed indentation on files with 2 spaces to 4 spaces (#4462)

* Edited Project.xml to make indent size 4

* Changed files with 2 space indentation to use 4 space indentation

* Edited Project.xml to make indent size 4

* changed files with 2 space indent to 4 space indent

* fix :Back Pressed Event not work in Explore tab when user not login (#4404)

* fix :Back Pressed Event not work in Explore tab

* minor changes

* fix :Upload count or number of contribution does not get updated when media  is successful uploaded (#4399)

* * fix:Number of Contributions not updated
 * Add javadocs

* minor changes

* made minor changes

* String was nonsense and untranslatible, fixed (#4466)

* Ability to show captions and descriptions in all entered languages (#4355)

* implement Ability to show captions and descriptions in all entered languages
*Add Javadoc

* handle Back event of fragment(mediaDetailFragment)

* fix minor bugs

* add internationalization

* revert previous changes

* fix visibility bug

* resolve conflict

Co-authored-by: Prince kushwaha <65972015+Prince-kushwaha@users.noreply.github.com>
Co-authored-by: neslihanturan <tur.neslihan@gmail.com>
This commit is contained in:
Jamie Brown 2021-06-21 04:33:11 +01:00 committed by GitHub
parent b202f553f0
commit ca9f6f5e47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 3047 additions and 2988 deletions

View file

@ -111,7 +111,6 @@
<option name="WHILE_BRACE_FORCE" value="3" /> <option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" /> <option name="FOR_BRACE_FORCE" value="3" />
<indentOptions> <indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" /> <option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" /> <option name="TAB_SIZE" value="2" />
</indentOptions> </indentOptions>
@ -325,4 +324,4 @@
</indentOptions> </indentOptions>
</codeStyleSettings> </codeStyleSettings>
</code_scheme> </code_scheme>
</component> </component>

View file

@ -63,38 +63,39 @@ import org.wikipedia.language.AppLanguageLookUpTable;
import timber.log.Timber; import timber.log.Timber;
@AcraCore( @AcraCore(
buildConfigClass = BuildConfig.class, buildConfigClass = BuildConfig.class,
resReportSendSuccessToast = R.string.crash_dialog_ok_toast, resReportSendSuccessToast = R.string.crash_dialog_ok_toast,
reportFormat = StringFormat.KEY_VALUE_LIST, reportFormat = StringFormat.KEY_VALUE_LIST,
reportContent = {USER_COMMENT, APP_VERSION_CODE, APP_VERSION_NAME, ANDROID_VERSION, PHONE_MODEL, STACK_TRACE} reportContent = {USER_COMMENT, APP_VERSION_CODE, APP_VERSION_NAME, ANDROID_VERSION, PHONE_MODEL,
STACK_TRACE}
) )
@AcraMailSender( @AcraMailSender(
mailTo = "commons-app-android-private@googlegroups.com", mailTo = "commons-app-android-private@googlegroups.com",
reportAsFile = false reportAsFile = false
) )
@AcraDialog( @AcraDialog(
resTheme = R.style.Theme_AppCompat_Dialog, resTheme = R.style.Theme_AppCompat_Dialog,
resText = R.string.crash_dialog_text, resText = R.string.crash_dialog_text,
resTitle = R.string.crash_dialog_title, resTitle = R.string.crash_dialog_title,
resCommentPrompt = R.string.crash_dialog_comment_prompt resCommentPrompt = R.string.crash_dialog_comment_prompt
) )
public class CommonsApplication extends MultiDexApplication { public class CommonsApplication extends MultiDexApplication {
public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled"; public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled";
@Inject @Inject
SessionManager sessionManager; SessionManager sessionManager;
@Inject @Inject
DBOpenHelper dbOpenHelper; DBOpenHelper dbOpenHelper;
@Inject @Inject
@Named("default_preferences") @Named("default_preferences")
JsonKvStore defaultPrefs; JsonKvStore defaultPrefs;
@Inject @Inject
CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher; CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher;
/** /**
* Constants begin * Constants begin
@ -118,23 +119,26 @@ public class CommonsApplication extends MultiDexApplication {
private RefWatcher refWatcher; private RefWatcher refWatcher;
private static CommonsApplication INSTANCE; private static CommonsApplication INSTANCE;
public static CommonsApplication getInstance() { public static CommonsApplication getInstance() {
return INSTANCE; return INSTANCE;
} }
private AppLanguageLookUpTable languageLookUpTable; private AppLanguageLookUpTable languageLookUpTable;
public AppLanguageLookUpTable getLanguageLookUpTable() { public AppLanguageLookUpTable getLanguageLookUpTable() {
return languageLookUpTable; return languageLookUpTable;
} }
@Inject ContributionDao contributionDao; @Inject
ContributionDao contributionDao;
/** /**
* In memory list of contributios whose uploads ahve been paused by the user * In memory list of contributios whose uploads ahve been paused by the user
*/ */
public static Map<String, Boolean> pauseUploads = new HashMap<>(); public static Map<String, Boolean> pauseUploads = new HashMap<>();
/** /**
* Used to declare and initialize various components and dependencies * Used to declare and initialize various components and dependencies
*/ */
@Override @Override
@ -146,15 +150,14 @@ public class CommonsApplication extends MultiDexApplication {
Mapbox.getInstance(this, getString(R.string.mapbox_commons_app_token)); Mapbox.getInstance(this, getString(R.string.mapbox_commons_app_token));
ApplicationlessInjection ApplicationlessInjection
.getInstance(this) .getInstance(this)
.getCommonsApplicationComponent() .getCommonsApplicationComponent()
.inject(this); .inject(this);
AppAdapter.set(new CommonsAppAdapter(sessionManager, defaultPrefs)); AppAdapter.set(new CommonsAppAdapter(sessionManager, defaultPrefs));
initTimber(); initTimber();
if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) { if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) {
Set<String> defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS); Set<String> defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS);
if (null == defaultExifTagsSet) { if (null == defaultExifTagsSet) {
@ -166,9 +169,9 @@ public class CommonsApplication extends MultiDexApplication {
// Set DownsampleEnabled to True to downsample the image in case it's heavy // Set DownsampleEnabled to True to downsample the image in case it's heavy
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this) ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this)
.setNetworkFetcher(customOkHttpNetworkFetcher) .setNetworkFetcher(customOkHttpNetworkFetcher)
.setDownsampleEnabled(true) .setDownsampleEnabled(true)
.build(); .build();
try { try {
Fresco.initialize(this, config); Fresco.initialize(this, config);
} catch (Exception e) { } catch (Exception e) {
@ -192,46 +195,44 @@ public class CommonsApplication extends MultiDexApplication {
} }
/** /**
* Plants debug and file logging tree. * Plants debug and file logging tree. Timber lets you plant your own logging trees.
* Timber lets you plant your own logging trees.
*
*/ */
private void initTimber() { private void initTimber() {
boolean isBeta = ConfigUtils.isBetaFlavour(); boolean isBeta = ConfigUtils.isBetaFlavour();
String logFileName = String logFileName =
isBeta ? "CommonsBetaAppLogs" : "CommonsAppLogs"; isBeta ? "CommonsBetaAppLogs" : "CommonsAppLogs";
String logDirectory = LogUtils.getLogDirectory(); String logDirectory = LogUtils.getLogDirectory();
//Delete stale logs if they have exceeded the specified size //Delete stale logs if they have exceeded the specified size
deleteStaleLogs(logFileName, logDirectory); deleteStaleLogs(logFileName, logDirectory);
FileLoggingTree tree = new FileLoggingTree( FileLoggingTree tree = new FileLoggingTree(
Log.VERBOSE, Log.VERBOSE,
logFileName, logFileName,
logDirectory, logDirectory,
1000, 1000,
getFileLoggingThreadPool()); getFileLoggingThreadPool());
Timber.plant(tree); Timber.plant(tree);
Timber.plant(new Timber.DebugTree()); Timber.plant(new Timber.DebugTree());
} }
/** /**
* Deletes the logs zip file at the specified directory and file locations specified in the * Deletes the logs zip file at the specified directory and file locations specified in the
* params * params
* *
* @param logFileName * @param logFileName
* @param logDirectory * @param logDirectory
*/ */
private void deleteStaleLogs(String logFileName, String logDirectory) { private void deleteStaleLogs(String logFileName, String logDirectory) {
try { try {
File file = new File(logDirectory + "/zip/" + logFileName + ".zip"); File file = new File(logDirectory + "/zip/" + logFileName + ".zip");
if (file.exists() && file.getTotalSpace() > 1000000) {// In Kbs if (file.exists() && file.getTotalSpace() > 1000000) {// In Kbs
file.delete(); file.delete();
} }
} catch (Exception e) { } catch (Exception e) {
Timber.e(e); Timber.e(e);
}
} }
}
public static boolean isRoboUnitTest() { public static boolean isRoboUnitTest() {
return "robolectric".equals(Build.FINGERPRINT); return "robolectric".equals(Build.FINGERPRINT);
@ -239,30 +240,35 @@ public class CommonsApplication extends MultiDexApplication {
private ThreadPoolService getFileLoggingThreadPool() { private ThreadPoolService getFileLoggingThreadPool() {
return new ThreadPoolService.Builder("file-logging-thread") return new ThreadPoolService.Builder("file-logging-thread")
.setPriority(Process.THREAD_PRIORITY_LOWEST) .setPriority(Process.THREAD_PRIORITY_LOWEST)
.setPoolSize(1) .setPoolSize(1)
.setExceptionHandler(new BackgroundPoolExceptionHandler()) .setExceptionHandler(new BackgroundPoolExceptionHandler())
.build(); .build();
} }
public static void createNotificationChannel(@NonNull Context context) { public static void createNotificationChannel(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); NotificationManager manager = (NotificationManager) context
NotificationChannel channel = manager.getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL); .getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = manager
.getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL);
if (channel == null) { if (channel == null) {
channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID_ALL, channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID_ALL,
context.getString(R.string.notifications_channel_name_all), NotificationManager.IMPORTANCE_DEFAULT); context.getString(R.string.notifications_channel_name_all),
NotificationManager.IMPORTANCE_DEFAULT);
manager.createNotificationChannel(channel); manager.createNotificationChannel(channel);
} }
} }
} }
public String getUserAgent() { public String getUserAgent() {
return "Commons/" + ConfigUtils.getVersionNameWithSha(this) + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE; return "Commons/" + ConfigUtils.getVersionNameWithSha(this)
+ " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE;
} }
/** /**
* Helps in setting up LeakCanary library * Helps in setting up LeakCanary library
*
* @return instance of LeakCanary * @return instance of LeakCanary
*/ */
protected RefWatcher setupLeakCanary() { protected RefWatcher setupLeakCanary() {
@ -272,7 +278,7 @@ public class CommonsApplication extends MultiDexApplication {
return LeakCanary.install(this); return LeakCanary.install(this);
} }
/** /**
* Provides a way to get member refWatcher * Provides a way to get member refWatcher
* *
* @param context Application context * @param context Application context
@ -285,7 +291,8 @@ public class CommonsApplication extends MultiDexApplication {
/** /**
* clears data of current application * clears data of current application
* @param context Application context *
* @param context Application context
* @param logoutListener Implementation of interface LogoutListener * @param logoutListener Implementation of interface LogoutListener
*/ */
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
@ -302,13 +309,13 @@ public class CommonsApplication extends MultiDexApplication {
} }
sessionManager.logout() sessionManager.logout()
.andThen(Completable.fromAction(() ->{ .andThen(Completable.fromAction(() -> {
Timber.d("All accounts have been removed"); Timber.d("All accounts have been removed");
clearImageCache(); clearImageCache();
//TODO: fix preference manager //TODO: fix preference manager
defaultPrefs.clearAll(); defaultPrefs.clearAll();
defaultPrefs.putBoolean("firstrun", false); defaultPrefs.putBoolean("firstrun", false);
updateAllDatabases(); updateAllDatabases();
} }
)) ))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@ -332,12 +339,13 @@ public class CommonsApplication extends MultiDexApplication {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
CategoryDao.Table.onDelete(db); CategoryDao.Table.onDelete(db);
dbOpenHelper.deleteTable(db,CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions dbOpenHelper.deleteTable(db,
CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions
try { try {
contributionDao.deleteAll(); contributionDao.deleteAll();
} catch (SQLiteException e) { } catch (SQLiteException e) {
Timber.e(e); Timber.e(e);
} }
BookmarkPicturesDao.Table.onDelete(db); BookmarkPicturesDao.Table.onDelete(db);
BookmarkLocationsDao.Table.onDelete(db); BookmarkLocationsDao.Table.onDelete(db);
@ -348,6 +356,7 @@ public class CommonsApplication extends MultiDexApplication {
* Interface used to get log-out events * Interface used to get log-out events
*/ */
public interface LogoutListener { public interface LogoutListener {
void onLogoutComplete(); void onLogoutComplete();
} }
} }

View file

@ -10,45 +10,48 @@ import com.mapbox.mapboxsdk.camera.CameraPosition;
*/ */
public final class LocationPicker { public final class LocationPicker {
/**
* Getting camera position from the intent using constants
* @param data intent
* @return CameraPosition
*/
public static CameraPosition getCameraPosition(final Intent data) {
return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
}
public static class IntentBuilder {
private final Intent intent;
/** /**
* Creates a new builder that creates an intent to launch the place picker activity. * Getting camera position from the intent using constants
*
* @param data intent
* @return CameraPosition
*/ */
public IntentBuilder() { public static CameraPosition getCameraPosition(final Intent data) {
intent = new Intent(); return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
} }
/** public static class IntentBuilder {
* Gets and puts location in intent
* @param position CameraPosition
* @return LocationPicker.IntentBuilder
*/
public LocationPicker.IntentBuilder defaultLocation(
final CameraPosition position) {
intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position);
return this;
}
/** private final Intent intent;
* Gets and sets the activity
* @param activity Activity /**
* @return Intent * Creates a new builder that creates an intent to launch the place picker activity.
*/ */
public Intent build(final Activity activity) { public IntentBuilder() {
intent.setClass(activity, LocationPickerActivity.class); intent = new Intent();
return intent; }
/**
* Gets and puts location in intent
*
* @param position CameraPosition
* @return LocationPicker.IntentBuilder
*/
public LocationPicker.IntentBuilder defaultLocation(
final CameraPosition position) {
intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position);
return this;
}
/**
* Gets and sets the activity
*
* @param activity Activity
* @return Intent
*/
public Intent build(final Activity activity) {
intent.setClass(activity, LocationPickerActivity.class);
return intent;
}
} }
}
} }

View file

@ -42,247 +42,252 @@ import timber.log.Timber;
public class LocationPickerActivity extends AppCompatActivity implements OnMapReadyCallback, public class LocationPickerActivity extends AppCompatActivity implements OnMapReadyCallback,
OnCameraMoveStartedListener, OnCameraIdleListener, Observer<CameraPosition> { OnCameraMoveStartedListener, OnCameraIdleListener, Observer<CameraPosition> {
/** /**
* cameraPosition : position of picker * cameraPosition : position of picker
*/ */
private CameraPosition cameraPosition; private CameraPosition cameraPosition;
/** /**
* markerImage : picker image * markerImage : picker image
*/ */
private ImageView markerImage; private ImageView markerImage;
/** /**
* mapboxMap : map * mapboxMap : map
*/ */
private MapboxMap mapboxMap; private MapboxMap mapboxMap;
/** /**
* mapView : view of the map * mapView : view of the map
*/ */
private MapView mapView; private MapView mapView;
/** /**
* tvAttribution : credit * tvAttribution : credit
*/ */
private AppCompatTextView tvAttribution; private AppCompatTextView tvAttribution;
@Override @Override
protected void onCreate(@Nullable final Bundle savedInstanceState) { protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_ACTION_BAR); getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
final ActionBar actionBar = getSupportActionBar(); final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) { if (actionBar != null) {
actionBar.hide(); actionBar.hide();
} }
setContentView(R.layout.activity_location_picker); setContentView(R.layout.activity_location_picker);
if (savedInstanceState == null) { if (savedInstanceState == null) {
cameraPosition = getIntent().getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION); cameraPosition = getIntent()
.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
}
final LocationPickerViewModel viewModel = new ViewModelProvider(this)
.get(LocationPickerViewModel.class);
viewModel.getResult().observe(this, this);
bindViews();
addBackButtonListener();
addPlaceSelectedButton();
addCredits();
getToolbarUI();
mapView.onCreate(savedInstanceState);
mapView.getMapAsync(this);
} }
final LocationPickerViewModel viewModel = new ViewModelProvider(this) /**
.get(LocationPickerViewModel.class); * For showing credits
viewModel.getResult().observe(this, this); */
private void addCredits() {
bindViews(); tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
addBackButtonListener(); tvAttribution.setMovementMethod(LinkMovementMethod.getInstance());
addPlaceSelectedButton();
addCredits();
getToolbarUI();
mapView.onCreate(savedInstanceState);
mapView.getMapAsync(this);
}
/**
* For showing credits
*/
private void addCredits() {
tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
tvAttribution.setMovementMethod(LinkMovementMethod.getInstance());
}
/**
* Clicking back button destroy locationPickerActivity
*/
private void addBackButtonListener() {
final ImageView backButton = findViewById(R.id.mapbox_place_picker_toolbar_back_button);
backButton.setOnClickListener(view -> finish());
}
/**
* Binds mapView and location picker icon
*/
private void bindViews() {
mapView = findViewById(R.id.map_view);
markerImage = findViewById(R.id.location_picker_image_view_marker);
tvAttribution = findViewById(R.id.tv_attribution);
}
/**
* Binds the listeners
*/
private void bindListeners() {
mapboxMap.addOnCameraMoveStartedListener(
this);
mapboxMap.addOnCameraIdleListener(
this);
}
/**
* Gets toolbar color
*/
private void getToolbarUI() {
final ConstraintLayout toolbar = findViewById(R.id.location_picker_toolbar);
toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor));
}
/**
* Takes action when map is ready to show
* @param mapboxMap map
*/
@Override
public void onMapReady(final MapboxMap mapboxMap) {
this.mapboxMap = mapboxMap;
mapboxMap.setStyle(Style.MAPBOX_STREETS, style -> {
adjustCameraBasedOnOptions();
bindListeners();
enableLocationComponent(style);
});
}
/**
* move the location to the current media coordinates
*/
private void adjustCameraBasedOnOptions() {
mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
}
/**
* Enables location components
* @param loadedMapStyle Style
*/
@SuppressWarnings( {"MissingPermission"})
private void enableLocationComponent(@NonNull final Style loadedMapStyle) {
final UiSettings uiSettings = mapboxMap.getUiSettings();
uiSettings.setAttributionEnabled(false);
// Check if permissions are enabled and if not request
if (PermissionsManager.areLocationPermissionsGranted(this)) {
// Get an instance of the component
final LocationComponent locationComponent = mapboxMap.getLocationComponent();
// Activate with options
locationComponent.activateLocationComponent(
LocationComponentActivationOptions.builder(this, loadedMapStyle).build());
// Enable to make component visible
locationComponent.setLocationComponentEnabled(true);
// Set the component's camera mode
locationComponent.setCameraMode(CameraMode.NONE);
// Set the component's render mode
locationComponent.setRenderMode(RenderMode.NORMAL);
} }
}
/** /**
* Acts on camera moving * Clicking back button destroy locationPickerActivity
* @param reason int */
*/ private void addBackButtonListener() {
@Override final ImageView backButton = findViewById(R.id.mapbox_place_picker_toolbar_back_button);
public void onCameraMoveStarted(final int reason) { backButton.setOnClickListener(view -> finish());
Timber.v("Map camera has begun moving.");
if (markerImage.getTranslationY() == 0) {
markerImage.animate().translationY(-75)
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
} }
}
/** /**
* Acts on camera idle * Binds mapView and location picker icon
*/ */
@Override private void bindViews() {
public void onCameraIdle() { mapView = findViewById(R.id.map_view);
Timber.v("Map camera is now idling."); markerImage = findViewById(R.id.location_picker_image_view_marker);
markerImage.animate().translationY(0) tvAttribution = findViewById(R.id.tv_attribution);
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
}
/**
* Takes action on camera position
* @param position position of picker
*/
@Override
public void onChanged(@Nullable CameraPosition position) {
if (position == null) {
position = new Builder()
.target(new LatLng(mapboxMap.getCameraPosition().target.getLatitude(),
mapboxMap.getCameraPosition().target.getLongitude()))
.zoom(16).build();
} }
cameraPosition = position;
}
/** /**
* Select the preferable location * Binds the listeners
*/ */
private void addPlaceSelectedButton() { private void bindListeners() {
final FloatingActionButton placeSelectedButton = findViewById(R.id.location_chosen_button); mapboxMap.addOnCameraMoveStartedListener(
placeSelectedButton.setOnClickListener(view -> placeSelected()); this);
} mapboxMap.addOnCameraIdleListener(
this);
}
/** /**
* Return the intent with required data * Gets toolbar color
*/ */
void placeSelected() { private void getToolbarUI() {
final Intent returningIntent = new Intent(); final ConstraintLayout toolbar = findViewById(R.id.location_picker_toolbar);
returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor));
mapboxMap.getCameraPosition()); }
setResult(AppCompatActivity.RESULT_OK, returningIntent);
finish();
}
@Override /**
protected void onStart() { * Takes action when map is ready to show
super.onStart(); *
mapView.onStart(); * @param mapboxMap map
} */
@Override
public void onMapReady(final MapboxMap mapboxMap) {
this.mapboxMap = mapboxMap;
mapboxMap.setStyle(Style.MAPBOX_STREETS, style -> {
adjustCameraBasedOnOptions();
bindListeners();
enableLocationComponent(style);
});
}
@Override /**
protected void onResume() { * move the location to the current media coordinates
super.onResume(); */
mapView.onResume(); private void adjustCameraBasedOnOptions() {
} mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
}
@Override /**
protected void onPause() { * Enables location components
super.onPause(); *
mapView.onPause(); * @param loadedMapStyle Style
} */
@SuppressWarnings({"MissingPermission"})
private void enableLocationComponent(@NonNull final Style loadedMapStyle) {
final UiSettings uiSettings = mapboxMap.getUiSettings();
uiSettings.setAttributionEnabled(false);
@Override // Check if permissions are enabled and if not request
protected void onStop() { if (PermissionsManager.areLocationPermissionsGranted(this)) {
super.onStop();
mapView.onStop();
}
@Override // Get an instance of the component
protected void onSaveInstanceState(final @NotNull Bundle outState) { final LocationComponent locationComponent = mapboxMap.getLocationComponent();
super.onSaveInstanceState(outState);
mapView.onSaveInstanceState(outState);
}
@Override // Activate with options
protected void onDestroy() { locationComponent.activateLocationComponent(
super.onDestroy(); LocationComponentActivationOptions.builder(this, loadedMapStyle).build());
mapView.onDestroy();
}
@Override // Enable to make component visible
public void onLowMemory() { locationComponent.setLocationComponentEnabled(true);
super.onLowMemory();
mapView.onLowMemory(); // Set the component's camera mode
} locationComponent.setCameraMode(CameraMode.NONE);
// Set the component's render mode
locationComponent.setRenderMode(RenderMode.NORMAL);
}
}
/**
* Acts on camera moving
*
* @param reason int
*/
@Override
public void onCameraMoveStarted(final int reason) {
Timber.v("Map camera has begun moving.");
if (markerImage.getTranslationY() == 0) {
markerImage.animate().translationY(-75)
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
}
}
/**
* Acts on camera idle
*/
@Override
public void onCameraIdle() {
Timber.v("Map camera is now idling.");
markerImage.animate().translationY(0)
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
}
/**
* Takes action on camera position
*
* @param position position of picker
*/
@Override
public void onChanged(@Nullable CameraPosition position) {
if (position == null) {
position = new Builder()
.target(new LatLng(mapboxMap.getCameraPosition().target.getLatitude(),
mapboxMap.getCameraPosition().target.getLongitude()))
.zoom(16).build();
}
cameraPosition = position;
}
/**
* Select the preferable location
*/
private void addPlaceSelectedButton() {
final FloatingActionButton placeSelectedButton = findViewById(R.id.location_chosen_button);
placeSelectedButton.setOnClickListener(view -> placeSelected());
}
/**
* Return the intent with required data
*/
void placeSelected() {
final Intent returningIntent = new Intent();
returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION,
mapboxMap.getCameraPosition());
setResult(AppCompatActivity.RESULT_OK, returningIntent);
finish();
}
@Override
protected void onStart() {
super.onStart();
mapView.onStart();
}
@Override
protected void onResume() {
super.onResume();
mapView.onResume();
}
@Override
protected void onPause() {
super.onPause();
mapView.onPause();
}
@Override
protected void onStop() {
super.onStop();
mapView.onStop();
}
@Override
protected void onSaveInstanceState(final @NotNull Bundle outState) {
super.onSaveInstanceState(outState);
mapView.onSaveInstanceState(outState);
}
@Override
protected void onDestroy() {
super.onDestroy();
mapView.onDestroy();
}
@Override
public void onLowMemory() {
super.onLowMemory();
mapView.onLowMemory();
}
} }

View file

@ -5,9 +5,9 @@ package fr.free.nrw.commons.LocationPicker;
*/ */
public final class LocationPickerConstants { public final class LocationPickerConstants {
public static final String MAP_CAMERA_POSITION public static final String MAP_CAMERA_POSITION
= "location.picker.cameraPosition"; = "location.picker.cameraPosition";
private LocationPickerConstants() { private LocationPickerConstants() {
} }
} }

View file

@ -16,45 +16,48 @@ import timber.log.Timber;
*/ */
public class LocationPickerViewModel extends AndroidViewModel implements Callback<CameraPosition> { public class LocationPickerViewModel extends AndroidViewModel implements Callback<CameraPosition> {
/** /**
* Wrapping CameraPosition with MutableLiveData * Wrapping CameraPosition with MutableLiveData
*/ */
private final MutableLiveData<CameraPosition> result = new MutableLiveData<>(); private final MutableLiveData<CameraPosition> result = new MutableLiveData<>();
/** /**
* Constructor for this class * Constructor for this class
* @param application Application *
*/ * @param application Application
public LocationPickerViewModel(@NonNull final Application application) { */
super(application); public LocationPickerViewModel(@NonNull final Application application) {
} super(application);
/**
* Responses on camera position changing
* @param call Call<CameraPosition>
* @param response Response<CameraPosition>
*/
@Override
public void onResponse(final @NotNull Call<CameraPosition> call,
final Response<CameraPosition> response) {
if(response.body()==null){
result.setValue(null);
return;
} }
result.setValue(response.body());
}
@Override /**
public void onFailure(final @NotNull Call<CameraPosition> call, final @NotNull Throwable t) { * Responses on camera position changing
Timber.e(t); *
} * @param call Call<CameraPosition>
* @param response Response<CameraPosition>
*/
@Override
public void onResponse(final @NotNull Call<CameraPosition> call,
final Response<CameraPosition> response) {
if (response.body() == null) {
result.setValue(null);
return;
}
result.setValue(response.body());
}
/** @Override
* Gets live CameraPosition public void onFailure(final @NotNull Call<CameraPosition> call, final @NotNull Throwable t) {
* @return MutableLiveData<CameraPosition> Timber.e(t);
*/ }
public MutableLiveData<CameraPosition> getResult() {
return result; /**
} * Gets live CameraPosition
*
* @return MutableLiveData<CameraPosition>
*/
public MutableLiveData<CameraPosition> getResult() {
return result;
}
} }

View file

@ -27,84 +27,85 @@ import javax.inject.Named;
public class BookmarkFragment extends CommonsDaggerSupportFragment { public class BookmarkFragment extends CommonsDaggerSupportFragment {
private FragmentManager supportFragmentManager; private FragmentManager supportFragmentManager;
private BookmarksPagerAdapter adapter; private BookmarksPagerAdapter adapter;
@BindView(R.id.viewPagerBookmarks) @BindView(R.id.viewPagerBookmarks)
ParentViewPager viewPager; ParentViewPager viewPager;
@BindView(R.id.tab_layout) @BindView(R.id.tab_layout)
TabLayout tabLayout; TabLayout tabLayout;
@BindView(R.id.fragmentContainer) @BindView(R.id.fragmentContainer)
FrameLayout fragmentContainer; FrameLayout fragmentContainer;
@Inject @Inject
ContributionController controller; ContributionController controller;
/** /**
* To check if the user is loggedIn or not. * To check if the user is loggedIn or not.
*/ */
@Inject @Inject
@Named("default_preferences") @Named("default_preferences")
public public
JsonKvStore applicationKvStore; JsonKvStore applicationKvStore;
@NonNull @NonNull
public static BookmarkFragment newInstance() { public static BookmarkFragment newInstance() {
BookmarkFragment fragment = new BookmarkFragment(); BookmarkFragment fragment = new BookmarkFragment();
fragment.setRetainInstance(true); fragment.setRetainInstance(true);
return fragment; return fragment;
}
public void setScroll(boolean canScroll){
viewPager.setCanScroll(canScroll);
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_bookmarks, container, false);
ButterKnife.bind(this, view);
// Activity can call methods in the fragment by acquiring a
// reference to the Fragment from FragmentManager, using findFragmentById()
supportFragmentManager = getChildFragmentManager();
adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(),
applicationKvStore.getBoolean("login_skipped"));
viewPager.setAdapter(adapter);
tabLayout.setupWithViewPager(viewPager);
((MainActivity)getActivity()).showTabs();
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
setupTabLayout();
return view;
}
/**
* This method sets up the tab layout.
* If the adapter has only one element it sets the visibility of tabLayout to gone.
*/
public void setupTabLayout(){
tabLayout.setVisibility(View.VISIBLE);
if (adapter.getCount() == 1) {
tabLayout.setVisibility(View.GONE);
} }
}
public void setScroll(boolean canScroll) {
public void onBackPressed() { viewPager.setCanScroll(canScroll);
if(((BookmarkListRootFragment)(adapter.getItem(tabLayout.getSelectedTabPosition()))).backPressed()) { }
// The event is handled internally by the adapter , no further action required.
return; @Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_bookmarks, container, false);
ButterKnife.bind(this, view);
// Activity can call methods in the fragment by acquiring a
// reference to the Fragment from FragmentManager, using findFragmentById()
supportFragmentManager = getChildFragmentManager();
adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(),
applicationKvStore.getBoolean("login_skipped"));
viewPager.setAdapter(adapter);
tabLayout.setupWithViewPager(viewPager);
((MainActivity) getActivity()).showTabs();
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
setupTabLayout();
return view;
}
/**
* This method sets up the tab layout. If the adapter has only one element it sets the
* visibility of tabLayout to gone.
*/
public void setupTabLayout() {
tabLayout.setVisibility(View.VISIBLE);
if (adapter.getCount() == 1) {
tabLayout.setVisibility(View.GONE);
}
}
public void onBackPressed() {
if (((BookmarkListRootFragment) (adapter.getItem(tabLayout.getSelectedTabPosition())))
.backPressed()) {
// The event is handled internally by the adapter , no further action required.
return;
}
// Event is not handled by the adapter ( performed back action ) change action bar.
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
} }
// Event is not handled by the adapter ( performed back action ) change action bar.
((BaseActivity)getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
} }

View file

@ -30,223 +30,226 @@ import java.util.Iterator;
public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements
FragmentManager.OnBackStackChangedListener, FragmentManager.OnBackStackChangedListener,
MediaDetailPagerFragment.MediaDetailProvider, MediaDetailPagerFragment.MediaDetailProvider,
AdapterView.OnItemClickListener, CategoryImagesCallback{ AdapterView.OnItemClickListener, CategoryImagesCallback {
private MediaDetailPagerFragment mediaDetails; private MediaDetailPagerFragment mediaDetails;
//private BookmarkPicturesFragment bookmarkPicturesFragment; //private BookmarkPicturesFragment bookmarkPicturesFragment;
private BookmarkLocationsFragment bookmarkLocationsFragment; private BookmarkLocationsFragment bookmarkLocationsFragment;
public Fragment listFragment; public Fragment listFragment;
private BookmarksPagerAdapter bookmarksPagerAdapter; private BookmarksPagerAdapter bookmarksPagerAdapter;
@BindView(R.id.explore_container) @BindView(R.id.explore_container)
FrameLayout container; FrameLayout container;
public BookmarkListRootFragment(){ public BookmarkListRootFragment() {
//empty constructor necessary otherwise crashes on recreate //empty constructor necessary otherwise crashes on recreate
}
public BookmarkListRootFragment(Bundle bundle, BookmarksPagerAdapter bookmarksPagerAdapter) {
String title = bundle.getString("categoryName");
int order = bundle.getInt("order");
if (order == 0) {
listFragment = new BookmarkPicturesFragment();
} else {
listFragment = new BookmarkLocationsFragment();
} }
Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", title);
listFragment.setArguments(featuredArguments);
this.bookmarksPagerAdapter = bookmarksPagerAdapter;
}
@Nullable public BookmarkListRootFragment(Bundle bundle, BookmarksPagerAdapter bookmarksPagerAdapter) {
@Override String title = bundle.getString("categoryName");
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, int order = bundle.getInt("order");
@Nullable final Bundle savedInstanceState) { if (order == 0) {
super.onCreate(savedInstanceState); listFragment = new BookmarkPicturesFragment();
View view = inflater.inflate(R.layout.fragment_featured_root, container, false); } else {
ButterKnife.bind(this, view); listFragment = new BookmarkLocationsFragment();
return view; }
} Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", title);
@Override listFragment.setArguments(featuredArguments);
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { this.bookmarksPagerAdapter = bookmarksPagerAdapter;
super.onViewCreated(view, savedInstanceState);
if(savedInstanceState==null) {
setFragment(listFragment, mediaDetails);
} }
}
public void setFragment(Fragment fragment, Fragment otherFragment) { @Nullable
if (fragment.isAdded() && otherFragment != null) { @Override
getChildFragmentManager() public View onCreateView(@NonNull final LayoutInflater inflater,
.beginTransaction() @Nullable final ViewGroup container,
.hide(otherFragment) @Nullable final Bundle savedInstanceState) {
.show( fragment) super.onCreate(savedInstanceState);
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") View view = inflater.inflate(R.layout.fragment_featured_root, container, false);
.commit(); ButterKnife.bind(this, view);
getChildFragmentManager().executePendingTransactions(); return view;
} else if (fragment.isAdded() && otherFragment == null) {
getChildFragmentManager()
.beginTransaction()
.show( fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
}else if (!fragment.isAdded() && otherFragment != null ) {
getChildFragmentManager()
.beginTransaction()
.hide(otherFragment)
.add(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded()) {
getChildFragmentManager()
.beginTransaction()
.replace(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} }
}
public void removeFragment(Fragment fragment) { @Override
getChildFragmentManager() public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
.beginTransaction() super.onViewCreated(view, savedInstanceState);
.remove(fragment) if (savedInstanceState == null) {
.commit(); setFragment(listFragment, mediaDetails);
getChildFragmentManager().executePendingTransactions(); }
} }
@Override public void setFragment(Fragment fragment, Fragment otherFragment) {
public void onAttach(final Context context) { if (fragment.isAdded() && otherFragment != null) {
super.onAttach(context); getChildFragmentManager()
} .beginTransaction()
.hide(otherFragment)
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (fragment.isAdded() && otherFragment == null) {
getChildFragmentManager()
.beginTransaction()
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded() && otherFragment != null) {
getChildFragmentManager()
.beginTransaction()
.hide(otherFragment)
.add(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded()) {
getChildFragmentManager()
.beginTransaction()
.replace(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
}
}
@Override public void removeFragment(Fragment fragment) {
public void onMediaClicked(int position) { getChildFragmentManager()
Log.d("deneme8","on media clicked"); .beginTransaction()
.remove(fragment)
.commit();
getChildFragmentManager().executePendingTransactions();
}
@Override
public void onAttach(final Context context) {
super.onAttach(context);
}
@Override
public void onMediaClicked(int position) {
Log.d("deneme8", "on media clicked");
/*container.setVisibility(View.VISIBLE); /*container.setVisibility(View.VISIBLE);
((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE); ((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE);
mediaDetails = new MediaDetailPagerFragment(false, true, position); mediaDetails = new MediaDetailPagerFragment(false, true, position);
setFragment(mediaDetails, bookmarkPicturesFragment);*/ setFragment(mediaDetails, bookmarkPicturesFragment);*/
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
*
* @param i It is the index of which media object is to be returned which is same as current
* index of viewPager.
* @return Media Object
*/
@Override
public Media getMediaAtPosition(int i) {
if (bookmarksPagerAdapter.getMediaAdapter() == null) {
// not yet ready to return data
return null;
} else {
return (Media) bookmarksPagerAdapter.getMediaAdapter().getItem(i);
} }
}
/** /**
* This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
* same number of media items as that of media elements in adapter. *
* * @param i It is the index of which media object is to be returned which is same as current
* @return Total Media count in the adapter * index of viewPager.
*/ * @return Media Object
@Override */
public int getTotalMediaCount() { @Override
if (bookmarksPagerAdapter.getMediaAdapter() == null) { public Media getMediaAtPosition(int i) {
return 0; if (bookmarksPagerAdapter.getMediaAdapter() == null) {
} // not yet ready to return data
return bookmarksPagerAdapter.getMediaAdapter().getCount(); return null;
} } else {
return (Media) bookmarksPagerAdapter.getMediaAdapter().getItem(i);
@Override
public Integer getContributionStateAt(int position) {
return null;
}
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
@Override
public void refreshNominatedMedia(int index) {
if(mediaDetails != null && !listFragment.isVisible()) {
removeFragment(mediaDetails);
mediaDetails = new MediaDetailPagerFragment(false, true);
((BookmarkFragment) getParentFragment()).setScroll(false);
setFragment(mediaDetails, listFragment);
mediaDetails.showImage(index);
}
}
/**
* This method is called on success of API call for featured images or mobile uploads. The
* viewpager will notified that number of items have changed.
*/
@Override
public void viewPagerNotifyDataSetChanged() {
if (mediaDetails != null) {
mediaDetails.notifyDataSetChanged();
}
}
public boolean backPressed() {
//check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException
if(mediaDetails!=null) {
if (mediaDetails.isVisible()) {
if(mediaDetails.backButtonClicked()) {
// mediaDetails handled the back clicked , no further action required.
return true;
} }
// todo add get list fragment }
((BookmarkFragment) getParentFragment()).setupTabLayout();
ArrayList<Integer> removed=mediaDetails.getRemovedItems(); /**
removeFragment(mediaDetails); * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain
((BookmarkFragment) getParentFragment()).setScroll(true); * same number of media items as that of media elements in adapter.
setFragment(listFragment, mediaDetails); *
* @return Total Media count in the adapter
*/
@Override
public int getTotalMediaCount() {
if (bookmarksPagerAdapter.getMediaAdapter() == null) {
return 0;
}
return bookmarksPagerAdapter.getMediaAdapter().getCount();
}
@Override
public Integer getContributionStateAt(int position) {
return null;
}
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
@Override
public void refreshNominatedMedia(int index) {
if (mediaDetails != null && !listFragment.isVisible()) {
removeFragment(mediaDetails);
mediaDetails = new MediaDetailPagerFragment(false, true);
((BookmarkFragment) getParentFragment()).setScroll(false);
setFragment(mediaDetails, listFragment);
mediaDetails.showImage(index);
}
}
/**
* This method is called on success of API call for featured images or mobile uploads. The
* viewpager will notified that number of items have changed.
*/
@Override
public void viewPagerNotifyDataSetChanged() {
if (mediaDetails != null) {
mediaDetails.notifyDataSetChanged();
}
}
public boolean backPressed() {
//check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException
if (mediaDetails != null) {
if (mediaDetails.isVisible()) {
if (mediaDetails.backButtonClicked()) {
// mediaDetails handled the back clicked , no further action required.
return true;
}
// todo add get list fragment
((BookmarkFragment) getParentFragment()).setupTabLayout();
ArrayList<Integer> removed = mediaDetails.getRemovedItems();
removeFragment(mediaDetails);
((BookmarkFragment) getParentFragment()).setScroll(true);
setFragment(listFragment, mediaDetails);
((MainActivity) getActivity()).showTabs();
if (listFragment instanceof BookmarkPicturesFragment) {
GridViewAdapter adapter = ((GridViewAdapter) ((BookmarkPicturesFragment) listFragment)
.getAdapter());
Iterator i = removed.iterator();
while (i.hasNext()) {
adapter.remove(adapter.getItem((int) i.next()));
}
mediaDetails.clearRemoved();
}
} else {
moveToContributionsFragment();
}
} else {
moveToContributionsFragment();
}
// notify mediaDetails did not handled the backPressed further actions required.
return false;
}
void moveToContributionsFragment() {
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
((MainActivity) getActivity()).showTabs(); ((MainActivity) getActivity()).showTabs();
if(listFragment instanceof BookmarkPicturesFragment){
GridViewAdapter adapter=((GridViewAdapter)((BookmarkPicturesFragment)listFragment).getAdapter());
Iterator i = removed.iterator();
while (i.hasNext()) {
adapter.remove(adapter.getItem((int)i.next()));
}
mediaDetails.clearRemoved();
}
} else {
moveToContributionsFragment();
}
} else {
moveToContributionsFragment();
} }
// notify mediaDetails did not handled the backPressed further actions required.
return false;
}
void moveToContributionsFragment(){ @Override
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code()); public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
((MainActivity) getActivity()).showTabs(); Log.d("deneme8", "on media clicked");
} container.setVisibility(View.VISIBLE);
@Override ((BookmarkFragment) getParentFragment()).tabLayout.setVisibility(View.GONE);
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { mediaDetails = new MediaDetailPagerFragment(false, true);
Log.d("deneme8","on media clicked"); ((BookmarkFragment) getParentFragment()).setScroll(false);
container.setVisibility(View.VISIBLE); setFragment(mediaDetails, listFragment);
((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE); mediaDetails.showImage(position);
mediaDetails = new MediaDetailPagerFragment(false, true); }
((BookmarkFragment) getParentFragment()).setScroll(false);
setFragment(mediaDetails, listFragment);
mediaDetails.showImage(position);
}
@Override @Override
public void onBackStackChanged() { public void onBackStackChanged() {
} }
} }

View file

@ -18,61 +18,61 @@ import java.util.List;
@Dao @Dao
public abstract class ContributionDao { public abstract class ContributionDao {
@Query("SELECT * FROM contribution order by media_dateUploaded DESC") @Query("SELECT * FROM contribution order by media_dateUploaded DESC")
abstract DataSource.Factory<Integer, Contribution> fetchContributions(); abstract DataSource.Factory<Integer, Contribution> fetchContributions();
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract void saveSynchronous(Contribution contribution); public abstract void saveSynchronous(Contribution contribution);
public Completable save(final Contribution contribution) { public Completable save(final Contribution contribution) {
return Completable return Completable
.fromAction(() -> { .fromAction(() -> {
contribution.setDateModified(Calendar.getInstance().getTime()); contribution.setDateModified(Calendar.getInstance().getTime());
saveSynchronous(contribution); saveSynchronous(contribution);
}); });
} }
@Transaction @Transaction
public void deleteAndSaveContribution(final Contribution oldContribution, public void deleteAndSaveContribution(final Contribution oldContribution,
final Contribution newContribution) { final Contribution newContribution) {
deleteSynchronous(oldContribution); deleteSynchronous(oldContribution);
saveSynchronous(newContribution); saveSynchronous(newContribution);
} }
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract Single<List<Long>> save(List<Contribution> contribution); public abstract Single<List<Long>> save(List<Contribution> contribution);
@Delete @Delete
public abstract void deleteSynchronous(Contribution contribution); public abstract void deleteSynchronous(Contribution contribution);
public Completable delete(final Contribution contribution) { public Completable delete(final Contribution contribution) {
return Completable return Completable
.fromAction(() -> deleteSynchronous(contribution)); .fromAction(() -> deleteSynchronous(contribution));
} }
@Query("SELECT * from contribution WHERE media_filename=:fileName") @Query("SELECT * from contribution WHERE media_filename=:fileName")
public abstract List<Contribution> getContributionWithTitle(String fileName); public abstract List<Contribution> getContributionWithTitle(String fileName);
@Query("SELECT * from contribution WHERE pageId=:pageId") @Query("SELECT * from contribution WHERE pageId=:pageId")
public abstract Contribution getContribution(String pageId); public abstract Contribution getContribution(String pageId);
@Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC") @Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
public abstract Single<List<Contribution>> getContribution(List<Integer> states); public abstract Single<List<Contribution>> getContribution(List<Integer> states);
@Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)") @Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)")
public abstract Single<Integer> getPendingUploads(int[] toUpdateStates); public abstract Single<Integer> getPendingUploads(int[] toUpdateStates);
@Query("Delete FROM contribution") @Query("Delete FROM contribution")
public abstract void deleteAll() throws SQLiteException; public abstract void deleteAll() throws SQLiteException;
@Update @Update
public abstract void updateSynchronous(Contribution contribution); public abstract void updateSynchronous(Contribution contribution);
public Completable update(final Contribution contribution) { public Completable update(final Contribution contribution) {
return Completable return Completable
.fromAction(() -> { .fromAction(() -> {
contribution.setDateModified(Calendar.getInstance().getTime()); contribution.setDateModified(Calendar.getInstance().getTime());
updateSynchronous(contribution); updateSynchronous(contribution);
}); });
} }
} }

View file

@ -24,244 +24,243 @@ import io.reactivex.schedulers.Schedulers;
public class ContributionViewHolder extends RecyclerView.ViewHolder { public class ContributionViewHolder extends RecyclerView.ViewHolder {
private final Callback callback; private final Callback callback;
@BindView(R.id.contributionImage) @BindView(R.id.contributionImage)
SimpleDraweeView imageView; SimpleDraweeView imageView;
@BindView(R.id.contributionTitle) @BindView(R.id.contributionTitle)
TextView titleView; TextView titleView;
@BindView(R.id.authorView) @BindView(R.id.authorView)
TextView authorView; TextView authorView;
@BindView(R.id.contributionState) @BindView(R.id.contributionState)
TextView stateView; TextView stateView;
@BindView(R.id.contributionSequenceNumber) @BindView(R.id.contributionSequenceNumber)
TextView seqNumView; TextView seqNumView;
@BindView(R.id.contributionProgress) @BindView(R.id.contributionProgress)
ProgressBar progressView; ProgressBar progressView;
@BindView(R.id.image_options) @BindView(R.id.image_options)
RelativeLayout imageOptions; RelativeLayout imageOptions;
@BindView(R.id.wikipediaButton) @BindView(R.id.wikipediaButton)
ImageButton addToWikipediaButton; ImageButton addToWikipediaButton;
@BindView(R.id.retryButton) @BindView(R.id.retryButton)
ImageButton retryButton; ImageButton retryButton;
@BindView(R.id.cancelButton) @BindView(R.id.cancelButton)
ImageButton cancelButton; ImageButton cancelButton;
@BindView(R.id.pauseResumeButton) @BindView(R.id.pauseResumeButton)
ImageButton pauseResumeButton; ImageButton pauseResumeButton;
private int position; private int position;
private Contribution contribution; private Contribution contribution;
private final CompositeDisposable compositeDisposable = new CompositeDisposable(); private final CompositeDisposable compositeDisposable = new CompositeDisposable();
private final MediaClient mediaClient; private final MediaClient mediaClient;
private boolean isWikipediaButtonDisplayed; private boolean isWikipediaButtonDisplayed;
ContributionViewHolder(final View parent, final Callback callback, ContributionViewHolder(final View parent, final Callback callback,
final MediaClient mediaClient) { final MediaClient mediaClient) {
super(parent); super(parent);
this.mediaClient = mediaClient; this.mediaClient = mediaClient;
ButterKnife.bind(this, parent); ButterKnife.bind(this, parent);
this.callback = callback; this.callback = callback;
}
public void init(final int position, final Contribution contribution) {
//handling crashes when the contribution is null.
if( null == contribution) {
return;
} }
this.contribution = contribution; public void init(final int position, final Contribution contribution) {
this.position = position;
titleView.setText(contribution.getMedia().getMostRelevantCaption());
authorView.setText(contribution.getMedia().getAuthor());
//Removes flicker of loading image. //handling crashes when the contribution is null.
imageView.getHierarchy().setFadeDuration(0); if (null == contribution) {
return;
imageView.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
imageView.getHierarchy().setFailureImage(R.drawable.image_placeholder);
final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(),
contribution.getLocalUri());
if (!TextUtils.isEmpty(imageSource)) {
final ImageRequest imageRequest =
ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
.setProgressiveRenderingEnabled(true)
.build();
imageView.setImageRequest(imageRequest);
}
seqNumView.setText(String.valueOf(position + 1));
seqNumView.setVisibility(View.VISIBLE);
addToWikipediaButton.setVisibility(View.GONE);
switch (contribution.getState()) {
case Contribution.STATE_COMPLETED:
stateView.setVisibility(View.GONE);
progressView.setVisibility(View.GONE);
imageOptions.setVisibility(View.GONE);
stateView.setText("");
checkIfMediaExistsOnWikipediaPage(contribution);
break;
case Contribution.STATE_QUEUED:
case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE:
stateView.setVisibility(View.VISIBLE);
progressView.setVisibility(View.GONE);
stateView.setText(R.string.contribution_state_queued);
imageOptions.setVisibility(View.GONE);
break;
case Contribution.STATE_IN_PROGRESS:
stateView.setVisibility(View.GONE);
progressView.setVisibility(View.VISIBLE);
addToWikipediaButton.setVisibility(View.GONE);
pauseResumeButton.setVisibility(View.VISIBLE);
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE);
final long total = contribution.getDataLength();
final long transferred = contribution.getTransferred();
if (transferred == 0 || transferred >= total) {
progressView.setIndeterminate(true);
} else {
progressView.setProgress((int) (((double) transferred / (double) total) * 100));
} }
break;
case Contribution.STATE_PAUSED: this.contribution = contribution;
stateView.setVisibility(View.VISIBLE); this.position = position;
stateView.setText(R.string.paused); titleView.setText(contribution.getMedia().getMostRelevantCaption());
authorView.setText(contribution.getMedia().getAuthor());
//Removes flicker of loading image.
imageView.getHierarchy().setFadeDuration(0);
imageView.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
imageView.getHierarchy().setFailureImage(R.drawable.image_placeholder);
final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(),
contribution.getLocalUri());
if (!TextUtils.isEmpty(imageSource)) {
final ImageRequest imageRequest =
ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
.setProgressiveRenderingEnabled(true)
.build();
imageView.setImageRequest(imageRequest);
}
seqNumView.setText(String.valueOf(position + 1));
seqNumView.setVisibility(View.VISIBLE);
addToWikipediaButton.setVisibility(View.GONE);
switch (contribution.getState()) {
case Contribution.STATE_COMPLETED:
stateView.setVisibility(View.GONE);
progressView.setVisibility(View.GONE);
imageOptions.setVisibility(View.GONE);
stateView.setText("");
checkIfMediaExistsOnWikipediaPage(contribution);
break;
case Contribution.STATE_QUEUED:
case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE:
stateView.setVisibility(View.VISIBLE);
progressView.setVisibility(View.GONE);
stateView.setText(R.string.contribution_state_queued);
imageOptions.setVisibility(View.GONE);
break;
case Contribution.STATE_IN_PROGRESS:
stateView.setVisibility(View.GONE);
progressView.setVisibility(View.VISIBLE);
addToWikipediaButton.setVisibility(View.GONE);
pauseResumeButton.setVisibility(View.VISIBLE);
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE);
final long total = contribution.getDataLength();
final long transferred = contribution.getTransferred();
if (transferred == 0 || transferred >= total) {
progressView.setIndeterminate(true);
} else {
progressView.setProgress((int) (((double) transferred / (double) total) * 100));
}
break;
case Contribution.STATE_PAUSED:
stateView.setVisibility(View.VISIBLE);
stateView.setText(R.string.paused);
setResume();
progressView.setVisibility(View.GONE);
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
pauseResumeButton.setVisibility(View.VISIBLE);
imageOptions.setVisibility(View.VISIBLE);
break;
case Contribution.STATE_FAILED:
stateView.setVisibility(View.VISIBLE);
stateView.setText(R.string.contribution_state_failed);
progressView.setVisibility(View.GONE);
cancelButton.setVisibility(View.VISIBLE);
retryButton.setVisibility(View.VISIBLE);
pauseResumeButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE);
break;
}
}
/**
* Checks if a media exists on the corresponding Wikipedia article Currently the check is made
* for the device's current language Wikipedia
*
* @param contribution
*/
private void checkIfMediaExistsOnWikipediaPage(final Contribution contribution) {
if (contribution.getWikidataPlace() == null
|| contribution.getWikidataPlace().getWikipediaArticle() == null) {
return;
}
final String wikipediaArticle = contribution.getWikidataPlace().getWikipediaPageTitle();
compositeDisposable.add(mediaClient.doesPageContainMedia(wikipediaArticle)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(mediaExists -> {
displayWikipediaButton(mediaExists);
}));
}
/**
* Handle action buttons visibility if the corresponding wikipedia page doesn't contain any
* media. This method needs to control the state of just the scenario where media does not
* exists as other scenarios are already handled in the init method.
*
* @param mediaExists
*/
private void displayWikipediaButton(Boolean mediaExists) {
if (!mediaExists) {
addToWikipediaButton.setVisibility(View.VISIBLE);
isWikipediaButtonDisplayed = true;
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE);
}
}
/**
* Returns the image source for the image view, first preference is given to thumbUrl if that is
* null, moves to local uri and if both are null return null
*
* @param thumbUrl
* @param localUri
* @return
*/
@Nullable
private String chooseImageSource(final String thumbUrl, final Uri localUri) {
return !TextUtils.isEmpty(thumbUrl) ? thumbUrl :
localUri != null ? localUri.toString() :
null;
}
/**
* Retry upload when it is failed
*/
@OnClick(R.id.retryButton)
public void retryUpload() {
callback.retryUpload(contribution);
}
/**
* Delete a failed upload attempt
*/
@OnClick(R.id.cancelButton)
public void deleteUpload() {
callback.deleteUpload(contribution);
}
@OnClick(R.id.contributionImage)
public void imageClicked() {
callback.openMediaDetail(position, isWikipediaButtonDisplayed);
}
@OnClick(R.id.wikipediaButton)
public void wikipediaButtonClicked() {
callback.addImageToWikipedia(contribution);
}
/**
* Triggers a callback for pause/resume
*/
@OnClick(R.id.pauseResumeButton)
public void onPauseResumeButtonClicked() {
if (pauseResumeButton.getTag().toString().equals("pause")) {
pause();
} else {
resume();
}
}
private void resume() {
callback.resumeUpload(contribution);
setPaused();
}
private void pause() {
callback.pauseUpload(contribution);
setResume(); setResume();
progressView.setVisibility(View.GONE);
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
pauseResumeButton.setVisibility(View.VISIBLE);
imageOptions.setVisibility(View.VISIBLE);
break;
case Contribution.STATE_FAILED:
stateView.setVisibility(View.VISIBLE);
stateView.setText(R.string.contribution_state_failed);
progressView.setVisibility(View.GONE);
cancelButton.setVisibility(View.VISIBLE);
retryButton.setVisibility(View.VISIBLE);
pauseResumeButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE);
break;
} }
}
/** /**
* Checks if a media exists on the corresponding Wikipedia article Currently the check is made for * Update pause/resume button to show pause state
* the device's current language Wikipedia */
* private void setPaused() {
* @param contribution pauseResumeButton.setImageResource(R.drawable.pause_icon);
*/ pauseResumeButton.setTag(R.string.pause);
private void checkIfMediaExistsOnWikipediaPage(final Contribution contribution) {
if (contribution.getWikidataPlace() == null
|| contribution.getWikidataPlace().getWikipediaArticle() == null) {
return;
} }
final String wikipediaArticle = contribution.getWikidataPlace().getWikipediaPageTitle();
compositeDisposable.add(mediaClient.doesPageContainMedia(wikipediaArticle)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(mediaExists -> {
displayWikipediaButton(mediaExists);
}));
}
/** /**
* Handle action buttons visibility if the corresponding wikipedia page doesn't contain any media. * Update pause/resume button to show resume state
* This method needs to control the state of just the scenario where media does not exists as */
* other scenarios are already handled in the init method. private void setResume() {
* pauseResumeButton.setImageResource(R.drawable.play_icon);
* @param mediaExists pauseResumeButton.setTag(R.string.resume);
*/
private void displayWikipediaButton(Boolean mediaExists) {
if (!mediaExists) {
addToWikipediaButton.setVisibility(View.VISIBLE);
isWikipediaButtonDisplayed = true;
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE);
} }
}
/**
* Returns the image source for the image view, first preference is given to thumbUrl if that is
* null, moves to local uri and if both are null return null
*
* @param thumbUrl
* @param localUri
* @return
*/
@Nullable
private String chooseImageSource(final String thumbUrl, final Uri localUri) {
return !TextUtils.isEmpty(thumbUrl) ? thumbUrl :
localUri != null ? localUri.toString() :
null;
}
/**
* Retry upload when it is failed
*/
@OnClick(R.id.retryButton)
public void retryUpload() {
callback.retryUpload(contribution);
}
/**
* Delete a failed upload attempt
*/
@OnClick(R.id.cancelButton)
public void deleteUpload() {
callback.deleteUpload(contribution);
}
@OnClick(R.id.contributionImage)
public void imageClicked() {
callback.openMediaDetail(position, isWikipediaButtonDisplayed);
}
@OnClick(R.id.wikipediaButton)
public void wikipediaButtonClicked() {
callback.addImageToWikipedia(contribution);
}
/**
* Triggers a callback for pause/resume
*/
@OnClick(R.id.pauseResumeButton)
public void onPauseResumeButtonClicked() {
if (pauseResumeButton.getTag().toString().equals("pause")) {
pause();
} else {
resume();
}
}
private void resume() {
callback.resumeUpload(contribution);
setPaused();
}
private void pause() {
callback.pauseUpload(contribution);
setResume();
}
/**
* Update pause/resume button to show pause state
*/
private void setPaused() {
pauseResumeButton.setImageResource(R.drawable.pause_icon);
pauseResumeButton.setTag(R.string.pause);
}
/**
* Update pause/resume button to show resume state
*/
private void setResume() {
pauseResumeButton.setImageResource(R.drawable.play_icon);
pauseResumeButton.setTag(R.string.resume);
}
} }

View file

@ -8,17 +8,17 @@ import java.util.List;
*/ */
public class ContributionsListContract { public class ContributionsListContract {
public interface View { public interface View {
void showWelcomeTip(boolean numberOfUploads); void showWelcomeTip(boolean numberOfUploads);
void showProgress(boolean shouldShow); void showProgress(boolean shouldShow);
void showNoContributionsUI(boolean shouldShow); void showNoContributionsUI(boolean shouldShow);
} }
public interface UserActionListener extends BasePresenter<View> { public interface UserActionListener extends BasePresenter<View> {
void deleteUpload(Contribution contribution); void deleteUpload(Contribution contribution);
} }
} }

View file

@ -51,384 +51,388 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
ContributionsListContract.View, ContributionsListAdapter.Callback, ContributionsListContract.View, ContributionsListAdapter.Callback,
WikipediaInstructionsDialogFragment.Callback { WikipediaInstructionsDialogFragment.Callback {
private static final String RV_STATE = "rv_scroll_state"; private static final String RV_STATE = "rv_scroll_state";
@BindView(R.id.contributionsList) @BindView(R.id.contributionsList)
RecyclerView rvContributionsList; RecyclerView rvContributionsList;
@BindView(R.id.loadingContributionsProgressBar) @BindView(R.id.loadingContributionsProgressBar)
ProgressBar progressBar; ProgressBar progressBar;
@BindView(R.id.fab_plus) @BindView(R.id.fab_plus)
FloatingActionButton fabPlus; FloatingActionButton fabPlus;
@BindView(R.id.fab_camera) @BindView(R.id.fab_camera)
FloatingActionButton fabCamera; FloatingActionButton fabCamera;
@BindView(R.id.fab_gallery) @BindView(R.id.fab_gallery)
FloatingActionButton fabGallery; FloatingActionButton fabGallery;
@BindView(R.id.noContributionsYet) @BindView(R.id.noContributionsYet)
TextView noContributionsYet; TextView noContributionsYet;
@BindView(R.id.fab_layout) @BindView(R.id.fab_layout)
LinearLayout fab_layout; LinearLayout fab_layout;
@Inject @Inject
ContributionController controller; ContributionController controller;
@Inject @Inject
MediaClient mediaClient; MediaClient mediaClient;
@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
@Inject @Inject
WikiSite languageWikipediaSite; WikiSite languageWikipediaSite;
@Inject @Inject
ContributionsListPresenter contributionsListPresenter; ContributionsListPresenter contributionsListPresenter;
private Animation fab_close; private Animation fab_close;
private Animation fab_open; private Animation fab_open;
private Animation rotate_forward; private Animation rotate_forward;
private Animation rotate_backward; private Animation rotate_backward;
private boolean isFabOpen; private boolean isFabOpen;
private ContributionsListAdapter adapter; private ContributionsListAdapter adapter;
private Callback callback; private Callback callback;
private final int SPAN_COUNT_LANDSCAPE = 3; private final int SPAN_COUNT_LANDSCAPE = 3;
private final int SPAN_COUNT_PORTRAIT = 1; private final int SPAN_COUNT_PORTRAIT = 1;
private int contributionsSize; private int contributionsSize;
@Override @Override
public View onCreateView( public View onCreateView(
final LayoutInflater inflater, @Nullable final ViewGroup container, final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) { @Nullable final Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_contributions_list, container, false); final View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
contributionsListPresenter.onAttachView(this); contributionsListPresenter.onAttachView(this);
initAdapter(); initAdapter();
return view; return view;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (getParentFragment() != null && getParentFragment() instanceof ContributionsFragment) {
callback = ((ContributionsFragment) getParentFragment());
}
}
@Override
public void onDetach() {
super.onDetach();
callback = null;//To avoid possible memory leak
}
private void initAdapter() {
adapter = new ContributionsListAdapter(this, mediaClient);
}
@Override
public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initRecyclerView();
initializeAnimations();
setListeners();
}
private void initRecyclerView() {
final GridLayoutManager layoutManager = new GridLayoutManager(getContext(),
getSpanCount(getResources().getConfiguration().orientation));
rvContributionsList.setLayoutManager(layoutManager);
//Setting flicker animation of recycler view to false.
final ItemAnimator animator = rvContributionsList.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
} }
contributionsListPresenter.setup(); @Override
contributionsListPresenter.contributionList.observe(this.getViewLifecycleOwner(), list -> { public void onAttach(Context context) {
contributionsSize = list.size(); super.onAttach(context);
adapter.submitList(list); if (getParentFragment() != null && getParentFragment() instanceof ContributionsFragment) {
callback.notifyDataSetChanged(); callback = ((ContributionsFragment) getParentFragment());
});
rvContributionsList.setAdapter(adapter);
adapter.registerAdapterDataObserver(new AdapterDataObserver() {
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
if (itemCount > 0 && positionStart == 0) {
if(adapter.getContributionForPosition(positionStart)!=null) {
rvContributionsList.scrollToPosition(0);//Newly upload items are always added to the top
}
} }
} }
/** @Override
* Called whenever items in the list have changed public void onDetach() {
* Calls viewPagerNotifyDataSetChanged() that will notify the viewpager super.onDetach();
*/ callback = null;//To avoid possible memory leak
@Override }
public void onItemRangeChanged(final int positionStart, final int itemCount) {
super.onItemRangeChanged(positionStart, itemCount);
callback.viewPagerNotifyDataSetChanged();
}
});
//Fab close on touch outside (Scrolling or taping on item triggers this action). private void initAdapter() {
rvContributionsList.addOnItemTouchListener(new OnItemTouchListener() { adapter = new ContributionsListAdapter(this, mediaClient);
}
/** @Override
* Silently observe and/or take over touch events sent to the RecyclerView before public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) {
* they are handled by either the RecyclerView itself or its child views. super.onViewCreated(view, savedInstanceState);
*/ initRecyclerView();
@Override initializeAnimations();
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { setListeners();
if (e.getAction() == MotionEvent.ACTION_DOWN) { }
if (isFabOpen) {
animateFAB(isFabOpen); private void initRecyclerView() {
} final GridLayoutManager layoutManager = new GridLayoutManager(getContext(),
getSpanCount(getResources().getConfiguration().orientation));
rvContributionsList.setLayoutManager(layoutManager);
//Setting flicker animation of recycler view to false.
final ItemAnimator animator = rvContributionsList.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
} }
return false;
}
/** contributionsListPresenter.setup();
* Process a touch event as part of a gesture that was claimed by returning true contributionsListPresenter.contributionList.observe(this.getViewLifecycleOwner(), list -> {
* from a previous call to {@link #onInterceptTouchEvent}. contributionsSize = list.size();
* adapter.submitList(list);
* @param rv callback.notifyDataSetChanged();
* @param e MotionEvent describing the touch event. All coordinates are in the
* RecyclerView's coordinate system.
*/
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
//required abstract method DO NOT DELETE
}
/**
* Called when a child of RecyclerView does not want RecyclerView and its ancestors
* to intercept touch events with {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
*
* @param disallowIntercept True if the child does not want the parent to intercept
* touch events.
*/
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
//required abstract method DO NOT DELETE
}
});
}
private int getSpanCount(final int orientation) {
return orientation == Configuration.ORIENTATION_LANDSCAPE ?
SPAN_COUNT_LANDSCAPE : SPAN_COUNT_PORTRAIT;
}
@Override
public void onConfigurationChanged(final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// check orientation
fab_layout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
rvContributionsList
.setLayoutManager(new GridLayoutManager(getContext(), getSpanCount(newConfig.orientation)));
}
private void initializeAnimations() {
fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open);
fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close);
rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward);
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward);
}
private void setListeners() {
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
fabCamera.setOnClickListener(view -> {
controller.initiateCameraPick(getActivity());
animateFAB(isFabOpen);
});
fabGallery.setOnClickListener(view -> {
controller.initiateGalleryPick(getActivity(), true);
animateFAB(isFabOpen);
});
}
private void animateFAB(final boolean isFabOpen) {
this.isFabOpen = !isFabOpen;
if (fabPlus.isShown()) {
if (isFabOpen) {
fabPlus.startAnimation(rotate_backward);
fabCamera.startAnimation(fab_close);
fabGallery.startAnimation(fab_close);
fabCamera.hide();
fabGallery.hide();
} else {
fabPlus.startAnimation(rotate_forward);
fabCamera.startAnimation(fab_open);
fabGallery.startAnimation(fab_open);
fabCamera.show();
fabGallery.show();
}
this.isFabOpen = !isFabOpen;
}
}
/**
* Shows welcome message if user has no contributions yet i.e. new user.
*/
@Override
public void showWelcomeTip(final boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
/**
* Responsible to set progress bar invisible and visible
*
* @param shouldShow True when contributions list should be hidden.
*/
@Override
public void showProgress(final boolean shouldShow) {
progressBar.setVisibility(shouldShow ? VISIBLE : GONE);
}
@Override
public void showNoContributionsUI(final boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
final GridLayoutManager layoutManager = (GridLayoutManager) rvContributionsList
.getLayoutManager();
outState.putParcelable(RV_STATE, layoutManager.onSaveInstanceState());
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (null != savedInstanceState) {
final Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(RV_STATE);
rvContributionsList.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState);
}
}
@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) {
contributionsListPresenter.deleteUpload(contribution);
}
@Override
public void openMediaDetail(final int position, boolean isWikipediaButtonDisplayed) {
if (null != callback) {//Just being safe, ideally they won't be called when detached
callback.showDetail(position, isWikipediaButtonDisplayed);
}
}
/**
* Handle callback for wikipedia icon clicked
*
* @param contribution
*/
@Override
public void addImageToWikipedia(Contribution contribution) {
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.add_picture_to_wikipedia_article_title),
String.format(getString(R.string.add_picture_to_wikipedia_article_desc),
Locale.getDefault().getDisplayLanguage()),
() -> {
showAddImageToWikipediaInstructions(contribution);
}, () -> {
// do nothing
}); });
} rvContributionsList.setAdapter(adapter);
adapter.registerAdapterDataObserver(new AdapterDataObserver() {
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
if (itemCount > 0 && positionStart == 0) {
if (adapter.getContributionForPosition(positionStart) != null) {
rvContributionsList
.scrollToPosition(0);//Newly upload items are always added to the top
}
}
}
/** /**
* Pauses the current upload * Called whenever items in the list have changed
* @param contribution * Calls viewPagerNotifyDataSetChanged() that will notify the viewpager
*/ */
@Override @Override
public void pauseUpload(Contribution contribution) { public void onItemRangeChanged(final int positionStart, final int itemCount) {
ViewUtil.showShortToast(getContext(), R.string.pausing_upload); super.onItemRangeChanged(positionStart, itemCount);
callback.pauseUpload(contribution); callback.viewPagerNotifyDataSetChanged();
} }
});
/** //Fab close on touch outside (Scrolling or taping on item triggers this action).
* Resumes the current upload rvContributionsList.addOnItemTouchListener(new OnItemTouchListener() {
* @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 * Silently observe and/or take over touch events sent to the RecyclerView before
* * they are handled by either the RecyclerView itself or its child views.
* @param contribution */
*/ @Override
private void showAddImageToWikipediaInstructions(Contribution contribution) { public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
FragmentManager fragmentManager = getFragmentManager(); if (e.getAction() == MotionEvent.ACTION_DOWN) {
WikipediaInstructionsDialogFragment fragment = WikipediaInstructionsDialogFragment if (isFabOpen) {
.newInstance(contribution); animateFAB(isFabOpen);
fragment.setCallback(this::onConfirmClicked); }
fragment.show(fragmentManager, "WikimediaFragment"); }
} return false;
}
/**
* Process a touch event as part of a gesture that was claimed by returning true
* from a previous call to {@link #onInterceptTouchEvent}.
*
* @param rv
* @param e MotionEvent describing the touch event. All coordinates are in the
* RecyclerView's coordinate system.
*/
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
//required abstract method DO NOT DELETE
}
public Media getMediaAtPosition(final int i) { /**
if(adapter.getContributionForPosition(i) != null) { * Called when a child of RecyclerView does not want RecyclerView and its ancestors
return adapter.getContributionForPosition(i).getMedia(); * to intercept touch events with {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
} *
return null; * @param disallowIntercept True if the child does not want the parent to intercept
} * touch events.
*/
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
//required abstract method DO NOT DELETE
}
public int getTotalMediaCount() { });
return contributionsSize;
}
/**
* Open the editor for the language Wikipedia
*
* @param contribution
*/
@Override
public void onConfirmClicked(@Nullable Contribution contribution, boolean copyWikicode) {
if (copyWikicode) {
String wikicode = contribution.getMedia().getWikiCode();
Utils.copy("wikicode", wikicode, getContext());
} }
final String url = private int getSpanCount(final int orientation) {
languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace() return orientation == Configuration.ORIENTATION_LANDSCAPE ?
.getWikipediaPageTitle(); SPAN_COUNT_LANDSCAPE : SPAN_COUNT_PORTRAIT;
Utils.handleWebUrl(getContext(), Uri.parse(url)); }
}
public Integer getContributionStateAt(int position) { @Override
return adapter.getContributionForPosition(position).getState(); public void onConfigurationChanged(final Configuration newConfig) {
} super.onConfigurationChanged(newConfig);
// check orientation
fab_layout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
rvContributionsList
.setLayoutManager(
new GridLayoutManager(getContext(), getSpanCount(newConfig.orientation)));
}
public interface Callback { private void initializeAnimations() {
fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open);
fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close);
rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward);
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward);
}
void notifyDataSetChanged(); private void setListeners() {
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
fabCamera.setOnClickListener(view -> {
controller.initiateCameraPick(getActivity());
animateFAB(isFabOpen);
});
fabGallery.setOnClickListener(view -> {
controller.initiateGalleryPick(getActivity(), true);
animateFAB(isFabOpen);
});
}
void retryUpload(Contribution contribution); private void animateFAB(final boolean isFabOpen) {
this.isFabOpen = !isFabOpen;
if (fabPlus.isShown()) {
if (isFabOpen) {
fabPlus.startAnimation(rotate_backward);
fabCamera.startAnimation(fab_close);
fabGallery.startAnimation(fab_close);
fabCamera.hide();
fabGallery.hide();
} else {
fabPlus.startAnimation(rotate_forward);
fabCamera.startAnimation(fab_open);
fabGallery.startAnimation(fab_open);
fabCamera.show();
fabGallery.show();
}
this.isFabOpen = !isFabOpen;
}
}
void showDetail(int position, boolean isWikipediaButtonDisplayed); /**
* Shows welcome message if user has no contributions yet i.e. new user.
*/
@Override
public void showWelcomeTip(final boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
void pauseUpload(Contribution contribution); /**
* Responsible to set progress bar invisible and visible
*
* @param shouldShow True when contributions list should be hidden.
*/
@Override
public void showProgress(final boolean shouldShow) {
progressBar.setVisibility(shouldShow ? VISIBLE : GONE);
}
// Notify the viewpager that number of items have changed. @Override
void viewPagerNotifyDataSetChanged(); public void showNoContributionsUI(final boolean shouldShow) {
} noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
final GridLayoutManager layoutManager = (GridLayoutManager) rvContributionsList
.getLayoutManager();
outState.putParcelable(RV_STATE, layoutManager.onSaveInstanceState());
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (null != savedInstanceState) {
final Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(RV_STATE);
rvContributionsList.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState);
}
}
@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) {
contributionsListPresenter.deleteUpload(contribution);
}
@Override
public void openMediaDetail(final int position, boolean isWikipediaButtonDisplayed) {
if (null != callback) {//Just being safe, ideally they won't be called when detached
callback.showDetail(position, isWikipediaButtonDisplayed);
}
}
/**
* Handle callback for wikipedia icon clicked
*
* @param contribution
*/
@Override
public void addImageToWikipedia(Contribution contribution) {
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.add_picture_to_wikipedia_article_title),
String.format(getString(R.string.add_picture_to_wikipedia_article_desc),
Locale.getDefault().getDisplayLanguage()),
() -> {
showAddImageToWikipediaInstructions(contribution);
}, () -> {
// do nothing
});
}
/**
* 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
*
* @param contribution
*/
private void showAddImageToWikipediaInstructions(Contribution contribution) {
FragmentManager fragmentManager = getFragmentManager();
WikipediaInstructionsDialogFragment fragment = WikipediaInstructionsDialogFragment
.newInstance(contribution);
fragment.setCallback(this::onConfirmClicked);
fragment.show(fragmentManager, "WikimediaFragment");
}
public Media getMediaAtPosition(final int i) {
if (adapter.getContributionForPosition(i) != null) {
return adapter.getContributionForPosition(i).getMedia();
}
return null;
}
public int getTotalMediaCount() {
return contributionsSize;
}
/**
* Open the editor for the language Wikipedia
*
* @param contribution
*/
@Override
public void onConfirmClicked(@Nullable Contribution contribution, boolean copyWikicode) {
if (copyWikicode) {
String wikicode = contribution.getMedia().getWikiCode();
Utils.copy("wikicode", wikicode, getContext());
}
final String url =
languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace()
.getWikipediaPageTitle();
Utils.handleWebUrl(getContext(), Uri.parse(url));
}
public Integer getContributionStateAt(int position) {
return adapter.getContributionForPosition(position).getState();
}
public interface Callback {
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

@ -15,57 +15,58 @@ import javax.inject.Named;
*/ */
public class ContributionsListPresenter implements UserActionListener { public class ContributionsListPresenter implements UserActionListener {
private final ContributionBoundaryCallback contributionBoundaryCallback; private final ContributionBoundaryCallback contributionBoundaryCallback;
private final ContributionsRepository repository; private final ContributionsRepository repository;
private final Scheduler ioThreadScheduler; private final Scheduler ioThreadScheduler;
private final CompositeDisposable compositeDisposable; private final CompositeDisposable compositeDisposable;
LiveData<PagedList<Contribution>> contributionList; LiveData<PagedList<Contribution>> contributionList;
@Inject @Inject
ContributionsListPresenter( ContributionsListPresenter(
final ContributionBoundaryCallback contributionBoundaryCallback, final ContributionBoundaryCallback contributionBoundaryCallback,
final ContributionsRepository repository, final ContributionsRepository repository,
@Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) { @Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) {
this.contributionBoundaryCallback = contributionBoundaryCallback; this.contributionBoundaryCallback = contributionBoundaryCallback;
this.repository = repository; this.repository = repository;
this.ioThreadScheduler = ioThreadScheduler; this.ioThreadScheduler = ioThreadScheduler;
compositeDisposable = new CompositeDisposable(); compositeDisposable = new CompositeDisposable();
} }
@Override @Override
public void onAttachView(final ContributionsListContract.View view) { public void onAttachView(final ContributionsListContract.View view) {
} }
/** /**
* Setup the paged list. This method sets the configuration for paged list and ties it up with the * Setup the paged list. This method sets the configuration for paged list and ties it up with
* live data object. This method can be tweaked to update the lazy loading behavior of the * the live data object. This method can be tweaked to update the lazy loading behavior of the
* contributions list * contributions list
*/ */
void setup() { void setup() {
final PagedList.Config pagedListConfig = final PagedList.Config pagedListConfig =
(new PagedList.Config.Builder()) (new PagedList.Config.Builder())
.setPrefetchDistance(50) .setPrefetchDistance(50)
.setPageSize(10).build(); .setPageSize(10).build();
contributionList = (new LivePagedListBuilder(repository.fetchContributions(), pagedListConfig) contributionList = (new LivePagedListBuilder(repository.fetchContributions(),
.setBoundaryCallback(contributionBoundaryCallback)).build(); pagedListConfig)
} .setBoundaryCallback(contributionBoundaryCallback)).build();
}
@Override @Override
public void onDetachView() { public void onDetachView() {
compositeDisposable.clear(); compositeDisposable.clear();
} }
/** /**
* Delete a failed contribution from the local db * Delete a failed contribution from the local db
*/ */
@Override @Override
public void deleteUpload(final Contribution contribution) { public void deleteUpload(final Contribution contribution) {
compositeDisposable.add(repository compositeDisposable.add(repository
.deleteContributionFromDB(contribution) .deleteContributionFromDB(contribution)
.subscribeOn(ioThreadScheduler) .subscribeOn(ioThreadScheduler)
.subscribe()); .subscribe());
} }
} }

View file

@ -12,9 +12,9 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
* The database for accessing the respective DAOs * The database for accessing the respective DAOs
* *
*/ */
@Database(entities = [Contribution::class,Depicts::class], version = 8, exportSchema = false) @Database(entities = [Contribution::class, Depicts::class], version = 8, exportSchema = false)
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun contributionDao(): ContributionDao abstract fun contributionDao(): ContributionDao
abstract fun DepictsDao (): DepictsDao; abstract fun DepictsDao(): DepictsDao;
} }

View file

@ -23,181 +23,183 @@ import fr.free.nrw.commons.navtab.NavTab;
public class ExploreListRootFragment extends CommonsDaggerSupportFragment implements public class ExploreListRootFragment extends CommonsDaggerSupportFragment implements
MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback { MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
private MediaDetailPagerFragment mediaDetails; private MediaDetailPagerFragment mediaDetails;
private CategoriesMediaFragment listFragment; private CategoriesMediaFragment listFragment;
@BindView(R.id.explore_container) @BindView(R.id.explore_container)
FrameLayout container; FrameLayout container;
public ExploreListRootFragment(){ public ExploreListRootFragment() {
//empty constructor necessary otherwise crashes on recreate //empty constructor necessary otherwise crashes on recreate
}
public ExploreListRootFragment(Bundle bundle) {
String title = bundle.getString("categoryName");
listFragment = new CategoriesMediaFragment();
Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", title);
listFragment.setArguments(featuredArguments);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View view = inflater.inflate(R.layout.fragment_featured_root, container, false);
ButterKnife.bind(this, view);
return view;
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if(savedInstanceState == null) {
setFragment(listFragment, mediaDetails);
} }
}
public void setFragment(Fragment fragment, Fragment otherFragment) { public ExploreListRootFragment(Bundle bundle) {
if (fragment.isAdded() && otherFragment != null) { String title = bundle.getString("categoryName");
getChildFragmentManager() listFragment = new CategoriesMediaFragment();
.beginTransaction() Bundle featuredArguments = new Bundle();
.hide(otherFragment) featuredArguments.putString("categoryName", title);
.show( fragment) listFragment.setArguments(featuredArguments);
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (fragment.isAdded() && otherFragment == null) {
getChildFragmentManager()
.beginTransaction()
.show( fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded() && otherFragment != null ) {
getChildFragmentManager()
.beginTransaction()
.hide(otherFragment)
.add(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded()) {
getChildFragmentManager()
.beginTransaction()
.replace(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} }
}
public void removeFragment(Fragment fragment) { @Nullable
getChildFragmentManager() @Override
.beginTransaction() public View onCreateView(@NonNull final LayoutInflater inflater,
.remove(fragment) @Nullable final ViewGroup container,
.commit(); @Nullable final Bundle savedInstanceState) {
getChildFragmentManager().executePendingTransactions(); super.onCreate(savedInstanceState);
} View view = inflater.inflate(R.layout.fragment_featured_root, container, false);
ButterKnife.bind(this, view);
@Override return view;
public void onAttach(final Context context) {
super.onAttach(context);
}
@Override
public void onMediaClicked(int position) {
container.setVisibility(View.VISIBLE);
((ExploreFragment)getParentFragment()).tabLayout.setVisibility(View.GONE);
mediaDetails = new MediaDetailPagerFragment(false, true);
setFragment(mediaDetails, listFragment);
mediaDetails.showImage(position);
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
*
* @param i It is the index of which media object is to be returned which is same as current
* index of viewPager.
* @return Media Object
*/
@Override
public Media getMediaAtPosition(int i) {
if (listFragment != null) {
return listFragment.getMediaAtPosition(i);
} else {
return null;
} }
}
/** @Override
* This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
* same number of media items as that of media elements in adapter. super.onViewCreated(view, savedInstanceState);
* if (savedInstanceState == null) {
* @return Total Media count in the adapter setFragment(listFragment, mediaDetails);
*/ }
@Override
public int getTotalMediaCount() {
if (listFragment!=null) {
return listFragment.getTotalMediaCount();
} else {
return 0;
} }
}
@Override public void setFragment(Fragment fragment, Fragment otherFragment) {
public Integer getContributionStateAt(int position) { if (fragment.isAdded() && otherFragment != null) {
return null; getChildFragmentManager()
} .beginTransaction()
.hide(otherFragment)
/** .show(fragment)
* Reload media detail fragment once media is nominated .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
* .commit();
* @param index item position that has been nominated getChildFragmentManager().executePendingTransactions();
*/ } else if (fragment.isAdded() && otherFragment == null) {
@Override getChildFragmentManager()
public void refreshNominatedMedia(int index) { .beginTransaction()
if(mediaDetails != null && !listFragment.isVisible()) { .show(fragment)
removeFragment(mediaDetails); .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
onMediaClicked(index); .commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded() && otherFragment != null) {
getChildFragmentManager()
.beginTransaction()
.hide(otherFragment)
.add(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded()) {
getChildFragmentManager()
.beginTransaction()
.replace(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
}
} }
}
/** public void removeFragment(Fragment fragment) {
* This method is called on success of API call for featured images or mobile uploads. The getChildFragmentManager()
* viewpager will notified that number of items have changed. .beginTransaction()
*/ .remove(fragment)
@Override .commit();
public void viewPagerNotifyDataSetChanged() { getChildFragmentManager().executePendingTransactions();
if (mediaDetails != null) {
mediaDetails.notifyDataSetChanged();
} }
}
/** @Override
* Performs back pressed action on the fragment. public void onAttach(final Context context) {
* Return true if the event was handled by the mediaDetails otherwise returns false. super.onAttach(context);
* @return }
*/
public boolean backPressed() { @Override
if (null != mediaDetails && mediaDetails.isVisible()) { public void onMediaClicked(int position) {
// todo add get list fragment container.setVisibility(View.VISIBLE);
if (mediaDetails.backButtonClicked()) { ((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.GONE);
// MediaDetails handled the event no further action required. mediaDetails = new MediaDetailPagerFragment(false, true);
return true; setFragment(mediaDetails, listFragment);
} else { mediaDetails.showImage(position);
((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.VISIBLE); }
removeFragment(mediaDetails);
((ExploreFragment) getParentFragment()).setScroll(true); /**
setFragment(listFragment, mediaDetails); * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
*
* @param i It is the index of which media object is to be returned which is same as current
* index of viewPager.
* @return Media Object
*/
@Override
public Media getMediaAtPosition(int i) {
if (listFragment != null) {
return listFragment.getMediaAtPosition(i);
} else {
return null;
}
}
/**
* This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain
* same number of media items as that of media elements in adapter.
*
* @return Total Media count in the adapter
*/
@Override
public int getTotalMediaCount() {
if (listFragment != null) {
return listFragment.getTotalMediaCount();
} else {
return 0;
}
}
@Override
public Integer getContributionStateAt(int position) {
return null;
}
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
@Override
public void refreshNominatedMedia(int index) {
if (mediaDetails != null && !listFragment.isVisible()) {
removeFragment(mediaDetails);
onMediaClicked(index);
}
}
/**
* This method is called on success of API call for featured images or mobile uploads. The
* viewpager will notified that number of items have changed.
*/
@Override
public void viewPagerNotifyDataSetChanged() {
if (mediaDetails != null) {
mediaDetails.notifyDataSetChanged();
}
}
/**
* Performs back pressed action on the fragment. Return true if the event was handled by the
* mediaDetails otherwise returns false.
*
* @return
*/
public boolean backPressed() {
if (null != mediaDetails && mediaDetails.isVisible()) {
// todo add get list fragment
if (mediaDetails.backButtonClicked()) {
// MediaDetails handled the event no further action required.
return true;
} else {
((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.VISIBLE);
removeFragment(mediaDetails);
((ExploreFragment) getParentFragment()).setScroll(true);
setFragment(listFragment, mediaDetails);
((MainActivity) getActivity()).showTabs();
return true;
}
} else {
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
}
((MainActivity) getActivity()).showTabs(); ((MainActivity) getActivity()).showTabs();
return true; return false;
}
} else {
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
} }
((MainActivity) getActivity()).showTabs();
return false;
}
} }

View file

@ -6,64 +6,61 @@ import android.view.MotionEvent;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
/** /**
* ParentViewPager * ParentViewPager A custom viewPager whose scrolling can be enabled and disabled.
* A custom viewPager whose scrolling can be enabled and disabled. */
*/
public class ParentViewPager extends ViewPager { public class ParentViewPager extends ViewPager {
/** /**
* Boolean variable that stores the current state of pager scroll i.e(enabled or disabled) * Boolean variable that stores the current state of pager scroll i.e(enabled or disabled)
*/ */
private boolean canScroll = true; private boolean canScroll = true;
/** /**
* Default constructors * Default constructors
*/ */
public ParentViewPager(Context context) { public ParentViewPager(Context context) {
super(context); super(context);
} }
public ParentViewPager(Context context, AttributeSet attrs) { public ParentViewPager(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
} }
/** /**
* Setter method for canScroll. * Setter method for canScroll.
*/ */
public void setCanScroll(boolean canScroll) { public void setCanScroll(boolean canScroll) {
this.canScroll = canScroll; this.canScroll = canScroll;
} }
/** /**
* Getter method for canScroll. * Getter method for canScroll.
*/ */
public boolean isCanScroll() { public boolean isCanScroll() {
return canScroll; return canScroll;
} }
/** /**
* Method that prevents scrolling if canScroll is set to false. * Method that prevents scrolling if canScroll is set to false.
*/ */
@Override @Override
public boolean onTouchEvent(MotionEvent ev) { public boolean onTouchEvent(MotionEvent ev) {
return canScroll && super.onTouchEvent(ev); return canScroll && super.onTouchEvent(ev);
} }
/**
* A facilitator method that allows parent to intercept touch events before its children.
* thus making it possible to prevent swiping parent on child end.
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return canScroll && super.onInterceptTouchEvent(ev);
}
/**
* A facilitator method that allows parent to intercept touch events before its children. thus
* making it possible to prevent swiping parent on child end.
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return canScroll && super.onInterceptTouchEvent(ev);
}
} }

View file

@ -27,8 +27,11 @@ import fr.free.nrw.commons.explore.SearchActivity;
* Displays the recent searches screen. * Displays the recent searches screen.
*/ */
public class RecentSearchesFragment extends CommonsDaggerSupportFragment { public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
@Inject RecentSearchesDao recentSearchesDao;
@BindView(R.id.recent_searches_list) ListView recentSearchesList; @Inject
RecentSearchesDao recentSearchesDao;
@BindView(R.id.recent_searches_list)
ListView recentSearchesList;
List<String> recentSearches; List<String> recentSearches;
ArrayAdapter adapter; ArrayAdapter adapter;
@BindView(R.id.recent_searches_delete_button) @BindView(R.id.recent_searches_delete_button)
@ -38,16 +41,16 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_search_history, container, false); View rootView = inflater.inflate(R.layout.fragment_search_history, container, false);
ButterKnife.bind(this, rootView); ButterKnife.bind(this, rootView);
recentSearches = recentSearchesDao.recentSearches(10); recentSearches = recentSearchesDao.recentSearches(10);
if(recentSearches.isEmpty()) { if (recentSearches.isEmpty()) {
recent_searches_delete_button.setVisibility(View.GONE); recent_searches_delete_button.setVisibility(View.GONE);
recent_searches_text_view.setText(R.string.no_recent_searches); recent_searches_text_view.setText(R.string.no_recent_searches);
} }
recent_searches_delete_button.setOnClickListener(v -> { recent_searches_delete_button.setOnClickListener(v -> {
new AlertDialog.Builder(getContext()) new AlertDialog.Builder(getContext())
.setMessage(getString(R.string.delete_recent_searches_dialog)) .setMessage(getString(R.string.delete_recent_searches_dialog))
@ -55,9 +58,11 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
recentSearchesDao.deleteAll(); recentSearchesDao.deleteAll();
recent_searches_delete_button.setVisibility(View.GONE); recent_searches_delete_button.setVisibility(View.GONE);
recent_searches_text_view.setText(R.string.no_recent_searches); recent_searches_text_view.setText(R.string.no_recent_searches);
Toast.makeText(getContext(),getString(R.string.search_history_deleted),Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), getString(R.string.search_history_deleted),
Toast.LENGTH_SHORT).show();
recentSearches = recentSearchesDao.recentSearches(10); recentSearches = recentSearchesDao.recentSearches(10);
adapter = new ArrayAdapter<>(getContext(), R.layout.item_recent_searches, recentSearches); adapter = new ArrayAdapter<>(getContext(), R.layout.item_recent_searches,
recentSearches);
recentSearchesList.setAdapter(adapter); recentSearchesList.setAdapter(adapter);
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
dialog.dismiss(); dialog.dismiss();
@ -67,24 +72,26 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
.show(); .show();
}); });
adapter = new ArrayAdapter<>(requireContext(), R.layout.item_recent_searches, recentSearches); adapter = new ArrayAdapter<>(requireContext(), R.layout.item_recent_searches,
recentSearches);
recentSearchesList.setAdapter(adapter); recentSearchesList.setAdapter(adapter);
recentSearchesList.setOnItemClickListener((parent, view, position, id) -> ( recentSearchesList.setOnItemClickListener((parent, view, position, id) -> (
(SearchActivity)getContext()).updateText(recentSearches.get(position))); (SearchActivity) getContext()).updateText(recentSearches.get(position)));
recentSearchesList.setOnItemLongClickListener((parent, view, position, id) -> { recentSearchesList.setOnItemLongClickListener((parent, view, position, id) -> {
new AlertDialog.Builder(getContext()) new AlertDialog.Builder(getContext())
.setMessage(R.string.delete_search_dialog) .setMessage(R.string.delete_search_dialog)
.setPositiveButton(getString(R.string.delete).toUpperCase(),((dialog, which) -> { .setPositiveButton(getString(R.string.delete).toUpperCase(), ((dialog, which) -> {
recentSearchesDao.delete(recentSearchesDao.find(recentSearches.get(position))); recentSearchesDao.delete(recentSearchesDao.find(recentSearches.get(position)));
recentSearches = recentSearchesDao.recentSearches(10); recentSearches = recentSearchesDao.recentSearches(10);
adapter = new ArrayAdapter<>(getContext(), R.layout.item_recent_searches, recentSearches); adapter = new ArrayAdapter<>(getContext(), R.layout.item_recent_searches,
recentSearchesList.setAdapter(adapter); recentSearches);
adapter.notifyDataSetChanged(); recentSearchesList.setAdapter(adapter);
dialog.dismiss(); adapter.notifyDataSetChanged();
})) dialog.dismiss();
.setNegativeButton(android.R.string.cancel,null) }))
.create() .setNegativeButton(android.R.string.cancel, null)
.show(); .create()
.show();
return true; return true;
}); });
updateRecentSearches(); updateRecentSearches();
@ -92,8 +99,8 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
} }
/** /**
* This method is called on back press of activity * This method is called on back press of activity so we are updating the list from database to
* so we are updating the list from database to refresh the recent searches list. * refresh the recent searches list.
*/ */
@Override @Override
public void onResume() { public void onResume() {
@ -108,7 +115,7 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
recentSearches = recentSearchesDao.recentSearches(10); recentSearches = recentSearchesDao.recentSearches(10);
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
if(!recentSearches.isEmpty()) { if (!recentSearches.isEmpty()) {
recent_searches_delete_button.setVisibility(View.VISIBLE); recent_searches_delete_button.setVisibility(View.VISIBLE);
recent_searches_text_view.setText(R.string.search_recent_header); recent_searches_text_view.setText(R.string.search_recent_header);
} }

View file

@ -35,195 +35,199 @@ import timber.log.Timber;
public class CustomOkHttpNetworkFetcher public class CustomOkHttpNetworkFetcher
extends BaseNetworkFetcher<CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState> { extends BaseNetworkFetcher<CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState> {
private static final String QUEUE_TIME = "queue_time"; private static final String QUEUE_TIME = "queue_time";
private static final String FETCH_TIME = "fetch_time"; private static final String FETCH_TIME = "fetch_time";
private static final String TOTAL_TIME = "total_time"; private static final String TOTAL_TIME = "total_time";
private static final String IMAGE_SIZE = "image_size"; private static final String IMAGE_SIZE = "image_size";
private final Call.Factory mCallFactory; private final Call.Factory mCallFactory;
private final @Nullable private final @Nullable
CacheControl mCacheControl; CacheControl mCacheControl;
private Executor mCancellationExecutor; private Executor mCancellationExecutor;
private JsonKvStore defaultKvStore; private JsonKvStore defaultKvStore;
/** /**
* @param okHttpClient client to use * @param okHttpClient client to use
*/ */
@Inject @Inject
public CustomOkHttpNetworkFetcher(OkHttpClient okHttpClient, public CustomOkHttpNetworkFetcher(OkHttpClient okHttpClient,
@Named("default_preferences") JsonKvStore defaultKvStore) { @Named("default_preferences") JsonKvStore defaultKvStore) {
this(okHttpClient, okHttpClient.dispatcher().executorService(), defaultKvStore); this(okHttpClient, okHttpClient.dispatcher().executorService(), defaultKvStore);
}
/**
* @param callFactory custom {@link Call.Factory} for fetching image from the network
* @param cancellationExecutor executor on which fetching cancellation is performed if
* cancellation is requested from the UI Thread
*/
public CustomOkHttpNetworkFetcher(Call.Factory callFactory, Executor cancellationExecutor,
JsonKvStore defaultKvStore) {
this(callFactory, cancellationExecutor, defaultKvStore, true);
}
/**
* @param callFactory custom {@link Call.Factory} for fetching image from the network
* @param cancellationExecutor executor on which fetching cancellation is performed if
* cancellation is requested from the UI Thread
* @param disableOkHttpCache true if network requests should not be cached by OkHttp
*/
public CustomOkHttpNetworkFetcher(
Call.Factory callFactory, Executor cancellationExecutor, JsonKvStore defaultKvStore,
boolean disableOkHttpCache) {
this.defaultKvStore = defaultKvStore;
mCallFactory = callFactory;
mCancellationExecutor = cancellationExecutor;
mCacheControl = disableOkHttpCache ? new CacheControl.Builder().noStore().build() : null;
}
@Override
public OkHttpNetworkFetchState createFetchState(
Consumer<EncodedImage> consumer, ProducerContext context) {
return new OkHttpNetworkFetchState(consumer, context);
}
@Override
public void fetch(
final OkHttpNetworkFetchState fetchState, final NetworkFetcher.Callback callback) {
fetchState.submitTime = SystemClock.elapsedRealtime();
final Uri uri = fetchState.getUri();
try {
if (defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
Timber.d("Skipping loading of image as limited connection mode is enabled");
callback.onFailure(
new Exception("Failing image request as limited connection mode is enabled"));
return;
}
final Request.Builder requestBuilder = new Request.Builder().url(uri.toString()).get();
if (mCacheControl != null) {
requestBuilder.cacheControl(mCacheControl);
}
final BytesRange bytesRange = fetchState.getContext().getImageRequest().getBytesRange();
if (bytesRange != null) {
requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue());
}
fetchWithRequest(fetchState, callback, requestBuilder.build());
} catch (Exception e) {
// handle error while creating the request
callback.onFailure(e);
} }
}
@Override /**
public void onFetchCompletion(OkHttpNetworkFetchState fetchState, int byteSize) { * @param callFactory custom {@link Call.Factory} for fetching image from the network
fetchState.fetchCompleteTime = SystemClock.elapsedRealtime(); * @param cancellationExecutor executor on which fetching cancellation is performed if
} * cancellation is requested from the UI Thread
*/
public CustomOkHttpNetworkFetcher(Call.Factory callFactory, Executor cancellationExecutor,
JsonKvStore defaultKvStore) {
this(callFactory, cancellationExecutor, defaultKvStore, true);
}
@Override /**
public Map<String, String> getExtraMap(OkHttpNetworkFetchState fetchState, int byteSize) { * @param callFactory custom {@link Call.Factory} for fetching image from the network
Map<String, String> extraMap = new HashMap<>(4); * @param cancellationExecutor executor on which fetching cancellation is performed if
extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime)); * cancellation is requested from the UI Thread
extraMap.put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime)); * @param disableOkHttpCache true if network requests should not be cached by OkHttp
extraMap.put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime)); */
extraMap.put(IMAGE_SIZE, Integer.toString(byteSize)); public CustomOkHttpNetworkFetcher(
return extraMap; Call.Factory callFactory, Executor cancellationExecutor, JsonKvStore defaultKvStore,
} boolean disableOkHttpCache) {
this.defaultKvStore = defaultKvStore;
mCallFactory = callFactory;
mCancellationExecutor = cancellationExecutor;
mCacheControl = disableOkHttpCache ? new CacheControl.Builder().noStore().build() : null;
}
protected void fetchWithRequest( @Override
final OkHttpNetworkFetchState fetchState, public OkHttpNetworkFetchState createFetchState(
final NetworkFetcher.Callback callback, Consumer<EncodedImage> consumer, ProducerContext context) {
final Request request) { return new OkHttpNetworkFetchState(consumer, context);
final Call call = mCallFactory.newCall(request); }
fetchState @Override
.getContext() public void fetch(
.addCallbacks( final OkHttpNetworkFetchState fetchState, final NetworkFetcher.Callback callback) {
new BaseProducerContextCallbacks() { fetchState.submitTime = SystemClock.elapsedRealtime();
@Override final Uri uri = fetchState.getUri();
public void onCancellationRequested() {
if (Looper.myLooper() != Looper.getMainLooper()) {
call.cancel();
} else {
mCancellationExecutor.execute(
new Runnable() {
@Override
public void run() {
call.cancel();
}
});
}
}
});
call.enqueue( try {
new okhttp3.Callback() { if (defaultKvStore
@Override .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
public void onResponse(Call call, Response response) throws IOException { Timber.d("Skipping loading of image as limited connection mode is enabled");
fetchState.responseTime = SystemClock.elapsedRealtime(); callback.onFailure(
final ResponseBody body = response.body(); new Exception("Failing image request as limited connection mode is enabled"));
try {
if (!response.isSuccessful()) {
handleException(
call, new IOException("Unexpected HTTP code " + response), callback);
return; return;
}
BytesRange responseRange =
BytesRange.fromContentRangeHeader(response.header("Content-Range"));
if (responseRange != null
&& !(responseRange.from == 0
&& responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
// Only treat as a partial image if the range is not all of the content
fetchState.setResponseBytesRange(responseRange);
fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT);
}
long contentLength = body.contentLength();
if (contentLength < 0) {
contentLength = 0;
}
callback.onResponse(body.byteStream(), (int) contentLength);
} catch (Exception e) {
handleException(call, e, callback);
} finally {
body.close();
} }
} final Request.Builder requestBuilder = new Request.Builder().url(uri.toString()).get();
@Override if (mCacheControl != null) {
public void onFailure(Call call, IOException e) { requestBuilder.cacheControl(mCacheControl);
handleException(call, e, callback); }
}
});
}
/** final BytesRange bytesRange = fetchState.getContext().getImageRequest().getBytesRange();
* Handles exceptions. if (bytesRange != null) {
* requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue());
* <p>OkHttp notifies callers of cancellations via an IOException. If IOException is caught after }
* request cancellation, then the exception is interpreted as successful cancellation and
* onCancellation is called. Otherwise onFailure is called. fetchWithRequest(fetchState, callback, requestBuilder.build());
*/ } catch (Exception e) {
private void handleException(final Call call, final Exception e, final Callback callback) { // handle error while creating the request
if (call.isCanceled()) { callback.onFailure(e);
callback.onCancellation(); }
} else {
callback.onFailure(e);
} }
}
public static class OkHttpNetworkFetchState extends FetchState { @Override
public void onFetchCompletion(OkHttpNetworkFetchState fetchState, int byteSize) {
public long submitTime; fetchState.fetchCompleteTime = SystemClock.elapsedRealtime();
public long responseTime; }
public long fetchCompleteTime;
@Override
public OkHttpNetworkFetchState( public Map<String, String> getExtraMap(OkHttpNetworkFetchState fetchState, int byteSize) {
Consumer<EncodedImage> consumer, ProducerContext producerContext) { Map<String, String> extraMap = new HashMap<>(4);
super(consumer, producerContext); extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime));
extraMap
.put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime));
extraMap
.put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime));
extraMap.put(IMAGE_SIZE, Integer.toString(byteSize));
return extraMap;
}
protected void fetchWithRequest(
final OkHttpNetworkFetchState fetchState,
final NetworkFetcher.Callback callback,
final Request request) {
final Call call = mCallFactory.newCall(request);
fetchState
.getContext()
.addCallbacks(
new BaseProducerContextCallbacks() {
@Override
public void onCancellationRequested() {
if (Looper.myLooper() != Looper.getMainLooper()) {
call.cancel();
} else {
mCancellationExecutor.execute(
new Runnable() {
@Override
public void run() {
call.cancel();
}
});
}
}
});
call.enqueue(
new okhttp3.Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
fetchState.responseTime = SystemClock.elapsedRealtime();
final ResponseBody body = response.body();
try {
if (!response.isSuccessful()) {
handleException(
call, new IOException("Unexpected HTTP code " + response),
callback);
return;
}
BytesRange responseRange =
BytesRange.fromContentRangeHeader(response.header("Content-Range"));
if (responseRange != null
&& !(responseRange.from == 0
&& responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
// Only treat as a partial image if the range is not all of the content
fetchState.setResponseBytesRange(responseRange);
fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT);
}
long contentLength = body.contentLength();
if (contentLength < 0) {
contentLength = 0;
}
callback.onResponse(body.byteStream(), (int) contentLength);
} catch (Exception e) {
handleException(call, e, callback);
} finally {
body.close();
}
}
@Override
public void onFailure(Call call, IOException e) {
handleException(call, e, callback);
}
});
}
/**
* Handles exceptions.
*
* <p>OkHttp notifies callers of cancellations via an IOException. If IOException is caught
* after
* request cancellation, then the exception is interpreted as successful cancellation and
* onCancellation is called. Otherwise onFailure is called.
*/
private void handleException(final Call call, final Exception e, final Callback callback) {
if (call.isCanceled()) {
callback.onCancellation();
} else {
callback.onFailure(e);
}
}
public static class OkHttpNetworkFetchState extends FetchState {
public long submitTime;
public long responseTime;
public long fetchCompleteTime;
public OkHttpNetworkFetchState(
Consumer<EncodedImage> consumer, ProducerContext producerContext) {
super(consumer, producerContext);
}
} }
}
} }

View file

@ -42,319 +42,325 @@ import timber.log.Timber;
@Singleton @Singleton
public class OkHttpJsonApiClient { public class OkHttpJsonApiClient {
private final OkHttpClient okHttpClient; private final OkHttpClient okHttpClient;
private final DepictsClient depictsClient; private final DepictsClient depictsClient;
private final HttpUrl wikiMediaToolforgeUrl; private final HttpUrl wikiMediaToolforgeUrl;
private final HttpUrl wikiMediaTestToolforgeUrl; private final HttpUrl wikiMediaTestToolforgeUrl;
private final String sparqlQueryUrl; private final String sparqlQueryUrl;
private final String campaignsUrl; private final String campaignsUrl;
private final Gson gson; private final Gson gson;
@Inject @Inject
public OkHttpJsonApiClient(OkHttpClient okHttpClient, public OkHttpJsonApiClient(OkHttpClient okHttpClient,
DepictsClient depictsClient, DepictsClient depictsClient,
HttpUrl wikiMediaToolforgeUrl, HttpUrl wikiMediaToolforgeUrl,
HttpUrl wikiMediaTestToolforgeUrl, HttpUrl wikiMediaTestToolforgeUrl,
String sparqlQueryUrl, String sparqlQueryUrl,
String campaignsUrl, String campaignsUrl,
Gson gson) { Gson gson) {
this.okHttpClient = okHttpClient; this.okHttpClient = okHttpClient;
this.depictsClient = depictsClient; this.depictsClient = depictsClient;
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl; this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
this.wikiMediaTestToolforgeUrl = wikiMediaTestToolforgeUrl; this.wikiMediaTestToolforgeUrl = wikiMediaTestToolforgeUrl;
this.sparqlQueryUrl = sparqlQueryUrl; this.sparqlQueryUrl = sparqlQueryUrl;
this.campaignsUrl = campaignsUrl; this.campaignsUrl = campaignsUrl;
this.gson = gson; this.gson = gson;
}
/**
* The method will gradually calls the leaderboard API and fetches the leaderboard
* @param userName username of leaderboard user
* @param duration duration for leaderboard
* @param category category for leaderboard
* @param limit page size limit for list
* @param offset offset for the list
* @return LeaderboardResponse object
*/
@NonNull
public Observable<LeaderboardResponse> getLeaderboard(String userName, String duration, String category, String limit, String offset) {
final String fetchLeaderboardUrlTemplate = wikiMediaTestToolforgeUrl
+ LEADERBOARD_END_POINT;
String url = String.format(Locale.ENGLISH,
fetchLeaderboardUrlTemplate,
userName,
duration,
category,
limit,
offset);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
urlBuilder.addQueryParameter("duration", duration);
urlBuilder.addQueryParameter("category", category);
urlBuilder.addQueryParameter("limit", limit);
urlBuilder.addQueryParameter("offset", offset);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
return Observable.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return new LeaderboardResponse();
}
Timber.d("Response for leaderboard is %s", json);
try {
return gson.fromJson(json, LeaderboardResponse.class);
} catch (Exception e) {
return new LeaderboardResponse();
}
}
return new LeaderboardResponse();
});
}
/**
* This method will update the leaderboard user avatar
* @param username username to update
* @param avatar url of the new avatar
* @return UpdateAvatarResponse object
*/
@NonNull
public Single<UpdateAvatarResponse> setAvatar(String username, String avatar) {
final String urlTemplate = wikiMediaTestToolforgeUrl
+ UPDATE_AVATAR_END_POINT;
return Single.fromCallable(() -> {
String url = String.format(Locale.ENGLISH,
urlTemplate,
username,
avatar);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", username);
urlBuilder.addQueryParameter("avatar", avatar);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
try {
return gson.fromJson(json, UpdateAvatarResponse.class);
} catch (Exception e) {
return new UpdateAvatarResponse();
}
}
return null;
});
}
@NonNull
public Single<Integer> getUploadCount(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
urlBuilder
.addPathSegments("uploadsbyuser.py")
.addQueryParameter("user", userName);
if (ConfigUtils.isBetaFlavour()) {
urlBuilder.addQueryParameter("labs", "commonswiki");
} }
Request request = new Request.Builder() /**
.url(urlBuilder.build()) * The method will gradually calls the leaderboard API and fetches the leaderboard
.build(); *
* @param userName username of leaderboard user
return Single.fromCallable(() -> { * @param duration duration for leaderboard
Response response = okHttpClient.newCall(request).execute(); * @param category category for leaderboard
if (response != null && response.isSuccessful()) { * @param limit page size limit for list
ResponseBody responseBody = response.body(); * @param offset offset for the list
if (null != responseBody) { * @return LeaderboardResponse object
String responseBodyString = responseBody.string().trim(); */
if (!TextUtils.isEmpty(responseBodyString)) { @NonNull
try { public Observable<LeaderboardResponse> getLeaderboard(String userName, String duration,
return Integer.parseInt(responseBodyString); String category, String limit, String offset) {
} catch (NumberFormatException e) { final String fetchLeaderboardUrlTemplate = wikiMediaTestToolforgeUrl
Timber.e(e); + LEADERBOARD_END_POINT;
String url = String.format(Locale.ENGLISH,
fetchLeaderboardUrlTemplate,
userName,
duration,
category,
limit,
offset);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
urlBuilder.addQueryParameter("duration", duration);
urlBuilder.addQueryParameter("category", category);
urlBuilder.addQueryParameter("limit", limit);
urlBuilder.addQueryParameter("offset", offset);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
return Observable.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return new LeaderboardResponse();
}
Timber.d("Response for leaderboard is %s", json);
try {
return gson.fromJson(json, LeaderboardResponse.class);
} catch (Exception e) {
return new LeaderboardResponse();
}
} }
} return new LeaderboardResponse();
} });
}
return 0;
});
}
@NonNull
public Single<Integer> getWikidataEdits(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
urlBuilder
.addPathSegments("wikidataedits.py")
.addQueryParameter("user", userName);
if (ConfigUtils.isBetaFlavour()) {
urlBuilder.addQueryParameter("labs", "commonswiki");
} }
Request request = new Request.Builder() /**
.url(urlBuilder.build()) * This method will update the leaderboard user avatar
.build(); *
* @param username username to update
* @param avatar url of the new avatar
* @return UpdateAvatarResponse object
*/
@NonNull
public Single<UpdateAvatarResponse> setAvatar(String username, String avatar) {
final String urlTemplate = wikiMediaTestToolforgeUrl
+ UPDATE_AVATAR_END_POINT;
return Single.fromCallable(() -> {
String url = String.format(Locale.ENGLISH,
urlTemplate,
username,
avatar);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", username);
urlBuilder.addQueryParameter("avatar", avatar);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
try {
return gson.fromJson(json, UpdateAvatarResponse.class);
} catch (Exception e) {
return new UpdateAvatarResponse();
}
}
return null;
});
}
return Single.fromCallable(() -> { @NonNull
Response response = okHttpClient.newCall(request).execute(); public Single<Integer> getUploadCount(String userName) {
if (response != null && HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
response.isSuccessful() && response.body() != null) { urlBuilder
String json = response.body().string(); .addPathSegments("uploadsbyuser.py")
if (json == null) { .addQueryParameter("user", userName);
return 0;
}
GetWikidataEditCountResponse countResponse = gson
.fromJson(json, GetWikidataEditCountResponse.class);
if (null != countResponse) {
return countResponse.getWikidataEditCount();
}
}
return 0;
});
}
/** if (ConfigUtils.isBetaFlavour()) {
* This takes userName as input, which is then used to fetch the feedback/achievements statistics urlBuilder.addQueryParameter("labs", "commonswiki");
* using OkHttp and JavaRx. This function return JSONObject
*
* @param userName MediaWiki user name
* @return
*/
public Single<FeedbackResponse> getAchievements(String userName) {
final String fetchAchievementUrlTemplate =
wikiMediaToolforgeUrl + (ConfigUtils.isBetaFlavour() ? "/feedback.py?labs=commonswiki"
: "/feedback.py");
return Single.fromCallable(() -> {
String url = String.format(
Locale.ENGLISH,
fetchAchievementUrlTemplate,
userName);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
Timber.d("Response for achievements is %s", json);
try {
return gson.fromJson(json, FeedbackResponse.class);
} catch (Exception e) {
return new FeedbackResponse(0, 0, 0, new FeaturedImages(0, 0), 0, "");
} }
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
} return Single.fromCallable(() -> {
return null; Response response = okHttpClient.newCall(request).execute();
}); if (response != null && response.isSuccessful()) {
} ResponseBody responseBody = response.body();
if (null != responseBody) {
String responseBodyString = responseBody.string().trim();
if (!TextUtils.isEmpty(responseBodyString)) {
try {
return Integer.parseInt(responseBodyString);
} catch (NumberFormatException e) {
Timber.e(e);
}
}
}
}
return 0;
});
}
public Observable<List<Place>> getNearbyPlaces(LatLng cur, String language, double radius) throws IOException { @NonNull
public Single<Integer> getWikidataEdits(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
urlBuilder
.addPathSegments("wikidataedits.py")
.addQueryParameter("user", userName);
if (ConfigUtils.isBetaFlavour()) {
urlBuilder.addQueryParameter("labs", "commonswiki");
}
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return Single.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null &&
response.isSuccessful() && response.body() != null) {
String json = response.body().string();
if (json == null) {
return 0;
}
GetWikidataEditCountResponse countResponse = gson
.fromJson(json, GetWikidataEditCountResponse.class);
if (null != countResponse) {
return countResponse.getWikidataEditCount();
}
}
return 0;
});
}
/**
* This takes userName as input, which is then used to fetch the feedback/achievements
* statistics using OkHttp and JavaRx. This function return JSONObject
*
* @param userName MediaWiki user name
* @return
*/
public Single<FeedbackResponse> getAchievements(String userName) {
final String fetchAchievementUrlTemplate =
wikiMediaToolforgeUrl + (ConfigUtils.isBetaFlavour() ? "/feedback.py?labs=commonswiki"
: "/feedback.py");
return Single.fromCallable(() -> {
String url = String.format(
Locale.ENGLISH,
fetchAchievementUrlTemplate,
userName);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
Timber.d("Response for achievements is %s", json);
try {
return gson.fromJson(json, FeedbackResponse.class);
} catch (Exception e) {
return new FeedbackResponse(0, 0, 0, new FeaturedImages(0, 0), 0, "");
}
}
return null;
});
}
public Observable<List<Place>> getNearbyPlaces(LatLng cur, String language, double radius)
throws IOException {
String wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq"); String wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq");
String query = wikidataQuery String query = wikidataQuery
.replace("${RAD}", String.format(Locale.ROOT, "%.2f", radius)) .replace("${RAD}", String.format(Locale.ROOT, "%.2f", radius))
.replace("${LAT}", String.format(Locale.ROOT, "%.4f", cur.getLatitude())) .replace("${LAT}", String.format(Locale.ROOT, "%.4f", cur.getLatitude()))
.replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.getLongitude())) .replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.getLongitude()))
.replace("${LANG}", language); .replace("${LANG}", language);
HttpUrl.Builder urlBuilder = HttpUrl HttpUrl.Builder urlBuilder = HttpUrl
.parse(sparqlQueryUrl) .parse(sparqlQueryUrl)
.newBuilder() .newBuilder()
.addQueryParameter("query", query) .addQueryParameter("query", query)
.addQueryParameter("format", "json"); .addQueryParameter("format", "json");
Request request = new Request.Builder() Request request = new Request.Builder()
.url(urlBuilder.build()) .url(urlBuilder.build())
.build(); .build();
return Observable.fromCallable(() -> { return Observable.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute(); Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) { if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string(); String json = response.body().string();
if (json == null) { if (json == null) {
return new ArrayList<>(); return new ArrayList<>();
} }
NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class); NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class);
List<NearbyResultItem> bindings = nearbyResponse.getResults().getBindings(); List<NearbyResultItem> bindings = nearbyResponse.getResults().getBindings();
List<Place> places = new ArrayList<>(); List<Place> places = new ArrayList<>();
for (NearbyResultItem item : bindings) { for (NearbyResultItem item : bindings) {
places.add(Place.from(item)); places.add(Place.from(item));
} }
return places; return places;
} }
return new ArrayList<>(); return new ArrayList<>();
}); });
} }
/** /**
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example: * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
* bridge -> suspended bridge, aqueduct, etc * bridge -> suspended bridge, aqueduct, etc
*/ */
public Single<List<DepictedItem>> getChildDepictions(String qid, int startPosition, public Single<List<DepictedItem>> getChildDepictions(String qid, int startPosition,
int limit) throws IOException { int limit) throws IOException {
return depictedItemsFrom(sparqlQuery(qid, startPosition, limit,"/queries/subclasses_query.rq")); return depictedItemsFrom(
} sparqlQuery(qid, startPosition, limit, "/queries/subclasses_query.rq"));
}
/** /**
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example: * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
* bridge -> suspended bridge, aqueduct, etc * bridge -> suspended bridge, aqueduct, etc
*/ */
public Single<List<DepictedItem>> getParentDepictions(String qid, int startPosition, public Single<List<DepictedItem>> getParentDepictions(String qid, int startPosition,
int limit) throws IOException { int limit) throws IOException {
return depictedItemsFrom(sparqlQuery(qid, startPosition, limit, return depictedItemsFrom(sparqlQuery(qid, startPosition, limit,
"/queries/parentclasses_query.rq")); "/queries/parentclasses_query.rq"));
} }
private Single<List<DepictedItem>> depictedItemsFrom(Request request) { private Single<List<DepictedItem>> depictedItemsFrom(Request request) {
return depictsClient.toDepictions(Single.fromCallable(() -> { return depictsClient.toDepictions(Single.fromCallable(() -> {
try (ResponseBody body = okHttpClient.newCall(request).execute().body()) { try (ResponseBody body = okHttpClient.newCall(request).execute().body()) {
return gson.fromJson(body.string(), SparqlResponse.class); return gson.fromJson(body.string(), SparqlResponse.class);
} }
}).doOnError(Timber::e)); }).doOnError(Timber::e));
} }
@NotNull @NotNull
private Request sparqlQuery(String qid, int startPosition, int limit, String fileName) throws IOException { private Request sparqlQuery(String qid, int startPosition, int limit, String fileName)
String query = FileUtils.readFromResource(fileName) throws IOException {
.replace("${QID}", qid) String query = FileUtils.readFromResource(fileName)
.replace("${LANG}", "\"" + Locale.getDefault().getLanguage() + "\"") .replace("${QID}", qid)
.replace("${LIMIT}",""+ limit) .replace("${LANG}", "\"" + Locale.getDefault().getLanguage() + "\"")
.replace("${OFFSET}",""+ startPosition); .replace("${LIMIT}", "" + limit)
HttpUrl.Builder urlBuilder = HttpUrl .replace("${OFFSET}", "" + startPosition);
.parse(sparqlQueryUrl) HttpUrl.Builder urlBuilder = HttpUrl
.newBuilder() .parse(sparqlQueryUrl)
.addQueryParameter("query", query) .newBuilder()
.addQueryParameter("format", "json"); .addQueryParameter("query", query)
return new Request.Builder() .addQueryParameter("format", "json");
.url(urlBuilder.build()) return new Request.Builder()
.build(); .url(urlBuilder.build())
} .build();
}
public Single<CampaignResponseDTO> getCampaigns() { public Single<CampaignResponseDTO> getCampaigns() {
return Single.fromCallable(() -> { return Single.fromCallable(() -> {
Request request = new Request.Builder().url(campaignsUrl) Request request = new Request.Builder().url(campaignsUrl)
.build(); .build();
Response response = okHttpClient.newCall(request).execute(); Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) { if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string(); String json = response.body().string();
if (json == null) { if (json == null) {
return null; return null;
} }
return gson.fromJson(json, CampaignResponseDTO.class); return gson.fromJson(json, CampaignResponseDTO.class);
} }
return null; return null;
}); });
} }
} }

View file

@ -16,82 +16,81 @@ import org.wikipedia.model.EnumCodeMap;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
public enum NavTab implements EnumCode { public enum NavTab implements EnumCode {
CONTRIBUTIONS(R.string.contributions_fragment, R.drawable.ic_baseline_person_24) { CONTRIBUTIONS(R.string.contributions_fragment, R.drawable.ic_baseline_person_24) {
@NonNull
@Override
public Fragment newInstance() {
return ContributionsFragment.newInstance();
}
},
NEARBY(R.string.nearby_fragment, R.drawable.ic_location_on_black_24dp) {
@NonNull
@Override
public Fragment newInstance() {
return NearbyParentFragment.newInstance();
}
},
EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) {
@NonNull
@Override
public Fragment newInstance() {
return ExploreFragment.newInstance();
}
},
FAVORITES(R.string.favorites, R.drawable.ic_round_star_border_24px) {
@NonNull
@Override
public Fragment newInstance() {
return BookmarkFragment.newInstance();
}
},
MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
@NonNull
@Override
public Fragment newInstance() {
return null;
}
};
private static final EnumCodeMap<NavTab> MAP = new EnumCodeMap<>(NavTab.class);
@StringRes
private final int text;
@DrawableRes
private final int icon;
@NonNull @NonNull
@Override public static NavTab of(int code) {
public Fragment newInstance() { return MAP.get(code);
return ContributionsFragment.newInstance();
} }
},
NEARBY(R.string.nearby_fragment, R.drawable.ic_location_on_black_24dp){ public static int size() {
return MAP.size();
}
@StringRes
public int text() {
return text;
}
@DrawableRes
public int icon() {
return icon;
}
@NonNull @NonNull
public abstract Fragment newInstance();
@Override @Override
public Fragment newInstance() { public int code() {
return NearbyParentFragment.newInstance(); // This enumeration is not marshalled so tying declaration order to presentation order is
// convenient and consistent.
return ordinal();
} }
},
EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) { NavTab(@StringRes int text, @DrawableRes int icon) {
@NonNull this.text = text;
@Override this.icon = icon;
public Fragment newInstance() {
return ExploreFragment.newInstance();
} }
},
FAVORITES(R.string.favorites, R.drawable.ic_round_star_border_24px) {
@NonNull
@Override
public Fragment newInstance() {
return BookmarkFragment.newInstance();
}
},
MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
@NonNull
@Override
public Fragment newInstance() {
return null;
}
};
private static final EnumCodeMap<NavTab> MAP = new EnumCodeMap<>(NavTab.class);
@StringRes
private final int text;
@DrawableRes
private final int icon;
@NonNull
public static NavTab of(int code) {
return MAP.get(code);
}
public static int size() {
return MAP.size();
}
@StringRes
public int text() {
return text;
}
@DrawableRes
public int icon() {
return icon;
}
@NonNull
public abstract Fragment newInstance();
@Override
public int code() {
// This enumeration is not marshalled so tying declaration order to presentation order is
// convenient and consistent.
return ordinal();
}
NavTab(@StringRes int text, @DrawableRes int icon) {
this.text = text;
this.icon = icon;
}
} }

View file

@ -8,28 +8,31 @@ import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter; import androidx.fragment.app.FragmentPagerAdapter;
public class NavTabFragmentPagerAdapter extends FragmentPagerAdapter { public class NavTabFragmentPagerAdapter extends FragmentPagerAdapter {
private Fragment currentFragment;
public NavTabFragmentPagerAdapter(FragmentManager mgr) { private Fragment currentFragment;
super(mgr);
}
@Nullable public NavTabFragmentPagerAdapter(FragmentManager mgr) {
public Fragment getCurrentFragment() { super(mgr);
return currentFragment; }
}
@Override public Fragment getItem(int pos) { @Nullable
return NavTab.of(pos).newInstance(); public Fragment getCurrentFragment() {
} return currentFragment;
}
@Override public int getCount() { @Override
return NavTab.size(); public Fragment getItem(int pos) {
} return NavTab.of(pos).newInstance();
}
@Override @Override
public void setPrimaryItem(ViewGroup container, int position, Object object) { public int getCount() {
currentFragment = ((Fragment) object); return NavTab.size();
super.setPrimaryItem(container, position, object); }
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
currentFragment = ((Fragment) object);
super.setPrimaryItem(container, position, object);
}
} }

View file

@ -10,32 +10,32 @@ import fr.free.nrw.commons.contributions.MainActivity;
public class NavTabLayout extends BottomNavigationView { public class NavTabLayout extends BottomNavigationView {
public NavTabLayout(Context context) { public NavTabLayout(Context context) {
super(context); super(context);
setTabViews(); setTabViews();
} }
public NavTabLayout(Context context, AttributeSet attrs) { public NavTabLayout(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
setTabViews(); setTabViews();
} }
public NavTabLayout(Context context, AttributeSet attrs, int defStyleAttr) { public NavTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
setTabViews(); setTabViews();
} }
private void setTabViews() { private void setTabViews() {
if (((MainActivity)getContext()).applicationKvStore.getBoolean("login_skipped") == true) { if (((MainActivity) getContext()).applicationKvStore.getBoolean("login_skipped") == true) {
for (int i = 0; i < NavTabLoggedOut.size(); i++) { for (int i = 0; i < NavTabLoggedOut.size(); i++) {
NavTabLoggedOut navTab = NavTabLoggedOut.of(i); NavTabLoggedOut navTab = NavTabLoggedOut.of(i);
getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()); getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon());
} }
} else { } else {
for (int i = 0; i < NavTab.size(); i++) { for (int i = 0; i < NavTab.size(); i++) {
NavTab navTab = NavTab.of(i); NavTab navTab = NavTab.of(i);
getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()); getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon());
} }
}
} }
}
} }

View file

@ -13,66 +13,67 @@ import org.wikipedia.model.EnumCodeMap;
public enum NavTabLoggedOut implements EnumCode { public enum NavTabLoggedOut implements EnumCode {
EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) { EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) {
@NonNull
@Override
public Fragment newInstance() {
return ExploreFragment.newInstance();
}
},
FAVORITES(R.string.favorites, R.drawable.ic_round_star_border_24px) {
@NonNull
@Override
public Fragment newInstance() {
return BookmarkFragment.newInstance();
}
},
MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
@NonNull
@Override
public Fragment newInstance() {
return null;
}
};
private static final EnumCodeMap<NavTabLoggedOut> MAP = new EnumCodeMap<>(
NavTabLoggedOut.class);
@StringRes
private final int text;
@DrawableRes
private final int icon;
@NonNull @NonNull
@Override public static NavTabLoggedOut of(int code) {
public Fragment newInstance() { return MAP.get(code);
return ExploreFragment.newInstance();
} }
},
FAVORITES(R.string.favorites, R.drawable.ic_round_star_border_24px) { public static int size() {
return MAP.size();
}
@StringRes
public int text() {
return text;
}
@DrawableRes
public int icon() {
return icon;
}
@NonNull @NonNull
public abstract Fragment newInstance();
@Override @Override
public Fragment newInstance() { public int code() {
return BookmarkFragment.newInstance(); // This enumeration is not marshalled so tying declaration order to presentation order is
// convenient and consistent.
return ordinal();
} }
},
MORE(R.string.more, R.drawable.ic_menu_black_24dp) { NavTabLoggedOut(@StringRes int text, @DrawableRes int icon) {
@NonNull this.text = text;
@Override this.icon = icon;
public Fragment newInstance() {
return null;
} }
};
private static final EnumCodeMap<NavTabLoggedOut> MAP = new EnumCodeMap<>(NavTabLoggedOut.class);
@StringRes
private final int text;
@DrawableRes
private final int icon;
@NonNull
public static NavTabLoggedOut of(int code) {
return MAP.get(code);
}
public static int size() {
return MAP.size();
}
@StringRes
public int text() {
return text;
}
@DrawableRes
public int icon() {
return icon;
}
@NonNull
public abstract Fragment newInstance();
@Override
public int code() {
// This enumeration is not marshalled so tying declaration order to presentation order is
// convenient and consistent.
return ordinal();
}
NavTabLoggedOut(@StringRes int text, @DrawableRes int icon) {
this.text = text;
this.icon = icon;
}
} }

View file

@ -20,65 +20,65 @@ import timber.log.Timber;
@Singleton @Singleton
public class FileUtilsWrapper { public class FileUtilsWrapper {
@Inject @Inject
public FileUtilsWrapper() { public FileUtilsWrapper() {
}
public String getFileExt(String fileName) {
return FileUtils.getFileExt(fileName);
}
public String getSHA1(InputStream is) {
return FileUtils.getSHA1(is);
}
public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
return FileUtils.getFileInputStream(filePath);
}
public String getGeolocationOfFile(String filePath) {
return FileUtils.getGeolocationOfFile(filePath);
}
/**
* Takes a file as input and returns an Observable of files with the specified chunk size
*/
public List<File> getFileChunks(Context context, File file, final int chunkSize)
throws IOException {
final byte[] buffer = new byte[chunkSize];
//try-with-resources to ensure closing stream
try (final FileInputStream fis = new FileInputStream(file);
final BufferedInputStream bis = new BufferedInputStream(fis)) {
final List<File> buffers = new ArrayList<>();
int size;
while ((size = bis.read(buffer)) > 0) {
buffers.add(writeToFile(context, Arrays.copyOf(buffer, size), file.getName(),
getFileExt(file.getName())));
}
return buffers;
} }
}
/** public String getFileExt(String fileName) {
* Create a temp file containing the passed byte data. return FileUtils.getFileExt(fileName);
*/ }
private File writeToFile(Context context, final byte[] data, final String fileName,
String fileExtension) public String getSHA1(InputStream is) {
throws IOException { return FileUtils.getSHA1(is);
final File file = File.createTempFile(fileName, fileExtension, context.getCacheDir()); }
try {
if (!file.exists()) { public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
file.createNewFile(); return FileUtils.getFileInputStream(filePath);
} }
final FileOutputStream fos = new FileOutputStream(file);
fos.write(data); public String getGeolocationOfFile(String filePath) {
fos.close(); return FileUtils.getGeolocationOfFile(filePath);
} catch (final Exception throwable) { }
Timber.e(throwable, "Failed to create file");
/**
* Takes a file as input and returns an Observable of files with the specified chunk size
*/
public List<File> getFileChunks(Context context, File file, final int chunkSize)
throws IOException {
final byte[] buffer = new byte[chunkSize];
//try-with-resources to ensure closing stream
try (final FileInputStream fis = new FileInputStream(file);
final BufferedInputStream bis = new BufferedInputStream(fis)) {
final List<File> buffers = new ArrayList<>();
int size;
while ((size = bis.read(buffer)) > 0) {
buffers.add(writeToFile(context, Arrays.copyOf(buffer, size), file.getName(),
getFileExt(file.getName())));
}
return buffers;
}
}
/**
* Create a temp file containing the passed byte data.
*/
private File writeToFile(Context context, final byte[] data, final String fileName,
String fileExtension)
throws IOException {
final File file = File.createTempFile(fileName, fileExtension, context.getCacheDir());
try {
if (!file.exists()) {
file.createNewFile();
}
final FileOutputStream fos = new FileOutputStream(file);
fos.write(data);
fos.close();
} catch (final Exception throwable) {
Timber.e(throwable, "Failed to create file");
}
return file;
} }
return file;
}
} }

View file

@ -22,6 +22,7 @@ import timber.log.Timber;
*/ */
@Singleton @Singleton
public class ImageProcessingService { public class ImageProcessingService {
private final FileUtilsWrapper fileUtilsWrapper; private final FileUtilsWrapper fileUtilsWrapper;
private final ImageUtilsWrapper imageUtilsWrapper; private final ImageUtilsWrapper imageUtilsWrapper;
private final ReadFBMD readFBMD; private final ReadFBMD readFBMD;
@ -30,9 +31,9 @@ public class ImageProcessingService {
@Inject @Inject
public ImageProcessingService(FileUtilsWrapper fileUtilsWrapper, public ImageProcessingService(FileUtilsWrapper fileUtilsWrapper,
ImageUtilsWrapper imageUtilsWrapper, ImageUtilsWrapper imageUtilsWrapper,
ReadFBMD readFBMD, EXIFReader EXIFReader, ReadFBMD readFBMD, EXIFReader EXIFReader,
MediaClient mediaClient, Context context) { MediaClient mediaClient, Context context) {
this.fileUtilsWrapper = fileUtilsWrapper; this.fileUtilsWrapper = fileUtilsWrapper;
this.imageUtilsWrapper = imageUtilsWrapper; this.imageUtilsWrapper = imageUtilsWrapper;
this.readFBMD = readFBMD; this.readFBMD = readFBMD;
@ -41,33 +42,34 @@ public class ImageProcessingService {
} }
/** /**
* Check image quality before upload - checks duplicate image - checks dark image - checks * Check image quality before upload - checks duplicate image - checks dark image - checks
* geolocation for image - check for valid title * geolocation for image - check for valid title
*/ */
Single<Integer> validateImage(UploadItem uploadItem) { Single<Integer> validateImage(UploadItem uploadItem) {
int currentImageQuality = uploadItem.getImageQuality(); int currentImageQuality = uploadItem.getImageQuality();
Timber.d("Current image quality is %d", currentImageQuality); Timber.d("Current image quality is %d", currentImageQuality);
if (currentImageQuality == ImageUtils.IMAGE_KEEP) { if (currentImageQuality == ImageUtils.IMAGE_KEEP) {
return Single.just(ImageUtils.IMAGE_OK); return Single.just(ImageUtils.IMAGE_OK);
}
Timber.d("Checking the validity of image");
String filePath = uploadItem.getMediaUri().getPath();
return Single.zip(
checkDuplicateImage(filePath),
checkImageGeoLocation(uploadItem.getPlace(), filePath),
checkDarkImage(filePath),
validateItemTitle(uploadItem),
checkFBMD(filePath),
checkEXIF(filePath),
(duplicateImage, wrongGeoLocation, darkImage, itemTitle, fbmd, exif) -> {
Timber.d("duplicate: %d, geo: %d, dark: %d, title: %d" + "fbmd:" + fbmd + "exif:" + exif,
duplicateImage, wrongGeoLocation, darkImage, itemTitle);
return duplicateImage | wrongGeoLocation | darkImage | itemTitle | fbmd | exif;
} }
); Timber.d("Checking the validity of image");
} String filePath = uploadItem.getMediaUri().getPath();
return Single.zip(
checkDuplicateImage(filePath),
checkImageGeoLocation(uploadItem.getPlace(), filePath),
checkDarkImage(filePath),
validateItemTitle(uploadItem),
checkFBMD(filePath),
checkEXIF(filePath),
(duplicateImage, wrongGeoLocation, darkImage, itemTitle, fbmd, exif) -> {
Timber.d("duplicate: %d, geo: %d, dark: %d, title: %d" + "fbmd:" + fbmd + "exif:"
+ exif,
duplicateImage, wrongGeoLocation, darkImage, itemTitle);
return duplicateImage | wrongGeoLocation | darkImage | itemTitle | fbmd | exif;
}
);
}
/** /**
* We want to discourage users from uploading images to Commons that were taken from Facebook. * We want to discourage users from uploading images to Commons that were taken from Facebook.
@ -79,10 +81,10 @@ public class ImageProcessingService {
} }
/** /**
* We try to minimize uploads from the Commons app that might be copyright violations. * We try to minimize uploads from the Commons app that might be copyright violations. If an
* If an image does not have any Exif metadata, then it was likely downloaded from the internet, * image does not have any Exif metadata, then it was likely downloaded from the internet, and
* and is probably not an original work by the user. We detect these kinds of images by looking * is probably not an original work by the user. We detect these kinds of images by looking for
* for the presence of some basic Exif metadata. * the presence of some basic Exif metadata.
*/ */
private Single<Integer> checkEXIF(String filepath) { private Single<Integer> checkEXIF(String filepath) {
return EXIFReader.processMetadata(filepath); return EXIFReader.processMetadata(filepath);
@ -90,9 +92,7 @@ public class ImageProcessingService {
/** /**
* Checks item caption * Checks item caption - empty caption - existing caption
* - empty caption
* - existing caption
* *
* @param uploadItem * @param uploadItem
* @return * @return
@ -105,11 +105,11 @@ public class ImageProcessingService {
} }
return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.getFileName()) return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.getFileName())
.map(doesFileExist -> { .map(doesFileExist -> {
Timber.d("Result for valid title is %s", doesFileExist); Timber.d("Result for valid title is %s", doesFileExist);
return doesFileExist ? FILE_NAME_EXISTS : IMAGE_OK; return doesFileExist ? FILE_NAME_EXISTS : IMAGE_OK;
}) })
.subscribeOn(Schedulers.io()); .subscribeOn(Schedulers.io());
} }
/** /**
@ -121,13 +121,13 @@ public class ImageProcessingService {
private Single<Integer> checkDuplicateImage(String filePath) { private Single<Integer> checkDuplicateImage(String filePath) {
Timber.d("Checking for duplicate image %s", filePath); Timber.d("Checking for duplicate image %s", filePath);
return Single.fromCallable(() -> fileUtilsWrapper.getFileInputStream(filePath)) return Single.fromCallable(() -> fileUtilsWrapper.getFileInputStream(filePath))
.map(fileUtilsWrapper::getSHA1) .map(fileUtilsWrapper::getSHA1)
.flatMap(mediaClient::checkFileExistsUsingSha) .flatMap(mediaClient::checkFileExistsUsingSha)
.map(b -> { .map(b -> {
Timber.d("Result for duplicate image %s", b); Timber.d("Result for duplicate image %s", b);
return b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK; return b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK;
}) })
.subscribeOn(Schedulers.io()); .subscribeOn(Schedulers.io());
} }
/** /**
@ -142,8 +142,8 @@ public class ImageProcessingService {
} }
/** /**
* Checks for image geolocation * Checks for image geolocation returns IMAGE_OK if the place is null or if the file doesn't
* returns IMAGE_OK if the place is null or if the file doesn't contain a geolocation * contain a geolocation
* *
* @param filePath file to be checked * @param filePath file to be checked
* @return IMAGE_GEOLOCATION_DIFFERENT or IMAGE_OK * @return IMAGE_GEOLOCATION_DIFFERENT or IMAGE_OK
@ -154,14 +154,15 @@ public class ImageProcessingService {
return Single.just(ImageUtils.IMAGE_OK); return Single.just(ImageUtils.IMAGE_OK);
} }
return Single.fromCallable(() -> filePath) return Single.fromCallable(() -> filePath)
.map(fileUtilsWrapper::getGeolocationOfFile) .map(fileUtilsWrapper::getGeolocationOfFile)
.flatMap(geoLocation -> { .flatMap(geoLocation -> {
if (StringUtils.isBlank(geoLocation)) { if (StringUtils.isBlank(geoLocation)) {
return Single.just(ImageUtils.IMAGE_OK); return Single.just(ImageUtils.IMAGE_OK);
} }
return imageUtilsWrapper.checkImageGeolocationIsDifferent(geoLocation, place.getLocation()); return imageUtilsWrapper
}) .checkImageGeolocationIsDifferent(geoLocation, place.getLocation());
.subscribeOn(Schedulers.io()); })
.subscribeOn(Schedulers.io());
} }
} }

View file

@ -16,96 +16,96 @@ import org.apache.commons.lang3.StringUtils;
class PageContentsCreator { class PageContentsCreator {
//{{According to Exif data|2009-01-09}} //{{According to Exif data|2009-01-09}}
private static final String TEMPLATE_DATE_ACC_TO_EXIF = "{{According to Exif data|%s}}"; private static final String TEMPLATE_DATE_ACC_TO_EXIF = "{{According to Exif data|%s}}";
//2009-01-09 9 January 2009 //2009-01-09 9 January 2009
private static final String TEMPLATE_DATA_OTHER_SOURCE = "%s"; private static final String TEMPLATE_DATA_OTHER_SOURCE = "%s";
private final Context context; private final Context context;
@Inject @Inject
public PageContentsCreator(Context context) { public PageContentsCreator(Context context) {
this.context = context; this.context = context;
}
public String createFrom(Contribution contribution) {
StringBuilder buffer = new StringBuilder();
final Media media = contribution.getMedia();
buffer
.append("== {{int:filedesc}} ==\n")
.append("{{Information\n")
.append("|description=").append(media.getFallbackDescription()).append("\n")
.append("|source=").append("{{own}}\n")
.append("|author=[[User:").append(media.getAuthor()).append("|")
.append(media.getAuthor()).append("]]\n");
String templatizedCreatedDate = getTemplatizedCreatedDate(
contribution.getDateCreated(), contribution.getDateCreatedSource());
if (!StringUtils.isBlank(templatizedCreatedDate)) {
buffer.append("|date=").append(templatizedCreatedDate);
} }
buffer.append("}}").append("\n"); public String createFrom(Contribution contribution) {
StringBuilder buffer = new StringBuilder();
final Media media = contribution.getMedia();
buffer
.append("== {{int:filedesc}} ==\n")
.append("{{Information\n")
.append("|description=").append(media.getFallbackDescription()).append("\n")
.append("|source=").append("{{own}}\n")
.append("|author=[[User:").append(media.getAuthor()).append("|")
.append(media.getAuthor()).append("]]\n");
//Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null String templatizedCreatedDate = getTemplatizedCreatedDate(
final String decimalCoords = contribution.getDecimalCoords(); contribution.getDateCreated(), contribution.getDateCreatedSource());
if (decimalCoords != null) { if (!StringUtils.isBlank(templatizedCreatedDate)) {
buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n"); buffer.append("|date=").append(templatizedCreatedDate);
}
buffer.append("}}").append("\n");
//Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null
final String decimalCoords = contribution.getDecimalCoords();
if (decimalCoords != null) {
buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n");
}
buffer.append("== {{int:license-header}} ==\n")
.append(licenseTemplateFor(media.getLicense())).append("\n\n")
.append("{{Uploaded from Mobile|platform=Android|version=")
.append(ConfigUtils.getVersionNameWithSha(context)).append("}}\n");
final List<String> categories = media.getCategories();
if (categories != null && categories.size() != 0) {
for (int i = 0; i < categories.size(); i++) {
buffer.append("\n[[Category:").append(categories.get(i)).append("]]");
}
} else {
buffer.append("{{subst:unc}}");
}
return buffer.toString();
} }
buffer.append("== {{int:license-header}} ==\n") /**
.append(licenseTemplateFor(media.getLicense())).append("\n\n") * Returns upload date in either TEMPLATE_DATE_ACC_TO_EXIF or TEMPLATE_DATA_OTHER_SOURCE
.append("{{Uploaded from Mobile|platform=Android|version=") *
.append(ConfigUtils.getVersionNameWithSha(context)).append("}}\n"); * @param dateCreated
final List<String> categories = media.getCategories(); * @param dateCreatedSource
if (categories != null && categories.size() != 0) { * @return
for (int i = 0; i < categories.size(); i++) { */
buffer.append("\n[[Category:").append(categories.get(i)).append("]]"); private String getTemplatizedCreatedDate(Date dateCreated, String dateCreatedSource) {
} if (dateCreated != null) {
} else { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
buffer.append("{{subst:unc}}"); return String.format(Locale.ENGLISH,
} isExif(dateCreatedSource) ? TEMPLATE_DATE_ACC_TO_EXIF : TEMPLATE_DATA_OTHER_SOURCE,
return buffer.toString(); dateFormat.format(dateCreated)
} ) + "\n";
}
/** return "";
* Returns upload date in either TEMPLATE_DATE_ACC_TO_EXIF or TEMPLATE_DATA_OTHER_SOURCE
*
* @param dateCreated
* @param dateCreatedSource
* @return
*/
private String getTemplatizedCreatedDate(Date dateCreated, String dateCreatedSource) {
if (dateCreated != null) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
return String.format(Locale.ENGLISH,
isExif(dateCreatedSource) ? TEMPLATE_DATE_ACC_TO_EXIF : TEMPLATE_DATA_OTHER_SOURCE,
dateFormat.format(dateCreated)
) + "\n";
}
return "";
}
private boolean isExif(String dateCreatedSource) {
return DateTimeWithSource.EXIF_SOURCE.equals(dateCreatedSource);
}
@NonNull
private String licenseTemplateFor(String license) {
switch (license) {
case Licenses.CC_BY_3:
return "{{self|cc-by-3.0}}";
case Licenses.CC_BY_4:
return "{{self|cc-by-4.0}}";
case Licenses.CC_BY_SA_3:
return "{{self|cc-by-sa-3.0}}";
case Licenses.CC_BY_SA_4:
return "{{self|cc-by-sa-4.0}}";
case Licenses.CC0:
return "{{self|cc-zero}}";
} }
throw new RuntimeException("Unrecognized license value: " + license); private boolean isExif(String dateCreatedSource) {
} return DateTimeWithSource.EXIF_SOURCE.equals(dateCreatedSource);
}
@NonNull
private String licenseTemplateFor(String license) {
switch (license) {
case Licenses.CC_BY_3:
return "{{self|cc-by-3.0}}";
case Licenses.CC_BY_4:
return "{{self|cc-by-4.0}}";
case Licenses.CC_BY_SA_3:
return "{{self|cc-by-sa-3.0}}";
case Licenses.CC_BY_SA_4:
return "{{self|cc-by-sa-4.0}}";
case Licenses.CC0:
return "{{self|cc-zero}}";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
} }

View file

@ -15,34 +15,34 @@ import javax.inject.Singleton;
@Singleton @Singleton
public class ReadFBMD { public class ReadFBMD {
@Inject @Inject
public ReadFBMD() { public ReadFBMD() {
} }
public Single<Integer> processMetadata(String path) { public Single<Integer> processMetadata(String path) {
return Single.fromCallable(() -> { return Single.fromCallable(() -> {
try { try {
int psBlockOffset; int psBlockOffset;
int fbmdOffset; int fbmdOffset;
try (FileInputStream fs = new FileInputStream(path)) { try (FileInputStream fs = new FileInputStream(path)) {
byte[] bytes = new byte[4096]; byte[] bytes = new byte[4096];
fs.read(bytes); fs.read(bytes);
fs.close(); fs.close();
String fileStr = new String(bytes); String fileStr = new String(bytes);
psBlockOffset = fileStr.indexOf("8BIM"); psBlockOffset = fileStr.indexOf("8BIM");
fbmdOffset = fileStr.indexOf("FBMD"); fbmdOffset = fileStr.indexOf("FBMD");
} }
if (psBlockOffset > 0 && fbmdOffset > 0 if (psBlockOffset > 0 && fbmdOffset > 0
&& fbmdOffset > psBlockOffset && fbmdOffset - psBlockOffset < 0x80) { && fbmdOffset > psBlockOffset && fbmdOffset - psBlockOffset < 0x80) {
return ImageUtils.FILE_FBMD; return ImageUtils.FILE_FBMD;
} }
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
return ImageUtils.IMAGE_OK; return ImageUtils.IMAGE_OK;
}); });
} }
} }

View file

@ -35,199 +35,204 @@ import timber.log.Timber;
@Singleton @Singleton
public class UploadClient { public class UploadClient {
private final int CHUNK_SIZE = 512 * 1024; // 512 KB private final int CHUNK_SIZE = 512 * 1024; // 512 KB
//This is maximum duration for which a stash is persisted on MediaWiki //This is maximum duration for which a stash is persisted on MediaWiki
// https://www.mediawiki.org/wiki/Manual:$wgUploadStashMaxAge // https://www.mediawiki.org/wiki/Manual:$wgUploadStashMaxAge
private final int MAX_CHUNK_AGE = 6 * 3600 * 1000; // 6 hours private final int MAX_CHUNK_AGE = 6 * 3600 * 1000; // 6 hours
private final UploadInterface uploadInterface; private final UploadInterface uploadInterface;
private final CsrfTokenClient csrfTokenClient; private final CsrfTokenClient csrfTokenClient;
private final PageContentsCreator pageContentsCreator; private final PageContentsCreator pageContentsCreator;
private final FileUtilsWrapper fileUtilsWrapper; private final FileUtilsWrapper fileUtilsWrapper;
private final Gson gson; private final Gson gson;
private final CompositeDisposable compositeDisposable = new CompositeDisposable(); private final CompositeDisposable compositeDisposable = new CompositeDisposable();
@Inject @Inject
public UploadClient(final UploadInterface uploadInterface, public UploadClient(final UploadInterface uploadInterface,
@Named(NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient, @Named(NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient,
final PageContentsCreator pageContentsCreator, final PageContentsCreator pageContentsCreator,
final FileUtilsWrapper fileUtilsWrapper, final Gson gson) { final FileUtilsWrapper fileUtilsWrapper, final Gson gson) {
this.uploadInterface = uploadInterface; this.uploadInterface = uploadInterface;
this.csrfTokenClient = csrfTokenClient; this.csrfTokenClient = csrfTokenClient;
this.pageContentsCreator = pageContentsCreator; this.pageContentsCreator = pageContentsCreator;
this.fileUtilsWrapper = fileUtilsWrapper; this.fileUtilsWrapper = fileUtilsWrapper;
this.gson = gson; this.gson = gson;
}
/**
* Upload file to stash in chunks of specified size. Uploading files in chunks will make handling
* of large files easier. Also, it will be useful in supporting pause/resume of uploads
*/
public Observable<StashUploadResult> uploadFileToStash(
final Context context, final String filename, final Contribution contribution,
final NotificationUpdateProgressListener notificationUpdater) throws IOException {
if (contribution.getChunkInfo() != null
&& contribution.getChunkInfo().getTotalChunks() == contribution.getChunkInfo()
.getIndexOfNextChunkToUpload()) {
return Observable.just(new StashUploadResult(StashUploadState.SUCCESS,
contribution.getChunkInfo().getUploadResult().getFilekey()));
} }
CommonsApplication.pauseUploads.put(contribution.getPageId(), false); /**
* Upload file to stash in chunks of specified size. Uploading files in chunks will make
* handling of large files easier. Also, it will be useful in supporting pause/resume of
* uploads
*/
public Observable<StashUploadResult> uploadFileToStash(
final Context context, final String filename, final Contribution contribution,
final NotificationUpdateProgressListener notificationUpdater) throws IOException {
if (contribution.getChunkInfo() != null
&& contribution.getChunkInfo().getTotalChunks() == contribution.getChunkInfo()
.getIndexOfNextChunkToUpload()) {
return Observable.just(new StashUploadResult(StashUploadState.SUCCESS,
contribution.getChunkInfo().getUploadResult().getFilekey()));
}
final File file = new File(contribution.getLocalUri().getPath()); CommonsApplication.pauseUploads.put(contribution.getPageId(), false);
final List<File> fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE);
final int totalChunks = fileChunks.size(); final File file = new File(contribution.getLocalUri().getPath());
final List<File> fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE);
final MediaType mediaType = MediaType final int totalChunks = fileChunks.size();
.parse(FileUtils.getMimeType(context, Uri.parse(file.getPath())));
final AtomicReference<ChunkInfo> chunkInfo = new AtomicReference<>(); final MediaType mediaType = MediaType
if (isStashValid(contribution)) { .parse(FileUtils.getMimeType(context, Uri.parse(file.getPath())));
chunkInfo.set(contribution.getChunkInfo());
Timber.d("Chunk: Next Chunk: %s, Total Chunks: %s", final AtomicReference<ChunkInfo> chunkInfo = new AtomicReference<>();
contribution.getChunkInfo().getIndexOfNextChunkToUpload(), if (isStashValid(contribution)) {
contribution.getChunkInfo().getTotalChunks()); chunkInfo.set(contribution.getChunkInfo());
}
final AtomicInteger index = new AtomicInteger(); Timber.d("Chunk: Next Chunk: %s, Total Chunks: %s",
final AtomicBoolean failures = new AtomicBoolean(); contribution.getChunkInfo().getIndexOfNextChunkToUpload(),
contribution.getChunkInfo().getTotalChunks());
}
compositeDisposable.add(Observable.fromIterable(fileChunks).forEach(chunkFile -> { final AtomicInteger index = new AtomicInteger();
if (CommonsApplication.pauseUploads.get(contribution.getPageId()) || failures.get()) { final AtomicBoolean failures = new AtomicBoolean();
return;
}
if (chunkInfo.get() != null && index.get() < chunkInfo.get().getIndexOfNextChunkToUpload()) { compositeDisposable.add(Observable.fromIterable(fileChunks).forEach(chunkFile -> {
index.incrementAndGet(); if (CommonsApplication.pauseUploads.get(contribution.getPageId()) || failures.get()) {
Timber.d("Chunk: Increment and return: %s", index.get()); return;
return;
}
index.getAndIncrement();
final int offset =
chunkInfo.get() != null ? chunkInfo.get().getUploadResult().getOffset() : 0;
Timber.d("Chunk: Sending Chunk number: %s, offset: %s", index.get(), offset);
final String filekey =
chunkInfo.get() != null ? chunkInfo.get().getUploadResult().getFilekey() : null;
final RequestBody requestBody = RequestBody
.create(mediaType, chunkFile);
final CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody,
notificationUpdater::onProgress, offset,
file.length());
compositeDisposable.add(uploadChunkToStash(filename,
file.length(),
offset,
filekey,
countingRequestBody).subscribe(uploadResult -> {
Timber.d("Chunk: Received Chunk number: %s, offset: %s", index.get(),
uploadResult.getOffset());
chunkInfo.set(
new ChunkInfo(uploadResult, index.get(), totalChunks));
notificationUpdater.onChunkUploaded(contribution, chunkInfo.get());
}, throwable -> {
Timber.e(throwable, "Received error in chunk upload");
failures.set(true);
}));
}));
if (CommonsApplication.pauseUploads.get(contribution.getPageId())) {
Timber.d("Upload stash paused %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.PAUSED, null));
} else if (failures.get()) {
Timber.d("Upload stash contains failures %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.FAILED, null));
} else if (chunkInfo.get() != null) {
Timber.d("Upload stash success %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.SUCCESS,
chunkInfo.get().getUploadResult().getFilekey()));
} else {
Timber.d("Upload stash failed %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.FAILED, null));
}
}
/**
* Stash is valid for 6 hours. This function checks the validity of stash
* @param contribution
* @return
*/
private boolean isStashValid(Contribution contribution) {
return contribution.getChunkInfo() != null &&
contribution.getDateModified()
.after(new Date(System.currentTimeMillis() - MAX_CHUNK_AGE));
}
/**
* Uploads a file chunk to stash
*
* @param filename The name of the file being uploaded
* @param fileSize The total size of the file
* @param offset The offset returned by the previous chunk upload
* @param fileKey The filekey returned by the previous chunk upload
* @param countingRequestBody Request body with chunk file
* @return
*/
Observable<UploadResult> uploadChunkToStash(final String filename,
final long fileSize,
final long offset,
final String fileKey,
final CountingRequestBody countingRequestBody) {
final MultipartBody.Part filePart;
try {
filePart = MultipartBody.Part
.createFormData("chunk", URLEncoder.encode(filename, "utf-8"), countingRequestBody);
return uploadInterface.uploadFileToStash(toRequestBody(filename),
toRequestBody(String.valueOf(fileSize)),
toRequestBody(String.valueOf(offset)),
toRequestBody(fileKey),
toRequestBody(csrfTokenClient.getTokenBlocking()),
filePart)
.map(UploadResponse::getUpload);
} catch (final Throwable throwable) {
Timber.e(throwable, "Failed to upload chunk to stash");
return Observable.error(throwable);
}
}
/**
* Converts string value to request body
*/
@Nullable
private RequestBody toRequestBody(@Nullable final String value) {
return value == null ? null : RequestBody.create(okhttp3.MultipartBody.FORM, value);
}
public Observable<UploadResult> uploadFileFromStash(
final Contribution contribution,
final String uniqueFileName,
final String fileKey) {
try {
return uploadInterface
.uploadFileFromStash(csrfTokenClient.getTokenBlocking(),
pageContentsCreator.createFrom(contribution),
CommonsApplication.DEFAULT_EDIT_SUMMARY,
uniqueFileName,
fileKey).map(uploadResponse -> {
UploadResponse uploadResult = gson.fromJson(uploadResponse, UploadResponse.class);
if (uploadResult.getUpload() == null) {
final MwException exception = gson.fromJson(uploadResponse, MwException.class);
Timber.e(exception, "Error in uploading file from stash");
throw new RuntimeException(exception.getErrorCode());
} }
return uploadResult.getUpload();
}); if (chunkInfo.get() != null && index.get() < chunkInfo.get()
} catch (final Throwable throwable) { .getIndexOfNextChunkToUpload()) {
Timber.e(throwable, "Exception occurred in uploading file from stash"); index.incrementAndGet();
return Observable.error(throwable); Timber.d("Chunk: Increment and return: %s", index.get());
return;
}
index.getAndIncrement();
final int offset =
chunkInfo.get() != null ? chunkInfo.get().getUploadResult().getOffset() : 0;
Timber.d("Chunk: Sending Chunk number: %s, offset: %s", index.get(), offset);
final String filekey =
chunkInfo.get() != null ? chunkInfo.get().getUploadResult().getFilekey() : null;
final RequestBody requestBody = RequestBody
.create(mediaType, chunkFile);
final CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody,
notificationUpdater::onProgress, offset,
file.length());
compositeDisposable.add(uploadChunkToStash(filename,
file.length(),
offset,
filekey,
countingRequestBody).subscribe(uploadResult -> {
Timber.d("Chunk: Received Chunk number: %s, offset: %s", index.get(),
uploadResult.getOffset());
chunkInfo.set(
new ChunkInfo(uploadResult, index.get(), totalChunks));
notificationUpdater.onChunkUploaded(contribution, chunkInfo.get());
}, throwable -> {
Timber.e(throwable, "Received error in chunk upload");
failures.set(true);
}));
}));
if (CommonsApplication.pauseUploads.get(contribution.getPageId())) {
Timber.d("Upload stash paused %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.PAUSED, null));
} else if (failures.get()) {
Timber.d("Upload stash contains failures %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.FAILED, null));
} else if (chunkInfo.get() != null) {
Timber.d("Upload stash success %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.SUCCESS,
chunkInfo.get().getUploadResult().getFilekey()));
} else {
Timber.d("Upload stash failed %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.FAILED, null));
}
}
/**
* Stash is valid for 6 hours. This function checks the validity of stash
*
* @param contribution
* @return
*/
private boolean isStashValid(Contribution contribution) {
return contribution.getChunkInfo() != null &&
contribution.getDateModified()
.after(new Date(System.currentTimeMillis() - MAX_CHUNK_AGE));
}
/**
* Uploads a file chunk to stash
*
* @param filename The name of the file being uploaded
* @param fileSize The total size of the file
* @param offset The offset returned by the previous chunk upload
* @param fileKey The filekey returned by the previous chunk upload
* @param countingRequestBody Request body with chunk file
* @return
*/
Observable<UploadResult> uploadChunkToStash(final String filename,
final long fileSize,
final long offset,
final String fileKey,
final CountingRequestBody countingRequestBody) {
final MultipartBody.Part filePart;
try {
filePart = MultipartBody.Part
.createFormData("chunk", URLEncoder.encode(filename, "utf-8"), countingRequestBody);
return uploadInterface.uploadFileToStash(toRequestBody(filename),
toRequestBody(String.valueOf(fileSize)),
toRequestBody(String.valueOf(offset)),
toRequestBody(fileKey),
toRequestBody(csrfTokenClient.getTokenBlocking()),
filePart)
.map(UploadResponse::getUpload);
} catch (final Throwable throwable) {
Timber.e(throwable, "Failed to upload chunk to stash");
return Observable.error(throwable);
}
}
/**
* Converts string value to request body
*/
@Nullable
private RequestBody toRequestBody(@Nullable final String value) {
return value == null ? null : RequestBody.create(okhttp3.MultipartBody.FORM, value);
}
public Observable<UploadResult> uploadFileFromStash(
final Contribution contribution,
final String uniqueFileName,
final String fileKey) {
try {
return uploadInterface
.uploadFileFromStash(csrfTokenClient.getTokenBlocking(),
pageContentsCreator.createFrom(contribution),
CommonsApplication.DEFAULT_EDIT_SUMMARY,
uniqueFileName,
fileKey).map(uploadResponse -> {
UploadResponse uploadResult = gson
.fromJson(uploadResponse, UploadResponse.class);
if (uploadResult.getUpload() == null) {
final MwException exception = gson
.fromJson(uploadResponse, MwException.class);
Timber.e(exception, "Error in uploading file from stash");
throw new RuntimeException(exception.getErrorCode());
}
return uploadResult.getUpload();
});
} catch (final Throwable throwable) {
Timber.e(throwable, "Exception occurred in uploading file from stash");
return Observable.error(throwable);
}
} }
}
} }

View file

@ -14,110 +14,111 @@ import java.util.List;
public class UploadItem { public class UploadItem {
private final Uri mediaUri; private final Uri mediaUri;
private final String mimeType; private final String mimeType;
private ImageCoordinates gpsCoords; private ImageCoordinates gpsCoords;
private List<UploadMediaDetail> uploadMediaDetails; private List<UploadMediaDetail> uploadMediaDetails;
private Place place; private Place place;
private final long createdTimestamp; private final long createdTimestamp;
private final String createdTimestampSource; private final String createdTimestampSource;
private final BehaviorSubject<Integer> imageQuality; private final BehaviorSubject<Integer> imageQuality;
private boolean hasInvalidLocation; private boolean hasInvalidLocation;
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
UploadItem(final Uri mediaUri, UploadItem(final Uri mediaUri,
final String mimeType, final String mimeType,
final ImageCoordinates gpsCoords, final ImageCoordinates gpsCoords,
final Place place, final Place place,
final long createdTimestamp, final long createdTimestamp,
final String createdTimestampSource) { final String createdTimestampSource) {
this.createdTimestampSource = createdTimestampSource; this.createdTimestampSource = createdTimestampSource;
uploadMediaDetails = new ArrayList<>(Collections.singletonList(new UploadMediaDetail())); uploadMediaDetails = new ArrayList<>(Collections.singletonList(new UploadMediaDetail()));
this.place = place; this.place = place;
this.mediaUri = mediaUri; this.mediaUri = mediaUri;
this.mimeType = mimeType; this.mimeType = mimeType;
this.gpsCoords = gpsCoords; this.gpsCoords = gpsCoords;
this.createdTimestamp = createdTimestamp; this.createdTimestamp = createdTimestamp;
imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT); imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT);
}
public String getCreatedTimestampSource() {
return createdTimestampSource;
}
public ImageCoordinates getGpsCoords() {
return gpsCoords;
}
public List<UploadMediaDetail> getUploadMediaDetails() {
return uploadMediaDetails;
}
public long getCreatedTimestamp() {
return createdTimestamp;
}
public Uri getMediaUri() {
return mediaUri;
}
public int getImageQuality() {
return imageQuality.getValue();
}
public void setImageQuality(final int imageQuality) {
this.imageQuality.onNext(imageQuality);
}
/**
* Sets the corresponding place to the uploadItem
* @param place geolocated Wikidata item
*/
public void setPlace(Place place) {
this.place = place;
}
public Place getPlace() {
return place;
}
public void setMediaDetails(final List<UploadMediaDetail> uploadMediaDetails) {
this.uploadMediaDetails = uploadMediaDetails;
}
@Override
public boolean equals(@Nullable final Object obj) {
if (!(obj instanceof UploadItem)) {
return false;
} }
return mediaUri.toString().contains(((UploadItem) (obj)).mediaUri.toString());
} public String getCreatedTimestampSource() {
return createdTimestampSource;
}
@Override public ImageCoordinates getGpsCoords() {
public int hashCode() { return gpsCoords;
return mediaUri.hashCode(); }
}
/** public List<UploadMediaDetail> getUploadMediaDetails() {
* Choose a filename for the media. Currently, the caption is used as a filename. If several return uploadMediaDetails;
* languages have been entered, the first language is used. }
*/
public String getFileName() {
return Utils.fixExtension(uploadMediaDetails.get(0).getCaptionText(),
MimeTypeMapWrapper.getExtensionFromMimeType(mimeType));
}
public void setGpsCoords(final ImageCoordinates gpsCoords) { public long getCreatedTimestamp() {
this.gpsCoords = gpsCoords; return createdTimestamp;
} }
public void setHasInvalidLocation(boolean hasInvalidLocation) { public Uri getMediaUri() {
this.hasInvalidLocation=hasInvalidLocation; return mediaUri;
} }
public boolean hasInvalidLocation() { public int getImageQuality() {
return hasInvalidLocation; return imageQuality.getValue();
} }
public void setImageQuality(final int imageQuality) {
this.imageQuality.onNext(imageQuality);
}
/**
* Sets the corresponding place to the uploadItem
*
* @param place geolocated Wikidata item
*/
public void setPlace(Place place) {
this.place = place;
}
public Place getPlace() {
return place;
}
public void setMediaDetails(final List<UploadMediaDetail> uploadMediaDetails) {
this.uploadMediaDetails = uploadMediaDetails;
}
@Override
public boolean equals(@Nullable final Object obj) {
if (!(obj instanceof UploadItem)) {
return false;
}
return mediaUri.toString().contains(((UploadItem) (obj)).mediaUri.toString());
}
@Override
public int hashCode() {
return mediaUri.hashCode();
}
/**
* Choose a filename for the media. Currently, the caption is used as a filename. If several
* languages have been entered, the first language is used.
*/
public String getFileName() {
return Utils.fixExtension(uploadMediaDetails.get(0).getCaptionText(),
MimeTypeMapWrapper.getExtensionFromMimeType(mimeType));
}
public void setGpsCoords(final ImageCoordinates gpsCoords) {
this.gpsCoords = gpsCoords;
}
public void setHasInvalidLocation(boolean hasInvalidLocation) {
this.hasInvalidLocation = hasInvalidLocation;
}
public boolean hasInvalidLocation() {
return hasInvalidLocation;
}
} }

View file

@ -4,11 +4,12 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
public class ActivityUtils { public class ActivityUtils {
public static <T> void startActivityWithFlags(Context context, Class<T> cls, int... flags) {
Intent intent = new Intent(context, cls); public static <T> void startActivityWithFlags(Context context, Class<T> cls, int... flags) {
for (int flag: flags) { Intent intent = new Intent(context, cls);
intent.addFlags(flag); for (int flag : flags) {
intent.addFlags(flag);
}
context.startActivity(intent);
} }
context.startActivity(intent);
}
} }

View file

@ -9,22 +9,22 @@ import javax.inject.Singleton;
@Singleton @Singleton
public class ImageUtilsWrapper { public class ImageUtilsWrapper {
@Inject @Inject
public ImageUtilsWrapper() { public ImageUtilsWrapper() {
} }
public Single<Integer> checkIfImageIsTooDark(String bitmapPath) { public Single<Integer> checkIfImageIsTooDark(String bitmapPath) {
return Single.fromCallable(() -> ImageUtils.checkIfImageIsTooDark(bitmapPath)) return Single.fromCallable(() -> ImageUtils.checkIfImageIsTooDark(bitmapPath))
.subscribeOn(Schedulers.computation()); .subscribeOn(Schedulers.computation());
} }
public Single<Integer> checkImageGeolocationIsDifferent(String geolocationOfFileString, public Single<Integer> checkImageGeolocationIsDifferent(String geolocationOfFileString,
LatLng latLng) { LatLng latLng) {
return Single.fromCallable( return Single.fromCallable(
() -> ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng)) () -> ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng))
.subscribeOn(Schedulers.computation()) .subscribeOn(Schedulers.computation())
.map(isDifferent -> isDifferent ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT .map(isDifferent -> isDifferent ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT
: ImageUtils.IMAGE_OK); : ImageUtils.IMAGE_OK);
} }
} }

View file

@ -19,43 +19,44 @@ import timber.log.Timber;
@Singleton @Singleton
public class WikiBaseClient { public class WikiBaseClient {
private final WikiBaseInterface wikiBaseInterface; private final WikiBaseInterface wikiBaseInterface;
private final CsrfTokenClient csrfTokenClient; private final CsrfTokenClient csrfTokenClient;
@Inject @Inject
public WikiBaseClient(WikiBaseInterface wikiBaseInterface, public WikiBaseClient(WikiBaseInterface wikiBaseInterface,
@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) { @Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) {
this.wikiBaseInterface = wikiBaseInterface; this.wikiBaseInterface = wikiBaseInterface;
this.csrfTokenClient = csrfTokenClient; this.csrfTokenClient = csrfTokenClient;
} }
public Observable<Boolean> postEditEntity(String fileEntityId, String data) { public Observable<Boolean> postEditEntity(String fileEntityId, String data) {
return csrfToken() return csrfToken()
.switchMap(editToken -> wikiBaseInterface.postEditEntity(fileEntityId, editToken, data) .switchMap(editToken -> wikiBaseInterface.postEditEntity(fileEntityId, editToken, data)
.map(response -> (response.getSuccessVal() == 1))); .map(response -> (response.getSuccessVal() == 1)));
} }
public Observable<Long> getFileEntityId(UploadResult uploadResult) { public Observable<Long> getFileEntityId(UploadResult uploadResult) {
return wikiBaseInterface.getFileEntityId(uploadResult.createCanonicalFileName()) return wikiBaseInterface.getFileEntityId(uploadResult.createCanonicalFileName())
.map(response -> (long) (response.query().pages().get(0).pageId())); .map(response -> (long) (response.query().pages().get(0).pageId()));
} }
public Observable<MwPostResponse> addLabelstoWikidata(long fileEntityId, public Observable<MwPostResponse> addLabelstoWikidata(long fileEntityId,
String languageCode, String captionValue) { String languageCode, String captionValue) {
return csrfToken() return csrfToken()
.switchMap(editToken -> wikiBaseInterface .switchMap(editToken -> wikiBaseInterface
.addLabelstoWikidata(PAGE_ID_PREFIX + fileEntityId, editToken, languageCode, captionValue)); .addLabelstoWikidata(PAGE_ID_PREFIX + fileEntityId, editToken, languageCode,
captionValue));
} }
private Observable<String> csrfToken() { private Observable<String> csrfToken() {
return Observable.fromCallable(() -> { return Observable.fromCallable(() -> {
try { try {
return csrfTokenClient.getTokenBlocking(); return csrfTokenClient.getTokenBlocking();
} catch (Throwable throwable) { } catch (Throwable throwable) {
Timber.e(throwable); Timber.e(throwable);
return ""; return "";
} }
}); });
} }
} }

View file

@ -15,32 +15,33 @@ import org.wikipedia.wikidata.Statement_partial;
public class WikidataClient { public class WikidataClient {
private final WikidataInterface wikidataInterface; private final WikidataInterface wikidataInterface;
private final Gson gson; private final Gson gson;
@Inject @Inject
public WikidataClient(WikidataInterface wikidataInterface, final Gson gson) { public WikidataClient(WikidataInterface wikidataInterface, final Gson gson) {
this.wikidataInterface = wikidataInterface; this.wikidataInterface = wikidataInterface;
this.gson = gson; this.gson = gson;
} }
/** /**
* Create wikidata claim to add P18 value * Create wikidata claim to add P18 value
* *
* @return revisionID of the edit * @return revisionID of the edit
*/ */
Observable<Long> setClaim(Statement_partial claim, String tags) { Observable<Long> setClaim(Statement_partial claim, String tags) {
return getCsrfToken() return getCsrfToken()
.flatMap(csrfToken -> wikidataInterface.postSetClaim(gson.toJson(claim), tags, csrfToken)) .flatMap(
.map(mwPostResponse -> mwPostResponse.getPageinfo().getLastrevid()); csrfToken -> wikidataInterface.postSetClaim(gson.toJson(claim), tags, csrfToken))
} .map(mwPostResponse -> mwPostResponse.getPageinfo().getLastrevid());
}
/** /**
* Get csrf token for wikidata edit * Get csrf token for wikidata edit
*/ */
@NotNull @NotNull
private Observable<String> getCsrfToken() { private Observable<String> getCsrfToken() {
return wikidataInterface.getCsrfToken() return wikidataInterface.getCsrfToken()
.map(mwQueryResponse -> mwQueryResponse.query().csrfToken()); .map(mwQueryResponse -> mwQueryResponse.query().csrfToken());
} }
} }

View file

@ -44,169 +44,172 @@ import timber.log.Timber;
@Singleton @Singleton
public class WikidataEditService { public class WikidataEditService {
public static final String COMMONS_APP_TAG = "wikimedia-commons-app"; public static final String COMMONS_APP_TAG = "wikimedia-commons-app";
private final Context context; private final Context context;
private final WikidataEditListener wikidataEditListener; private final WikidataEditListener wikidataEditListener;
private final JsonKvStore directKvStore; private final JsonKvStore directKvStore;
private final WikiBaseClient wikiBaseClient; private final WikiBaseClient wikiBaseClient;
private final WikidataClient wikidataClient; private final WikidataClient wikidataClient;
private final Gson gson; private final Gson gson;
@Inject @Inject
public WikidataEditService(final Context context, public WikidataEditService(final Context context,
final WikidataEditListener wikidataEditListener, final WikidataEditListener wikidataEditListener,
@Named("default_preferences") final JsonKvStore directKvStore, @Named("default_preferences") final JsonKvStore directKvStore,
final WikiBaseClient wikiBaseClient, final WikiBaseClient wikiBaseClient,
final WikidataClient wikidataClient, final Gson gson) { final WikidataClient wikidataClient, final Gson gson) {
this.context = context; this.context = context;
this.wikidataEditListener = wikidataEditListener; this.wikidataEditListener = wikidataEditListener;
this.directKvStore = directKvStore; this.directKvStore = directKvStore;
this.wikiBaseClient = wikiBaseClient; this.wikiBaseClient = wikiBaseClient;
this.wikidataClient = wikidataClient; this.wikidataClient = wikidataClient;
this.gson = gson; this.gson = gson;
}
/**
* Edits the wikibase entity by adding DEPICTS property. Adding DEPICTS property requires call to
* the wikibase API to set tag against the entity.
*/
@SuppressLint("CheckResult")
private Observable<Boolean> addDepictsProperty(final String fileEntityId,
final WikidataItem depictedItem) {
final EditClaim data = editClaim(
ConfigUtils.isBetaFlavour() ? "Q10" // Wikipedia:Sandbox (Q10)
: depictedItem.getId()
);
return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data))
.doOnNext(success -> {
if (success) {
Timber.d("DEPICTS property was set successfully for %s", fileEntityId);
} else {
Timber.d("Unable to set DEPICTS property for %s", fileEntityId);
}
})
.doOnError(throwable -> {
Timber.e(throwable, "Error occurred while setting DEPICTS property");
ViewUtil.showLongToast(context, throwable.toString());
})
.subscribeOn(Schedulers.io());
}
private EditClaim editClaim(final String entityId) {
return EditClaim.from(entityId, WikidataProperties.DEPICTS.getPropertyName());
}
/**
* Show a success toast when the edit is made successfully
*/
private void showSuccessToast(final String wikiItemName) {
final String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
final String successMessage = String
.format(Locale.getDefault(), successStringTemplate, wikiItemName);
ViewUtil.showLongToast(context, successMessage);
}
/**
* Adds label to Wikidata using the fileEntityId and the edit token, obtained from
* csrfTokenClient
*
* @param fileEntityId
* @return
*/
@SuppressLint("CheckResult")
private Observable<Boolean> addCaption(final long fileEntityId, final String languageCode,
final String captionValue) {
return wikiBaseClient.addLabelstoWikidata(fileEntityId, languageCode, captionValue)
.doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse))
.doOnError(throwable -> {
Timber.e(throwable, "Error occurred while setting Captions");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
})
.map(mwPostResponse -> mwPostResponse != null);
}
private void onAddCaptionResponse(Long fileEntityId, MwPostResponse response) {
if (response != null) {
Timber.d("Caption successfully set, revision id = %s", response);
} else {
Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId);
}
}
public Long createClaim(@Nullable final WikidataPlace wikidataPlace, final String fileName, final
Map<String, String> captions) {
if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) {
Timber
.d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
return null;
}
return addImageAndMediaLegends(wikidataPlace, fileName, captions);
}
public Long addImageAndMediaLegends(final WikidataItem wikidataItem, final String fileName,
final Map<String, String> captions) {
final Snak_partial p18 = new Snak_partial("value", WikidataProperties.IMAGE.getPropertyName(),
new ValueString(fileName.replace("File:", "")));
final List<Snak_partial> snaks = new ArrayList<>();
for (final Map.Entry<String, String> entry : captions.entrySet()) {
snaks.add(new Snak_partial("value",
WikidataProperties.MEDIA_LEGENDS.getPropertyName(), new DataValue.MonoLingualText(
new WikiBaseMonolingualTextValue(entry.getValue(), entry.getKey()))));
} }
final String id = wikidataItem.getId() + "$" + UUID.randomUUID().toString(); /**
final Statement_partial claim = new Statement_partial(p18, "statement", "normal", id, * Edits the wikibase entity by adding DEPICTS property. Adding DEPICTS property requires call
Collections.singletonMap(WikidataProperties.MEDIA_LEGENDS.getPropertyName(), snaks), * to the wikibase API to set tag against the entity.
Arrays.asList(WikidataProperties.MEDIA_LEGENDS.getPropertyName())); */
@SuppressLint("CheckResult")
private Observable<Boolean> addDepictsProperty(final String fileEntityId,
final WikidataItem depictedItem) {
return wikidataClient.setClaim(claim, COMMONS_APP_TAG).blockingSingle(); final EditClaim data = editClaim(
} ConfigUtils.isBetaFlavour() ? "Q10" // Wikipedia:Sandbox (Q10)
: depictedItem.getId()
public void handleImageClaimResult(final WikidataItem wikidataItem, final Long revisionId) {
if (revisionId != null) {
if (wikidataEditListener != null) {
wikidataEditListener.onSuccessfulWikidataEdit();
}
showSuccessToast(wikidataItem.getName());
} else {
Timber.d("Unable to make wiki data edit for entity %s", wikidataItem);
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
}
}
public Observable addDepictionsAndCaptions(final UploadResult uploadResult, final Contribution contribution) {
return wikiBaseClient.getFileEntityId(uploadResult)
.doOnError(throwable -> {
Timber.e(throwable, "Error occurred while getting EntityID to set DEPICTS property");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
})
.switchMap(fileEntityId -> {
if (fileEntityId != null) {
Timber.d("EntityId for image was received successfully: %s", fileEntityId);
return Observable.concat(
depictionEdits(contribution, fileEntityId),
captionEdits(contribution, fileEntityId)
);
} else {
Timber.d("Error acquiring EntityId for image: %s", uploadResult);
return Observable.empty();
}
}
); );
}
private Observable<Boolean> captionEdits(Contribution contribution, Long fileEntityId) { return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data))
return Observable.fromIterable(contribution.getMedia().getCaptions().entrySet()) .doOnNext(success -> {
.concatMap(entry -> addCaption(fileEntityId, entry.getKey(), entry.getValue())); if (success) {
} Timber.d("DEPICTS property was set successfully for %s", fileEntityId);
} else {
Timber.d("Unable to set DEPICTS property for %s", fileEntityId);
}
})
.doOnError(throwable -> {
Timber.e(throwable, "Error occurred while setting DEPICTS property");
ViewUtil.showLongToast(context, throwable.toString());
})
.subscribeOn(Schedulers.io());
}
private Observable<Boolean> depictionEdits(Contribution contribution, Long fileEntityId) { private EditClaim editClaim(final String entityId) {
return Observable.fromIterable(contribution.getDepictedItems()) return EditClaim.from(entityId, WikidataProperties.DEPICTS.getPropertyName());
.concatMap(wikidataItem -> addDepictsProperty(fileEntityId.toString(), wikidataItem)); }
}
/**
* Show a success toast when the edit is made successfully
*/
private void showSuccessToast(final String wikiItemName) {
final String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
final String successMessage = String
.format(Locale.getDefault(), successStringTemplate, wikiItemName);
ViewUtil.showLongToast(context, successMessage);
}
/**
* Adds label to Wikidata using the fileEntityId and the edit token, obtained from
* csrfTokenClient
*
* @param fileEntityId
* @return
*/
@SuppressLint("CheckResult")
private Observable<Boolean> addCaption(final long fileEntityId, final String languageCode,
final String captionValue) {
return wikiBaseClient.addLabelstoWikidata(fileEntityId, languageCode, captionValue)
.doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse))
.doOnError(throwable -> {
Timber.e(throwable, "Error occurred while setting Captions");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
})
.map(mwPostResponse -> mwPostResponse != null);
}
private void onAddCaptionResponse(Long fileEntityId, MwPostResponse response) {
if (response != null) {
Timber.d("Caption successfully set, revision id = %s", response);
} else {
Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId);
}
}
public Long createClaim(@Nullable final WikidataPlace wikidataPlace, final String fileName,
final Map<String, String> captions) {
if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) {
Timber
.d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
return null;
}
return addImageAndMediaLegends(wikidataPlace, fileName, captions);
}
public Long addImageAndMediaLegends(final WikidataItem wikidataItem, final String fileName,
final Map<String, String> captions) {
final Snak_partial p18 = new Snak_partial("value",
WikidataProperties.IMAGE.getPropertyName(),
new ValueString(fileName.replace("File:", "")));
final List<Snak_partial> snaks = new ArrayList<>();
for (final Map.Entry<String, String> entry : captions.entrySet()) {
snaks.add(new Snak_partial("value",
WikidataProperties.MEDIA_LEGENDS.getPropertyName(), new DataValue.MonoLingualText(
new WikiBaseMonolingualTextValue(entry.getValue(), entry.getKey()))));
}
final String id = wikidataItem.getId() + "$" + UUID.randomUUID().toString();
final Statement_partial claim = new Statement_partial(p18, "statement", "normal", id,
Collections.singletonMap(WikidataProperties.MEDIA_LEGENDS.getPropertyName(), snaks),
Arrays.asList(WikidataProperties.MEDIA_LEGENDS.getPropertyName()));
return wikidataClient.setClaim(claim, COMMONS_APP_TAG).blockingSingle();
}
public void handleImageClaimResult(final WikidataItem wikidataItem, final Long revisionId) {
if (revisionId != null) {
if (wikidataEditListener != null) {
wikidataEditListener.onSuccessfulWikidataEdit();
}
showSuccessToast(wikidataItem.getName());
} else {
Timber.d("Unable to make wiki data edit for entity %s", wikidataItem);
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
}
}
public Observable addDepictionsAndCaptions(final UploadResult uploadResult,
final Contribution contribution) {
return wikiBaseClient.getFileEntityId(uploadResult)
.doOnError(throwable -> {
Timber
.e(throwable, "Error occurred while getting EntityID to set DEPICTS property");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
})
.switchMap(fileEntityId -> {
if (fileEntityId != null) {
Timber.d("EntityId for image was received successfully: %s", fileEntityId);
return Observable.concat(
depictionEdits(contribution, fileEntityId),
captionEdits(contribution, fileEntityId)
);
} else {
Timber.d("Error acquiring EntityId for image: %s", uploadResult);
return Observable.empty();
}
}
);
}
private Observable<Boolean> captionEdits(Contribution contribution, Long fileEntityId) {
return Observable.fromIterable(contribution.getMedia().getCaptions().entrySet())
.concatMap(entry -> addCaption(fileEntityId, entry.getKey(), entry.getValue()));
}
private Observable<Boolean> depictionEdits(Contribution contribution, Long fileEntityId) {
return Observable.fromIterable(contribution.getDepictedItems())
.concatMap(wikidataItem -> addDepictsProperty(fileEntityId.toString(), wikidataItem));
}
} }

View file

@ -8,14 +8,14 @@ import org.robolectric.annotation.Implements;
@Implements(ActionBar.class) @Implements(ActionBar.class)
public class ShadowActionBar { public class ShadowActionBar {
private boolean showHomeAsUp; private boolean showHomeAsUp;
public boolean getShowHomeAsUp() { public boolean getShowHomeAsUp() {
return showHomeAsUp; return showHomeAsUp;
} }
@Implementation @Implementation
void setDisplayHomeAsUpEnabled(final boolean showHomeAsUp) { void setDisplayHomeAsUpEnabled(final boolean showHomeAsUp) {
this.showHomeAsUp = showHomeAsUp; this.showHomeAsUp = showHomeAsUp;
} }
} }

View file

@ -12,60 +12,60 @@ import org.wikipedia.login.LoginResult;
public class TestAppAdapter extends AppAdapter { public class TestAppAdapter extends AppAdapter {
@Override @Override
public String getMediaWikiBaseUrl() { public String getMediaWikiBaseUrl() {
return Service.WIKIPEDIA_URL; return Service.WIKIPEDIA_URL;
} }
@Override @Override
public String getRestbaseUriFormat() { public String getRestbaseUriFormat() {
return "%1$s://%2$s/api/rest_v1/"; return "%1$s://%2$s/api/rest_v1/";
} }
@Override @Override
public OkHttpClient getOkHttpClient(@NonNull WikiSite wikiSite) { public OkHttpClient getOkHttpClient(@NonNull WikiSite wikiSite) {
return new OkHttpClient.Builder() return new OkHttpClient.Builder()
.addInterceptor(new UnsuccessfulResponseInterceptor()) .addInterceptor(new UnsuccessfulResponseInterceptor())
.addInterceptor(new TestStubInterceptor()) .addInterceptor(new TestStubInterceptor())
.build(); .build();
} }
@Override @Override
public int getDesiredLeadImageDp() { public int getDesiredLeadImageDp() {
return 0; return 0;
} }
@Override @Override
public boolean isLoggedIn() { public boolean isLoggedIn() {
return false; return false;
} }
@Override @Override
public String getUserName() { public String getUserName() {
return null; return null;
} }
@Override @Override
public String getPassword() { public String getPassword() {
return null; return null;
} }
@Override @Override
public void updateAccount(@NonNull LoginResult result) { public void updateAccount(@NonNull LoginResult result) {
} }
@Override @Override
public SharedPreferenceCookieManager getCookies() { public SharedPreferenceCookieManager getCookies() {
return null; return null;
} }
@Override @Override
public void setCookies(@NonNull SharedPreferenceCookieManager cookies) { public void setCookies(@NonNull SharedPreferenceCookieManager cookies) {
} }
@Override @Override
public boolean logErrorsInsteadOfCrashing() { public boolean logErrorsInsteadOfCrashing() {
return false; return false;
} }
} }