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,370 @@
package fr.free.nrw.commons.contributions;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Parcel;
import android.os.RemoteException;
import android.text.TextUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.EventLog;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.Prefs;
import fr.free.nrw.commons.Utils;
public class Contribution extends Media {
public static Creator<Contribution> CREATOR = new Creator<Contribution>() {
public Contribution createFromParcel(Parcel parcel) {
return new Contribution(parcel);
}
public Contribution[] newArray(int i) {
return new Contribution[0];
}
};
// No need to be bitwise - they're mutually exclusive
public static final int STATE_COMPLETED = -1;
public static final int STATE_FAILED = 1;
public static final int STATE_QUEUED = 2;
public static final int STATE_IN_PROGRESS = 3;
public static final String SOURCE_CAMERA = "camera";
public static final String SOURCE_GALLERY = "gallery";
public static final String SOURCE_EXTERNAL = "external";
private ContentProviderClient client;
private Uri contentUri;
private String source;
private String editSummary;
private Date timestamp;
private int state;
private long transferred;
private boolean isMultiple;
public boolean getMultiple() {
return isMultiple;
}
public void setMultiple(boolean multiple) {
isMultiple = multiple;
}
public EventLog.LogBuilder event;
@Override
public void writeToParcel(Parcel parcel, int flags) {
super.writeToParcel(parcel, flags);
parcel.writeParcelable(contentUri, flags);
parcel.writeString(source);
parcel.writeSerializable(timestamp);
parcel.writeInt(state);
parcel.writeLong(transferred);
parcel.writeInt(isMultiple ? 1 : 0);
}
public Contribution(Parcel in) {
super(in);
contentUri = (Uri)in.readParcelable(Uri.class.getClassLoader());
source = in.readString();
timestamp = (Date) in.readSerializable();
state = in.readInt();
transferred = in.readLong();
isMultiple = in.readInt() == 1;
}
public long getTransferred() {
return transferred;
}
public void setTransferred(long transferred) {
this.transferred = transferred;
}
public String getEditSummary() {
return editSummary != null ? editSummary : CommonsApplication.DEFAULT_EDIT_SUMMARY;
}
public Uri getContentUri() {
return contentUri;
}
public Date getTimestamp() {
return timestamp;
}
public Contribution(Uri localUri, String remoteUri, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator, String editSummary) {
super(localUri, remoteUri, filename, description, dataLength, dateCreated, dateUploaded, creator);
this.editSummary = editSummary;
timestamp = new Date(System.currentTimeMillis());
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public void setDateUploaded(Date date) {
this.dateUploaded = date;
}
public String getTrackingTemplates() {
return "{{subst:unc}}"; // Remove when we have categorization
}
public String getPageContents() {
StringBuffer buffer = new StringBuffer();
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd");
buffer
.append("== {{int:filedesc}} ==\n")
.append("{{Information\n")
.append("|description=").append(getDescription()).append("\n")
.append("|source=").append("{{own}}\n")
.append("|author=[[User:").append(creator).append("|").append(creator).append("]]\n");
if(dateCreated != null) {
buffer
.append("|date={{According to EXIF data|").append(isoFormat.format(dateCreated)).append("}}\n");
}
buffer
.append("}}").append("\n")
.append("== {{int:license-header}} ==\n")
.append(Utils.licenseTemplateFor(getLicense())).append("\n\n")
.append("{{Uploaded from Mobile|platform=Android|version=").append(CommonsApplication.APPLICATION_VERSION).append("}}\n")
.append(getTrackingTemplates());
return buffer.toString();
}
public void setContentProviderClient(ContentProviderClient client) {
this.client = client;
}
public void save() {
try {
if(contentUri == null) {
contentUri = client.insert(fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI, this.toContentValues());
} else {
client.update(contentUri, toContentValues(), null, null);
}
} catch(RemoteException e) {
throw new RuntimeException(e);
}
}
public void delete() {
try {
if(contentUri == null) {
// noooo
throw new RuntimeException("tried to delete item with no content URI");
} else {
client.delete(contentUri, null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
public ContentValues toContentValues() {
ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_FILENAME, getFilename());
if(getLocalUri() != null) {
cv.put(Table.COLUMN_LOCAL_URI, getLocalUri().toString());
}
if(getImageUrl() != null) {
cv.put(Table.COLUMN_IMAGE_URL, getImageUrl().toString());
}
if(getDateUploaded() != null) {
cv.put(Table.COLUMN_UPLOADED, getDateUploaded().getTime());
}
cv.put(Table.COLUMN_LENGTH, getDataLength());
cv.put(Table.COLUMN_TIMESTAMP, getTimestamp().getTime());
cv.put(Table.COLUMN_STATE, getState());
cv.put(Table.COLUMN_TRANSFERRED, transferred);
cv.put(Table.COLUMN_SOURCE, source);
cv.put(Table.COLUMN_DESCRIPTION, description);
cv.put(Table.COLUMN_CREATOR, creator);
cv.put(Table.COLUMN_MULTIPLE, isMultiple ? 1 : 0);
cv.put(Table.COLUMN_WIDTH, width);
cv.put(Table.COLUMN_HEIGHT, height);
cv.put(Table.COLUMN_LICENSE, license);
return cv;
}
public void setFilename(String filename) {
this.filename = filename;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public Contribution() {
super();
timestamp = new Date(System.currentTimeMillis());
}
public static Contribution fromCursor(Cursor cursor) {
// Hardcoding column positions!
Contribution c = new Contribution();
//Check that cursor has a value to avoid CursorIndexOutOfBoundsException
if (cursor.getCount() > 0) {
c.contentUri = fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId(cursor.getInt(0));
c.filename = cursor.getString(1);
c.localUri = TextUtils.isEmpty(cursor.getString(2)) ? null : Uri.parse(cursor.getString(2));
c.imageUrl = cursor.getString(3);
c.timestamp = cursor.getLong(4) == 0 ? null : new Date(cursor.getLong(4));
c.state = cursor.getInt(5);
c.dataLength = cursor.getLong(6);
c.dateUploaded = cursor.getLong(7) == 0 ? null : new Date(cursor.getLong(7));
c.transferred = cursor.getLong(8);
c.source = cursor.getString(9);
c.description = cursor.getString(10);
c.creator = cursor.getString(11);
c.isMultiple = cursor.getInt(12) == 1;
c.width = cursor.getInt(13);
c.height = cursor.getInt(14);
c.license = cursor.getString(15);
}
return c;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public void setLocalUri(Uri localUri) {
this.localUri = localUri;
}
public static class Table {
public static final String TABLE_NAME = "contributions";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_FILENAME = "filename";
public static final String COLUMN_LOCAL_URI = "local_uri";
public static final String COLUMN_IMAGE_URL = "image_url";
public static final String COLUMN_TIMESTAMP = "timestamp";
public static final String COLUMN_STATE = "state";
public static final String COLUMN_LENGTH = "length";
public static final String COLUMN_UPLOADED = "uploaded";
public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes
public static final String COLUMN_SOURCE = "source";
public static final String COLUMN_DESCRIPTION = "description";
public static final String COLUMN_CREATOR = "creator"; // Initial uploader
public static final String COLUMN_MULTIPLE = "multiple";
public static final String COLUMN_WIDTH = "width";
public static final String COLUMN_HEIGHT = "height";
public static final String COLUMN_LICENSE = "license";
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = {
COLUMN_ID,
COLUMN_FILENAME,
COLUMN_LOCAL_URI,
COLUMN_IMAGE_URL,
COLUMN_TIMESTAMP,
COLUMN_STATE,
COLUMN_LENGTH,
COLUMN_UPLOADED,
COLUMN_TRANSFERRED,
COLUMN_SOURCE,
COLUMN_DESCRIPTION,
COLUMN_CREATOR,
COLUMN_MULTIPLE,
COLUMN_WIDTH,
COLUMN_HEIGHT,
COLUMN_LICENSE
};
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ "_id INTEGER PRIMARY KEY,"
+ "filename STRING,"
+ "local_uri STRING,"
+ "image_url STRING,"
+ "uploaded INTEGER,"
+ "timestamp INTEGER,"
+ "state INTEGER,"
+ "length INTEGER,"
+ "transferred INTEGER,"
+ "source STRING,"
+ "description STRING,"
+ "creator STRING,"
+ "multiple INTEGER,"
+ "width INTEGER,"
+ "height INTEGER,"
+ "LICENSE STRING"
+ ");";
public static void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
}
public static void onUpdate(SQLiteDatabase db, int from, int to) {
if(from == to) {
return;
}
if(from == 1) {
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;");
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;");
from++;
onUpdate(db, from, to);
return;
}
if(from == 2) {
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;");
db.execSQL("UPDATE " + TABLE_NAME + " SET multiple = 0");
from++;
onUpdate(db, from, to);
return;
}
if(from == 3) {
// Do nothing
from++;
onUpdate(db, from, to);
return;
}
if(from == 4) {
// Do nothing -- added Category
from++;
onUpdate(db, from, to);
return;
}
if(from == 5) {
// Added width and height fields
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;");
db.execSQL("UPDATE " + TABLE_NAME + " SET width = 0");
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;");
db.execSQL("UPDATE " + TABLE_NAME + " SET height = 0");
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;");
db.execSQL("UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA + "';");
from++;
onUpdate(db, from, to);
return;
}
}
}
}

View file

@ -0,0 +1,98 @@
package fr.free.nrw.commons.contributions;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.v4.app.Fragment;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import fr.free.nrw.commons.upload.ShareActivity;
import fr.free.nrw.commons.upload.UploadService;
public class ContributionController {
private Fragment fragment;
private Activity activity;
private final static int SELECT_FROM_GALLERY = 1;
private final static int SELECT_FROM_CAMERA = 2;
public ContributionController(Fragment fragment) {
this.fragment = fragment;
this.activity = fragment.getActivity();
}
// See http://stackoverflow.com/a/5054673/17865 for why this is done
private Uri lastGeneratedCaptureURI;
private Uri reGenerateImageCaptureURI() {
String storageState = Environment.getExternalStorageState();
if(storageState.equals(Environment.MEDIA_MOUNTED)) {
String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Commons/images/" + new Date().getTime() + ".jpg";
File _photoFile = new File(path);
try {
if(_photoFile.exists() == false) {
_photoFile.getParentFile().mkdirs();
_photoFile.createNewFile();
}
} catch (IOException e) {
Log.e("Commons", "Could not create file: " + path, e);
}
return Uri.fromFile(_photoFile);
} else {
throw new RuntimeException("No external storage found!");
}
}
public void startCameraCapture() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
lastGeneratedCaptureURI = reGenerateImageCaptureURI();
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, lastGeneratedCaptureURI);
fragment.startActivityForResult(takePictureIntent, SELECT_FROM_CAMERA);
}
public void startGalleryPick() {
Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT);
pickImageIntent.setType("image/*");
fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY);
}
public void handleImagePicked(int requestCode, Intent data) {
Intent shareIntent = new Intent(activity, ShareActivity.class);
shareIntent.setAction(Intent.ACTION_SEND);
switch(requestCode) {
case SELECT_FROM_GALLERY:
shareIntent.setType(activity.getContentResolver().getType(data.getData()));
shareIntent.putExtra(Intent.EXTRA_STREAM, data.getData());
shareIntent.putExtra(UploadService.EXTRA_SOURCE, fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY);
break;
case SELECT_FROM_CAMERA:
shareIntent.setType("image/jpeg"); //FIXME: Find out appropriate mime type
shareIntent.putExtra(Intent.EXTRA_STREAM, lastGeneratedCaptureURI);
shareIntent.putExtra(UploadService.EXTRA_SOURCE, fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA);
break;
}
Log.i("Image", "Image selected");
activity.startActivity(shareIntent);
}
public void saveState(Bundle outState) {
outState.putParcelable("lastGeneratedCaptureURI", lastGeneratedCaptureURI);
}
public void loadState(Bundle savedInstanceState) {
if(savedInstanceState != null) {
lastGeneratedCaptureURI = (Uri) savedInstanceState.getParcelable("lastGeneratedCaptureURI");
}
}
}

View file

@ -0,0 +1,25 @@
package fr.free.nrw.commons.contributions;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.R;
class ContributionViewHolder {
final MediaWikiImageView imageView;
final TextView titleView;
final TextView stateView;
final TextView seqNumView;
final ProgressBar progressView;
String url;
ContributionViewHolder(View parent) {
imageView = (MediaWikiImageView) parent.findViewById(R.id.contributionImage);
titleView = (TextView)parent.findViewById(R.id.contributionTitle);
stateView = (TextView)parent.findViewById(R.id.contributionState);
seqNumView = (TextView)parent.findViewById(R.id.contributionSequenceNumber);
progressView = (ProgressBar)parent.findViewById(R.id.contributionProgress);
}
}

View file

@ -0,0 +1,291 @@
package fr.free.nrw.commons.contributions;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Adapter;
import android.widget.AdapterView;
import java.util.ArrayList;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.*;
import fr.free.nrw.commons.media.*;
import fr.free.nrw.commons.upload.UploadService;
public class ContributionsActivity
extends AuthenticatedActivity
implements LoaderManager.LoaderCallbacks<Object>,
AdapterView.OnItemClickListener,
MediaDetailPagerFragment.MediaDetailProvider,
FragmentManager.OnBackStackChangedListener,
ContributionsListFragment.SourceRefresher {
private Cursor allContributions;
private ContributionsListFragment contributionsList;
private MediaDetailPagerFragment mediaDetails;
private UploadService uploadService;
private boolean isUploadServiceConnected;
private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<DataSetObserver>();
private String CONTRIBUTION_SELECTION = "";
/*
This sorts in the following order:
Currently Uploading
Failed (Sorted in ascending order of time added - FIFO)
Queued to Upload (Sorted in ascending order of time added - FIFO)
Completed (Sorted in descending order of time added)
This is why Contribution.STATE_COMPLETED is -1.
*/
private String CONTRIBUTION_SORT = Contribution.Table.COLUMN_STATE + " DESC, " + Contribution.Table.COLUMN_UPLOADED + " DESC , (" + Contribution.Table.COLUMN_TIMESTAMP + " * " + Contribution.Table.COLUMN_STATE + ")";
public ContributionsActivity() {
super(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE);
}
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!");
}
};
@Override
protected void onDestroy() {
super.onDestroy();
if(isUploadServiceConnected) {
unbindService(uploadServiceConnection);
}
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onAuthCookieAcquired(String authCookie) {
// Do a sync everytime we get here!
ContentResolver.requestSync(((CommonsApplication) getApplicationContext()).getCurrentAccount(), ContributionsContentProvider.AUTHORITY, new Bundle());
Intent uploadServiceIntent = new Intent(this, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
startService(uploadServiceIntent);
bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
allContributions = getContentResolver().query(ContributionsContentProvider.BASE_URI, Contribution.Table.ALL_FIELDS, CONTRIBUTION_SELECTION, null, CONTRIBUTION_SORT);
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(R.string.title_activity_contributions);
setContentView(R.layout.activity_contributions);
contributionsList = (ContributionsListFragment)getSupportFragmentManager().findFragmentById(R.id.contributionsListFragment);
getSupportFragmentManager().addOnBackStackChangedListener(this);
if (savedInstanceState != null) {
mediaDetails = (MediaDetailPagerFragment)getSupportFragmentManager().findFragmentById(R.id.contributionsFragmentContainer);
// onBackStackChanged uses mediaDetails.isVisible() but this returns false now.
// Use the saved value from before pause or orientation change.
if (mediaDetails != null && savedInstanceState.getBoolean("mediaDetailsVisible")) {
// Feels awful that we have to reset this manually!
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
}
requestAuthToken();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("mediaDetailsVisible", (mediaDetails != null && mediaDetails.isVisible()));
}
private void showDetail(int i) {
if(mediaDetails == null ||!mediaDetails.isVisible()) {
mediaDetails = new MediaDetailPagerFragment();
this.getSupportFragmentManager()
.beginTransaction()
.replace(R.id.contributionsFragmentContainer, mediaDetails)
.addToBackStack(null)
.commit();
this.getSupportFragmentManager().executePendingTransactions();
}
mediaDetails.showImage(i);
}
public void retryUpload(int i) {
allContributions.moveToPosition(i);
Contribution c = Contribution.fromCursor(allContributions);
if(c.getState() == Contribution.STATE_FAILED) {
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c);
Log.d("Commons", "Restarting for" + c.toContentValues().toString());
} else {
Log.d("Commons", "Skipping re-upload for non-failed " + c.toContentValues().toString());
}
}
public void deleteUpload(int i) {
allContributions.moveToPosition(i);
Contribution c = Contribution.fromCursor(allContributions);
if(c.getState() == Contribution.STATE_FAILED) {
Log.d("Commons", "Deleting failed contrib " + c.toContentValues().toString());
c.setContentProviderClient(getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY));
c.delete();
} else {
Log.d("Commons", "Skipping deletion for non-failed contrib " + c.toContentValues().toString());
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case android.R.id.home:
if(mediaDetails.isVisible()) {
getSupportFragmentManager().popBackStack();
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onAuthFailure() {
super.onAuthFailure();
finish(); // If authentication failed, we just exit
}
public void onItemClick(AdapterView<?> adapterView, View view, int position, long item) {
showDetail(position);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return super.onCreateOptionsMenu(menu);
}
public Loader onCreateLoader(int i, Bundle bundle) {
return new CursorLoader(this, ContributionsContentProvider.BASE_URI, Contribution.Table.ALL_FIELDS, CONTRIBUTION_SELECTION, null, CONTRIBUTION_SORT);
}
public void onLoadFinished(Loader cursorLoader, Object result) {
Cursor cursor = (Cursor) result;
if(contributionsList.getAdapter() == null) {
contributionsList.setAdapter(new ContributionsListAdapter(this, cursor, 0));
} else {
((CursorAdapter)contributionsList.getAdapter()).swapCursor(cursor);
}
getSupportActionBar().setSubtitle(getResources().getQuantityString(R.plurals.contributions_subtitle, cursor.getCount(), cursor.getCount()));
contributionsList.clearSyncMessage();
notifyAndMigrateDataSetObservers();
}
public void onLoaderReset(Loader cursorLoader) {
((CursorAdapter) contributionsList.getAdapter()).swapCursor(null);
}
public Media getMediaAtPosition(int i) {
if (contributionsList.getAdapter() == null) {
// not yet ready to return data
return null;
} else {
return Contribution.fromCursor((Cursor) contributionsList.getAdapter().getItem(i));
}
}
public int getTotalMediaCount() {
if(contributionsList.getAdapter() == null) {
return 0;
}
return contributionsList.getAdapter().getCount();
}
public void notifyDatasetChanged() {
// Do nothing for now
}
private void notifyAndMigrateDataSetObservers() {
Adapter adapter = contributionsList.getAdapter();
// First, move the observers over to the adapter now that we have it.
for (DataSetObserver observer : observersWaitingForLoad) {
adapter.registerDataSetObserver(observer);
}
observersWaitingForLoad.clear();
// Now fire off a first notification...
for (DataSetObserver observer : observersWaitingForLoad) {
observer.onChanged();
}
}
public void registerDataSetObserver(DataSetObserver observer) {
Adapter adapter = contributionsList.getAdapter();
if (adapter == null) {
observersWaitingForLoad.add(observer);
} else {
adapter.registerDataSetObserver(observer);
}
}
public void unregisterDataSetObserver(DataSetObserver observer) {
Adapter adapter = contributionsList.getAdapter();
if (adapter == null) {
observersWaitingForLoad.remove(observer);
} else {
adapter.unregisterDataSetObserver(observer);
}
}
public void onBackStackChanged() {
if(mediaDetails != null && mediaDetails.isVisible()) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
} else {
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
}
public void refreshSource() {
getSupportLoaderManager().restartLoader(0, null, this);
}
}

View file

@ -0,0 +1,176 @@
package fr.free.nrw.commons.contributions;
import android.content.*;
import android.database.*;
import android.database.sqlite.*;
import android.net.*;
import android.text.*;
import android.util.*;
import fr.free.nrw.commons.data.*;
import fr.free.nrw.commons.CommonsApplication;
public class ContributionsContentProvider extends ContentProvider{
private static final int CONTRIBUTIONS = 1;
private static final int CONTRIBUTIONS_ID = 2;
public static final String AUTHORITY = "fr.free.nrw.commons.contributions.contentprovider";
private static final String BASE_PATH = "contributions";
public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH);
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI(AUTHORITY, BASE_PATH, CONTRIBUTIONS);
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID);
}
public static Uri uriForId(int id) {
return Uri.parse(BASE_URI.toString() + "/" + id);
}
private DBOpenHelper dbOpenHelper;
@Override
public boolean onCreate() {
dbOpenHelper = DBOpenHelper.getInstance(getContext());
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(Contribution.Table.TABLE_NAME);
int uriType = uriMatcher.match(uri);
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor;
switch(uriType) {
case CONTRIBUTIONS:
cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
break;
case CONTRIBUTIONS_ID:
cursor = queryBuilder.query(db,
Contribution.Table.ALL_FIELDS,
"_id = ?",
new String[] { uri.getLastPathSegment() },
null,
null,
sortOrder
);
break;
default:
throw new IllegalArgumentException("Unknown URI" + uri);
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id = 0;
switch (uriType) {
case CONTRIBUTIONS:
id = sqlDB.insert(Contribution.Table.TABLE_NAME, null, contentValues);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(BASE_URI + "/" + id);
}
@Override
public int delete(Uri uri, String s, String[] strings) {
int rows = 0;
int uriType = uriMatcher.match(uri);
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
switch(uriType) {
case CONTRIBUTIONS_ID:
Log.d("Commons", "Deleting contribution id " + uri.getLastPathSegment());
rows = db.delete(Contribution.Table.TABLE_NAME,
"_id = ?",
new String[] { uri.getLastPathSegment() }
);
break;
default:
throw new IllegalArgumentException("Unknown URI" + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return rows;
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
Log.d("Commons", "Hello, bulk insert!");
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
sqlDB.beginTransaction();
switch (uriType) {
case CONTRIBUTIONS:
for(ContentValues value: values) {
Log.d("Commons", "Inserting! " + value.toString());
sqlDB.insert(Contribution.Table.TABLE_NAME, null, value);
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
sqlDB.setTransactionSuccessful();
sqlDB.endTransaction();
getContext().getContentResolver().notifyChange(uri, null);
return values.length;
}
@Override
public int update(Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
/*
SQL Injection warnings: First, note that we're not exposing this to the outside world (exported="false")
Even then, we should make sure to sanitize all user input appropriately. Input that passes through ContentValues
should be fine. So only issues are those that pass in via concating.
In here, the only concat created argument is for id. It is cast to an int, and will error out otherwise.
*/
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated = 0;
switch (uriType) {
case CONTRIBUTIONS:
rowsUpdated = sqlDB.update(Contribution.Table.TABLE_NAME,
contentValues,
selection,
selectionArgs);
break;
case CONTRIBUTIONS_ID:
int id = Integer.valueOf(uri.getLastPathSegment());
if (TextUtils.isEmpty(selection)) {
rowsUpdated = sqlDB.update(Contribution.Table.TABLE_NAME,
contentValues,
Contribution.Table.COLUMN_ID + " = ?",
new String[] { String.valueOf(id) } );
} else {
throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID");
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType);
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsUpdated;
}
}

View file

@ -0,0 +1,115 @@
package fr.free.nrw.commons.contributions;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.support.v4.widget.CursorAdapter;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.assist.SimpleImageLoadingListener;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.R;
class ContributionsListAdapter extends CursorAdapter {
private DisplayImageOptions contributionDisplayOptions = Utils.getGenericDisplayOptions().build();
private Activity activity;
public ContributionsListAdapter(Activity activity, Cursor c, int flags) {
super(activity, c, flags);
this.activity = activity;
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
View parent = activity.getLayoutInflater().inflate(R.layout.layout_contribution, viewGroup, false);
parent.setTag(new ContributionViewHolder(parent));
return parent;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
final ContributionViewHolder views = (ContributionViewHolder)view.getTag();
final Contribution contribution = Contribution.fromCursor(cursor);
String actualUrl = (contribution.getLocalUri() != null && !TextUtils.isEmpty(contribution.getLocalUri().toString())) ? contribution.getLocalUri().toString() : contribution.getThumbnailUrl(640);
if(views.url == null || !views.url.equals(actualUrl)) {
if(actualUrl.startsWith("http")) {
MediaWikiImageView mwImageView = (MediaWikiImageView)views.imageView;
mwImageView.setMedia(contribution, ((CommonsApplication) activity.getApplicationContext()).getImageLoader());
// FIXME: For transparent images
} else {
com.nostra13.universalimageloader.core.ImageLoader.getInstance().displayImage(actualUrl, views.imageView, contributionDisplayOptions, new SimpleImageLoadingListener() {
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
if(loadedImage.hasAlpha()) {
views.imageView.setBackgroundResource(android.R.color.white);
}
views.seqNumView.setVisibility(View.GONE);
}
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
super.onLoadingFailed(imageUri, view, failReason);
MediaWikiImageView mwImageView = (MediaWikiImageView)views.imageView;
mwImageView.setMedia(contribution, ((CommonsApplication) activity.getApplicationContext()).getImageLoader());
}
});
}
views.url = actualUrl;
}
BitmapDrawable actualImageDrawable = (BitmapDrawable)views.imageView.getDrawable();
if(actualImageDrawable != null && actualImageDrawable.getBitmap() != null && actualImageDrawable.getBitmap().hasAlpha()) {
views.imageView.setBackgroundResource(android.R.color.white);
} else {
views.imageView.setBackgroundDrawable(null);
}
views.titleView.setText(contribution.getDisplayTitle());
views.seqNumView.setText(String.valueOf(cursor.getPosition() + 1));
views.seqNumView.setVisibility(View.VISIBLE);
switch(contribution.getState()) {
case Contribution.STATE_COMPLETED:
views.stateView.setVisibility(View.GONE);
views.progressView.setVisibility(View.GONE);
views.stateView.setText("");
break;
case Contribution.STATE_QUEUED:
views.stateView.setVisibility(View.VISIBLE);
views.progressView.setVisibility(View.GONE);
views.stateView.setText(R.string.contribution_state_queued);
break;
case Contribution.STATE_IN_PROGRESS:
views.stateView.setVisibility(View.GONE);
views.progressView.setVisibility(View.VISIBLE);
long total = contribution.getDataLength();
long transferred = contribution.getTransferred();
if(transferred == 0 || transferred >= total) {
views.progressView.setIndeterminate(true);
} else {
views.progressView.setProgress((int)(((double)transferred / (double)total) * 100));
}
break;
case Contribution.STATE_FAILED:
views.stateView.setVisibility(View.VISIBLE);
views.stateView.setText(R.string.contribution_state_failed);
views.progressView.setVisibility(View.GONE);
break;
}
}
}

View file

@ -0,0 +1,168 @@
package fr.free.nrw.commons.contributions;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ListAdapter;
import android.widget.TextView;
import android.support.v4.app.Fragment;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;
import fr.free.nrw.commons.AboutActivity;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.SettingsActivity;
public class ContributionsListFragment extends Fragment {
public interface SourceRefresher {
void refreshSource();
}
private GridView contributionsList;
private TextView waitingMessage;
private TextView emptyMessage;
private fr.free.nrw.commons.contributions.ContributionController controller;
private static final String TAG = "ContributionsList";
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_contributions, container, false);
contributionsList = (GridView) v.findViewById(R.id.contributionsList);
waitingMessage = (TextView) v.findViewById(R.id.waitingMessage);
emptyMessage = (TextView) v.findViewById(R.id.emptyMessage);
contributionsList.setOnItemClickListener((AdapterView.OnItemClickListener)getActivity());
if(savedInstanceState != null) {
Log.d(TAG, "Scrolling to " + savedInstanceState.getInt("grid-position"));
contributionsList.setSelection(savedInstanceState.getInt("grid-position"));
}
//TODO: Should this be in onResume?
SharedPreferences prefs = this.getActivity().getSharedPreferences("prefs", Context.MODE_PRIVATE);
String lastModified = prefs.getString("lastSyncTimestamp", "");
Log.d(TAG, "Last Sync Timestamp: " + lastModified);
if (lastModified.equals("")) {
waitingMessage.setVisibility(View.VISIBLE);
} else {
waitingMessage.setVisibility(View.GONE);
}
return v;
}
public ListAdapter getAdapter() {
return contributionsList.getAdapter();
}
public void setAdapter(ListAdapter adapter) {
this.contributionsList.setAdapter(adapter);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
controller.saveState(outState);
outState.putInt("grid-position", contributionsList.getFirstVisiblePosition());
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(resultCode == Activity.RESULT_OK) {
controller.handleImagePicked(requestCode, data);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case R.id.menu_from_gallery:
controller.startGalleryPick();
return true;
case R.id.menu_from_camera:
controller.startCameraCapture();
return true;
case R.id.menu_settings:
Intent settingsIntent = new Intent(getActivity(), SettingsActivity.class);
startActivity(settingsIntent);
return true;
case R.id.menu_about:
Intent aboutIntent = new Intent(getActivity(), AboutActivity.class);
startActivity(aboutIntent);
return true;
case R.id.menu_feedback:
Intent feedbackIntent = new Intent(Intent.ACTION_SEND);
feedbackIntent.setType("message/rfc822");
feedbackIntent.putExtra(Intent.EXTRA_EMAIL, new String[] { CommonsApplication.FEEDBACK_EMAIL });
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT, CommonsApplication.APPLICATION_VERSION));
try {
startActivity(feedbackIntent);
}
catch (ActivityNotFoundException e) {
Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show();
}
return true;
case R.id.menu_refresh:
((SourceRefresher)getActivity()).refreshSource();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
menu.clear(); // See http://stackoverflow.com/a/8495697/17865
inflater.inflate(R.menu.fragment_contributions_list, menu);
CommonsApplication app = (CommonsApplication)getActivity().getApplicationContext();
if (!app.deviceHasCamera()) {
menu.findItem(R.id.menu_from_camera).setEnabled(false);
}
menu.findItem(R.id.menu_refresh).setVisible(false);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
controller = new fr.free.nrw.commons.contributions.ContributionController(this);
controller.loadState(savedInstanceState);
}
protected void clearSyncMessage() {
waitingMessage.setVisibility(View.GONE);
}
}

View file

@ -0,0 +1,124 @@
package fr.free.nrw.commons.contributions;
import android.content.*;
import android.database.Cursor;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import android.accounts.Account;
import android.os.Bundle;
import java.io.*;
import java.util.*;
import org.mediawiki.api.*;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils;
public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
private static int COMMIT_THRESHOLD = 10;
public ContributionsSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
}
private int getLimit() {
return 500; // FIXME: Parameterize!
}
private static final String[] existsQuery = { Contribution.Table.COLUMN_FILENAME };
private static final String existsSelection = Contribution.Table.COLUMN_FILENAME + " = ?";
private boolean fileExists(ContentProviderClient client, String filename) {
Cursor cursor = null;
try {
cursor = client.query(ContributionsContentProvider.BASE_URI,
existsQuery,
existsSelection,
new String[] { filename },
""
);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
return cursor != null && cursor.getCount() != 0;
}
@Override
public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) {
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
String user = account.name;
MWApi api = CommonsApplication.createMWApi();
SharedPreferences prefs = this.getContext().getSharedPreferences("prefs", Context.MODE_PRIVATE);
String lastModified = prefs.getString("lastSyncTimestamp", "");
Date curTime = new Date();
ApiResult result;
Boolean done = false;
String queryContinue = null;
while(!done) {
try {
MWApi.RequestBuilder builder = api.action("query")
.param("list", "logevents")
.param("letype", "upload")
.param("leprop", "title|timestamp")
.param("leuser", user)
.param("lelimit", getLimit());
if(!TextUtils.isEmpty(lastModified)) {
builder.param("leend", lastModified);
}
if(!TextUtils.isEmpty(queryContinue)) {
builder.param("lestart", queryContinue);
}
result = builder.get();
} catch (IOException e) {
// There isn't really much we can do, eh?
// FIXME: Perhaps add EventLogging?
syncResult.stats.numIoExceptions += 1; // Not sure if this does anything. Shitty docs
Log.d("Commons", "Syncing failed due to " + e.toString());
return;
}
Log.d("Commons", "Last modified at " + lastModified);
ArrayList<ApiResult> uploads = result.getNodes("/api/query/logevents/item");
Log.d("Commons", uploads.size() + " results!");
ArrayList<ContentValues> imageValues = new ArrayList<ContentValues>();
for(ApiResult image: uploads) {
String filename = image.getString("@title");
if(fileExists(contentProviderClient, filename)) {
Log.d("Commons", "Skipping " + filename);
continue;
}
String thumbUrl = Utils.makeThumbBaseUrl(filename);
Date dateUpdated = Utils.parseMWDate(image.getString("@timestamp"));
Contribution contrib = new Contribution(null, thumbUrl, filename, "", -1, dateUpdated, dateUpdated, user, "");
contrib.setState(Contribution.STATE_COMPLETED);
imageValues.add(contrib.toContentValues());
if(imageValues.size() % COMMIT_THRESHOLD == 0) {
try {
contentProviderClient.bulkInsert(ContributionsContentProvider.BASE_URI, imageValues.toArray(new ContentValues[]{}));
} catch (RemoteException e) {
throw new RuntimeException(e);
}
imageValues.clear();
}
}
if(imageValues.size() != 0) {
try {
contentProviderClient.bulkInsert(ContributionsContentProvider.BASE_URI, imageValues.toArray(new ContentValues[]{}));
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
queryContinue = result.getString("/api/query-continue/logevents/@lestart");
if(TextUtils.isEmpty(queryContinue)) {
done = true;
}
}
prefs.edit().putString("lastSyncTimestamp", Utils.toMWDate(curTime)).apply();
Log.d("Commons", "Oh hai, everyone! Look, a kitty!");
}
}

View file

@ -0,0 +1,26 @@
package fr.free.nrw.commons.contributions;
import android.app.*;
import android.content.*;
import android.os.*;
public class ContributionsSyncService extends Service {
private static final Object sSyncAdapterLock = new Object();
private static ContributionsSyncAdapter sSyncAdapter = null;
@Override
public void onCreate() {
synchronized (sSyncAdapterLock) {
if (sSyncAdapter == null) {
sSyncAdapter = new ContributionsSyncAdapter(getApplicationContext(), true);
}
}
}
@Override
public IBinder onBind(Intent intent) {
return sSyncAdapter.getSyncAdapterBinder();
}
}

View file

@ -0,0 +1,61 @@
package fr.free.nrw.commons.contributions;
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import java.util.ArrayList;
public class MediaListAdapter extends BaseAdapter {
private ArrayList<Media> mediaList;
private Activity activity;
public MediaListAdapter(Activity activity, ArrayList<Media> mediaList) {
this.mediaList = mediaList;
this.activity = activity;
}
public void updateMediaList(ArrayList<Media> newMediaList) {
// FIXME: Hack for now, replace with something more efficient later on
for(Media newMedia: newMediaList) {
boolean isDuplicate = false;
for(Media oldMedia: mediaList ) {
if(newMedia.getFilename().equals(oldMedia.getFilename())) {
isDuplicate = true;
break;
}
}
if(!isDuplicate) {
mediaList.add(0, newMedia);
}
}
}
public int getCount() {
return mediaList.size();
}
public Object getItem(int i) {
return mediaList.get(i);
}
public long getItemId(int i) {
return i;
}
public View getView(int i, View view, ViewGroup viewGroup) {
if(view == null) {
view = activity.getLayoutInflater().inflate(R.layout.layout_contribution, null, false);
view.setTag(new ContributionViewHolder(view));
}
Media m = (Media) getItem(i);
ContributionViewHolder holder = (ContributionViewHolder) view.getTag();
holder.imageView.setMedia(m, ((CommonsApplication)activity.getApplicationContext()).getImageLoader());
holder.titleView.setText(m.getDisplayTitle());
return view;
}
}