Merge branch 'master' of github.com:nicolas-raoul/apps-android-commons

This commit is contained in:
Nicolas Raoul 2016-02-16 17:13:59 +09:00
commit 1fdad8a1aa
12 changed files with 430 additions and 136 deletions

View file

@ -1,5 +1,19 @@
# Wikimedia Commons for Android
## v1.10
- Bugfix for null location crash
## v1.9
- Bugfix for null pages array crash
- New feature: Added option to use GPS to find nearby categories if picture is not geotagged
## v1.8
- New feature: Improved category search function (not limited to prefix search now)
## v1.7
- Fixed bug with uploading images in Marshmallow
- Fixed links in About page
## v1.6
- Bugfix for invalid images

View file

@ -9,3 +9,6 @@ their contribution to the product.
* Siebrand Mazeland
* Translatewiki.net Translators https://translatewiki.net/wiki/Special:ListUsers/translator
* Yuvi Panda
* Nicolas Raoul
* Stephen Niedzielski
* Josephine Lim

View file

@ -1,8 +1,8 @@
# Upload to Commons #
Upload pictures from your Android phone/tablet to Wikimedia Commons!
Upload pictures from your Android phone/tablet to Wikimedia Commons.
Community-maintained app, independant from the Wikimedia Foundation. Volunteers welcome!
Initially started by the Wikimedia Foundation, this app is now maintained by volunteers. Anyone is welcome to improve it, just choose among the [open issues](https://github.com/nicolas-raoul/apps-android-commons/issues) and send us a pull request :-)
## Build Requirements ##

View file

@ -1,11 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="fr.free.nrw.commons"
android:versionCode="22"
android:versionName="1.6" >
android:versionCode="26"
android:versionName="1.10" >
<uses-sdk
android:minSdkVersion="9"
android:targetSdkVersion="23" />
android:targetSdkVersion="22" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
@ -17,6 +17,7 @@
<uses-permission android:name="android.permission.READ_SYNC_STATS"/>
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:name=".CommonsApplication"

View file

@ -93,6 +93,9 @@
<string name="menu_download">Download</string>
<string name="preference_license">License</string>
<string name="allow_gps">Automatically get current location</string>
<string name="allow_gps_summary">Retrieve current location to offer category suggestions if image is not geotagged</string>
<!-- These three are semi-legacy entries, and should be changed in future -->
<string name="license_name_cc_by_sa">CC\u00A0Attribution-ShareAlike\u00A03.0</string>
<string name="license_name_cc_by">CC\u00A0Attribution\u00A03.0</string>

View file

@ -7,6 +7,11 @@
android:title="@string/preference_license"
android:defaultValue="CC BY-SA"
/>
<CheckBoxPreference
android:title="@string/allow_gps"
android:defaultValue="false"
android:summary="@string/allow_gps_summary"
android:key="allowGps" />
</PreferenceScreen>

View file

@ -22,35 +22,44 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.upload.MwVolleyApi;
import java.io.IOException;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledThreadPoolExecutor;
public class CategorizationFragment extends SherlockFragment{
public static interface OnCategoriesSaveHandler {
public void onCategoriesSave(ArrayList<String> categories);
}
ListView categoriesList;
EditText categoriesFilter;
protected EditText categoriesFilter;
ProgressBar categoriesSearchInProgress;
TextView categoriesNotFoundView;
TextView categoriesSkip;
CategoriesAdapter categoriesAdapter;
CategoriesUpdater lastUpdater = null;
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
private OnCategoriesSaveHandler onCategoriesSaveHandler;
private HashMap<String, ArrayList<String>> categoriesCache;
protected HashMap<String, ArrayList<String>> categoriesCache;
// LHS guarantees ordered insertions, allowing for prioritized method A results
private final Set<String> results = new LinkedHashSet<String>();
PrefixUpdater prefixUpdaterSub;
MethodAUpdater methodAUpdaterSub;
private ContentProviderClient client;
private final int SEARCH_CATS_LIMIT = 25;
protected final static int SEARCH_CATS_LIMIT = 25;
private static final String TAG = CategorizationFragment.class.getName();
public static class CategoryItem implements Parcelable {
@ -87,32 +96,57 @@ public class CategorizationFragment extends SherlockFragment{
}
}
private class CategoriesUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
protected ArrayList<String> recentCatQuery() {
ArrayList<String> items = new ArrayList<String>();
Set<String> mergedItems = new LinkedHashSet<String>();
private String filter;
@Override
protected void onPreExecute() {
super.onPreExecute();
filter = categoriesFilter.getText().toString();
categoriesSearchInProgress.setVisibility(View.VISIBLE);
categoriesNotFoundView.setVisibility(View.GONE);
try {
Cursor cursor = client.query(
CategoryContentProvider.BASE_URI,
Category.Table.ALL_FIELDS,
null,
new String[]{},
Category.Table.COLUMN_LAST_USED + " DESC");
// fixme add a limit on the original query instead of falling out of the loop?
while (cursor.moveToNext() && cursor.getPosition() < SEARCH_CATS_LIMIT) {
Category cat = Category.fromCursor(cursor);
items.add(cat.getName());
}
cursor.close();
categoriesSkip.setVisibility(View.GONE);
if (MwVolleyApi.GpsCatExists.getGpsCatExists() == true) {
//Log.d(TAG, "GPS cats found in CategorizationFragment.java" + MwVolleyApi.getGpsCat().toString());
List<String> gpsItems = new ArrayList<String>(MwVolleyApi.getGpsCat());
//Log.d(TAG, "GPS items: " + gpsItems.toString());
mergedItems.addAll(gpsItems);
}
mergedItems.addAll(items);
}
catch (RemoteException e) {
throw new RuntimeException(e);
}
@Override
protected void onPostExecute(ArrayList<String> categories) {
super.onPostExecute(categories);
//Needs to be an ArrayList and not a List unless we want to modify a big portion of preexisting code
ArrayList<String> mergedItemsList = new ArrayList<String>(mergedItems);
return mergedItemsList;
}
protected void setCatsAfterAsync(ArrayList<String> categories, String filter) {
if (getActivity() != null) {
ArrayList<CategoryItem> items = new ArrayList<CategoryItem>();
HashSet<String> existingKeys = new HashSet<String>();
for(CategoryItem item : categoriesAdapter.getItems()) {
if(item.selected) {
for (CategoryItem item : categoriesAdapter.getItems()) {
if (item.selected) {
items.add(item);
existingKeys.add(item.name);
}
}
for(String category : categories) {
if(!existingKeys.contains(category)) {
for (String category : categories) {
if (!existingKeys.contains(category)) {
items.add(new CategoryItem(category, false));
}
}
@ -120,8 +154,9 @@ public class CategorizationFragment extends SherlockFragment{
categoriesAdapter.setItems(items);
categoriesAdapter.notifyDataSetInvalidated();
categoriesSearchInProgress.setVisibility(View.GONE);
if (categories.size() == 0) {
if(TextUtils.isEmpty(filter)) {
if (categories.isEmpty()) {
if (TextUtils.isEmpty(filter)) {
// If we found no recent cats, show the skip message!
categoriesSkip.setVisibility(View.VISIBLE);
} else {
@ -132,68 +167,8 @@ public class CategorizationFragment extends SherlockFragment{
categoriesList.smoothScrollToPosition(existingKeys.size());
}
}
@Override
protected ArrayList<String> doInBackground(Void... voids) {
if(TextUtils.isEmpty(filter)) {
ArrayList<String> items = new ArrayList<String>();
ArrayList<String> mergedItems= new ArrayList<String>();
try {
Cursor cursor = client.query(
CategoryContentProvider.BASE_URI,
Category.Table.ALL_FIELDS,
null,
new String[]{},
Category.Table.COLUMN_LAST_USED + " DESC");
// fixme add a limit on the original query instead of falling out of the loop?
while (cursor.moveToNext() && cursor.getPosition() < SEARCH_CATS_LIMIT) {
Category cat = Category.fromCursor(cursor);
items.add(cat.getName());
}
if (MwVolleyApi.GpsCatExists.getGpsCatExists() == true){
Log.d(TAG, "GPS cats found in CategorizationFragment.java" + MwVolleyApi.getGpsCat().toString());
List<String> gpsItems = new ArrayList<String>(MwVolleyApi.getGpsCat());
Log.d(TAG, "GPS items: " + gpsItems.toString());
mergedItems.addAll(gpsItems);
}
mergedItems.addAll(items);
}
catch (RemoteException e) {
// faaaail
throw new RuntimeException(e);
}
Log.d(TAG, "Merged items: " + mergedItems.toString());
return mergedItems;
}
if(categoriesCache.containsKey(filter)) {
return categoriesCache.get(filter);
}
MWApi api = CommonsApplication.createMWApi();
ApiResult result;
ArrayList<String> categories = new ArrayList<String>();
try {
result = api.action("query")
.param("list", "allcategories")
.param("acprefix", filter)
.param("aclimit", SEARCH_CATS_LIMIT)
.get();
} catch (IOException e) {
throw new RuntimeException(e);
}
ArrayList<ApiResult> categoryNodes = result.getNodes("/api/query/allcategories/c");
for(ApiResult categoryNode: categoryNodes) {
categories.add(categoryNode.getDocument().getTextContent());
}
categoriesCache.put(filter, categories);
return categories;
else {
Log.e(TAG, "Error: Fragment is null");
}
}
@ -363,12 +338,70 @@ public class CategorizationFragment extends SherlockFragment{
return rootView;
}
private void requestSearchResults() {
final CountDownLatch latch = new CountDownLatch(1);
prefixUpdaterSub = new PrefixUpdater(this) {
@Override
protected ArrayList<String> doInBackground(Void... voids) {
ArrayList<String> result = new ArrayList<String>();
try {
result = super.doInBackground();
latch.await();
}
catch (InterruptedException e) {
Log.w(TAG, e);
//Thread.currentThread().interrupt();
}
return result;
}
@Override
protected void onPostExecute(ArrayList<String> result) {
super.onPostExecute(result);
results.addAll(result);
Log.d(TAG, "Prefix result: " + result);
String filter = categoriesFilter.getText().toString();
ArrayList<String> resultsList = new ArrayList<String>(results);
categoriesCache.put(filter, resultsList);
Log.d(TAG, "Final results List: " + resultsList);
categoriesAdapter.notifyDataSetChanged();
setCatsAfterAsync(resultsList, filter);
}
};
methodAUpdaterSub = new MethodAUpdater(this) {
@Override
protected void onPostExecute(ArrayList<String> result) {
results.clear();
super.onPostExecute(result);
results.addAll(result);
Log.d(TAG, "Method A result: " + result);
categoriesAdapter.notifyDataSetChanged();
latch.countDown();
}
};
Utils.executeAsyncTask(prefixUpdaterSub);
Utils.executeAsyncTask(methodAUpdaterSub);
}
private void startUpdatingCategoryList() {
if (lastUpdater != null) {
lastUpdater.cancel(true);
if (prefixUpdaterSub != null) {
prefixUpdaterSub.cancel(true);
}
lastUpdater = new CategoriesUpdater();
Utils.executeAsyncTask(lastUpdater, executor);
if (methodAUpdaterSub != null) {
methodAUpdaterSub.cancel(true);
}
requestSearchResults();
}
@Override

View file

@ -0,0 +1,78 @@
package fr.free.nrw.commons.category;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import org.mediawiki.api.ApiResult;
import org.mediawiki.api.MWApi;
import java.io.IOException;
import java.util.ArrayList;
import fr.free.nrw.commons.CommonsApplication;
public class MethodAUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
private String filter;
private static final String TAG = MethodAUpdater.class.getName();
CategorizationFragment catFragment;
public MethodAUpdater(CategorizationFragment catFragment) {
this.catFragment = catFragment;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
filter = catFragment.categoriesFilter.getText().toString();
catFragment.categoriesSearchInProgress.setVisibility(View.VISIBLE);
catFragment.categoriesNotFoundView.setVisibility(View.GONE);
catFragment.categoriesSkip.setVisibility(View.GONE);
}
@Override
protected ArrayList<String> doInBackground(Void... voids) {
//If user hasn't typed anything in yet, get GPS and recent items
if(TextUtils.isEmpty(filter)) {
return catFragment.recentCatQuery();
}
//if user types in something that is in cache, return cached category
if(catFragment.categoriesCache.containsKey(filter)) {
return catFragment.categoriesCache.get(filter);
}
//otherwise if user has typed something in that isn't in cache, search API for matching categories
MWApi api = CommonsApplication.createMWApi();
ApiResult result;
ArrayList<String> categories = new ArrayList<String>();
//URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch=
try {
result = api.action("query")
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", catFragment.SEARCH_CATS_LIMIT)
.param("srsearch", filter)
.get();
Log.d(TAG, "Method A URL filter" + result.toString());
} catch (IOException e) {
throw new RuntimeException(e);
}
ArrayList<ApiResult> categoryNodes = result.getNodes("/api/query/search/p/@title");
for(ApiResult categoryNode: categoryNodes) {
String cat = categoryNode.getDocument().getTextContent();
String catString = cat.replace("Category:", "");
categories.add(catString);
}
return categories;
}
}

View file

@ -0,0 +1,70 @@
package fr.free.nrw.commons.category;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import org.mediawiki.api.ApiResult;
import org.mediawiki.api.MWApi;
import java.io.IOException;
import java.util.ArrayList;
import fr.free.nrw.commons.CommonsApplication;
public class PrefixUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
private String filter;
private static final String TAG = PrefixUpdater.class.getName();
private CategorizationFragment catFragment;
public PrefixUpdater(CategorizationFragment catFragment) {
this.catFragment = catFragment;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
filter = catFragment.categoriesFilter.getText().toString();
catFragment.categoriesSearchInProgress.setVisibility(View.VISIBLE);
catFragment.categoriesNotFoundView.setVisibility(View.GONE);
catFragment.categoriesSkip.setVisibility(View.GONE);
}
@Override
protected ArrayList<String> doInBackground(Void... voids) {
//If user hasn't typed anything in yet, get GPS and recent items
if(TextUtils.isEmpty(filter)) {
return catFragment.recentCatQuery();
}
//if user types in something that is in cache, return cached category
if(catFragment.categoriesCache.containsKey(filter)) {
return catFragment.categoriesCache.get(filter);
}
//otherwise if user has typed something in that isn't in cache, search API for matching categories
MWApi api = CommonsApplication.createMWApi();
ApiResult result;
ArrayList<String> categories = new ArrayList<String>();
try {
result = api.action("query")
.param("list", "allcategories")
.param("acprefix", filter)
.param("aclimit", catFragment.SEARCH_CATS_LIMIT)
.get();
Log.d(TAG, "Prefix URL filter" + result.toString());
} catch (IOException e) {
throw new RuntimeException(e);
}
ArrayList<ApiResult> categoryNodes = result.getNodes("/api/query/allcategories/c");
for(ApiResult categoryNode: categoryNodes) {
categories.add(categoryNode.getDocument().getTextContent());
}
return categories;
}
}

View file

@ -1,18 +1,58 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.media.ExifInterface;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import java.io.IOException;
public class GPSExtractor {
private static final String TAG = GPSExtractor.class.getName();
private String filePath;
private double decLatitude, decLongitude;
private double currentLatitude, currentLongitude;
private Context context;
public boolean imageCoordsExists;
private MyLocationListener myLocationListener;
private LocationManager locationManager;
private String provider;
private Criteria criteria;
public GPSExtractor(String filePath){
public GPSExtractor(String filePath, Context context){
this.filePath = filePath;
this.context = context;
}
private boolean gpsPreferenceEnabled() {
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
boolean gpsPref = sharedPref.getBoolean("allowGps", false);
Log.d(TAG, "Gps pref set to: " + gpsPref);
return gpsPref;
}
protected void registerLocationManager() {
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
criteria = new Criteria();
provider = locationManager.getBestProvider(criteria, true);
myLocationListener = new MyLocationListener();
locationManager.requestLocationUpdates(provider, 400, 1, myLocationListener);
Location location = locationManager.getLastKnownLocation(provider);
if (location != null) {
myLocationListener.onLocationChanged(location);
}
}
protected void unregisterLocationManager() {
locationManager.removeUpdates(myLocationListener);
}
//Extract GPS coords of image
@ -33,21 +73,58 @@ public class GPSExtractor {
}
if (exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) {
Log.d("Image", "Picture has no GPS info");
return null;
}
else {
imageCoordsExists = false;
Log.d(TAG, "Picture has no GPS info");
//Check what user's preference is for automatic location detection
boolean gpsPrefEnabled = gpsPreferenceEnabled();
if (gpsPrefEnabled) {
Log.d(TAG, "Current location values: Lat = " + currentLatitude + " Long = " + currentLongitude);
String currentCoords = String.valueOf(currentLatitude) + "|" + String.valueOf(currentLongitude);
return currentCoords;
} else {
//Otherwise treat as if no coords found
return null;
}
} else {
imageCoordsExists = true;
Log.d(TAG, "Picture has GPS info");
latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
latitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
longitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
Log.d("Image", "Latitude: " + latitude + " " + latitude_ref);
Log.d("Image", "Longitude: " + longitude + " " + longitude_ref);
decimalCoords = getDecimalCoords(latitude, latitude_ref, longitude, longitude_ref);
return decimalCoords;
}
}
private class MyLocationListener implements LocationListener {
@Override
public void onLocationChanged(Location location) {
currentLatitude = location.getLatitude();
currentLongitude = location.getLongitude();
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
Log.d(TAG, provider + "'s status changed to " + status);
}
@Override
public void onProviderEnabled(String provider) {
Log.d(TAG, "Provider " + provider + " enabled");
}
@Override
public void onProviderDisabled(String provider) {
Log.d(TAG, "Provider " + provider + " disabled");
}
}
@ -62,19 +139,15 @@ public class GPSExtractor {
//Converts format of coords into decimal coords as required by MediaWiki API
private String getDecimalCoords(String latitude, String latitude_ref, String longitude, String longitude_ref) {
if(latitude_ref.equals("N")){
if (latitude_ref.equals("N")) {
decLatitude = convertToDegree(latitude);
}
else{
} else {
decLatitude = 0 - convertToDegree(latitude);
}
if(longitude_ref.equals("E")){
if (longitude_ref.equals("E")) {
decLongitude = convertToDegree(longitude);
}
else{
} else {
decLongitude = 0 - convertToDegree(longitude);
}
@ -83,7 +156,6 @@ public class GPSExtractor {
return decimalCoords;
}
private double convertToDegree(String stringDMS){
double result;
String[] DMS = stringDMS.split(",", 3);
@ -106,5 +178,4 @@ public class GPSExtractor {
result = degrees + (minutes/60) + (seconds/3600);
return result;
}
}

View file

@ -196,13 +196,16 @@ public class MwVolleyApi {
@Override
public String toString() {
StringBuilder builder = new StringBuilder("pages=" + "\n");
for (Page page : pages) {
builder.append(page.toString());
builder.append("\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";
}
builder.replace(builder.length() - 1, builder.length(), "");
return builder.toString();
}
}

View file

@ -34,6 +34,8 @@ public class ShareActivity
implements SingleUploadFragment.OnUploadActionInitiated,
CategorizationFragment.OnCategoriesSaveHandler {
private static final String TAG = ShareActivity.class.getName();
private SingleUploadFragment shareView;
private CategorizationFragment categorizationFragment;
@ -44,17 +46,14 @@ public class ShareActivity
private String mediaUriString;
private Uri mediaUri;
private Contribution contribution;
private ImageView backgroundImageView;
private UploadController uploadController;
private CommonsApplication cacheObj;
private boolean cacheFound;
private static final String TAG = ShareActivity.class.getName();
private GPSExtractor imageObj;
public ShareActivity() {
super(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE);
@ -190,6 +189,16 @@ public class ShareActivity
mediaUriString = mediaUri.toString();
ImageLoader.getInstance().displayImage(mediaUriString, backgroundImageView);
if(savedInstanceState != null) {
contribution = savedInstanceState.getParcelable("contribution");
}
requestAuthToken();
}
@Override
public void onResume() {
super.onResume();
Log.d(TAG, "Uri: " + mediaUriString);
Log.d(TAG, "Ext storage dir: " + Environment.getExternalStorageDirectory());
@ -197,19 +206,23 @@ public class ShareActivity
String filePath = FileUtils.getPath(this, mediaUri);
Log.d(TAG, "Filepath: " + filePath);
Log.d(TAG, "Calling GPSExtractor");
imageObj = new GPSExtractor(filePath, this);
imageObj.registerLocationManager();
if (filePath != null && !filePath.equals("")) {
//extract the coordinates of image in decimal degrees
Log.d(TAG, "Calling GPSExtractor");
GPSExtractor imageObj = new GPSExtractor(filePath);
//Gets image coords if exist, otherwise gets last known coords
String decimalCoords = imageObj.getCoords();
if (decimalCoords != null) {
double decLongitude = imageObj.getDecLongitude();
double decLatitude = imageObj.getDecLatitude();
Log.d(TAG, "Decimal coords of image: " + decimalCoords);
app.cacheData.setQtPoint(decLongitude, decLatitude);
//Only set cache for this point if image has coords
if (imageObj.imageCoordsExists) {
double decLongitude = imageObj.getDecLongitude();
double decLatitude = imageObj.getDecLatitude();
app.cacheData.setQtPoint(decLongitude, decLatitude);
}
MwVolleyApi apiCall = new MwVolleyApi(this);
@ -229,13 +242,13 @@ public class ShareActivity
}
}
}
if(savedInstanceState != null) {
contribution = savedInstanceState.getParcelable("contribution");
}
requestAuthToken();
}
@Override
public void onPause() {
super.onPause();
imageObj.unregisterLocationManager();
}
@Override
protected void onDestroy() {