Merge "commons" into the project root directory

This commit is contained in:
Yusuke Matsubara 2016-07-02 16:20:43 +09:00
parent d42db0612e
commit b4231bbfdc
324 changed files with 22 additions and 23 deletions

View file

@ -0,0 +1,138 @@
package fr.free.nrw.commons.upload;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.DocumentsContract;
public class FileUtils {
/**
* 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
* other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @author paulburke
*/
public static String getPath(final Context context, final Uri uri) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[]{
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {
column
};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
}

View file

@ -0,0 +1,202 @@
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.support.design.widget.Snackbar;
import android.util.Log;
import java.io.IOException;
/**
* Extracts geolocation to be passed to API for category suggestions. If a picture with geolocation
* is uploaded, extract latitude and longitude from EXIF data of image. If a picture without
* geolocation is uploaded, retrieve user's location (if enabled in Settings).
*/
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, Context context){
this.filePath = filePath;
this.context = context;
}
/**
* Check if user enabled retrieval of their current location in Settings
* @return true if enabled, false if disabled
*/
private boolean gpsPreferenceEnabled() {
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
boolean gpsPref = sharedPref.getBoolean("allowGps", false);
Log.d(TAG, "Gps pref set to: " + gpsPref);
return gpsPref;
}
/**
* Registers a LocationManager to listen for current location
*/
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);
}
/**
* Extracts geolocation of image from EXIF data.
* @return coordinates of image as string (needs to be passed as a String in API query)
*/
public String getCoords(boolean useGPS) {
ExifInterface exif;
String latitude = "";
String longitude = "";
String latitude_ref = "";
String longitude_ref = "";
String decimalCoords = "";
try {
exif = new ExifInterface(filePath);
} catch (IOException e) {
Log.w("Image", e);
return null;
}
if (exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null && useGPS) {
registerLocationManager();
imageCoordsExists = false;
Log.d(TAG, "EXIF data has no location 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);
return String.valueOf(currentLatitude) + "|" + String.valueOf(currentLongitude);
} else {
// No coords found
return null;
}
} else if(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) {
return null;
} else {
imageCoordsExists = true;
Log.d(TAG, "EXIF data has location 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");
}
}
public double getDecLatitude() {
return decLatitude;
}
public double getDecLongitude() {
return decLongitude;
}
/**
* Converts format of geolocation into decimal coordinates as required by MediaWiki API
* @return the coordinates in decimals
*/
private String getDecimalCoords(String latitude, String latitude_ref, String longitude, String longitude_ref) {
if (latitude_ref.equals("N")) {
decLatitude = convertToDegree(latitude);
} else {
decLatitude = 0 - convertToDegree(latitude);
}
if (longitude_ref.equals("E")) {
decLongitude = convertToDegree(longitude);
} else {
decLongitude = 0 - convertToDegree(longitude);
}
String decimalCoords = String.valueOf(decLatitude) + "|" + String.valueOf(decLongitude);
Log.d("Coords", "Latitude and Longitude are " + decimalCoords);
return decimalCoords;
}
private double convertToDegree(String stringDMS){
double result;
String[] DMS = stringDMS.split(",", 3);
String[] stringD = DMS[0].split("/", 2);
double d0 = Double.parseDouble(stringD[0]);
double d1 = Double.parseDouble(stringD[1]);
double degrees = d0/d1;
String[] stringM = DMS[1].split("/", 2);
double m0 = Double.parseDouble(stringM[0]);
double m1 = Double.parseDouble(stringM[1]);
double minutes = m0/m1;
String[] stringS = DMS[2].split("/", 2);
double s0 = Double.parseDouble(stringS[0]);
double s1 = Double.parseDouble(stringS[1]);
double seconds = s0/s1;
result = degrees + (minutes/60) + (seconds/3600);
return result;
}
}

View file

