This commit is contained in:
misaochan 2018-06-06 18:59:37 +10:00
commit a458b8ad06
149 changed files with 2572 additions and 1220 deletions

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase;
@ -21,13 +20,14 @@ import java.io.File;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.category.CategoryDao;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
import fr.free.nrw.commons.utils.FileUtils;
import fr.free.nrw.commons.upload.FileUtils;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;

View file

@ -178,6 +178,7 @@ public class Utils {
}
public static void handleWebUrl(Context context, Uri url) {
Timber.d("Launching web url %s", url.toString());
Intent browserIntent = new Intent(Intent.ACTION_VIEW, url);
if (browserIntent.resolveActivity(context.getPackageManager()) == null) {
Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT);

View file

@ -271,11 +271,11 @@ public class LoginActivity extends AccountAuthenticatorActivity {
showMessageAndCancelDialog(R.string.login_failed_network);
} else if (result.toLowerCase(Locale.getDefault()).contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
// Matches nosuchuser, nosuchusershort, noname
showMessageAndCancelDialog(R.string.login_failed_username);
showMessageAndCancelDialog(R.string.login_failed_wrong_credentials);
emptySensitiveEditFields();
} else if (result.toLowerCase(Locale.getDefault()).contains("wrongpassword".toLowerCase())) {
// Matches wrongpassword, wrongpasswordempty
showMessageAndCancelDialog(R.string.login_failed_password);
showMessageAndCancelDialog(R.string.login_failed_wrong_credentials);
emptySensitiveEditFields();
} else if (result.toLowerCase(Locale.getDefault()).contains("throttle".toLowerCase())) {
// Matches unknown throttle error codes

View file

@ -7,18 +7,25 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import fr.free.nrw.commons.upload.MwVolleyApi;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.upload.GpsCategoryModel;
import timber.log.Timber;
@Singleton
public class CacheController {
private final GpsCategoryModel gpsCategoryModel;
private final QuadTree<List<String>> quadTree;
private double x, y;
private QuadTree<List<String>> quadTree;
private double xMinus, xPlus, yMinus, yPlus;
private static final int EARTH_RADIUS = 6378137;
public CacheController() {
@Inject
CacheController(GpsCategoryModel gpsCategoryModel) {
this.gpsCategoryModel = gpsCategoryModel;
quadTree = new QuadTree<>(-180, -90, +180, +90);
}
@ -31,8 +38,8 @@ public class CacheController {
public void cacheCategory() {
List<String> pointCatList = new ArrayList<>();
if (MwVolleyApi.GpsCatExists.getGpsCatExists()) {
pointCatList.addAll(MwVolleyApi.getGpsCat());
if (gpsCategoryModel.getGpsCatExists()) {
pointCatList.addAll(gpsCategoryModel.getCategoryList());
Timber.d("Categories being cached: %s", pointCatList);
} else {
Timber.d("No categories found, so no categories cached");
@ -65,7 +72,7 @@ public class CacheController {
}
//Based on algorithm at http://gis.stackexchange.com/questions/2951/algorithm-for-offsetting-a-latitude-longitude-by-some-amount-of-meters
public void convertCoordRange() {
private void convertCoordRange() {
//Position, decimal degrees
double lat = y;
double lon = x;

View file

@ -39,7 +39,7 @@ import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.upload.MwVolleyApi;
import fr.free.nrw.commons.upload.GpsCategoryModel;
import fr.free.nrw.commons.utils.StringSortingUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
@ -73,6 +73,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
@Inject @Named("prefs") SharedPreferences prefsPrefs;
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
@Inject CategoryDao categoryDao;
@Inject GpsCategoryModel gpsCategoryModel;
private RVRendererAdapter<CategoryItem> categoriesAdapter;
private OnCategoriesSaveHandler onCategoriesSaveHandler;
@ -253,7 +254,6 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
}
private Observable<CategoryItem> defaultCategories() {
Observable<CategoryItem> directCat = directCategories();
if (hasDirectCategories) {
Timber.d("Image has direct Cat");
@ -287,9 +287,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
}
private Observable<CategoryItem> gpsCategories() {
return Observable.fromIterable(
MwVolleyApi.GpsCatExists.getGpsCatExists()
? MwVolleyApi.getGpsCat() : new ArrayList<>())
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
.map(name -> new CategoryItem(name, false));
}

View file

@ -224,4 +224,14 @@ public class CategoryImagesListFragment extends DaggerFragment {
public ListAdapter getAdapter() {
return gridView.getAdapter();
}
/**
* This method will be called on back pressed of CategoryImagesActivity.
* It initializes the grid view by setting adapter.
*/
@Override
public void onResume() {
gridView.setAdapter(gridAdapter);
super.onResume();
}
}

View file

@ -45,6 +45,7 @@ public class Contribution extends Media {
private long transferred;
private String decimalCoords;
private boolean isMultiple;
private String wikiDataEntityId;
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp,
int state, long dataLength, Date dateUploaded, long transferred,
@ -222,4 +223,17 @@ public class Contribution extends Media {
throw new RuntimeException("Unrecognized license value: " + license);
}
public String getWikiDataEntityId() {
return wikiDataEntityId;
}
/**
* When the corresponding wikidata entity is known as in case of nearby uploads, it can be set
* using the setter method
* @param wikiDataEntityId
*/
public void setWikiDataEntityId(String wikiDataEntityId) {
this.wikiDataEntityId = wikiDataEntityId;
}
}

View file

@ -90,7 +90,7 @@ public class ContributionController {
fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY);
}
public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload) {
public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload, String wikiDataEntityId) {
FragmentActivity activity = fragment.getActivity();
Timber.d("handleImagePicked() called with onActivityResult()");
Intent shareIntent = new Intent(activity, ShareActivity.class);
@ -102,9 +102,6 @@ public class ContributionController {
shareIntent.setType(activity.getContentResolver().getType(imageData));
shareIntent.putExtra(EXTRA_STREAM, imageData);
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY);
if (isDirectUpload) {
shareIntent.putExtra("isDirectUpload", true);
}
break;
case SELECT_FROM_CAMERA:
//FIXME: Find out appropriate mime type
@ -113,9 +110,6 @@ public class ContributionController {
shareIntent.setType("image/jpeg");
shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri);
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA);
if (isDirectUpload) {
shareIntent.putExtra("isDirectUpload", true);
}
break;
default:
@ -123,6 +117,10 @@ public class ContributionController {
}
Timber.i("Image selected");
try {
shareIntent.putExtra("isDirectUpload", isDirectUpload);
if (wikiDataEntityId != null && !wikiDataEntityId.equals("")) {
shareIntent.putExtra("wikiDataEntityId", wikiDataEntityId);
}
activity.startActivity(shareIntent);
} catch (SecurityException e) {
Timber.e(e, "Security Exception");

View file

@ -127,7 +127,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);
controller.handleImagePicked(requestCode, data, false);
controller.handleImagePicked(requestCode, data, false, null);
} else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);

View file

@ -9,17 +9,18 @@ import dagger.android.support.AndroidSupportInjectionModule;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
import fr.free.nrw.commons.delete.DeleteTask;
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.nearby.PlaceRenderer;
import fr.free.nrw.commons.upload.FileProcessor;
import fr.free.nrw.commons.settings.SettingsFragment;
@Singleton
@Component(modules = {
CommonsApplicationModule.class,
NetworkingModule.class,
AndroidInjectionModule.class,
AndroidSupportInjectionModule.class,
ActivityBuilderModule.class,
@ -47,6 +48,8 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
void inject(PlaceRenderer placeRenderer);
void inject(FileProcessor fileProcessor);
@Component.Builder
@SuppressWarnings({"WeakerAccess", "unused"})
interface Builder {

View file

@ -13,17 +13,18 @@ import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.upload.UploadController;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
import static android.content.Context.MODE_PRIVATE;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY;
@ -33,7 +34,6 @@ import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MOD
@SuppressWarnings({"WeakerAccess", "unused"})
public class CommonsApplicationModule {
public static final String CATEGORY_AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
private Context applicationContext;
@ -117,37 +117,12 @@ public class CommonsApplicationModule {
return new SessionManager(context, mediaWikiApi, sharedPreferences);
}
@Provides
@Singleton
public MediaWikiApi provideMediaWikiApi(Context context,
@Named("default_preferences") SharedPreferences defaultPreferences,
@Named("category_prefs") SharedPreferences categoryPrefs,
Gson gson) {
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, defaultPreferences, categoryPrefs, gson);
}
@Provides
@Singleton
public LocationServiceManager provideLocationServiceManager(Context context) {
return new LocationServiceManager(context);
}
/**
* Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere.
* @return returns a singleton Gson instance
*/
@Provides
@Singleton
public Gson provideGson() {
return new Gson();
}
@Provides
@Singleton
public CacheController provideCacheController() {
return new CacheController();
}
@Provides
@Singleton
public DBOpenHelper provideDBOpenHelper(Context context) {
@ -165,4 +140,10 @@ public class CommonsApplicationModule {
public LruCache<String, String> provideLruCache() {
return new LruCache<>(1024);
}
@Provides
@Singleton
public WikidataEditListener provideWikidataEditListener() {
return new WikidataEditListenerImpl();
}
}

View file

@ -0,0 +1,59 @@
package fr.free.nrw.commons.di;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javax.inject.Named;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public class NetworkingModule {
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
@Provides
@Singleton
public OkHttpClient provideOkHttpClient() {
return new OkHttpClient.Builder().build();
}
@Provides
@Singleton
public MediaWikiApi provideMediaWikiApi(Context context,
@Named("default_preferences") SharedPreferences defaultPreferences,
@Named("category_prefs") SharedPreferences categoryPrefs,
Gson gson) {
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultPreferences, categoryPrefs, gson);
}
@Provides
@Named("commons_mediawiki_url")
@NonNull
@SuppressWarnings("ConstantConditions")
public HttpUrl provideMwUrl() {
return HttpUrl.parse(BuildConfig.COMMONS_URL);
}
/**
* Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere.
* @return returns a singleton Gson instance
*/
@Provides
@Singleton
public Gson provideGson() {
return new GsonBuilder().create();
}
}

View file

@ -0,0 +1,36 @@
package fr.free.nrw.commons.glide;
import android.support.annotation.NonNull;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.SimpleResource;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import java.io.IOException;
import java.io.InputStream;
/**
* Decodes an SVG internal representation from an {@link InputStream}.
*/
public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {
@Override
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
// TODO: Can we tell?
return true;
}
public Resource<SVG> decode(@NonNull InputStream source, int width, int height,
@NonNull Options options)
throws IOException {
try {
SVG svg = SVG.getFromInputStream(source);
return new SimpleResource<>(svg);
} catch (SVGParseException ex) {
throw new IOException("Cannot load SVG from stream", ex);
}
}
}

View file

@ -0,0 +1,28 @@
package fr.free.nrw.commons.glide;
import android.graphics.Picture;
import android.graphics.drawable.PictureDrawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.SimpleResource;
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
import com.caverock.androidsvg.SVG;
/**
* Convert the {@link SVG}'s internal representation to an Android-compatible one
* ({@link Picture}).
*/
public class SvgDrawableTranscoder implements ResourceTranscoder<SVG, PictureDrawable> {
@Nullable
@Override
public Resource<PictureDrawable> transcode(@NonNull Resource<SVG> toTranscode,
@NonNull Options options) {
SVG svg = toTranscode.get();
Picture picture = svg.renderToPicture();
PictureDrawable drawable = new PictureDrawable(picture);
return new SimpleResource<>(drawable);
}
}

View file

@ -0,0 +1,51 @@
package fr.free.nrw.commons.glide;
import android.graphics.drawable.PictureDrawable;
import android.widget.ImageView;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.ImageViewTarget;
import com.bumptech.glide.request.target.Target;
/**
* Listener which updates the {@link ImageView} to be software rendered, because
* {@link com.caverock.androidsvg.SVG SVG}/{@link android.graphics.Picture Picture} can't render on
* a hardware backed {@link android.graphics.Canvas Canvas}.
*/
public class SvgSoftwareLayerSetter implements RequestListener<PictureDrawable> {
/**
* Sets the layer type to none if the load fails
* @param e
* @param model
* @param target
* @param isFirstResource
* @return
*/
@Override
public boolean onLoadFailed(GlideException e, Object model, Target<PictureDrawable> target,
boolean isFirstResource) {
ImageView view = ((ImageViewTarget<?>) target).getView();
view.setLayerType(ImageView.LAYER_TYPE_NONE, null);
return false;
}
/**
* Sets the layer type to software when the resource is ready
* @param resource
* @param model
* @param target
* @param dataSource
* @param isFirstResource
* @return
*/
@Override
public boolean onResourceReady(PictureDrawable resource, Object model,
Target<PictureDrawable> target, DataSource dataSource, boolean isFirstResource) {
ImageView view = ((ImageViewTarget<?>) target).getView();
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
return false;
}
}

View file

@ -284,6 +284,7 @@ public class LocationServiceManager implements LocationListener {
LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers
LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving
LOCATION_NOT_CHANGED,
PERMISSION_JUST_GRANTED
PERMISSION_JUST_GRANTED,
MAP_UPDATED
}
}

View file

@ -35,6 +35,9 @@ import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Provider;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.License;
import fr.free.nrw.commons.LicenseList;
import fr.free.nrw.commons.Media;
@ -56,16 +59,16 @@ import static android.widget.Toast.LENGTH_SHORT;
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private boolean editable;
private boolean isFeaturedMedia;
private boolean isCategoryImage;
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
private int index;
public static MediaDetailFragment forMedia(int index, boolean editable, boolean isFeaturedMedia) {
public static MediaDetailFragment forMedia(int index, boolean editable, boolean isCategoryImage) {
MediaDetailFragment mf = new MediaDetailFragment();
Bundle state = new Bundle();
state.putBoolean("editable", editable);
state.putBoolean("isFeaturedMedia", isFeaturedMedia);
state.putBoolean("isCategoryImage", isCategoryImage);
state.putInt("index", index);
state.putInt("listIndex", 0);
state.putInt("listTop", 0);
@ -128,7 +131,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
super.onSaveInstanceState(outState);
outState.putInt("index", index);
outState.putBoolean("editable", editable);
outState.putBoolean("isFeaturedMedia", isFeaturedMedia);
outState.putBoolean("isCategoryImage", isCategoryImage);
getScrollPosition();
outState.putInt("listTop", initialListTop);
@ -144,12 +147,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if (savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable");
isFeaturedMedia = savedInstanceState.getBoolean("isFeaturedMedia");
isCategoryImage = savedInstanceState.getBoolean("isCategoryImage");
index = savedInstanceState.getInt("index");
initialListTop = savedInstanceState.getInt("listTop");
} else {
editable = getArguments().getBoolean("editable");
isFeaturedMedia = getArguments().getBoolean("isFeaturedMedia");
isCategoryImage = getArguments().getBoolean("isCategoryImage");
index = getArguments().getInt("index");
initialListTop = 0;
}
@ -161,7 +164,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
ButterKnife.bind(this,view);
if (isFeaturedMedia){
if (isCategoryImage){
authorLayout.setVisibility(VISIBLE);
} else {
authorLayout.setVisibility(GONE);
@ -328,7 +331,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if (!TextUtils.isEmpty(licenseLink(media))) {
openWebBrowser(licenseLink(media));
} else {
if(isFeaturedMedia) {
if(isCategoryImage) {
Timber.d("Unable to fetch license URL for %s", media.getLicense());
} else {
Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT);
@ -503,8 +506,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if (media.getRequestedDeletion()){
delete.setVisibility(GONE);
nominatedForDeletion.setVisibility(VISIBLE);
}
else{
} else if (!isCategoryImage) {
delete.setVisibility(VISIBLE);
nominatedForDeletion.setVisibility(GONE);
}

View file

@ -2,9 +2,11 @@ package fr.free.nrw.commons.media;
import android.annotation.SuppressLint;
import android.app.DownloadManager;
import android.app.WallpaperManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@ -26,6 +28,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import java.io.IOException;
import butterknife.BindView;
import butterknife.ButterKnife;
import javax.inject.Inject;
@ -38,12 +42,15 @@ import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ImageUtils;
import timber.log.Timber;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.content.Context.DOWNLOAD_SERVICE;
import static android.content.Intent.ACTION_VIEW;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.widget.Toast.LENGTH_SHORT;
import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext;
public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener {
@ -140,6 +147,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
// Download
downloadMedia(m);
return true;
case R.id.menu_set_as_wallpaper:
// Set wallpaper
setWallpaper(m);
return true;
case R.id.menu_retry_current_image:
// Retry
((ContributionsActivity) getActivity()).retryUpload(pager.getCurrentItem());
@ -155,6 +166,19 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
}
}
/**
* Set the media as the device's wallpaper if the imageUrl is not null
* Fails silently if setting the wallpaper fails
* @param media
*/
private void setWallpaper(Media media) {
if(media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
Timber.d("Media URL not present");
return;
}
ImageUtils.setWallpaperFromImageUrl(getActivity(), Uri.parse(media.getImageUrl()));
}
/**
* Start the media file downloading to the local SD card/storage.
* The file can then be opened in Gallery or other apps.

View file

@ -25,6 +25,8 @@ import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.util.EntityUtils;
import org.mediawiki.api.ApiResult;
import org.mediawiki.api.MWApi;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.IOException;
@ -62,6 +64,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private static final String THUMB_SIZE = "640";
private AbstractHttpClient httpClient;
private MWApi api;
private MWApi wikidataApi;
private Context context;
private SharedPreferences defaultPreferences;
private SharedPreferences categoryPreferences;
@ -69,6 +72,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
public ApacheHttpClientMediaWikiApi(Context context,
String apiURL,
String wikidatApiURL,
SharedPreferences defaultPreferences,
SharedPreferences categoryPreferences,
Gson gson) {
@ -82,6 +86,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent());
httpClient = new DefaultHttpClient(cm, params);
api = new MWApi(apiURL, httpClient);
wikidataApi = new MWApi(wikidatApiURL, httpClient);
this.defaultPreferences = defaultPreferences;
this.categoryPreferences = categoryPreferences;
this.gson = gson;
@ -206,6 +211,15 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return api.getEditToken();
}
@Override
public String getCentralAuthToken() throws IOException {
String centralAuthToken = api.action("centralauthtoken")
.get()
.getString("/api/centralauthtoken/@centralauthtoken");
Timber.d("MediaWiki Central auth token is %s", centralAuthToken);
return centralAuthToken;
}
@Override
public boolean fileExistsWithName(String fileName) throws IOException {
return api.action("query")
@ -351,6 +365,98 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
}).flatMapObservable(Observable::fromIterable);
}
/**
* Get the edit token for making wiki data edits
* https://www.mediawiki.org/wiki/API:Tokens
* @return
* @throws IOException
*/
private String getWikidataEditToken() throws IOException {
return wikidataApi.getEditToken();
}
@Override
public String getWikidataCsrfToken() throws IOException {
String wikidataCsrfToken = wikidataApi.action("query")
.param("action", "query")
.param("centralauthtoken", getCentralAuthToken())
.param("meta", "tokens")
.post()
.getString("/api/query/tokens/@csrftoken");
Timber.d("Wikidata csrf token is %s", wikidataCsrfToken);
return wikidataCsrfToken;
}
/**
* Creates a new claim using the wikidata API
* https://www.mediawiki.org/wiki/Wikibase/API
* @param entityId the wikidata entity to be edited
* @param property the property to be edited, for eg P18 for images
* @param snaktype the type of value stored for that property
* @param value the actual value to be stored for the property, for eg filename in case of P18
* @return returns revisionId if the claim is successfully created else returns null
* @throws IOException
*/
@Nullable
@Override
public String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException {
Timber.d("Filename is %s", value);
ApiResult result = wikidataApi.action("wbcreateclaim")
.param("entity", entityId)
.param("centralauthtoken", getCentralAuthToken())
.param("token", getWikidataCsrfToken())
.param("snaktype", snaktype)
.param("property", property)
.param("value", value)
.post();
if (result == null || result.getNode("api") == null) {
return null;
}
Node node = result.getNode("api").getDocument();
Element element = (Element) node;
if (element != null && element.getAttribute("success").equals("1")) {
return result.getString("api/pageinfo/@lastrevid");
} else {
Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info"));
}
return null;
}
/**
* Adds the wikimedia-commons-app tag to the edits made on wikidata
* @param revisionId
* @return
* @throws IOException
*/
@Nullable
@Override
public boolean addWikidataEditTag(String revisionId) throws IOException {
ApiResult result = wikidataApi.action("tag")
.param("revid", revisionId)
.param("centralauthtoken", getCentralAuthToken())
.param("token", getWikidataCsrfToken())
.param("add", "wikimedia-commons-app")
.param("reason", "Add tag for edits made using Android Commons app")
.post();
if (result == null || result.getNode("api") == null) {
return false;
}
Node node = result.getNode("api").getDocument();
Element element = (Element) node;
if (element != null && element.getAttribute("status").equals("success")) {
return true;
} else {
Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info"));
}
return false;
}
@Override
@NonNull
public Observable<String> searchTitles(String title, int searchCatsLimit) {
@ -444,8 +550,8 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
.param("notprop", "list")
.param("format", "xml")
.param("meta", "notifications")
// .param("meta", "notifications")
.param("notformat", "model")
.param("notwikis", "wikidatawiki|commonswiki|enwiki")
.get()
.getNode("/api/query/notifications/list");
} catch (IOException e) {
@ -586,6 +692,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
String resultStatus = result.getString("/api/upload/@result");
if (!resultStatus.equals("Success")) {
String errorCode = result.getString("/api/error/@code");
Timber.e(errorCode);
return new UploadResult(resultStatus, errorCode);
} else {
Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));

View file

@ -0,0 +1,101 @@
package fr.free.nrw.commons.mwapi;
import com.google.gson.Gson;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.mwapi.model.ApiResponse;
import fr.free.nrw.commons.mwapi.model.Page;
import fr.free.nrw.commons.mwapi.model.PageCategory;
import io.reactivex.Single;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import timber.log.Timber;
/**
* Uses the OkHttp library to implement calls to the Commons MediaWiki API to match GPS coordinates
* with nearby Commons categories. Parses the results using GSON to obtain a list of relevant
* categories. Note: that caller is responsible for executing the request() method on a background
* thread.
*/
public class CategoryApi {
private final OkHttpClient okHttpClient;
private final HttpUrl mwUrl;
private final Gson gson;
@Inject
public CategoryApi(OkHttpClient okHttpClient, Gson gson,
@Named("commons_mediawiki_url") HttpUrl mwUrl) {
this.okHttpClient = okHttpClient;
this.mwUrl = mwUrl;
this.gson = gson;
}
public Single<List<String>> request(String coords) {
return Single.fromCallable(() -> {
HttpUrl apiUrl = buildUrl(coords);
Timber.d("URL: %s", apiUrl.toString());
Request request = new Request.Builder().get().url(apiUrl).build();
Response response = okHttpClient.newCall(request).execute();
ResponseBody body = response.body();
if (body == null) {
return Collections.emptyList();
}
ApiResponse apiResponse = gson.fromJson(body.charStream(), ApiResponse.class);
Set<String> categories = new LinkedHashSet<>();
if (apiResponse != null && apiResponse.hasPages()) {
for (Page page : apiResponse.query.pages) {
for (PageCategory category : page.getCategories()) {
categories.add(category.withoutPrefix());
}
}
}
return new ArrayList<>(categories);
});
}
/**
* Builds URL with image coords for MediaWiki API calls
* Example URL: https://commons.wikimedia.org/w/api.php?action=query&prop=categories|coordinates|pageprops&format=json&clshow=!hidden&coprop=type|name|dim|country|region|globe&codistancefrompoint=38.11386944444445|13.356263888888888&generator=geosearch&redirects=&ggscoord=38.11386944444445|1.356263888888888&ggsradius=100&ggslimit=10&ggsnamespace=6&ggsprop=type|name|dim|country|region|globe&ggsprimary=all&formatversion=2
*
* @param coords Coordinates to build query with
* @return URL for API query
*/
private HttpUrl buildUrl(String coords) {
return mwUrl.newBuilder()
.addPathSegment("w")
.addPathSegment("api.php")
.addQueryParameter("action", "query")
.addQueryParameter("prop", "categories|coordinates|pageprops")
.addQueryParameter("format", "json")
.addQueryParameter("clshow", "!hidden")
.addQueryParameter("coprop", "type|name|dim|country|region|globe")
.addQueryParameter("codistancefrompoint", coords)
.addQueryParameter("generator", "geosearch")
.addQueryParameter("ggscoord", coords)
.addQueryParameter("ggsradius", "10000")
.addQueryParameter("ggslimit", "10")
.addQueryParameter("ggsnamespace", "6")
.addQueryParameter("ggsprop", "type|name|dim|country|region|globe")
.addQueryParameter("ggsprimary", "all")
.addQueryParameter("formatversion", "2")
.build();
}
}

View file

@ -27,6 +27,10 @@ public interface MediaWikiApi {
String getEditToken() throws IOException;
String getWikidataCsrfToken() throws IOException;
String getCentralAuthToken() throws IOException;
boolean fileExistsWithName(String fileName) throws IOException;
boolean pageExists(String pageName) throws IOException;
@ -49,6 +53,12 @@ public interface MediaWikiApi {
@Nullable
String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
@Nullable
String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException;
@Nullable
boolean addWikidataEditTag(String revisionId) throws IOException;
@NonNull
MediaResult fetchMediaByFilename(String filename) throws IOException;

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.mwapi.model;
public class ApiResponse {
public Query query;
public ApiResponse() {
}
public boolean hasPages() {
return query != null && query.pages != null;
}
}

View file

@ -0,0 +1,17 @@
package fr.free.nrw.commons.mwapi.model;
import android.support.annotation.NonNull;
public class Page {
public String title;
public PageCategory[] categories;
public PageCategory category;
public Page() {
}
@NonNull
public PageCategory[] getCategories() {
return categories != null ? categories : new PageCategory[0];
}
}

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.mwapi.model;
public class PageCategory {
public String title;
public PageCategory() {
}
public String withoutPrefix() {
return title != null ? title.replace("Category:", "") : "";
}
}

View file

@ -0,0 +1,10 @@
package fr.free.nrw.commons.mwapi.model;
public class Query {
public Page[] pages;
public Query() {
pages = new Page[0];
}
}

View file

@ -42,11 +42,13 @@ import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.UriSerializer;
import fr.free.nrw.commons.utils.ViewUtil;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@ -55,8 +57,12 @@ import timber.log.Timber;
import uk.co.deanwild.materialshowcaseview.IShowcaseListener;
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.*;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED;
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener {
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener,
WikidataEditListener.WikidataP18EditListener {
private static final int LOCATION_REQUEST = 1;
@ -76,6 +82,8 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
LocationServiceManager locationManager;
@Inject
NearbyController nearbyController;
@Inject WikidataEditListener wikidataEditListener;
@Inject
@Named("application_preferences") SharedPreferences applicationPrefs;
private LatLng curLatLng;
@ -110,6 +118,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
initBottomSheetBehaviour();
initDrawer();
wikidataEditListener.setAuthenticationStateListener(this);
}
private void resumeFragment() {
@ -219,7 +228,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
//Still need to check if GPS is enabled
checkGps();
lastKnownLocation = locationManager.getLKL();
refreshView(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED);
refreshView(PERMISSION_JUST_GRANTED);
} else {
//If permission not granted, go to page that says Nearby Places cannot be displayed
hideProgressBar();
@ -279,7 +288,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
private void checkLocationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (locationManager.isLocationPermissionGranted()) {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
} else {
// Should we show an explanation?
if (locationManager.isPermissionExplanationRequired(this)) {
@ -305,7 +314,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
}
}
} else {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
}
@ -314,7 +323,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1) {
Timber.d("User is back from Settings page");
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
}
@ -373,8 +382,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
@Override
public void onReceive(Context context, Intent intent) {
if (NetworkUtils.isInternetConnectionEstablished(NearbyActivity.this)) {
refreshView(LocationServiceManager
.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
} else {
ViewUtil.showLongToast(NearbyActivity.this, getString(R.string.no_internet));
}
@ -390,7 +398,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
*
* @param locationChangeType defines if location shanged significantly or slightly
*/
private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) {
private void refreshView(LocationChangeType locationChangeType) {
if (lockNearbyView) {
return;
}
@ -403,12 +411,13 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
registerLocationUpdates();
LatLng lastLocation = locationManager.getLastLocation();
if (curLatLng != null && curLatLng.equals(lastLocation)) { //refresh view only if location has changed
if (curLatLng != null && curLatLng.equals(lastLocation)
&& !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed
return;
}
curLatLng = lastLocation;
if (locationChangeType.equals(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED)) {
if (locationChangeType.equals(PERMISSION_JUST_GRANTED)) {
curLatLng = lastKnownLocation;
}
@ -417,8 +426,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
return;
}
if (locationChangeType.equals(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)
|| locationChangeType.equals(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED)) {
if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)
|| locationChangeType.equals(PERMISSION_JUST_GRANTED)
|| locationChangeType.equals(MAP_UPDATED)) {
progressBar.setVisibility(View.VISIBLE);
//TODO: This hack inserts curLatLng before populatePlaces is called (see #1440). Ideally a proper fix should be found
@ -440,7 +450,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
progressBar.setVisibility(View.GONE);
});
} else if (locationChangeType
.equals(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) {
.equals(LOCATION_SLIGHTLY_CHANGED)) {
Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriSerializer())
.create();
@ -685,12 +695,12 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
@Override
public void onLocationChangedSignificantly(LatLng latLng) {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
@Override
public void onLocationChangedSlightly(LatLng latLng) {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED);
refreshView(LOCATION_SLIGHTLY_CHANGED);
}
public void prepareViewsForSheetPosition(int bottomSheetState) {
@ -700,4 +710,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
private void showErrorMessage(String message) {
ViewUtil.showLongToast(NearbyActivity.this, message);
}
@Override
public void onWikidataEditSuccessful() {
refreshView(MAP_UPDATED);
}
}

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.nearby;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
@ -21,6 +22,9 @@ import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.android.support.AndroidSupportInjection;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R;
@ -47,6 +51,11 @@ public class NearbyListFragment extends DaggerFragment {
private RecyclerView recyclerView;
private ContributionController controller;
@Inject
@Named("direct_nearby_upload_prefs")
SharedPreferences directPrefs;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -137,7 +146,7 @@ public class NearbyListFragment extends DaggerFragment {
if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);
controller.handleImagePicked(requestCode, data, true);
controller.handleImagePicked(requestCode, data, true, directPrefs.getString("WikiDataEntityId", null));
} else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);

View file

@ -731,6 +731,7 @@ public class NearbyMapFragment extends DaggerFragment {
editor.putString("Title", place.getName());
editor.putString("Desc", place.getLongDescription());
editor.putString("Category", place.getCategory());
editor.putString("WikiDataEntityId", place.getWikiDataEntityId());
editor.apply();
}
@ -766,7 +767,7 @@ public class NearbyMapFragment extends DaggerFragment {
if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);
controller.handleImagePicked(requestCode, data, true);
controller.handleImagePicked(requestCode, data, true, directPrefs.getString("WikiDataEntityId", null));
} else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);

View file

@ -17,7 +17,7 @@ import java.util.regex.Pattern;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.FileUtils;
import fr.free.nrw.commons.upload.FileUtils;
import timber.log.Timber;
public class NearbyPlaces {

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.nearby;
import android.graphics.Bitmap;
import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
@ -50,6 +51,20 @@ public class Place {
this.distance = distance;
}
/**
* Extracts the entity id from the wikidata link
* @return returns the entity id if wikidata link exists
*/
@Nullable
public String getWikiDataEntityId() {
if (!hasWikidataLink()) {
return null;
}
String wikiDataLink = siteLinks.getWikidataLink().toString();
return wikiDataLink.replace("http://www.wikidata.org/entity/", "");
}
public boolean hasWikipediaLink() {
return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getWikipediaLink()));
}

View file

@ -16,7 +16,6 @@ import android.widget.RelativeLayout;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.List;
@ -26,6 +25,7 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
@ -46,6 +46,8 @@ public class NotificationActivity extends NavigationBaseActivity {
@BindView(R.id.container) RelativeLayout relativeLayout;
@Inject NotificationController controller;
@Inject
MediaWikiApi mediaWikiApi;
private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment";
private NotificationWorkerFragment mNotificationWorkerFragment;
@ -81,7 +83,6 @@ public class NotificationActivity extends NavigationBaseActivity {
}
}
@SuppressLint("CheckResult")
private void addNotifications() {
Timber.d("Add notifications");

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.notification;
import android.util.Log;
import android.graphics.drawable.PictureDrawable;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -8,17 +9,23 @@ import android.widget.ImageView;
import android.widget.TextView;
import com.borjabravo.readmoretextview.ReadMoreTextView;
import com.bumptech.glide.RequestBuilder;
import com.pedrogomez.renderers.Renderer;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.glide.SvgSoftwareLayerSetter;
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
/**
* Created by root on 19.12.2017.
*/
public class NotificationRenderer extends Renderer<Notification> {
private RequestBuilder<PictureDrawable> requestBuilder;
@BindView(R.id.title) ReadMoreTextView title;
@BindView(R.id.time) TextView time;
@BindView(R.id.icon) ImageView icon;
@ -41,23 +48,32 @@ public class NotificationRenderer extends Renderer<Notification> {
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false);
ButterKnife.bind(this, inflatedView);
requestBuilder = GlideApp.with(inflatedView.getContext())
.as(PictureDrawable.class)
.error(R.drawable.round_icon_unknown)
.transition(withCrossFade())
.listener(new SvgSoftwareLayerSetter());
return inflatedView;
}
@Override
public void render() {
Notification notification = getContent();
String str = notification.notificationText.trim();
str = str.concat(" ");
title.setText(str);
setTitle(notification.notificationText);
time.setText(notification.date);
switch (notification.notificationType) {
case THANK_YOU_EDIT:
icon.setImageResource(R.drawable.ic_edit_black_24dp);
break;
default:
icon.setImageResource(R.drawable.round_icon_unknown);
}
requestBuilder.load(notification.iconUrl).into(icon);
}
/**
* Cleans up the notification text and sets it as the title
* Clean up is required to fix escaped HTML string and extra white spaces at the beginning of the notification
* @param notificationText
*/
private void setTitle(String notificationText) {
notificationText = notificationText.trim().replaceAll("(^\\h*)|(\\h*$)", "");
notificationText = Html.fromHtml(notificationText).toString();
notificationText = notificationText.concat(" ");
title.setText(notificationText);
}
public interface NotificationClicked{

View file

@ -16,12 +16,13 @@ import javax.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import static fr.free.nrw.commons.notification.NotificationType.THANK_YOU_EDIT;
import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN;
public class NotificationUtils {
private static final String COMMONS_WIKI = "commonswiki";
private static final String WIKIDATA_WIKI = "wikidatawiki";
private static final String WIKIPEDIA_WIKI = "enwiki";
public static boolean isCommonsNotification(Node document) {
if (document == null || !document.hasAttributes()) {
@ -31,6 +32,32 @@ public class NotificationUtils {
return COMMONS_WIKI.equals(element.getAttribute("wiki"));
}
/**
* Returns true if the wiki attribute corresponds to wikidatawiki
* @param document
* @return
*/
public static boolean isWikidataNotification(Node document) {
if (document == null || !document.hasAttributes()) {
return false;
}
Element element = (Element) document;
return WIKIDATA_WIKI.equals(element.getAttribute("wiki"));
}
/**
* Returns true if the wiki attribute corresponds to enwiki
* @param document
* @return
*/
public static boolean isWikipediaNotification(Node document) {
if (document == null || !document.hasAttributes()) {
return false;
}
Element element = (Element) document;
return WIKIPEDIA_WIKI.equals(element.getAttribute("wiki"));
}
public static NotificationType getNotificationType(Node document) {
Element element = (Element) document;
String type = element.getAttribute("type");
@ -68,10 +95,17 @@ public class NotificationUtils {
return notifications;
}
/**
* Currently the app is interested in showing notifications just from the following three wikis: commons, wikidata, wikipedia
* This function returns true only if the notification belongs to any of the above wikis and is of a known notification type
* @param node
* @return
*/
private static boolean isUsefulNotification(Node node) {
return isCommonsNotification(node)
&& !getNotificationType(node).equals(UNKNOWN)
&& !getNotificationType(node).equals(THANK_YOU_EDIT);
return (isCommonsNotification(node)
|| isWikidataNotification(node)
|| isWikipediaNotification(node))
&& !getNotificationType(node).equals(UNKNOWN);
}
public static boolean isBundledNotification(Node document) {
@ -97,7 +131,7 @@ public class NotificationUtils {
switch (type) {
case THANK_YOU_EDIT:
notificationText = context.getString(R.string.notifications_thank_you_edit);
notificationText = getThankYouEditDescription(document);
break;
case EDIT_USER_TALK:
notificationText = getNotificationText(document);
@ -146,6 +180,16 @@ public class NotificationUtils {
return body != null ? body.getTextContent() : "";
}
/**
* Gets the header node returned in the XML document to form the description for thank you edits
* @param document
* @return
*/
private static String getThankYouEditDescription(Node document) {
Node body = getNode(getModel(document), "header");
return body != null ? body.getTextContent() : "";
}
private static String getNotificationIconUrl(Node document) {
String format = "%s%s";
Node iconUrl = getNode(getModel(document), "iconUrl");

View file

@ -0,0 +1,35 @@
package fr.free.nrw.commons.notification;
import android.content.Context;
import android.graphics.drawable.PictureDrawable;
import android.support.annotation.NonNull;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
import com.caverock.androidsvg.SVG;
import java.io.InputStream;
import fr.free.nrw.commons.glide.SvgDecoder;
import fr.free.nrw.commons.glide.SvgDrawableTranscoder;
/**
* Module for the SVG sample app.
*/
@GlideModule
public class SvgModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide,
@NonNull Registry registry) {
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
.append(InputStream.class, SVG.class, new SvgDecoder());
}
// Disable manifest parsing to avoid adding similar modules twice.
@Override
public boolean isManifestParsingEnabled() {
return false;
}
}

View file

@ -3,13 +3,10 @@ package fr.free.nrw.commons.settings;
import android.Manifest;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@ -24,8 +21,6 @@ import android.support.v4.content.FileProvider;
import android.widget.Toast;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
@ -35,7 +30,7 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.utils.FileUtils;
import fr.free.nrw.commons.upload.FileUtils;
public class SettingsFragment extends PreferenceFragment {

View file

@ -0,0 +1,263 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.CategoryApi;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext;
/**
* Processing of the image file that is about to be uploaded via ShareActivity is done here
*/
public class FileProcessor implements SimilarImageDialogFragment.onResponse {
@Inject
CacheController cacheController;
@Inject
GpsCategoryModel gpsCategoryModel;
@Inject
CategoryApi apiCall;
@Inject
@Named("default_preferences")
SharedPreferences prefs;
private Uri mediaUri;
private ContentResolver contentResolver;
private GPSExtractor imageObj;
private Context context;
private String decimalCoords;
private boolean haveCheckedForOtherImages = false;
private String filePath;
private boolean useExtStorage;
private boolean cacheFound;
private GPSExtractor tempImageObj;
FileProcessor(Uri mediaUri, ContentResolver contentResolver, Context context) {
this.mediaUri = mediaUri;
this.contentResolver = contentResolver;
this.context = context;
ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this);
useExtStorage = prefs.getBoolean("useExternalStorage", true);
}
/**
* Gets file path from media URI.
* In older devices getPath() may fail depending on the source URI, creating and using a copy of the file seems to work instead.
*
* @return file path of media
*/
@Nullable
private String getPathOfMediaOrCopy() {
filePath = FileUtils.getPath(context, mediaUri);
Timber.d("Filepath: " + filePath);
if (filePath == null) {
String copyPath = null;
try {
ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r");
if (descriptor != null) {
if (useExtStorage) {
copyPath = FileUtils.createCopyPath(descriptor);
return copyPath;
}
copyPath = getApplicationContext().getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + ".jpg";
FileUtils.copy(descriptor.getFileDescriptor(), copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
} catch (IOException e) {
Timber.w(e, "Error in file " + copyPath);
return null;
}
}
return filePath;
}
/**
* Processes file coordinates, either from EXIF data or user location
*
* @param gpsEnabled if true use GPS
*/
GPSExtractor processFileCoordinates(boolean gpsEnabled) {
Timber.d("Calling GPSExtractor");
try {
ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (descriptor != null) {
imageObj = new GPSExtractor(descriptor.getFileDescriptor(), context, prefs);
}
} else {
String filePath = getPathOfMediaOrCopy();
if (filePath != null) {
imageObj = new GPSExtractor(filePath, context, prefs);
}
}
decimalCoords = imageObj.getCoords(gpsEnabled);
if (decimalCoords == null || !imageObj.imageCoordsExists) {
//Find other photos taken around the same time which has gps coordinates
if (!haveCheckedForOtherImages)
findOtherImages(gpsEnabled);// Do not do repeat the process
} else {
useImageCoords();
}
} catch (FileNotFoundException e) {
Timber.w("File not found: " + mediaUri, e);
}
return imageObj;
}
String getDecimalCoords() {
return decimalCoords;
}
/**
* Find other images around the same location that were taken within the last 20 sec
*
* @param gpsEnabled True if GPS is enabled
*/
private void findOtherImages(boolean gpsEnabled) {
Timber.d("filePath" + getPathOfMediaOrCopy());
long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created
File folder = new File(filePath.substring(0, filePath.lastIndexOf('/')));
File[] files = folder.listFiles();
Timber.d("folderTime Number:" + files.length);
for (File file : files) {
if (file.lastModified() - timeOfCreation <= (120 * 1000) && file.lastModified() - timeOfCreation >= -(120 * 1000)) {
//Make sure the photos were taken within 20seconds
Timber.d("fild date:" + file.lastModified() + " time of creation" + timeOfCreation);
tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos
ParcelFileDescriptor descriptor = null;
try {
descriptor = contentResolver.openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (descriptor != null) {
tempImageObj = new GPSExtractor(descriptor.getFileDescriptor(), context, prefs);
}
} else {
if (filePath != null) {
tempImageObj = new GPSExtractor(file.getAbsolutePath(), context, prefs);
}
}
if (tempImageObj != null) {
Timber.d("not null fild EXIF" + tempImageObj.imageCoordsExists + " coords" + tempImageObj.getCoords(gpsEnabled));
if (tempImageObj.getCoords(gpsEnabled) != null && tempImageObj.imageCoordsExists) {
// Current image has gps coordinates and it's not current gps locaiton
Timber.d("This file has image coords:" + file.getAbsolutePath());
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
Bundle args = new Bundle();
args.putString("originalImagePath", filePath);
args.putString("possibleImagePath", file.getAbsolutePath());
newFragment.setArguments(args);
newFragment.show(((AppCompatActivity) context).getSupportFragmentManager(), "dialog");
break;
}
}
}
}
haveCheckedForOtherImages = true; //Finished checking for other images
}
/**
* Initiates retrieval of image coordinates or user coordinates, and caching of coordinates.
* Then initiates the calls to MediaWiki API through an instance of CategoryApi.
*/
@SuppressLint("CheckResult")
public void useImageCoords() {
if (decimalCoords != null) {
Timber.d("Decimal coords of image: %s", decimalCoords);
Timber.d("is EXIF data present:" + imageObj.imageCoordsExists + " from findOther image");
// Only set cache for this point if image has coords
if (imageObj.imageCoordsExists) {
double decLongitude = imageObj.getDecLongitude();
double decLatitude = imageObj.getDecLatitude();
cacheController.setQtPoint(decLongitude, decLatitude);
}
List<String> displayCatList = cacheController.findCategory();
boolean catListEmpty = displayCatList.isEmpty();
// If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories
if (catListEmpty) {
cacheFound = false;
apiCall.request(decimalCoords)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(
gpsCategoryModel::setCategoryList,
throwable -> {
Timber.e(throwable);
gpsCategoryModel.clear();
}
);
Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList);
} else {
cacheFound = true;
Timber.d("Cache found, setting categoryList in model to %s", displayCatList);
gpsCategoryModel.setCategoryList(displayCatList);
}
} else {
Timber.d("EXIF: no coords");
}
}
boolean isCacheFound() {
return cacheFound;
}
/**
* Calls the async task that detects if image is fuzzy, too dark, etc
*/
void detectUnwantedPictures() {
String imageMediaFilePath = FileUtils.getPath(context, mediaUri);
DetectUnwantedPicturesAsync detectUnwantedPicturesAsync
= new DetectUnwantedPicturesAsync(new WeakReference<Activity>((Activity) context), imageMediaFilePath);
detectUnwantedPicturesAsync.execute();
}
@Override
public void onPositiveResponse() {
imageObj = tempImageObj;
decimalCoords = imageObj.getCoords(false);// Not necessary to use gps as image already ha EXIF data
Timber.d("EXIF from tempImageObj");
useImageCoords();
}
@Override
public void onNegativeResponse() {
Timber.d("EXIF from imageObj");
useImageCoords();
}
}

View file

@ -15,18 +15,84 @@ import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.math.BigInteger;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import timber.log.Timber;
public class FileUtils {
/**
* Get SHA1 of file from input stream
*/
static String getSHA1(InputStream is) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
Timber.e(e, "Exception while getting Digest");
return "";
}
byte[] buffer = new byte[8192];
int read;
try {
while ((read = is.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
byte[] md5sum = digest.digest();
BigInteger bigInt = new BigInteger(1, md5sum);
String output = bigInt.toString(16);
// Fill to 40 chars
output = String.format("%40s", output).replace(' ', '0');
Timber.i("File SHA1: %s", output);
return output;
} catch (IOException e) {
Timber.e(e, "IO Exception");
return "";
} finally {
try {
is.close();
} catch (IOException e) {
Timber.e(e, "Exception on closing MD5 input stream");
}
}
}
/**
* In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead.
* @return path of copy
*/
@Nullable
static String createCopyPath(ParcelFileDescriptor descriptor) {
try {
String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + ".jpg";
File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
newFile.mkdir();
FileUtils.copy(descriptor.getFileDescriptor(), copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
} catch (IOException e) {
Timber.e(e);
return null;
}
}
/**
* Get a file path from a Uri. This will get the the path for Storage Access
* Framework Documents, as well as the _data field for the MediaStore and
@ -235,4 +301,80 @@ public class FileUtils {
copy(new FileInputStream(source), new FileOutputStream(destination));
}
/**
* Read and return the content of a resource file as string.
* @param fileName asset file's path (e.g. "/queries/nearby_query.rq")
* @return the content of the file
*/
public static String readFromResource(String fileName) throws IOException {
StringBuilder buffer = new StringBuilder();
BufferedReader reader = null;
try {
InputStream inputStream = FileUtils.class.getResourceAsStream(fileName);
if (inputStream == null) {
throw new FileNotFoundException(fileName);
}
reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
buffer.append(line).append("\n");
}
} finally {
if (reader != null) {
reader.close();
}
}
return buffer.toString();
}
/**
* Deletes files.
* @param file context
*/
public static boolean deleteFile(File file) {
boolean deletedAll = true;
if (file != null) {
if (file.isDirectory()) {
String[] children = file.list();
for (String child : children) {
deletedAll = deleteFile(new File(file, child)) && deletedAll;
}
} else {
deletedAll = file.delete();
}
}
return deletedAll;
}
public static File createAndGetAppLogsFile(String logs) {
try {
File commonsAppDirectory = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
if (!commonsAppDirectory.exists()) {
commonsAppDirectory.mkdir();
}
File logsFile = new File(commonsAppDirectory,"logs.txt");
if (logsFile.exists()) {
//old logs file is useless
logsFile.delete();
}
logsFile.createNewFile();
FileOutputStream outputStream = new FileOutputStream(logsFile);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
outputStreamWriter.append(logs);
outputStreamWriter.close();
outputStream.flush();
outputStream.close();
return logsFile;
} catch (IOException ioe) {
Timber.e(ioe);
return null;
}
}
}

View file

@ -0,0 +1,40 @@
package fr.free.nrw.commons.upload;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class GpsCategoryModel {
private Set<String> categorySet;
@Inject
public GpsCategoryModel() {
clear();
}
public void clear() {
categorySet = new HashSet<>();
}
public boolean getGpsCatExists() {
return !categorySet.isEmpty();
}
public List<String> getCategoryList() {
return new ArrayList<>(categorySet);
}
public void setCategoryList(List<String> categoryList) {
clear();
categorySet.addAll(categoryList != null ? categoryList : new ArrayList<>());
}
public void add(String categoryString) {
categorySet.add(categoryString);
}
}

View file

@ -47,6 +47,8 @@ import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber;
//TODO: We should use this class to see how multiple uploads are handled, and then REMOVE it.
public class MultipleShareActivity extends AuthenticatedActivity
implements MediaDetailPagerFragment.MediaDetailProvider,
AdapterView.OnItemClickListener,

View file

@ -1,249 +0,0 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import android.net.Uri;
import com.android.volley.Cache;
import com.android.volley.NetworkResponse;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HttpHeaderParser;
import com.android.volley.toolbox.JsonRequest;
import com.android.volley.toolbox.Volley;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import timber.log.Timber;
/**
* Uses the Volley library to implement asynchronous calls to the Commons MediaWiki API to match
* GPS coordinates with nearby Commons categories. Parses the results using GSON to obtain a list
* of relevant categories.
*/
public class MwVolleyApi {
private static RequestQueue REQUEST_QUEUE;
private static final Gson GSON = new GsonBuilder().create();
private static Set<String> categorySet;
private static List<String> categoryList;
private static final String MWURL = "https://commons.wikimedia.org/";
private final Context context;
public MwVolleyApi(Context context) {
this.context = context;
categorySet = new HashSet<>();
}
public static List<String> getGpsCat() {
return categoryList;
}
public static void setGpsCat(List<String> cachedList) {
categoryList = new ArrayList<>();
categoryList.addAll(cachedList);
Timber.d("Setting GPS cats from cache: %s", categoryList);
}
public void request(String coords) {
String apiUrl = buildUrl(coords);
Timber.d("URL: %s", apiUrl);
JsonRequest<QueryResponse> request = new QueryRequest(apiUrl,
new LogResponseListener<>(), new LogResponseErrorListener());
getQueue().add(request);
}
/**
* Builds URL with image coords for MediaWiki API calls
* Example URL: https://commons.wikimedia.org/w/api.php?action=query&prop=categories|coordinates|pageprops&format=json&clshow=!hidden&coprop=type|name|dim|country|region|globe&codistancefrompoint=38.11386944444445|13.356263888888888&generator=geosearch&redirects=&ggscoord=38.11386944444445|1.356263888888888&ggsradius=100&ggslimit=10&ggsnamespace=6&ggsprop=type|name|dim|country|region|globe&ggsprimary=all&formatversion=2
* @param coords Coordinates to build query with
* @return URL for API query
*/
private String buildUrl(String coords) {
Uri.Builder builder = Uri.parse(MWURL).buildUpon();
builder.appendPath("w")
.appendPath("api.php")
.appendQueryParameter("action", "query")
.appendQueryParameter("prop", "categories|coordinates|pageprops")
.appendQueryParameter("format", "json")
.appendQueryParameter("clshow", "!hidden")
.appendQueryParameter("coprop", "type|name|dim|country|region|globe")
.appendQueryParameter("codistancefrompoint", coords)
.appendQueryParameter("generator", "geosearch")
.appendQueryParameter("ggscoord", coords)
.appendQueryParameter("ggsradius", "10000")
.appendQueryParameter("ggslimit", "10")
.appendQueryParameter("ggsnamespace", "6")
.appendQueryParameter("ggsprop", "type|name|dim|country|region|globe")
.appendQueryParameter("ggsprimary", "all")
.appendQueryParameter("formatversion", "2");
return builder.toString();
}
private synchronized RequestQueue getQueue() {
if (REQUEST_QUEUE == null) {
REQUEST_QUEUE = Volley.newRequestQueue(context);
}
return REQUEST_QUEUE;
}
private static class LogResponseListener<T> implements Response.Listener<T> {
@Override
public void onResponse(T response) {
Timber.d(response.toString());
}
}
private static class LogResponseErrorListener implements Response.ErrorListener {
@Override
public void onErrorResponse(VolleyError error) {
Timber.e(error.toString());
}
}
private static class QueryRequest extends JsonRequest<QueryResponse> {
public QueryRequest(String url,
Response.Listener<QueryResponse> listener,
Response.ErrorListener errorListener) {
super(Request.Method.GET, url, null, listener, errorListener);
}
@Override
protected Response<QueryResponse> parseNetworkResponse(NetworkResponse response) {
String json = parseString(response);
QueryResponse queryResponse = GSON.fromJson(json, QueryResponse.class);
return Response.success(queryResponse, cacheEntry(response));
}
private Cache.Entry cacheEntry(NetworkResponse response) {
return HttpHeaderParser.parseCacheHeaders(response);
}
private String parseString(NetworkResponse response) {
try {
return new String(response.data, HttpHeaderParser.parseCharset(response.headers));
} catch (UnsupportedEncodingException e) {
return new String(response.data);
}
}
}
public static class GpsCatExists {
private static boolean gpsCatExists;
public static void setGpsCatExists(boolean gpsCat) {
gpsCatExists = gpsCat;
}
public static boolean getGpsCatExists() {
return gpsCatExists;
}
}
private static class QueryResponse {
private Query query = new Query();
private String printSet() {
if (categorySet == null || categorySet.isEmpty()) {
GpsCatExists.setGpsCatExists(false);
Timber.d("gpsCatExists=%b", GpsCatExists.getGpsCatExists());
return "No collection of categories";
} else {
GpsCatExists.setGpsCatExists(true);
Timber.d("gpsCatExists=%b", GpsCatExists.getGpsCatExists());
return "CATEGORIES FOUND" + categorySet.toString();
}
}
@Override
public String toString() {
if (query != null) {
return "query=" + query.toString() + "\n" + printSet();
} else {
return "No pages found";
}
}
}
private static class Query {
private Page [] pages;
@Override
public String toString() {
StringBuilder builder = new StringBuilder("pages=" + "\n");
if (pages != null) {
for (Page page : pages) {
builder.append(page.toString());
builder.append("\n");
}
builder.replace(builder.length() - 1, builder.length(), "");
return builder.toString();
} else {
return "No pages found";
}
}
}
public static class Page {
private int pageid;
private int ns;
private String title;
private Category[] categories;
private Category category;
public Page() {
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("PAGEID=" + pageid + " ns=" + ns + " title=" + title + "\n" + " CATEGORIES= ");
if (categories == null || categories.length == 0) {
builder.append("no categories exist\n");
} else {
for (Category category : categories) {
builder.append(category.toString());
builder.append("\n");
if (category != null) {
String categoryString = category.toString().replace("Category:", "");
categorySet.add(categoryString);
}
}
}
categoryList = new ArrayList<>(categorySet);
builder.replace(builder.length() - 1, builder.length(), "");
return builder.toString();
}
}
private static class Category {
private String title;
@Override
public String toString() {
return title;
}
}
}

View file

@ -91,7 +91,7 @@ public class UploadController {
* @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615")
* @param onComplete the progress tracker
*/
public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, ContributionUploadProgress onComplete) {
public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, String wikiDataEntityId, ContributionUploadProgress onComplete) {
Contribution contribution;
//TODO: Modify this to include coords
@ -101,6 +101,7 @@ public class UploadController {
contribution.setTag("mimeType", mimeType);
contribution.setSource(source);
contribution.setWikiDataEntityId(wikiDataEntityId);
//Calls the next overloaded method
startUpload(contribution, onComplete);

View file

@ -18,6 +18,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -36,6 +37,12 @@ import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.UploadResult;
import fr.free.nrw.commons.utils.ViewUtil;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import fr.free.nrw.commons.wikidata.WikidataEditService;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
public class UploadService extends HandlerService<Contribution> {
@ -49,8 +56,8 @@ public class UploadService extends HandlerService<Contribution> {
public static final String EXTRA_CAMPAIGN = EXTRA_PREFIX + ".campaign";
@Inject MediaWikiApi mwApi;
@Inject WikidataEditService wikidataEditService;
@Inject SessionManager sessionManager;
@Inject @Named("default_preferences") SharedPreferences prefs;
@Inject ContributionDao contributionDao;
private NotificationManager notificationManager;
@ -137,6 +144,7 @@ public class UploadService extends HandlerService<Contribution> {
@Override
public void queue(int what, Contribution contribution) {
Timber.d("Upload service queue has contribution with wiki data entity id as %s", contribution.getWikiDataEntityId());
switch (what) {
case ACTION_UPLOAD_FILE:
@ -253,6 +261,7 @@ public class UploadService extends HandlerService<Contribution> {
if (!resultStatus.equals("Success")) {
showFailedNotification(contribution);
} else {
wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), filename);
contribution.setFilename(uploadResult.getCanonicalFilename());
contribution.setImageUrl(uploadResult.getImageUrl());
contribution.setState(Contribution.STATE_COMPLETED);

View file

@ -0,0 +1,115 @@
package fr.free.nrw.commons.upload;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.v4.graphics.BitmapCompat;
import android.view.View;
import android.widget.FrameLayout;
import java.io.IOException;
import java.io.InputStream;
import timber.log.Timber;
/**
* Contains utility methods for the Zoom function in ShareActivity.
*/
public class Zoom {
private View thumbView;
private ContentResolver contentResolver;
private FrameLayout flContainer;
Zoom(View thumbView, FrameLayout flContainer, ContentResolver contentResolver) {
this.thumbView = thumbView;
this.contentResolver = contentResolver;
this.flContainer = flContainer;
}
/**
* Create a scaled bitmap to display the zoomed-in image
* @param input the input stream corresponding to the uploaded image
* @param imageUri the uploaded image's URI
* @return a zoomable bitmap
*/
Bitmap createScaledImage(InputStream input, Uri imageUri) {
Bitmap scaled = null;
BitmapRegionDecoder decoder = null;
Bitmap bitmap = null;
try {
decoder = BitmapRegionDecoder.newInstance(input, false);
bitmap = decoder.decodeRegion(new Rect(10, 10, 50, 50), null);
} catch (IOException e) {
Timber.e(e);
} catch (NullPointerException e) {
Timber.e(e);
}
try {
//Compress the Image
System.gc();
Runtime rt = Runtime.getRuntime();
long maxMemory = rt.freeMemory();
bitmap = MediaStore.Images.Media.getBitmap(contentResolver, imageUri);
int bitmapByteCount = BitmapCompat.getAllocationByteCount(bitmap);
long height = bitmap.getHeight();
long width = bitmap.getWidth();
long calHeight = (long) ((height * maxMemory) / (bitmapByteCount * 1.1));
long calWidth = (long) ((width * maxMemory) / (bitmapByteCount * 1.1));
scaled = Bitmap.createScaledBitmap(bitmap, (int) Math.min(width, calWidth), (int) Math.min(height, calHeight), true);
} catch (IOException e) {
Timber.e(e);
} catch (NullPointerException e) {
Timber.e(e);
scaled = bitmap;
}
return scaled;
}
/**
* Calculate the starting and ending bounds for the zoomed-in image.
* Also set the container view's offset as the origin for the
* bounds, since that's the origin for the positioning animation
* properties (X, Y).
* @param startBounds the global visible rectangle of the thumbnail
* @param finalBounds the global visible rectangle of the container view
* @param globalOffset the container view's offset
* @return scaled start bounds
*/
float adjustStartEndBounds(Rect startBounds, Rect finalBounds, Point globalOffset) {
thumbView.getGlobalVisibleRect(startBounds);
flContainer.getGlobalVisibleRect(finalBounds, globalOffset);
startBounds.offset(-globalOffset.x, -globalOffset.y);
finalBounds.offset(-globalOffset.x, -globalOffset.y);
// Adjust the start bounds to be the same aspect ratio as the final
// bounds using the "center crop" technique. This prevents undesirable
// stretching during the animation. Also calculate the start scaling
// factor (the end scaling factor is always 1.0).
float startScale;
if ((float) finalBounds.width() / finalBounds.height()
> (float) startBounds.width() / startBounds.height()) {
// Extend start bounds horizontally
startScale = (float) startBounds.height() / finalBounds.height();
float startWidth = startScale * finalBounds.width();
float deltaWidth = (startWidth - startBounds.width()) / 2;
startBounds.left -= deltaWidth;
startBounds.right += deltaWidth;
} else {
// Extend start bounds vertically
startScale = (float) startBounds.width() / finalBounds.width();
float startHeight = startScale * finalBounds.height();
float deltaHeight = (startHeight - startBounds.height()) / 2;
startBounds.top -= deltaHeight;
startBounds.bottom += deltaHeight;
}
return startScale;
}
}

View file

@ -1,92 +0,0 @@
package fr.free.nrw.commons.utils;
import android.os.Environment;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import timber.log.Timber;
public class FileUtils {
/**
* Read and return the content of a resource file as string.
*
* @param fileName asset file's path (e.g. "/queries/nearby_query.rq")
* @return the content of the file
*/
public static String readFromResource(String fileName) throws IOException {
StringBuilder buffer = new StringBuilder();
BufferedReader reader = null;
try {
InputStream inputStream = FileUtils.class.getResourceAsStream(fileName);
if (inputStream == null) {
throw new FileNotFoundException(fileName);
}
reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
buffer.append(line).append("\n");
}
} finally {
if (reader != null) {
reader.close();
}
}
return buffer.toString();
}
/**
* Deletes files.
* @param file context
*/
public static boolean deleteFile(File file) {
boolean deletedAll = true;
if (file != null) {
if (file.isDirectory()) {
String[] children = file.list();
for (String child : children) {
deletedAll = deleteFile(new File(file, child)) && deletedAll;
}
} else {
deletedAll = file.delete();
}
}
return deletedAll;
}
public static File createAndGetAppLogsFile(String logs) {
try {
File commonsAppDirectory = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
if (!commonsAppDirectory.exists()) {
commonsAppDirectory.mkdir();
}
File logsFile = new File(commonsAppDirectory,"logs.txt");
if (logsFile.exists()) {
//old logs file is useless
logsFile.delete();
}
logsFile.createNewFile();
FileOutputStream outputStream = new FileOutputStream(logsFile);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
outputStreamWriter.append(logs);
outputStreamWriter.close();
outputStream.flush();
outputStream.close();
return logsFile;
} catch (IOException ioe) {
Timber.e(ioe);
return null;
}
}
}

View file

@ -1,12 +1,34 @@
package fr.free.nrw.commons.utils;
import android.app.WallpaperManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Color;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import java.io.IOException;
import fr.free.nrw.commons.R;
import timber.log.Timber;
import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext;
/**
* Created by bluesir9 on 3/10/17.
*/
@ -132,4 +154,52 @@ public class ImageUtils {
return isImageDark;
}
/**
* Downloads the image from the URL and sets it as the phone's wallpaper
* Fails silently if download or setting wallpaper fails.
* @param context
* @param imageUrl
*/
public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) {
Timber.d("Trying to set wallpaper from url %s", imageUrl.toString());
ImageRequest imageRequest = ImageRequestBuilder
.newBuilderWithSource(imageUrl)
.setAutoRotateEnabled(true)
.build();
ImagePipeline imagePipeline = Fresco.getImagePipeline();
final DataSource<CloseableReference<CloseableImage>>
dataSource = imagePipeline.fetchDecodedImage(imageRequest, context);
dataSource.subscribe(new BaseBitmapDataSubscriber() {
@Override
public void onNewResultImpl(@Nullable Bitmap bitmap) {
if (dataSource.isFinished() && bitmap != null){
Timber.d("Bitmap loaded from url %s", imageUrl.toString());
setWallpaper(context, Bitmap.createBitmap(bitmap));
dataSource.close();
}
}
@Override
public void onFailureImpl(DataSource dataSource) {
Timber.d("Error getting bitmap from image url %s", imageUrl.toString());
if (dataSource != null) {
dataSource.close();
}
}
}, CallerThreadExecutor.getInstance());
}
private static void setWallpaper(Context context, Bitmap bitmap) {
WallpaperManager wallpaperManager = WallpaperManager.getInstance(context);
try {
wallpaperManager.setBitmap(bitmap);
ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_successfully));
} catch (IOException e) {
Timber.e(e,"Error setting wallpaper");
}
}
}

View file

@ -0,0 +1,16 @@
package fr.free.nrw.commons.wikidata;
public abstract class WikidataEditListener {
protected WikidataP18EditListener wikidataP18EditListener;
public abstract void onSuccessfulWikidataEdit();
public void setAuthenticationStateListener(WikidataP18EditListener wikidataP18EditListener) {
this.wikidataP18EditListener = wikidataP18EditListener;
}
public interface WikidataP18EditListener {
void onWikidataEditSuccessful();
}
}

View file

@ -0,0 +1,20 @@
package fr.free.nrw.commons.wikidata;
/**
* Listener for wikidata edits
*/
public class WikidataEditListenerImpl extends WikidataEditListener {
public WikidataEditListenerImpl() {
}
/**
* Fired when wikidata P18 edit is successful. If there's an active listener, then it is fired
*/
@Override
public void onSuccessfulWikidataEdit() {
if (wikidataP18EditListener != null) {
wikidataP18EditListener.onWikidataEditSuccessful();
}
}
}

View file

@ -0,0 +1,134 @@
package fr.free.nrw.commons.wikidata;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
/**
* This class is meant to handle the Wikidata edits made through the app
* It will talk with MediaWikiApi to make necessary API calls, log the edits and fire listeners
* on successful edits
*/
@Singleton
public class WikidataEditService {
private final Context context;
private final MediaWikiApi mediaWikiApi;
private final WikidataEditListener wikidataEditListener;
private final SharedPreferences directPrefs;
@Inject
public WikidataEditService(Context context,
MediaWikiApi mediaWikiApi,
WikidataEditListener wikidataEditListener,
@Named("direct_nearby_upload_prefs") SharedPreferences directPrefs) {
this.context = context;
this.mediaWikiApi = mediaWikiApi;
this.wikidataEditListener = wikidataEditListener;
this.directPrefs = directPrefs;
}
/**
* Create a P18 claim and log the edit with custom tag
* @param wikidataEntityId
* @param fileName
*/
public void createClaimWithLogging(String wikidataEntityId, String fileName) {
if(wikidataEntityId == null
|| fileName == null) {
return;
}
editWikidataProperty(wikidataEntityId, fileName);
}
/**
* Edits the wikidata entity by adding the P18 property to it.
* Adding the P18 edit requires calling the wikidata API to create a claim against the entity
*
* @param wikidataEntityId
* @param fileName
*/
@SuppressLint("CheckResult")
private void editWikidataProperty(String wikidataEntityId, String fileName) {
Timber.d("Upload successful with wiki data entity id as %s", wikidataEntityId);
Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId);
Observable.fromCallable(() -> {
String propertyValue = getFileName(fileName);
return mediaWikiApi.wikidatCreateClaim(wikidataEntityId, "P18", "value", propertyValue);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(revisionId -> handleClaimResult(wikidataEntityId, revisionId), throwable -> {
Timber.e(throwable, "Error occurred while making claim");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
}
private void handleClaimResult(String wikidataEntityId, String revisionId) {
if (revisionId != null) {
wikidataEditListener.onSuccessfulWikidataEdit();
showSuccessToast();
logEdit(revisionId);
} else {
Timber.d("Unable to make wiki data edit for entity %s", wikidataEntityId);
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
}
}
/**
* Log the Wikidata edit by adding Wikimedia Commons App tag to the edit
* @param revisionId
*/
@SuppressLint("CheckResult")
private void logEdit(String revisionId) {
Observable.fromCallable(() -> mediaWikiApi.addWikidataEditTag(revisionId))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
if (result) {
Timber.d("Wikidata edit was tagged successfully");
} else {
Timber.d("Wikidata edit couldn't be tagged");
}
}, throwable -> {
Timber.e(throwable, "Error occurred while adding tag to the edit");
});
}
/**
* Show a success toast when the edit is made successfully
*/
private void showSuccessToast() {
String title = directPrefs.getString("Title", "");
String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
String successMessage = String.format(Locale.getDefault(), successStringTemplate, title);
ViewUtil.showLongToast(context, successMessage);
}
/**
* Formats and returns the filename as accepted by the wiki base API
* https://www.mediawiki.org/wiki/Wikibase/API#wbcreateclaim
*
* @param fileName
* @return
*/
private String getFileName(String fileName) {
fileName = String.format("\"%s\"", fileName.replace("File:", ""));
Timber.d("Wikidata property name is %s", fileName);
return fileName;
}
}