@ -0,0 +1,274 @@
package fr.free.nrw.commons.upload;
import java.util.*;
import android.app.*;
import android.content.*;
import android.database.DataSetObserver;
import android.net.*;
import android.os.*;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity;
import android.view.*;
import android.view.inputmethod.InputMethodManager;
import android.widget.*;
import fr.free.nrw.commons.*;
import fr.free.nrw.commons.auth.*;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.EventLog;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.modifications.CategoryModifier;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.contributions.*;
import fr.free.nrw.commons.media.*;
public class MultipleShareActivity
extends AuthenticatedActivity
implements MediaDetailPagerFragment.MediaDetailProvider,
AdapterView.OnItemClickListener,
FragmentManager.OnBackStackChangedListener,
MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
CategorizationFragment.OnCategoriesSaveHandler {
private CommonsApplication app;
private ArrayList<Contribution> photosList = null;
private MultipleUploadListFragment uploadsList;
private MediaDetailPagerFragment mediaDetails;
private CategorizationFragment categorizationFragment;
private UploadController uploadController;
public MultipleShareActivity() {
super(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE);
}
public Media getMediaAtPosition(int i) {
return photosList.get(i);
}
public int getTotalMediaCount() {
if(photosList == null) {
return 0;
}
return photosList.size();
}
public void notifyDatasetChanged() {
if(uploadsList != null) {
uploadsList.notifyDatasetChanged();
}
}
public void registerDataSetObserver(DataSetObserver observer) {
// fixme implement me if needed
}
public void unregisterDataSetObserver(DataSetObserver observer) {
// fixme implement me if needed
}
public void onItemClick(AdapterView<?> adapterView, View view, int index, long item) {
showDetail(index);
}
public void OnMultipleUploadInitiated() {
final ProgressDialog dialog = new ProgressDialog(MultipleShareActivity.this);
dialog.setIndeterminate(false);
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setMax(photosList.size());
dialog.setTitle(getResources().getQuantityString(R.plurals.starting_multiple_uploads, photosList.size(), photosList.size()));
dialog.show();
for(int i = 0; i < photosList.size(); i++) {
Contribution up = photosList.get(i);
final int uploadCount = i + 1; // Goddamn Java
uploadController.startUpload(up, new UploadController.ContributionUploadProgress() {
public void onUploadStarted(Contribution contribution) {
dialog.setProgress(uploadCount);
if(uploadCount == photosList.size()) {
dialog.dismiss();
Toast startingToast = Toast.makeText(getApplicationContext(), R.string.uploading_started, Toast.LENGTH_LONG);
startingToast.show();
}
}
});
}
uploadsList.setImageOnlyMode(true);
categorizationFragment = (CategorizationFragment) this.getSupportFragmentManager().findFragmentByTag("categorization");
if(categorizationFragment == null) {
categorizationFragment = new CategorizationFragment();
}
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
View target = this.getCurrentFocus();
if (target != null) {
InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(target.getWindowToken(), 0);
}
getSupportFragmentManager().beginTransaction()
.add(R.id.uploadsFragmentContainer, categorizationFragment, "categorization")
.commit();
}
public void onCategoriesSave(ArrayList<String> categories) {
if(categories.size() > 0) {
ContentProviderClient client = getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY);
for(Contribution contribution: photosList) {
ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
categoriesSequence.setContentProviderClient(client);
categoriesSequence.save();
}
}
// FIXME: Make sure that the content provider is up
// This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
ContentResolver.setSyncAutomatically(app.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT)
.param("username", app.getCurrentAccount().name)
.param("categories-count", categories.size())
.param("files-count", photosList.size())
.param("source", Contribution.SOURCE_EXTERNAL)
.param("result", "queued")
.log();
finish();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case android.R.id.home:
if(mediaDetails.isVisible()) {
getSupportFragmentManager().popBackStack();
}
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
uploadController = new UploadController(this);
setContentView(R.layout.activity_multiple_uploads);
app = (CommonsApplication)this.getApplicationContext();
if(savedInstanceState != null) {
photosList = savedInstanceState.getParcelableArrayList("uploadsList");
}
getSupportFragmentManager().addOnBackStackChangedListener(this);
requestAuthToken();
}
@Override
protected void onDestroy() {
super.onDestroy();
uploadController.cleanup();
}
private void showDetail(int i) {
if(mediaDetails == null ||!mediaDetails.isVisible()) {
mediaDetails = new MediaDetailPagerFragment(true);
this.getSupportFragmentManager()
.beginTransaction()
.replace(R.id.uploadsFragmentContainer, mediaDetails)
.addToBackStack(null)
.commit();
this.getSupportFragmentManager().executePendingTransactions();
}
mediaDetails.showImage(i);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList("uploadsList", photosList);
}
@Override
protected void onAuthCookieAcquired(String authCookie) {
app.getApi().setAuthCookie(authCookie);
Intent intent = getIntent();
if(intent.getAction().equals(Intent.ACTION_SEND_MULTIPLE)) {
if(photosList == null) {
photosList = new ArrayList<Contribution>();
ArrayList<Uri> urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
for(int i=0; i < urisList.size(); i++) {
Contribution up = new Contribution();
Uri uri = urisList.get(i);
up.setLocalUri(uri);
up.setTag("mimeType", intent.getType());
up.setTag("sequence", i);
up.setSource(Contribution.SOURCE_EXTERNAL);
up.setMultiple(true);
photosList.add(up);
}
}
uploadsList = (MultipleUploadListFragment) getSupportFragmentManager().findFragmentByTag("uploadsList");
if(uploadsList == null) {
uploadsList = new MultipleUploadListFragment();
this.getSupportFragmentManager()
.beginTransaction()
.add(R.id.uploadsFragmentContainer, uploadsList, "uploadsList")
.commit();
}
setTitle(getResources().getQuantityString(R.plurals.multiple_uploads_title, photosList.size(), photosList.size()));
uploadController.prepareService();
}
}
@Override
protected void onAuthFailure() {
super.onAuthFailure();
Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG);
failureToast.show();
finish();
}
@Override
public void onBackPressed() {
super.onBackPressed();
if(categorizationFragment != null && categorizationFragment.isVisible()) {
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT)
.param("username", app.getCurrentAccount().name)
.param("categories-count", categorizationFragment.getCurrentSelectedCount())
.param("files-count", photosList.size())
.param("source", Contribution.SOURCE_EXTERNAL)
.param("result", "cancelled")
.log();
} else {
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT)
.param("username", app.getCurrentAccount().name)
.param("source", getIntent().getStringExtra(UploadService.EXTRA_SOURCE))
.param("multiple", true)
.param("result", "cancelled")
.log();
}
}
public void onBackStackChanged() {
if(mediaDetails != null && mediaDetails.isVisible()) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
} else {
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
}
}

View file

@ -0,0 +1,212 @@
package fr.free.nrw.commons.upload;
import android.content.*;
import android.graphics.*;
import android.net.*;
import android.os.*;
import android.support.v4.app.Fragment;
import android.text.*;
import android.util.*;
import android.view.*;
import android.view.inputmethod.InputMethodManager;
import android.widget.*;
import com.nostra13.universalimageloader.core.*;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.*;
import fr.free.nrw.commons.media.*;
public class MultipleUploadListFragment extends Fragment {
public interface OnMultipleUploadInitiatedHandler {
public void OnMultipleUploadInitiated();
}
private GridView photosGrid;
private PhotoDisplayAdapter photosAdapter;
private EditText baseTitle;
private Point photoSize;
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
private OnMultipleUploadInitiatedHandler multipleUploadInitiatedHandler;
private DisplayImageOptions uploadDisplayOptions;
private boolean imageOnlyMode;
private static class UploadHolderView {
Uri imageUri;
ImageView image;
TextView title;
RelativeLayout overlay;
}
private class PhotoDisplayAdapter extends BaseAdapter {
public int getCount() {
return detailProvider.getTotalMediaCount();
}
public Object getItem(int i) {
return detailProvider.getMediaAtPosition(i);
}
public long getItemId(int i) {
return i;
}
public View getView(int i, View view, ViewGroup viewGroup) {
UploadHolderView holder;
if(view == null) {
view = getLayoutInflater(null).inflate(R.layout.layout_upload_item, null);
holder = new UploadHolderView();
holder.image = (ImageView) view.findViewById(R.id.uploadImage);
holder.title = (TextView) view.findViewById(R.id.uploadTitle);
holder.overlay = (RelativeLayout) view.findViewById(R.id.uploadOverlay);
holder.image.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, photoSize.y));
view.setTag(holder);
} else {
holder = (UploadHolderView)view.getTag();
}
Contribution up = (Contribution)this.getItem(i);
if(holder.imageUri == null || !holder.imageUri.equals(up.getLocalUri())) {
ImageLoader.getInstance().displayImage(up.getLocalUri().toString(), holder.image, uploadDisplayOptions);
holder.imageUri = up.getLocalUri();
}
if(!imageOnlyMode) {
holder.overlay.setVisibility(View.VISIBLE);
holder.title.setText(up.getFilename());
} else {
holder.overlay.setVisibility(View.GONE);
}
return view;
}
}
@Override
public void onStop() {
super.onStop();
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
View target = getView().findFocus();
if (target != null) {
InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(target.getWindowToken(), 0);
}
}
// FIXME: Wrong result type
private Point calculatePicDimension(int count) {
DisplayMetrics screenMetrics = getResources().getDisplayMetrics();
int screenWidth = screenMetrics.widthPixels;
int screenHeight = screenMetrics.heightPixels;
int picWidth = Math.min((int) Math.sqrt(screenWidth * screenHeight / count), screenWidth);
picWidth = Math.min((int)(192 * screenMetrics.density), Math.max((int) (120 * screenMetrics.density), picWidth / 48 * 48));
int picHeight = Math.min(picWidth, (int)(192 * screenMetrics.density)); // Max Height is same as Contributions list
return new Point(picWidth, picHeight);
}
public void notifyDatasetChanged() {
if(photosAdapter != null) {
photosAdapter.notifyDataSetChanged();
}
}
public void setImageOnlyMode(boolean mode) {
imageOnlyMode = mode;
if(imageOnlyMode) {
baseTitle.setVisibility(View.GONE);
} else {
baseTitle.setVisibility(View.VISIBLE);
}
photosAdapter.notifyDataSetChanged();
photosGrid.setEnabled(!mode);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_multiple_uploads_list, null);
photosGrid = (GridView)view.findViewById(R.id.multipleShareBackground);
baseTitle = (EditText)view.findViewById(R.id.multipleBaseTitle);
photosAdapter = new PhotoDisplayAdapter();
photosGrid.setAdapter(photosAdapter);
photosGrid.setOnItemClickListener((AdapterView.OnItemClickListener)getActivity());
photoSize = calculatePicDimension(detailProvider.getTotalMediaCount());
photosGrid.setColumnWidth(photoSize.x);
baseTitle.addTextChangedListener(new TextWatcher() {
public void beforeTextChanged(CharSequence charSequence, int i1, int i2, int i3) {
}
public void onTextChanged(CharSequence charSequence, int i1, int i2, int i3) {
for(int i = 0; i < detailProvider.getTotalMediaCount(); i++) {
Contribution up = (Contribution) detailProvider.getMediaAtPosition(i);
Boolean isDirty = (Boolean)up.getTag("isDirty");
if(isDirty == null || !isDirty) {
if(!TextUtils.isEmpty(charSequence)) {
up.setFilename(charSequence.toString() + " - " + ((Integer)up.getTag("sequence") + 1));
} else {
up.setFilename("");
}
}
}
detailProvider.notifyDatasetChanged();
}
public void afterTextChanged(Editable editable) {
}
});
return view;
}
@Override
public void onCreateOptionsMenu(Menu menu, android.view.MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
menu.clear();
inflater.inflate(R.menu.fragment_multiple_upload_list, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case R.id.menu_upload_multiple:
multipleUploadInitiatedHandler.OnMultipleUploadInitiated();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
uploadDisplayOptions = Utils.getGenericDisplayOptions().build();
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider)getActivity();
multipleUploadInitiatedHandler = (OnMultipleUploadInitiatedHandler) getActivity();
setHasOptionsMenu(true);
}
}

View file

@ -0,0 +1,257 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
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;
/**
* 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 Context context;
private String coordsLog;
protected static Set<String> categorySet;
private static List<String> categoryList;
private static final String MWURL = "https://commons.wikimedia.org/";
private static final String TAG = MwVolleyApi.class.getName();
public MwVolleyApi(Context context) {
this.context = context;
categorySet = new HashSet<String>();
}
public static List<String> getGpsCat() {
return categoryList;
}
public static void setGpsCat(List cachedList) {
categoryList = new ArrayList<String>();
categoryList.addAll(cachedList);
Log.d(TAG, "Setting GPS cats from cache: " + categoryList.toString());
}
public void request(String coords) {
coordsLog = coords;
String apiUrl = buildUrl(coords);
Log.d("Image", "URL: " + apiUrl);
JsonRequest<QueryResponse> request = new QueryRequest(apiUrl,
new LogResponseListener<QueryResponse>(), 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() {
return getQueue(context);
}
private static RequestQueue getQueue(Context context) {
if (REQUEST_QUEUE == null) {
REQUEST_QUEUE = Volley.newRequestQueue(context.getApplicationContext());
}
return REQUEST_QUEUE;
}
private static class LogResponseListener<T> implements Response.Listener<T> {
private static final String TAG = LogResponseListener.class.getName();
@Override
public void onResponse(T response) {
Log.d(TAG, response.toString());
}
}
private static class LogResponseErrorListener implements Response.ErrorListener {
private static final String TAG = LogResponseErrorListener.class.getName();
@Override
public void onErrorResponse(VolleyError error) {
Log.e(TAG, error.toString());
}
}
private static class QueryRequest extends JsonRequest<QueryResponse> {
private static final String TAG = QueryRequest.class.getName();
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);
Log.d(TAG, "gpsCatExists=" + GpsCatExists.getGpsCatExists());
return "No collection of categories";
} else {
GpsCatExists.setGpsCatExists(true);
Log.d(TAG, "gpsCatExists=" + 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";
}
}
}
private static class Page {
private int pageid;
private int ns;
private String title;
private Category[] categories;
private Category category;
@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<String>(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

@ -0,0 +1,394 @@
package fr.free.nrw.commons.upload;
import android.Manifest;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.NavUtils;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.nostra13.universalimageloader.core.ImageLoader;
import java.util.ArrayList;
import java.util.List;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.EventLog;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.auth.WikiAccountAuthenticator;
import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.modifications.CategoryModifier;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
/**
* Activity for the title/desc screen after image is selected. Also starts processing image
* GPS coordinates or user location (if enabled in Settings) for category suggestions.
*/
public class ShareActivity
extends AuthenticatedActivity
implements SingleUploadFragment.OnUploadActionInitiated,
CategorizationFragment.OnCategoriesSaveHandler {
private static final String TAG = ShareActivity.class.getName();
private SingleUploadFragment shareView;
private CategorizationFragment categorizationFragment;
private CommonsApplication app;
private String source;
private String mimeType;
private String mediaUriString;
private Uri mediaUri;
private Contribution contribution;
private ImageView backgroundImageView;
private UploadController uploadController;
private CommonsApplication cacheObj;
private boolean cacheFound;
private GPSExtractor imageObj;
private String filePath;
private String decimalCoords;
private boolean useNewPermissions = false;
private boolean storagePermission = false;
private boolean locationPermission = false;
public ShareActivity() {
super(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE);
}
public void uploadActionInitiated(String title, String description) {
Toast startingToast = Toast.makeText(getApplicationContext(), R.string.uploading_started, Toast.LENGTH_LONG);
startingToast.show();
if (cacheFound == false) {
//Has to be called after apiCall.request()
app.cacheData.cacheCategory();
Log.d(TAG, "Cache the categories found");
}
uploadController.startUpload(title, mediaUri, description, mimeType, source, new UploadController.ContributionUploadProgress() {
public void onUploadStarted(Contribution contribution) {
ShareActivity.this.contribution = contribution;
showPostUpload();
}
});
}
private void showPostUpload() {
if(categorizationFragment == null) {
categorizationFragment = new CategorizationFragment();
}
getSupportFragmentManager().beginTransaction()
.replace(R.id.single_upload_fragment_container, categorizationFragment, "categorization")
.commit();
}
public void onCategoriesSave(ArrayList<String> categories) {
if(categories.size() > 0) {
ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
categoriesSequence.setContentProviderClient(getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY));
categoriesSequence.save();
}
// FIXME: Make sure that the content provider is up
// This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
ContentResolver.setSyncAutomatically(app.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT)
.param("username", app.getCurrentAccount().name)
.param("categories-count", categories.size())
.param("files-count", 1)
.param("source", contribution.getSource())
.param("result", "queued")
.log();
finish();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if(contribution != null) {
outState.putParcelable("contribution", contribution);
}
}
@Override
public void onBackPressed() {
super.onBackPressed();
if(categorizationFragment != null && categorizationFragment.isVisible()) {
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT)
.param("username", app.getCurrentAccount().name)
.param("categories-count", categorizationFragment.getCurrentSelectedCount())
.param("files-count", 1)
.param("source", contribution.getSource())
.param("result", "cancelled")
.log();
} else {
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT)
.param("username", app.getCurrentAccount().name)
.param("source", getIntent().getStringExtra(UploadService.EXTRA_SOURCE))
.param("multiple", true)
.param("result", "cancelled")
.log();
}
}
@Override
protected void onAuthCookieAcquired(String authCookie) {
super.onAuthCookieAcquired(authCookie);
app.getApi().setAuthCookie(authCookie);
shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView");
categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization");
if(shareView == null && categorizationFragment == null) {
shareView = new SingleUploadFragment();
this.getSupportFragmentManager()
.beginTransaction()
.add(R.id.single_upload_fragment_container, shareView, "shareView")
.commit();
}
uploadController.prepareService();
}
@Override
protected void onAuthFailure() {
super.onAuthFailure();
Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG);
failureToast.show();
finish();
}
/**
* Initiates retrieval of image coordinates or user coordinates, and caching of coordinates.
* Then initiates the calls to MediaWiki API through an instance of MwVolleyApi.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
uploadController = new UploadController(this);
setContentView(R.layout.activity_share);
app = (CommonsApplication)this.getApplicationContext();
backgroundImageView = (ImageView)findViewById(R.id.backgroundImage);
Intent intent = getIntent();
if(intent.getAction().equals(Intent.ACTION_SEND)) {
mediaUri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
if(intent.hasExtra(UploadService.EXTRA_SOURCE)) {
source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
} else {
source = Contribution.SOURCE_EXTERNAL;
}
mimeType = intent.getType();
}
if (mediaUri != null) {
mediaUriString = mediaUri.toString();
ImageLoader.getInstance().displayImage(mediaUriString, backgroundImageView);
}
if(savedInstanceState != null) {
contribution = savedInstanceState.getParcelable("contribution");
}
requestAuthToken();
Log.d(TAG, "Uri: " + mediaUriString);
Log.d(TAG, "Ext storage dir: " + Environment.getExternalStorageDirectory());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
useNewPermissions = true;
if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
storagePermission = true;
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
locationPermission = true;
}
}
// Check storage permissions if marshmallow or newer
if (useNewPermissions && (!storagePermission || !locationPermission)) {
if (!storagePermission && !locationPermission) {
String permissionRationales = getResources().getString(R.string.storage_permission_rationale) + "\n" + getResources().getString(R.string.location_permission_rationale);
Snackbar snackbar = Snackbar.make(findViewById(android.R.id.content), permissionRationales,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
ActivityCompat.requestPermissions(ShareActivity.this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.ACCESS_FINE_LOCATION}, 3);
}
});
snackbar.show();
View snackbarView = snackbar.getView();
TextView textView = (TextView) snackbarView.findViewById(android.support.design.R.id.snackbar_text);
textView.setMaxLines(3);
} else if (!storagePermission) {
Snackbar.make(findViewById(android.R.id.content), R.string.storage_permission_rationale,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
ActivityCompat.requestPermissions(ShareActivity.this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
}
}).show();
} else if (!locationPermission) {
Snackbar.make(findViewById(android.R.id.content), R.string.location_permission_rationale,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
ActivityCompat.requestPermissions(ShareActivity.this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 2);
}
}).show();
}
} else if (useNewPermissions && storagePermission && !locationPermission) {
getFileMetadata();
} else if(!useNewPermissions || (storagePermission && locationPermission)) {
getFileMetadata();
getLocationData();
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
// 1 = Storage
case 1: {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getFileMetadata();
}
return;
}
// 2 = Location
case 2: {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getLocationData();
}
return;
}
// 3 = Storage + Location
case 3: {
if (grantResults.length > 1
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getFileMetadata();
}
if (grantResults.length > 1
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
getLocationData();
}
}
}
}
public void getFileMetadata() {
filePath = FileUtils.getPath(this, mediaUri);
Log.d(TAG, "Filepath: " + filePath);
Log.d(TAG, "Calling GPSExtractor");
imageObj = new GPSExtractor(filePath, this);
if (filePath != null && !filePath.equals("")) {
// Gets image coords from exif data
decimalCoords = imageObj.getCoords(false);
useImageCoords();
}
}
public void getLocationData() {
if(imageObj == null) {
imageObj = new GPSExtractor(filePath, this);
}
decimalCoords = imageObj.getCoords(true);
useImageCoords();
}
public void useImageCoords() {
if(decimalCoords != null) {
Log.d(TAG, "Decimal coords of image: " + decimalCoords);
// 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);
List displayCatList = app.cacheData.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);
Log.d(TAG, "displayCatList size 0, calling MWAPI" + displayCatList.toString());
} else {
cacheFound = true;
Log.d(TAG, "Cache found, setting categoryList in MwVolleyApi to " + displayCatList.toString());
MwVolleyApi.setGpsCat(displayCatList);
}
}
}
@Override
public void onPause() {
super.onPause();
try {
imageObj.unregisterLocationManager();
Log.d(TAG, "Unregistered locationManager");
}
catch (NullPointerException e) {
Log.d(TAG, "locationManager does not exist, not unregistered");
}
}
@Override
protected void onDestroy() {
super.onDestroy();
uploadController.cleanup();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
}

View file

@ -0,0 +1,127 @@
package fr.free.nrw.commons.upload;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import fr.free.nrw.commons.Prefs;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
public class SingleUploadFragment extends Fragment {
public interface OnUploadActionInitiated {
void uploadActionInitiated(String title, String description);
}
private EditText titleEdit;
private EditText descEdit;
private TextView licenseSummaryView;
private OnUploadActionInitiated uploadActionInitiatedHandler;
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.activity_share, menu);
if(titleEdit != null) {
menu.findItem(R.id.menu_upload_single).setEnabled(titleEdit.getText().length() != 0);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_upload_single:
uploadActionInitiatedHandler.uploadActionInitiated(titleEdit.getText().toString(), descEdit.getText().toString());
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_single_upload, null);
titleEdit = (EditText)rootView.findViewById(R.id.titleEdit);
descEdit = (EditText)rootView.findViewById(R.id.descEdit);
licenseSummaryView = (TextView)rootView.findViewById(R.id.share_license_summary);
TextWatcher uploadEnabler = new TextWatcher() {
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { }
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {}
public void afterTextChanged(Editable editable) {
if(getActivity() != null) {
getActivity().invalidateOptionsMenu();
}
}
};
titleEdit.addTextChangedListener(uploadEnabler);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
final String license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA);
licenseSummaryView.setText(getString(R.string.share_license_summary, getString(Utils.licenseNameFor(license))));
// Open license page on touch
licenseSummaryView.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(Utils.licenseUrlFor(license)));
startActivity(intent);
return true;
} else {
return false;
}
}
});
return rootView;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
uploadActionInitiatedHandler = (OnUploadActionInitiated) activity;
}
@Override
public void onStop() {
super.onStop();
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
View target = getView().findFocus();
if (target != null) {
InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(target.getWindowToken(), 0);
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
}

View file

@ -0,0 +1,162 @@
package fr.free.nrw.commons.upload;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import java.io.IOException;
import java.util.Date;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.Prefs;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.Contribution;
public class UploadController {
private UploadService uploadService;
private final Activity activity;
final CommonsApplication app;
public interface ContributionUploadProgress {
void onUploadStarted(Contribution contribution);
}
public UploadController(Activity activity) {
this.activity = activity;
app = (CommonsApplication)activity.getApplicationContext();
}
private boolean isUploadServiceConnected;
private ServiceConnection uploadServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName componentName, IBinder binder) {
uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder)binder).getService();
isUploadServiceConnected = true;
}
public void onServiceDisconnected(ComponentName componentName) {
// this should never happen
throw new RuntimeException("UploadService died but the rest of the process did not!");
}
};
public void prepareService() {
Intent uploadServiceIntent = new Intent(activity.getApplicationContext(), UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
activity.startService(uploadServiceIntent);
activity.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
}
public void cleanup() {
if(isUploadServiceConnected) {
activity.unbindService(uploadServiceConnection);
}
}
public void startUpload(String rawTitle, Uri mediaUri, String description, String mimeType, String source, ContributionUploadProgress onComplete) {
Contribution contribution;
String title = rawTitle;
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
// People are used to ".jpg" more than ".jpeg" which the system gives us.
if (extension != null && extension.toLowerCase().equals("jpeg")) {
extension = "jpg";
}
if(extension != null && !title.toLowerCase().endsWith(extension.toLowerCase())) {
title += "." + extension;
}
contribution = new Contribution(mediaUri, null, title, description, -1, null, null, app.getCurrentAccount().name, CommonsApplication.DEFAULT_EDIT_SUMMARY);
contribution.setTag("mimeType", mimeType);
contribution.setSource(source);
startUpload(contribution, onComplete);
}
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
if(TextUtils.isEmpty(contribution.getCreator())) {
contribution.setCreator(app.getCurrentAccount().name);
}
if(contribution.getDescription() == null) {
contribution.setDescription("");
}
String license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA);
contribution.setLicense(license);
Utils.executeAsyncTask(new AsyncTask<Void, Void, Contribution>() {
// Fills up missing information about Contributions
// Only does things that involve some form of IO
// Runs in background thread
@Override
protected Contribution doInBackground(Void... voids /* stare into you */) {
long length;
try {
if(contribution.getDataLength() <= 0) {
length = activity.getContentResolver().openAssetFileDescriptor(contribution.getLocalUri(), "r").getLength();
if(length == -1) {
// Let us find out the long way!
length = Utils.countBytes(activity.getContentResolver().openInputStream(contribution.getLocalUri()));
}
contribution.setDataLength(length);
}
} catch(IOException e) {
throw new RuntimeException(e);
}
String mimeType = (String)contribution.getTag("mimeType");
if(mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
mimeType = activity.getContentResolver().getType(contribution.getLocalUri());
if(mimeType != null) {
contribution.setTag("mimeType", mimeType);
}
}
if(mimeType.startsWith("image/") && contribution.getDateCreated() == null) {
Cursor cursor = activity.getContentResolver().query(contribution.getLocalUri(),
new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null);
if(cursor != null && cursor.getCount() != 0) {
cursor.moveToFirst();
Date dateCreated = new Date(cursor.getLong(0));
Date epochStart = new Date(0);
if(dateCreated.equals(epochStart) || dateCreated.before(epochStart)) {
// If date is incorrect (1st second of unix time) then set it to the current date
dateCreated = new Date();
}
contribution.setDateCreated(dateCreated);
} else {
contribution.setDateCreated(new Date());
}
}
return contribution;
}
@Override
protected void onPostExecute(Contribution contribution) {
super.onPostExecute(contribution);
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, contribution);
onComplete.onUploadStarted(contribution);
}
});
}
}

View file

@ -0,0 +1,325 @@
package fr.free.nrw.commons.upload;
import java.io.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.graphics.*;
import android.os.Bundle;
import fr.free.nrw.commons.*;
import fr.free.nrw.commons.EventLog;
import org.mediawiki.api.*;
import in.yuvi.http.fluent.ProgressListener;
import android.app.*;
import android.content.*;
import android.support.v4.app.NotificationCompat;
import android.util.*;
import android.widget.*;
import fr.free.nrw.commons.contributions.*;
import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
public class UploadService extends HandlerService<Contribution> {
private static final String EXTRA_PREFIX = "fr.free.nrw.commons.upload";
public static final int ACTION_UPLOAD_FILE = 1;
public static final String ACTION_START_SERVICE = EXTRA_PREFIX + ".upload";
public static final String EXTRA_SOURCE = EXTRA_PREFIX + ".source";
public static final String EXTRA_CAMPAIGN = EXTRA_PREFIX + ".campaign";
private NotificationManager notificationManager;
private ContentProviderClient contributionsProviderClient;
private CommonsApplication app;
private NotificationCompat.Builder curProgressNotification;
private int toUpload;
// DO NOT HAVE NOTIFICATION ID OF 0 FOR ANYTHING
// See http://stackoverflow.com/questions/8725909/startforeground-does-not-show-my-notification
// Seriously, Android?
public static final int NOTIFICATION_UPLOAD_IN_PROGRESS = 1;
public static final int NOTIFICATION_UPLOAD_COMPLETE = 2;
public static final int NOTIFICATION_UPLOAD_FAILED = 3;
public UploadService() {
super("UploadService");
}
private class NotificationUpdateProgressListener implements ProgressListener {
String notificationTag;
boolean notificationTitleChanged;
Contribution contribution;
String notificationProgressTitle;
String notificationFinishingTitle;
public NotificationUpdateProgressListener(String notificationTag, String notificationProgressTitle, String notificationFinishingTitle, Contribution contribution) {
this.notificationTag = notificationTag;
this.notificationProgressTitle = notificationProgressTitle;
this.notificationFinishingTitle = notificationFinishingTitle;
this.contribution = contribution;
}
public void onProgress(long transferred, long total) {
Log.d("Commons", String.format("Uploaded %d of %d", transferred, total));
if(!notificationTitleChanged) {
curProgressNotification.setContentTitle(notificationProgressTitle);
notificationTitleChanged = true;
contribution.setState(Contribution.STATE_IN_PROGRESS);
}
if(transferred == total) {
// Completed!
curProgressNotification.setContentTitle(notificationFinishingTitle);
curProgressNotification.setProgress(0, 100, true);
} else {
curProgressNotification.setProgress(100, (int) (((double) transferred / (double) total) * 100), false);
}
startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build());
contribution.setTransferred(transferred);
contribution.save();
}
}
@Override
public void onDestroy() {
super.onDestroy();
contributionsProviderClient.release();
Log.d("Commons", "ZOMG I AM BEING KILLED HALP!");
}
@Override
public void onCreate() {
super.onCreate();
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
app = (CommonsApplication) this.getApplicationContext();
contributionsProviderClient = this.getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY);
}
@Override
protected void handle(int what, Contribution contribution) {
switch(what) {
case ACTION_UPLOAD_FILE:
uploadContribution(contribution);
break;
default:
throw new IllegalArgumentException("Unknown value for what");
}
}
@Override
public void queue(int what, Contribution contribution) {
switch (what) {
case ACTION_UPLOAD_FILE:
contribution.setState(Contribution.STATE_QUEUED);
contribution.setTransferred(0);
contribution.setContentProviderClient(contributionsProviderClient);
contribution.save();
toUpload++;
if (curProgressNotification != null && toUpload != 1) {
curProgressNotification.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload));
Log.d("Commons", String.format("%d uploads left", toUpload));
this.startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build());
}
super.queue(what, contribution);
break;
default:
throw new IllegalArgumentException("Unknown value for what");
}
}
private boolean freshStart = true;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if(intent.getAction() == ACTION_START_SERVICE && freshStart) {
ContentValues failedValues = new ContentValues();
failedValues.put(Contribution.Table.COLUMN_STATE, Contribution.STATE_FAILED);
int updated = getContentResolver().update(ContributionsContentProvider.BASE_URI,
failedValues,
Contribution.Table.COLUMN_STATE + " = ? OR " + Contribution.Table.COLUMN_STATE + " = ?",
new String[]{ String.valueOf(Contribution.STATE_QUEUED), String.valueOf(Contribution.STATE_IN_PROGRESS) }
);
Log.d("Commons", "Set " + updated + " uploads to failed");
Log.d("Commons", "Flags is" + flags + " id is" + startId);
freshStart = false;
}
return START_REDELIVER_INTENT;
}
private void uploadContribution(Contribution contribution) {
MWApi api = app.getApi();
ApiResult result;
InputStream file = null;
String notificationTag = contribution.getLocalUri().toString();
try {
file = this.getContentResolver().openInputStream(contribution.getLocalUri());
} catch(FileNotFoundException e) {
Log.d("Exception", "File not found");
Toast fileNotFound = Toast.makeText(this, R.string.upload_failed, Toast.LENGTH_LONG);
fileNotFound.show();
}
Log.d("Commons", "Before execution!");
curProgressNotification = new NotificationCompat.Builder(this).setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher))
.setAutoCancel(true)
.setContentTitle(String.format(getString(R.string.upload_progress_notification_title_start), contribution.getDisplayTitle()))
.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload))
.setOngoing(true)
.setProgress(100, 0, true)
.setContentIntent(PendingIntent.getActivity(getApplicationContext(), 0, new Intent(this, ContributionsActivity.class), 0))
.setTicker(String.format(getString(R.string.upload_progress_notification_title_in_progress), contribution.getDisplayTitle()));
this.startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build());
try {
String filename = findUniqueFilename(contribution.getFilename());
if(!api.validateLogin()) {
// Need to revalidate!
if(app.revalidateAuthToken()) {
Log.d("Commons", "Successfully revalidated token!");
} else {
Log.d("Commons", "Unable to revalidate :(");
// TODO: Put up a new notification, ask them to re-login
stopForeground(true);
Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG);
failureToast.show();
return;
}
}
NotificationUpdateProgressListener notificationUpdater = new NotificationUpdateProgressListener(notificationTag,
String.format(getString(R.string.upload_progress_notification_title_in_progress), contribution.getDisplayTitle()),
String.format(getString(R.string.upload_progress_notification_title_finishing), contribution.getDisplayTitle()),
contribution
);
result = api.upload(filename, file, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), notificationUpdater);
Log.d("Commons", "Response is" + Utils.getStringFromDOM(result.getDocument()));
curProgressNotification = null;
String resultStatus = result.getString("/api/upload/@result");
if(!resultStatus.equals("Success")) {
String errorCode = result.getString("/api/error/@code");
showFailedNotification(contribution);
fr.free.nrw.commons.EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT)
.param("username", app.getCurrentAccount().name)
.param("source", contribution.getSource())
.param("multiple", contribution.getMultiple())
.param("result", errorCode)
.param("filename", contribution.getFilename())
.log();
} else {
Date dateUploaded = null;
dateUploaded = Utils.parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename
String imageUrl = result.getString("/api/upload/imageinfo/@url");
contribution.setFilename(canonicalFilename);
contribution.setImageUrl(imageUrl);
contribution.setState(Contribution.STATE_COMPLETED);
contribution.setDateUploaded(dateUploaded);
contribution.save();
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT)
.param("username", app.getCurrentAccount().name)
.param("source", contribution.getSource()) //FIXME
.param("filename", contribution.getFilename())
.param("multiple", contribution.getMultiple())
.param("result", "success")
.log();
}
} catch(IOException e) {
Log.d("Commons", "I have a network fuckup");
showFailedNotification(contribution);
return;
} finally {
toUpload--;
if(toUpload == 0) {
// Sync modifications right after all uplaods are processed
ContentResolver.requestSync(((CommonsApplication) getApplicationContext()).getCurrentAccount(), ModificationsContentProvider.AUTHORITY, new Bundle());
stopForeground(true);
}
}
}
private void showFailedNotification(Contribution contribution) {
Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
.setAutoCancel(true)
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0))
.setTicker(String.format(getString(R.string.upload_failed_notification_title), contribution.getDisplayTitle()))
.setContentTitle(String.format(getString(R.string.upload_failed_notification_title), contribution.getDisplayTitle()))
.setContentText(getString(R.string.upload_failed_notification_subtitle))
.build();
notificationManager.notify(NOTIFICATION_UPLOAD_FAILED, failureNotification);
contribution.setState(Contribution.STATE_FAILED);
contribution.save();
}
private String findUniqueFilename(String fileName) throws IOException {
return findUniqueFilename(fileName, 1);
}
private String findUniqueFilename(String fileName, int sequenceNumber) throws IOException {
String sequenceFileName;
if (sequenceNumber == 1) {
sequenceFileName = fileName;
} else {
if (fileName.indexOf('.') == -1) {
// We really should have appended a file type suffix already.
// But... we might not.
sequenceFileName = fileName + " " + sequenceNumber;
} else {
Pattern regex = Pattern.compile("^(.*)(\\..+?)$");
Matcher regexMatcher = regex.matcher(fileName);
sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2");
}
}
Log.d("Commons", "checking for uniqueness of name " + sequenceFileName);
if (fileExistsWithName(sequenceFileName)) {
return findUniqueFilename(fileName, sequenceNumber + 1);
} else {
return sequenceFileName;
}
}
private boolean fileExistsWithName(String fileName) throws IOException {
MWApi api = app.getApi();
ApiResult result;
result = api.action("query")
.param("prop", "imageinfo")
.param("titles", "File:" + fileName)
.get();
ArrayList<ApiResult> nodes = result.getNodes("/api/query/pages/page/imageinfo");
return nodes.size() > 0;
}
}