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

47
app/build.gradle Normal file
View file

@ -0,0 +1,47 @@
apply plugin: 'com.android.application'
dependencies {
compile fileTree(include: '*.jar', dir: 'libs')
compile 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
compile 'in.yuvi:http.fluent:1.3'
compile 'com.android.volley:volley:1.0.0'
compile 'com.nostra13.universalimageloader:universal-image-loader:1.8.4'
compile 'ch.acra:acra:4.7.0'
compile 'org.mediawiki:api:1.3'
compile 'commons-codec:commons-codec:1.10'
compile 'com.android.support:support-v4:23.4.0'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
//noinspection GradleDependency - old version has required feature
compile 'com.google.code.gson:gson:1.4'
}
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
useLibrary 'org.apache.http.legacy'
defaultConfig {
applicationId "fr.free.nrw.commons"
minSdkVersion 11
targetSdkVersion 23
ndk {
moduleName "libtranscode"
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
disable 'MissingTranslation'
disable 'ExtraTranslation'
}
}

Binary file not shown.

3
app/proguard-rules.txt Normal file
View file

@ -0,0 +1,3 @@
-dontobfuscate
-keep class org.apache.http.** { *; }
-dontwarn org.apache.http.**

View file

@ -0,0 +1,150 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="fr.free.nrw.commons"
android:versionCode="33"
android:versionName="1.15" >
<uses-sdk
android:minSdkVersion="9"
android:targetSdkVersion="23" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.READ_SYNC_STATS"/>
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
<uses-permission android:name="android.permission.USE_CREDENTIALS"/>
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
<application
android:name=".CommonsApplication"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat"
android:supportsRtl="true" >
<activity android:name="org.acra.CrashReportDialog"
android:theme="@android:style/Theme.Dialog"
android:launchMode="singleInstance"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true" />
<activity
android:name=".auth.LoginActivity"
android:theme="@style/NoTitle" >
</activity>
<activity
android:name=".WelcomeActivity"
android:theme="@style/NoTitle" >
</activity>
<activity
android:name=".upload.ShareActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="audio/ogg" />
</intent-filter>
</activity>
<activity
android:name=".upload.MultipleShareActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="audio/ogg" />
</intent-filter>
</activity>
<activity
android:name=".contributions.ContributionsActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
>
<intent-filter>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
<activity
android:name=".SettingsActivity"
android:label="@string/title_activity_settings"
/>
<activity android:name=".AboutActivity" android:label="@string/title_activity_about"/>
<service android:name=".upload.UploadService" >
</service>
<service
android:name=".auth.WikiAccountAuthenticatorService"
android:exported="true"
android:process=":auth" >
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service
android:name=".contributions.ContributionsSyncService"
android:exported="true">
<intent-filter>
<action
android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/contributions_sync_adapter" />
</service>
<service
android:name=".modifications.ModificationsSyncService"
android:exported="true">
<intent-filter>
<action
android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/modifications_sync_adapter" />
</service>
<provider
android:name=".contributions.ContributionsContentProvider"
android:label="@string/provider_contributions"
android:syncable="true"
android:authorities="fr.free.nrw.commons.contributions.contentprovider"
android:exported="false">
</provider>
<provider
android:name=".modifications.ModificationsContentProvider"
android:label="@string/provider_modifications"
android:syncable="true"
android:authorities="fr.free.nrw.commons.modifications.contentprovider"
android:exported="false">
</provider>
<provider
android:name=".category.CategoryContentProvider"
android:label="@string/provider_categories"
android:syncable="false"
android:authorities="fr.free.nrw.commons.categories.contentprovider"
android:exported="false">
</provider>
</application>
</manifest>

View file

@ -0,0 +1,126 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<!-- /etc/fonts/fonts.conf file to configure system font access -->
<fontconfig>
<!-- Font directory list -->
<dir prefix="xdg">fontconfig/fonts</dir>
<!-- Font cache directory list -->
<cachedir prefix="xdg">fontconfig</cachedir>
<!--
Accept deprecated 'mono' alias, replacing it with 'monospace'
-->
<match target="pattern">
<test qual="any" name="family">
<string>mono</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>monospace</string>
</edit>
</match>
<!--
Accept alternate 'sans serif' spelling, replacing it with 'sans-serif'
-->
<match target="pattern">
<test qual="any" name="family">
<string>sans serif</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>sans-serif</string>
</edit>
</match>
<!--
Accept deprecated 'sans' alias, replacing it with 'sans-serif'
-->
<match target="pattern">
<test qual="any" name="family">
<string>sans</string>
</test>
<edit name="family" mode="assign" binding="same">
<string>sans-serif</string>
</edit>
</match>
<config>
<!--
These are the default Unicode chars that are expected to be blank
in fonts. All other blank chars are assumed to be broken and
won't appear in the resulting charsets
-->
<blank>
<int>0x0020</int> <!-- SPACE -->
<int>0x00A0</int> <!-- NO-BREAK SPACE -->
<int>0x00AD</int> <!-- SOFT HYPHEN -->
<int>0x034F</int> <!-- COMBINING GRAPHEME JOINER -->
<int>0x0600</int> <!-- ARABIC NUMBER SIGN -->
<int>0x0601</int> <!-- ARABIC SIGN SANAH -->
<int>0x0602</int> <!-- ARABIC FOOTNOTE MARKER -->
<int>0x0603</int> <!-- ARABIC SIGN SAFHA -->
<int>0x06DD</int> <!-- ARABIC END OF AYAH -->
<int>0x070F</int> <!-- SYRIAC ABBREVIATION MARK -->
<int>0x115F</int> <!-- HANGUL CHOSEONG FILLER -->
<int>0x1160</int> <!-- HANGUL JUNGSEONG FILLER -->
<int>0x1680</int> <!-- OGHAM SPACE MARK -->
<int>0x17B4</int> <!-- KHMER VOWEL INHERENT AQ -->
<int>0x17B5</int> <!-- KHMER VOWEL INHERENT AA -->
<int>0x180E</int> <!-- MONGOLIAN VOWEL SEPARATOR -->
<int>0x2000</int> <!-- EN QUAD -->
<int>0x2001</int> <!-- EM QUAD -->
<int>0x2002</int> <!-- EN SPACE -->
<int>0x2003</int> <!-- EM SPACE -->
<int>0x2004</int> <!-- THREE-PER-EM SPACE -->
<int>0x2005</int> <!-- FOUR-PER-EM SPACE -->
<int>0x2006</int> <!-- SIX-PER-EM SPACE -->
<int>0x2007</int> <!-- FIGURE SPACE -->
<int>0x2008</int> <!-- PUNCTUATION SPACE -->
<int>0x2009</int> <!-- THIN SPACE -->
<int>0x200A</int> <!-- HAIR SPACE -->
<int>0x200B</int> <!-- ZERO WIDTH SPACE -->
<int>0x200C</int> <!-- ZERO WIDTH NON-JOINER -->
<int>0x200D</int> <!-- ZERO WIDTH JOINER -->
<int>0x200E</int> <!-- LEFT-TO-RIGHT MARK -->
<int>0x200F</int> <!-- RIGHT-TO-LEFT MARK -->
<int>0x2028</int> <!-- LINE SEPARATOR -->
<int>0x2029</int> <!-- PARAGRAPH SEPARATOR -->
<int>0x202A</int> <!-- LEFT-TO-RIGHT EMBEDDING -->
<int>0x202B</int> <!-- RIGHT-TO-LEFT EMBEDDING -->
<int>0x202C</int> <!-- POP DIRECTIONAL FORMATTING -->
<int>0x202D</int> <!-- LEFT-TO-RIGHT OVERRIDE -->
<int>0x202E</int> <!-- RIGHT-TO-LEFT OVERRIDE -->
<int>0x202F</int> <!-- NARROW NO-BREAK SPACE -->
<int>0x205F</int> <!-- MEDIUM MATHEMATICAL SPACE -->
<int>0x2060</int> <!-- WORD JOINER -->
<int>0x2061</int> <!-- FUNCTION APPLICATION -->
<int>0x2062</int> <!-- INVISIBLE TIMES -->
<int>0x2063</int> <!-- INVISIBLE SEPARATOR -->
<int>0x206A</int> <!-- INHIBIT SYMMETRIC SWAPPING -->
<int>0x206B</int> <!-- ACTIVATE SYMMETRIC SWAPPING -->
<int>0x206C</int> <!-- INHIBIT ARABIC FORM SHAPING -->
<int>0x206D</int> <!-- ACTIVATE ARABIC FORM SHAPING -->
<int>0x206E</int> <!-- NATIONAL DIGIT SHAPES -->
<int>0x206F</int> <!-- NOMINAL DIGIT SHAPES -->
<int>0x2800</int> <!-- BRAILLE PATTERN BLANK -->
<int>0x3000</int> <!-- IDEOGRAPHIC SPACE -->
<int>0x3164</int> <!-- HANGUL FILLER -->
<int>0xFEFF</int> <!-- ZERO WIDTH NO-BREAK SPACE -->
<int>0xFFA0</int> <!-- HALFWIDTH HANGUL FILLER -->
<int>0xFFF9</int> <!-- INTERLINEAR ANNOTATION ANCHOR -->
<int>0xFFFA</int> <!-- INTERLINEAR ANNOTATION SEPARATOR -->
<int>0xFFFB</int> <!-- INTERLINEAR ANNOTATION TERMINATOR -->
</blank>
<!--
Rescan configuration every 30 seconds when FcFontSetList is called
-->
<rescan>
<int>30</int>
</rescan>
</config>
</fontconfig>

View file

@ -0,0 +1,44 @@
package fr.free.nrw.commons;
import android.app.Activity;
import android.os.Bundle;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;
public class AboutActivity extends Activity {
private TextView versionText;
private TextView licenseText;
private TextView improveText;
private TextView privacyPolicyText;
private TextView uploadsToText;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
versionText = (TextView) findViewById(R.id.about_version);
licenseText = (TextView) findViewById(R.id.about_license);
improveText = (TextView) findViewById(R.id.about_improve);
privacyPolicyText = (TextView) findViewById(R.id.about_privacy_policy);
uploadsToText = (TextView) findViewById(R.id.about_uploads_to);
uploadsToText.setText(fr.free.nrw.commons.CommonsApplication.EVENTLOG_WIKI);
versionText.setText(fr.free.nrw.commons.CommonsApplication.APPLICATION_VERSION);
// We can't use formatted strings directly because it breaks with
// our localization tools. Grab an HTML string and turn it into
// a formatted string.
fixFormatting(licenseText, R.string.about_license);
fixFormatting(improveText, R.string.about_improve);
fixFormatting(privacyPolicyText, R.string.about_privacy_policy);
licenseText.setMovementMethod(LinkMovementMethod.getInstance());
improveText.setMovementMethod(LinkMovementMethod.getInstance());
privacyPolicyText.setMovementMethod(LinkMovementMethod.getInstance());
}
private void fixFormatting(TextView textView, int resource) {
textView.setText(Html.fromHtml(getResources().getString(resource)));
}
}

View file

@ -0,0 +1,217 @@
package fr.free.nrw.commons;
import java.io.IOException;
import android.accounts.*;
import android.app.Application;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.os.Build;
import android.support.v4.util.LruCache;
import android.util.Log;
import com.android.volley.RequestQueue;
import com.nostra13.universalimageloader.cache.disc.impl.TotalSizeLimitedDiscCache;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.utils.StorageUtils;
import fr.free.nrw.commons.auth.WikiAccountAuthenticator;
import org.acra.ACRA;
import org.acra.ReportingInteractionMode;
import org.acra.annotation.ReportsCrashes;
import org.apache.http.conn.*;
import org.apache.http.conn.scheme.*;
import org.apache.http.conn.ssl.*;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.mediawiki.api.*;
import org.apache.http.impl.client.*;
import org.apache.http.params.CoreProtocolPNames;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.data.*;
import com.android.volley.toolbox.*;
// TODO: Use ProGuard to rip out reporting when publishing
@ReportsCrashes(
mailTo = "commons-app-android@googlegroups.com",
mode = ReportingInteractionMode.DIALOG,
resDialogText = R.string.crash_dialog_text,
resDialogTitle = R.string.crash_dialog_title,
resDialogCommentPrompt = R.string.crash_dialog_comment_prompt,
resDialogOkToast = R.string.crash_dialog_ok_toast
)
public class CommonsApplication extends Application {
public static String APPLICATION_VERSION; // Populated in onCreate. Race conditions theoretically possible, but practically not?
private MWApi api;
private Account currentAccount = null; // Unlike a savings account...
public static final String API_URL = "https://commons.wikimedia.org/w/api.php";
public static final String IMAGE_URL_BASE = "https://upload.wikimedia.org/wikipedia/commons";
public static final String HOME_URL = "https://commons.wikimedia.org/wiki/";
public static final String EVENTLOG_URL = "https://bits.wikimedia.org/event.gif";
public static final String EVENTLOG_WIKI = "commonswiki";
public static final Object[] EVENT_UPLOAD_ATTEMPT = {"MobileAppUploadAttempts", 5334329L};
public static final Object[] EVENT_LOGIN_ATTEMPT = {"MobileAppLoginAttempts", 5257721L};
public static final Object[] EVENT_SHARE_ATTEMPT = {"MobileAppShareAttempts", 5346170L};
public static final Object[] EVENT_CATEGORIZATION_ATTEMPT = {"MobileAppCategorizationAttempts", 5359208L};
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using Android Commons app";
public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
public RequestQueue volleyQueue;
public CacheController cacheData;
public static AbstractHttpClient createHttpClient() {
BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
params.setParameter(CoreProtocolPNames.USER_AGENT, "Commons/" + APPLICATION_VERSION + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE);
DefaultHttpClient httpclient = new DefaultHttpClient(cm, params);
return httpclient;
}
public static MWApi createMWApi() {
return new MWApi(API_URL, createHttpClient());
}
@Override
public void onCreate() {
ACRA.init(this);
super.onCreate();
// Fire progress callbacks for every 3% of uploaded content
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
api = createMWApi();
ImageLoaderConfiguration imageLoaderConfiguration = new ImageLoaderConfiguration.Builder(getApplicationContext())
.discCache(new TotalSizeLimitedDiscCache(StorageUtils.getCacheDirectory(this), 128 * 1024 * 1024))
.build();
ImageLoader.getInstance().init(imageLoaderConfiguration);
try {
PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
APPLICATION_VERSION = pInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
// LET US WIN THE AWARD FOR DUMBEST CHECKED EXCEPTION EVER!
throw new RuntimeException(e);
}
// Initialize EventLogging
EventLog.setApp(this);
// based off https://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
// Cache for 1/8th of available VM memory
long maxMem = Runtime.getRuntime().maxMemory();
if (maxMem < 48L * 1024L * 1024L) {
// Cache only one bitmap if VM memory is too small (such as Nexus One);
Log.d("Commons", "Skipping bitmap cache; max mem is: " + maxMem);
imageCache = new LruCache<String, Bitmap>(1);
} else {
int cacheSize = (int) (maxMem / (1024 * 8));
Log.d("Commons", "Bitmap cache size " + cacheSize + " from max mem " + maxMem);
imageCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// bitmap.getByteCount() not available on older androids
int bitmapSize;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1) {
bitmapSize = bitmap.getRowBytes() * bitmap.getHeight();
} else {
bitmapSize = bitmap.getByteCount();
}
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmapSize / 1024;
}
};
}
//For caching area -> categories
cacheData = new CacheController();
DiskBasedCache cache = new DiskBasedCache(getCacheDir(), 16 * 1024 * 1024);
volleyQueue = new RequestQueue(cache, new BasicNetwork(new HurlStack()));
volleyQueue.start();
}
private com.android.volley.toolbox.ImageLoader imageLoader;
private LruCache<String, Bitmap> imageCache;
public com.android.volley.toolbox.ImageLoader getImageLoader() {
if(imageLoader == null) {
imageLoader = new com.android.volley.toolbox.ImageLoader(volleyQueue, new com.android.volley.toolbox.ImageLoader.ImageCache() {
public Bitmap getBitmap(String key) {
return imageCache.get(key);
}
public void putBitmap(String key, Bitmap bitmap) {
imageCache.put(key, bitmap);
}
});
imageLoader.setBatchedResponseDelay(0);
}
return imageLoader;
}
public MWApi getApi() {
return api;
}
public Account getCurrentAccount() {
if(currentAccount == null) {
AccountManager accountManager = AccountManager.get(this);
Account[] allAccounts = accountManager.getAccountsByType(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE);
if(allAccounts.length != 0) {
currentAccount = allAccounts[0];
}
}
return currentAccount;
}
public Boolean revalidateAuthToken() {
AccountManager accountManager = AccountManager.get(this);
Account curAccount = getCurrentAccount();
if(curAccount == null) {
return false; // This should never happen
}
accountManager.invalidateAuthToken(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE, api.getAuthCookie());
try {
String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false);
api.setAuthCookie(authCookie);
return true;
} catch (OperationCanceledException e) {
e.printStackTrace();
return false;
} catch (AuthenticatorException e) {
e.printStackTrace();
return false;
} catch (IOException e) {
e.printStackTrace();
return false;
} catch (NullPointerException e) {
e.printStackTrace();
return false;
}
}
public boolean deviceHasCamera() {
PackageManager pm = getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) ||
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
}
}

View file

@ -0,0 +1,126 @@
package fr.free.nrw.commons;
import android.content.SharedPreferences;
import android.os.*;
import android.preference.PreferenceManager;
import android.util.*;
import in.yuvi.http.fluent.Http;
import org.apache.http.HttpResponse;
import org.json.*;
import java.io.IOException;
import java.net.*;
public class EventLog {
private static CommonsApplication app;
private static class LogTask extends AsyncTask<LogBuilder, Void, Boolean> {
@Override
protected Boolean doInBackground(LogBuilder... logBuilders) {
boolean allSuccess = true;
// Not using the default URL connection, since that seems to have different behavior than the rest of the code
for(LogBuilder logBuilder: logBuilders) {
HttpURLConnection conn;
try {
URL url = logBuilder.toUrl();
HttpResponse response = Http.get(url.toString()).use(CommonsApplication.createHttpClient()).asResponse();
if(response.getStatusLine().getStatusCode() != 204) {
allSuccess = false;
}
Log.d("Commons", "EventLog hit " + url.toString());
} catch (IOException e) {
// Probably just ignore for now. Can be much more robust with a service, etc later on.
Log.d("Commons", "IO Error, EventLog hit skipped");
}
}
return allSuccess;
}
}
private static final String DEVICE;
static {
if (Build.MODEL.startsWith(Build.MANUFACTURER)) {
DEVICE = Utils.capitalize(Build.MODEL);
} else {
DEVICE = Utils.capitalize(Build.MANUFACTURER) + " " + Build.MODEL;
}
}
public static void setApp(CommonsApplication app) {
EventLog.app = app;
}
public static class LogBuilder {
private JSONObject data;
private long rev;
private String schema;
private LogBuilder(String schema, long revision) {
data = new JSONObject();
this.schema = schema;
this.rev = revision;
}
public LogBuilder param(String key, Object value) {
try {
data.put(key, value);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return this;
}
private URL toUrl() {
JSONObject fullData = new JSONObject();
try {
fullData.put("schema", schema);
fullData.put("revision", rev);
fullData.put("wiki", CommonsApplication.EVENTLOG_WIKI);
data.put("device", DEVICE);
data.put("platform", "Android/" + Build.VERSION.RELEASE);
data.put("appversion", "Android/" + CommonsApplication.APPLICATION_VERSION);
fullData.put("event", data);
return new URL(CommonsApplication.EVENTLOG_URL + "?" + Utils.urlEncode(fullData.toString()) + ";");
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
// force param disregards user preference
// Use *only* for tracking the user preference change for EventLogging
// Attempting to use anywhere else will cause kitten explosions
public void log(boolean force) {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(app);
if(!settings.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
return; // User has disabled tracking
}
LogTask logTask = new LogTask();
Utils.executeAsyncTask(logTask, this);
}
public void log() {
log(false);
}
}
public static LogBuilder schema(String schema, long revision) {
return new LogBuilder(schema, revision);
}
public static LogBuilder schema(Object[] scid) {
if(scid.length != 2) {
throw new IllegalArgumentException("Needs an object array with schema as first param and revision as second");
}
return schema((String)scid[0], (Long)scid[1]);
}
}

View file

@ -0,0 +1,68 @@
package fr.free.nrw.commons;
import android.app.*;
import android.content.*;
import android.os.*;
public abstract class HandlerService<T> extends Service {
private volatile Looper threadLooper;
private volatile ServiceHandler threadHandler;
private String serviceName;
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
handle(msg.what, (T)msg.obj);
stopSelf(msg.arg1);
}
}
@Override
public void onDestroy() {
super.onDestroy();
threadLooper.quit();
}
public class HandlerServiceLocalBinder extends Binder {
public HandlerService getService() {
return HandlerService.this;
}
}
private final IBinder localBinder = new HandlerServiceLocalBinder();
@Override
public IBinder onBind(Intent intent) {
return localBinder;
}
protected HandlerService(String serviceName) {
this.serviceName = serviceName;
}
@Override
public void onCreate() {
super.onCreate();
HandlerThread thread = new HandlerThread(serviceName);
thread.start();
threadLooper = thread.getLooper();
threadHandler = new ServiceHandler(threadLooper);
}
private void postMessage(int type, Object obj) {
Message msg = threadHandler.obtainMessage(type);
msg.obj = obj;
threadHandler.sendMessage(msg);
}
public void queue(int what, T t) {
postMessage(what, t);
}
protected abstract void handle(int what, T t);
}

View file

@ -0,0 +1,46 @@
package fr.free.nrw.commons;
public class License {
String key;
String template;
String url;
String name;
public License(String key, String template, String url, String name) {
if (key == null) {
throw new RuntimeException("License.key must not be null");
}
if (template == null) {
throw new RuntimeException("License.template must not be null");
}
this.key = key;
this.template = template;
this.url = url;
this.name = name;
}
public String getKey() {
return key;
}
public String getTemplate() {
return template;
}
public String getName() {
if (name == null) {
// hack
return getKey();
} else {
return name;
}
}
public String getUrl(String language) {
if (url == null) {
return null;
} else {
return url.replace("$lang", language);
}
}
}

View file

@ -0,0 +1,79 @@
package fr.free.nrw.commons;
import android.app.Activity;
import android.content.res.Resources;
import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class LicenseList {
Map<String, fr.free.nrw.commons.License> licenses = new HashMap<String, fr.free.nrw.commons.License>();
Resources res;
private static String XMLNS_LICENSE = "https://www.mediawiki.org/wiki/Extension:UploadWizard/xmlns/licenses";
public LicenseList(Activity activity) {
res = activity.getResources();
XmlPullParser parser = res.getXml(R.xml.wikimedia_licenses);
while (fr.free.nrw.commons.Utils.xmlFastForward(parser, XMLNS_LICENSE, "license")) {
String id = parser.getAttributeValue(null, "id");
String template = parser.getAttributeValue(null, "template");
String url = parser.getAttributeValue(null, "url");
String name = nameForTemplate(template);
fr.free.nrw.commons.License license = new fr.free.nrw.commons.License(id, template, url, name);
licenses.put(id, license);
}
}
public Set<String> keySet() {
return licenses.keySet();
}
public Collection<fr.free.nrw.commons.License> values() {
return licenses.values();
}
public fr.free.nrw.commons.License get(String key) {
return licenses.get(key);
}
public fr.free.nrw.commons.License licenseForTemplate(String template) {
String ucTemplate = fr.free.nrw.commons.Utils.capitalize(template);
for (fr.free.nrw.commons.License license : values()) {
if (ucTemplate.equals(fr.free.nrw.commons.Utils.capitalize(license.getTemplate()))) {
return license;
}
}
return null;
}
public String nameIdForTemplate(String template) {
// hack :D (converts dashes and periods to underscores)
// cc-by-sa-3.0 -> cc_by_sa_3_0
return "license_name_" + template.toLowerCase().replace("-", "_").replace(".", "_");
}
private int stringIdByName(String stringId) {
return res.getIdentifier("fr.free.nrw.commons:string/" + stringId, null, null);
}
public String nameForTemplate(String template) {
Log.d("Commons", "LicenseList.nameForTemplate: template: " + template);
String stringId = nameIdForTemplate(template);
Log.d("Commons", "LicenseList.nameForTemplate: stringId: " + stringId);
int nameId = stringIdByName(stringId);
Log.d("Commons", "LicenseList.nameForTemplate: nameId: " + nameId);
if(nameId != 0) {
String name = res.getString(nameId);
Log.d("Commons", "LicenseList.nameForTemplate: name: " + name);
return name;
}
return template;
}
}

View file

@ -0,0 +1,245 @@
package fr.free.nrw.commons;
import android.net.Uri;
import android.os.*;
import java.util.*;
import java.util.regex.*;
public class Media implements Parcelable {
public static Creator<Media> CREATOR = new Creator<Media>() {
public Media createFromParcel(Parcel parcel) {
return new Media(parcel);
}
public Media[] newArray(int i) {
return new Media[0];
}
};
protected Media() {
this.categories = new ArrayList<String>();
this.descriptions = new HashMap<String, String>();
}
private HashMap<String, Object> tags = new HashMap<String, Object>();
public Object getTag(String key) {
return tags.get(key);
}
public void setTag(String key, Object value) {
tags.put(key, value);
}
public static Pattern displayTitlePattern = Pattern.compile("(.*)(\\.\\w+)", Pattern.CASE_INSENSITIVE);
public String getDisplayTitle() {
if(filename == null) {
return "";
}
// FIXME: Gross hack bercause my regex skills suck maybe or I am too lazy who knows
String title = filename.replaceFirst("^File:", "");
Matcher matcher = displayTitlePattern.matcher(title);
if(matcher.matches()) {
return matcher.group(1);
} else {
return title;
}
}
public String getDescriptionUrl() {
// HACK! Geez
return CommonsApplication.HOME_URL + "File:" + Utils.urlEncode(getFilename().replace("File:", "").replace(" ", "_"));
}
public Uri getLocalUri() {
return localUri;
}
public String getImageUrl() {
if(imageUrl == null) {
imageUrl = Utils.makeThumbBaseUrl(this.getFilename());
}
return imageUrl;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public String getDescription() {
return description;
}
public long getDataLength() {
return dataLength;
}
public void setDataLength(long dataLength) {
this.dataLength = dataLength;
}
public Date getDateCreated() {
return dateCreated;
}
public void setDateCreated(Date date) {
this.dateCreated = date;
}
public Date getDateUploaded() {
return dateUploaded;
}
public String getCreator() {
return creator;
}
public void setCreator(String creator) {
this.creator = creator;
}
public String getThumbnailUrl(int width) {
return Utils.makeThumbUrl(getImageUrl(), getFilename(), width);
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public String getLicense() {
return license;
}
public void setLicense(String license) {
this.license = license;
}
// Primary metadata fields
protected Uri localUri;
protected String imageUrl;
protected String filename;
protected String description; // monolingual description on input...
protected long dataLength;
protected Date dateCreated;
protected Date dateUploaded;
protected int width;
protected int height;
protected String license;
protected String creator;
protected ArrayList<String> categories; // as loaded at runtime?
protected Map<String, String> descriptions; // multilingual descriptions as loaded
public ArrayList<String> getCategories() {
return (ArrayList<String>)categories.clone(); // feels dirty
}
public void setCategories(List<String> categories) {
this.categories.removeAll(this.categories);
this.categories.addAll(categories);
}
public void setDescriptions(Map<String,String> descriptions) {
for (String key : this.descriptions.keySet()) {
this.descriptions.remove(key);
}
for (String key : descriptions.keySet()) {
this.descriptions.put(key, descriptions.get(key));
}
}
public String getDescription(String preferredLanguage) {
if (descriptions.containsKey(preferredLanguage)) {
// See if the requested language is there.
return descriptions.get(preferredLanguage);
} else if (descriptions.containsKey("en")) {
// Ah, English. Language of the world, until the Chinese crush us.
return descriptions.get("en");
} else if (descriptions.containsKey("default")) {
// No languages marked...
return descriptions.get("default");
} else {
// FIXME: return the first available non-English description?
return "";
}
}
public Media(String filename) {
this();
this.filename = filename;
}
public Media(Uri localUri, String imageUrl, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator) {
this();
this.localUri = localUri;
this.imageUrl = imageUrl;
this.filename = filename;
this.description = description;
this.dataLength = dataLength;
this.dateCreated = dateCreated;
this.dateUploaded = dateUploaded;
this.creator = creator;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeParcelable(localUri, flags);
parcel.writeString(imageUrl);
parcel.writeString(filename);
parcel.writeString(description);
parcel.writeLong(dataLength);
parcel.writeSerializable(dateCreated);
parcel.writeSerializable(dateUploaded);
parcel.writeString(creator);
parcel.writeSerializable(tags);
parcel.writeInt(width);
parcel.writeInt(height);
parcel.writeString(license);
parcel.writeStringList(categories);
parcel.writeMap(descriptions);
}
public Media(Parcel in) {
localUri = (Uri)in.readParcelable(Uri.class.getClassLoader());
imageUrl = in.readString();
filename = in.readString();
description = in.readString();
dataLength = in.readLong();
dateCreated = (Date) in.readSerializable();
dateUploaded = (Date) in.readSerializable();
creator = in.readString();
tags = (HashMap<String, Object>)in.readSerializable();
width = in.readInt();
height = in.readInt();
license = in.readString();
in.readStringList(categories);
descriptions = in.readHashMap(ClassLoader.getSystemClassLoader());
}
public void setDescription(String description) {
this.description = description;
}
}

View file

@ -0,0 +1,296 @@
package fr.free.nrw.commons;
import android.util.Log;
import org.mediawiki.api.ApiResult;
import org.mediawiki.api.MWApi;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Fetch additional media data from the network that we don't store locally.
*
* This includes things like category lists and multilingual descriptions,
* which are not intrinsic to the media and may change due to editing.
*/
public class MediaDataExtractor {
private boolean fetched;
private boolean processed;
private String filename;
private ArrayList<String> categories;
private Map<String, String> descriptions;
private String author;
private Date date;
private String license;
private LicenseList licenseList;
/**
* @param filename of the target media object, should include 'File:' prefix
*/
public MediaDataExtractor(String filename, LicenseList licenseList) {
this.filename = filename;
categories = new ArrayList<String>();
descriptions = new HashMap<String, String>();
fetched = false;
processed = false;
this.licenseList = licenseList;
}
/**
* Actually fetch the data over the network.
* todo: use local caching?
*
* Warning: synchronous i/o, call on a background thread
*/
public void fetch() throws IOException {
if (fetched) {
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
}
MWApi api = CommonsApplication.createMWApi();
ApiResult result = api.action("query")
.param("prop", "revisions")
.param("titles", filename)
.param("rvprop", "content")
.param("rvlimit", 1)
.param("rvgeneratexml", 1)
.get();
processResult(result);
fetched = true;
}
private void processResult(ApiResult result) throws IOException {
String wikiSource = result.getString("/api/query/pages/page/revisions/rev");
String parseTreeXmlSource = result.getString("/api/query/pages/page/revisions/rev/@parsetree");
// In-page category links are extracted from source, as XML doesn't cover [[links]]
extractCategories(wikiSource);
// Description template info is extracted from preprocessor XML
processWikiParseTree(parseTreeXmlSource);
}
/**
* We could fetch all category links from API, but we actually only want the ones
* directly in the page source so they're editable. In the future this may change.
*
* @param source wikitext source code
*/
private void extractCategories(String source) {
Pattern regex = Pattern.compile("\\[\\[\\s*Category\\s*:([^]]*)\\s*\\]\\]", Pattern.CASE_INSENSITIVE);
Matcher matcher = regex.matcher(source);
while (matcher.find()) {
String cat = matcher.group(1).trim();
categories.add(cat);
}
}
private void processWikiParseTree(String source) throws IOException {
Document doc;
try {
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
doc = docBuilder.parse(new ByteArrayInputStream(source.getBytes("UTF-8")));
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
} catch (IllegalStateException e) {
throw new IOException(e);
} catch (SAXException e) {
throw new IOException(e);
}
Node templateNode = findTemplate(doc.getDocumentElement(), "information");
if (templateNode != null) {
Node descriptionNode = findTemplateParameter(templateNode, "description");
descriptions = getMultilingualText(descriptionNode);
Node authorNode = findTemplateParameter(templateNode, "author");
author = getFlatText(authorNode);
}
/*
Pull up the license data list...
look for the templates in two ways:
* look for 'self' template and check its first parameter
* if none, look for any of the known templates
*/
Log.d("Commons", "MediaDataExtractor searching for license");
Node selfLicenseNode = findTemplate(doc.getDocumentElement(), "self");
if (selfLicenseNode != null) {
Node firstNode = findTemplateParameter(selfLicenseNode, 1);
String licenseTemplate = getFlatText(firstNode);
License license = licenseList.licenseForTemplate(licenseTemplate);
if (license == null) {
Log.d("Commons", "MediaDataExtractor found no matching license for self parameter: " + licenseTemplate + "; faking it");
this.license = licenseTemplate; // hack hack! For non-selectable licenses that are still in the system.
} else {
// fixme: record the self-ness in here too... sigh
// all this needs better server-side metadata
this.license = license.getKey();
Log.d("Commons", "MediaDataExtractor found self-license " + this.license);
}
} else {
for (License license : licenseList.values()) {
String templateName = license.getTemplate();
Node template = findTemplate(doc.getDocumentElement(), templateName);
if (template != null) {
// Found!
this.license = license.getKey();
Log.d("Commons", "MediaDataExtractor found non-self license " + this.license);
break;
}
}
}
}
private Node findTemplate(Element parentNode, String title) throws IOException {
String ucTitle= Utils.capitalize(title);
NodeList nodes = parentNode.getChildNodes();
for (int i = 0, length = nodes.getLength(); i < length; i++) {
Node node = nodes.item(i);
if (node.getNodeName().equals("template")) {
String foundTitle = getTemplateTitle(node);
if (Utils.capitalize(foundTitle).equals(ucTitle)) {
return node;
}
}
}
return null;
}
private String getTemplateTitle(Node templateNode) throws IOException {
NodeList nodes = templateNode.getChildNodes();
for (int i = 0, length = nodes.getLength(); i < length; i++) {
Node node = nodes.item(i);
if (node.getNodeName().equals("title")) {
return node.getTextContent().trim();
}
}
throw new IOException("Template has no title element.");
}
private static abstract class TemplateChildNodeComparator {
abstract public boolean match(Node node);
}
private Node findTemplateParameter(Node templateNode, String name) throws IOException {
final String theName = name;
return findTemplateParameter(templateNode, new TemplateChildNodeComparator() {
@Override
public boolean match(Node node) {
return (Utils.capitalize(node.getTextContent().trim()).equals(Utils.capitalize(theName)));
}
});
}
private Node findTemplateParameter(Node templateNode, int index) throws IOException {
final String theIndex = "" + index;
return findTemplateParameter(templateNode, new TemplateChildNodeComparator() {
@Override
public boolean match(Node node) {
Element el = (Element)node;
if (el.getTextContent().trim().equals(theIndex)) {
return true;
} else if (el.getAttribute("index") != null && el.getAttribute("index").trim().equals(theIndex)) {
return true;
} else {
return false;
}
}
});
}
private Node findTemplateParameter(Node templateNode, TemplateChildNodeComparator comparator) throws IOException {
NodeList nodes = templateNode.getChildNodes();
for (int i = 0, length = nodes.getLength(); i < length; i++) {
Node node = nodes.item(i);
if (node.getNodeName().equals("part")) {
NodeList childNodes = node.getChildNodes();
for (int j = 0, childNodesLength = childNodes.getLength(); j < childNodesLength; j++) {
Node childNode = childNodes.item(j);
if (childNode.getNodeName().equals("name")) {
if (comparator.match(childNode)) {
// yay! Now fetch the value node.
for (int k = j + 1; k < childNodesLength; k++) {
Node siblingNode = childNodes.item(k);
if (siblingNode.getNodeName().equals("value")) {
return siblingNode;
}
}
throw new IOException("No value node found for matched template parameter.");
}
}
}
}
}
throw new IOException("No matching template parameter node found.");
}
private String getFlatText(Node parentNode) throws IOException {
return parentNode.getTextContent();
}
// Extract a dictionary of multilingual texts from a subset of the parse tree.
// Texts are wrapped in things like {{en|foo} or {{en|1=foo bar}}.
// Text outside those wrappers is stuffed into a 'default' faux language key if present.
private Map<String, String> getMultilingualText(Node parentNode) throws IOException {
Map<String, String> texts = new HashMap<String, String>();
StringBuilder localText = new StringBuilder();
NodeList nodes = parentNode.getChildNodes();
for (int i = 0, length = nodes.getLength(); i < length; i++) {
Node node = nodes.item(i);
if (node.getNodeName().equals("template")) {
// process a template node
String title = getTemplateTitle(node);
if (title.length() < 3) {
// Hopefully a language code. Nasty hack!
String lang = title;
Node valueNode = findTemplateParameter(node, 1);
String value = valueNode.getTextContent(); // hope there's no subtemplates or formatting for now
texts.put(lang, value);
}
} else if (node.getNodeType() == Node.TEXT_NODE) {
localText.append(node.getTextContent());
}
}
// Some descriptions don't list multilingual variants
String defaultText = localText.toString().trim();
if (defaultText.length() > 0) {
texts.put("default", localText.toString());
}
return texts;
}
/**
* Take our metadata and inject it into a live Media object.
* Media object might contain stale or cached data, or emptiness.
* @param media
*/
public void fill(Media media) {
if (!fetched) {
throw new IllegalStateException("Tried to call MediaDataExtractor.fill() before fetch().");
}
media.setCategories(categories);
media.setDescriptions(descriptions);
if (license != null) {
media.setLicense(license);
}
// add author, date, etc fields
}
}

View file

@ -0,0 +1,228 @@
/**
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.free.nrw.commons;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.BitmapDrawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.ImageLoader.ImageContainer;
import com.android.volley.toolbox.ImageLoader.ImageListener;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
public class MediaWikiImageView extends ImageView {
private Media mMedia;
private ImageLoader mImageLoader;
private ImageContainer mImageContainer;
private View loadingView;
private boolean isThumbnail;
public MediaWikiImageView(Context context) {
this(context, null);
}
public MediaWikiImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
TypedArray actualAttrs = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MediaWikiImageView, 0, 0);
isThumbnail = actualAttrs.getBoolean(0, false);
actualAttrs.recycle();
}
public MediaWikiImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setMedia(Media media, ImageLoader imageLoader) {
this.mMedia = media;
mImageLoader = imageLoader;
loadImageIfNecessary(false);
}
public void setLoadingView(View loadingView) {
this.loadingView = loadingView;
}
public View getLoadingView() {
return loadingView;
}
private void loadImageIfNecessary(final boolean isInLayoutPass) {
loadImageIfNecessary(isInLayoutPass, false);
}
private void loadImageIfNecessary(final boolean isInLayoutPass, final boolean tryOriginal) {
int width = getWidth();
int height = getHeight();
// if the view's bounds aren't known yet, hold off on loading the image.
if (width == 0 && height == 0) {
return;
}
if(mMedia == null) {
return;
}
// Do not count for density when loading thumbnails.
// FIXME: Use another 'algorithm' that doesn't punish low res devices
if(isThumbnail) {
float dpFactor = Math.max(getResources().getDisplayMetrics().density, 1.0f);
width = (int) (width / dpFactor);
height = (int) (height / dpFactor);
}
final String mUrl;
if(tryOriginal) {
mUrl = mMedia.getImageUrl();
} else {
// Round it to the nearest 320
// Possible a similar size image has already been generated.
// Reduces Server cache fragmentation, also increases chance of cache hit
// If width is less than 320, we round up to 320
int bucketedWidth = width <= 320 ? 320 : Math.round((float)width / 320.0f) * 320;
if(mMedia.getWidth() != 0 && mMedia.getWidth() < bucketedWidth) {
// If we know that the width of the image is lesser than the required width
// We don't even try to load the thumbnai, go directly to the source
loadImageIfNecessary(isInLayoutPass, true);
return;
} else {
mUrl = mMedia.getThumbnailUrl(bucketedWidth);
}
}
// if the URL to be loaded in this view is empty, cancel any old requests and clear the
// currently loaded image.
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
setImageBitmap(null);
return;
}
// Don't repeat work. Prevents onLayout cascades
// We ignore it if the image request was for either the current URL of for the full URL
// Since the full URL is always the second, and
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
if (mImageContainer.getRequestUrl().equals(mMedia.getImageUrl()) || mImageContainer.getRequestUrl().equals(mUrl)) {
return;
} else {
// if there is a pre-existing request, cancel it if it's fetching a different URL.
mImageContainer.cancelRequest();
BitmapDrawable actualDrawable = (BitmapDrawable)getDrawable();
if(actualDrawable != null && actualDrawable.getBitmap() != null) {
setImageBitmap(null);
if(loadingView != null) {
loadingView.setVisibility(View.VISIBLE);
}
}
}
}
// The pre-existing content of this view didn't match the current URL. Load the new image
// from the network.
ImageContainer newContainer = mImageLoader.get(mUrl,
new ImageListener() {
@Override
public void onErrorResponse(final VolleyError error) {
if(!tryOriginal) {
post(new Runnable() {
public void run() {
loadImageIfNecessary(false, true);
}
});
}
}
@Override
public void onResponse(final ImageContainer response, boolean isImmediate) {
// If this was an immediate response that was delivered inside of a layout
// pass do not set the image immediately as it will trigger a requestLayout
// inside of a layout. Instead, defer setting the image by posting back to
// the main thread.
if (isImmediate && isInLayoutPass) {
post(new Runnable() {
@Override
public void run() {
onResponse(response, false);
}
});
return;
}
if (response.getBitmap() != null) {
setImageBitmap(response.getBitmap());
if(tryOriginal && mMedia instanceof Contribution && (response.getBitmap().getWidth() > mMedia.getWidth() || response.getBitmap().getHeight() > mMedia.getHeight())) {
// If there is no width information for this image, save it. This speeds up image loading massively for smaller images
mMedia.setHeight(response.getBitmap().getHeight());
mMedia.setWidth(response.getBitmap().getWidth());
((Contribution)mMedia).setContentProviderClient(MediaWikiImageView.this.getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY));
((Contribution)mMedia).save();
}
if(loadingView != null) {
loadingView.setVisibility(View.GONE);
}
} else {
// I'm not really sure where this would hit but not onError
}
}
});
// update the ImageContainer to be the new bitmap container.
mImageContainer = newContainer;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
loadImageIfNecessary(true);
}
@Override
protected void onDetachedFromWindow() {
if (mImageContainer != null) {
// If the view was bound to an image request, cancel it and clear
// out the image from the view.
mImageContainer.cancelRequest();
setImageBitmap(null);
// also clear out the container so we can reload the image if necessary.
mImageContainer = null;
}
super.onDetachedFromWindow();
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
invalidate();
}
}

View file

@ -0,0 +1,15 @@
package fr.free.nrw.commons;
public class Prefs {
public static String GLOBAL_PREFS = "fr.free.nrw.commons.preferences";
public static String TRACKING_ENABLED = "eventLogging";
public static final String DEFAULT_LICENSE = "defaultLicense";
public static class Licenses {
public static final String CC_BY_SA = "CC BY-SA";
public static final String CC_BY = "CC BY";
public static final String CC0 = "CC0";
}
}

View file

@ -0,0 +1,144 @@
package fr.free.nrw.commons;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.support.annotation.LayoutRes;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.Toolbar;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
public class SettingsActivity extends PreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
fr.free.nrw.commons.CommonsApplication app;
private AppCompatDelegate mDelegate;
@Override
protected void onCreate(Bundle savedInstanceState) {
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
ListPreference licensePreference = (ListPreference) findPreference(fr.free.nrw.commons.Prefs.DEFAULT_LICENSE);
// WARNING: ORDERING NEEDS TO MATCH FOR THE LICENSE NAMES AND DISPLAY VALUES
licensePreference.setEntries(new String[]{
getString(R.string.license_name_cc0),
getString(R.string.license_name_cc_by),
getString(R.string.license_name_cc_by_sa)
});
licensePreference.setEntryValues(new String[]{
fr.free.nrw.commons.Prefs.Licenses.CC0,
fr.free.nrw.commons.Prefs.Licenses.CC_BY,
fr.free.nrw.commons.Prefs.Licenses.CC_BY_SA
});
licensePreference.setSummary(getString(fr.free.nrw.commons.Utils.licenseNameFor(licensePreference.getValue())));
licensePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
preference.setSummary(getString(fr.free.nrw.commons.Utils.licenseNameFor((String)newValue)));
return true;
}
});
app = (fr.free.nrw.commons.CommonsApplication)getApplicationContext();
}
@Override
protected void onResume() {
super.onResume();
getPreferenceScreen().getSharedPreferences()
.registerOnSharedPreferenceChangeListener(this);
}
@Override
protected void onPause() {
super.onPause();
getPreferenceScreen().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(this);
}
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
}
// All the stuff below is just to get a actionbar that says settings...
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
getDelegate().onPostCreate(savedInstanceState);
}
@Override
public MenuInflater getMenuInflater() {
return getDelegate().getMenuInflater();
}
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
@Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().setContentView(view, params);
}
@Override
public void addContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().addContentView(view, params);
}
@Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
@Override
protected void onTitleChanged(CharSequence title, int color) {
super.onTitleChanged(title, color);
getDelegate().setTitle(title);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
getDelegate().onConfigurationChanged(newConfig);
}
@Override
protected void onStop() {
super.onStop();
getDelegate().onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
getDelegate().onDestroy();
}
public void invalidateOptionsMenu() {
getDelegate().invalidateOptionsMenu();
}
private AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, null);
}
return mDelegate;
}
}

View file

@ -0,0 +1,243 @@
package fr.free.nrw.commons;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.codec.net.URLCodec;
import org.w3c.dom.Node;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import java.util.concurrent.Executor;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
public class Utils {
public static Date parseMWDate(String mwDate) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); // Assuming MW always gives me UTC
try {
return isoFormat.parse(mwDate);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
public static String toMWDate(Date date) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); // Assuming MW always gives me UTC
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return isoFormat.format(date);
}
public static String makeThumbBaseUrl(String filename) {
String name = filename.replaceFirst("File:", "").replace(" ", "_");
String sha = new String(Hex.encodeHex(DigestUtils.md5(name)));
return String.format("%s/%s/%s/%s", CommonsApplication.IMAGE_URL_BASE, sha.substring(0, 1), sha.substring(0, 2), urlEncode(name));
}
public static String getStringFromDOM(Node dom) {
javax.xml.transform.Transformer transformer = null;
try {
transformer = TransformerFactory.newInstance().newTransformer();
} catch (TransformerConfigurationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (TransformerFactoryConfigurationError e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
StringWriter outputStream = new StringWriter();
javax.xml.transform.dom.DOMSource domSource = new javax.xml.transform.dom.DOMSource(dom);
javax.xml.transform.stream.StreamResult strResult = new javax.xml.transform.stream.StreamResult(outputStream);
try {
transformer.transform(domSource, strResult);
} catch (TransformerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return outputStream.toString();
}
static public <T> void executeAsyncTask(AsyncTask<T, ?, ?> task,
T... params) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
}
else {
task.execute(params);
}
}
static public <T> void executeAsyncTask(AsyncTask<T, ?, ?> task, Executor executor,
T... params) {
// FIXME: We're simply ignoring the executor on older androids
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
task.executeOnExecutor(executor, params);
}
else {
task.execute(params);
}
}
private static DisplayImageOptions.Builder defaultImageOptionsBuilder;
public static DisplayImageOptions.Builder getGenericDisplayOptions() {
if(defaultImageOptionsBuilder == null) {
defaultImageOptionsBuilder = new DisplayImageOptions.Builder().cacheInMemory()
.imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
// List views flicker badly during data updates on Android 2.3; we
// haven't quite figured out why but cells seem to be rearranged oddly.
// Disable the fade-in on 2.3 to reduce the effect.
defaultImageOptionsBuilder = defaultImageOptionsBuilder
.displayer(new FadeInBitmapDisplayer(300));
}
defaultImageOptionsBuilder = defaultImageOptionsBuilder
.cacheInMemory()
.resetViewBeforeLoading();
}
return defaultImageOptionsBuilder;
}
private static final URLCodec urlCodec = new URLCodec();
public static String urlEncode(String url) {
try {
return urlCodec.encode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public static long countBytes(InputStream stream) throws IOException {
long count = 0;
BufferedInputStream bis = new BufferedInputStream(stream);
while(bis.read() != -1) {
count++;
}
return count;
}
public static String makeThumbUrl(String imageUrl, String filename, int width) {
// Ugly Hack!
// Update: OH DEAR GOD WHAT A HORRIBLE HACK I AM SO SORRY
if(imageUrl.endsWith("webm")) {
return imageUrl.replaceFirst("test/", "test/thumb/").replace("commons/", "commons/thumb/") + "/" + width + "px--" + filename.replaceAll("File:", "").replaceAll(" ", "_") + ".jpg";
} else {
String thumbUrl = imageUrl.replaceFirst("test/", "test/thumb/").replace("commons/", "commons/thumb/") + "/" + width + "px-" + filename.replaceAll("File:", "").replaceAll(" ", "_");
if(thumbUrl.endsWith("jpg") || thumbUrl.endsWith("png") || thumbUrl.endsWith("jpeg")) {
return thumbUrl;
} else {
return thumbUrl + ".png";
}
}
}
public static String capitalize(String string) {
return string.substring(0,1).toUpperCase() + string.substring(1);
}
public static String licenseTemplateFor(String license) {
if(license.equals(Prefs.Licenses.CC_BY)) {
return "{{self|cc-by-3.0}}";
} else if(license.equals(Prefs.Licenses.CC_BY_SA)) {
return "{{self|cc-by-sa-3.0}}";
} else if(license.equals(Prefs.Licenses.CC0)) {
return "{{self|cc-zero}}";
}
throw new RuntimeException("Unrecognized license value");
}
public static int licenseNameFor(String license) {
if(license.equals(Prefs.Licenses.CC_BY)) {
return R.string.license_name_cc_by;
} else if(license.equals(Prefs.Licenses.CC_BY_SA)) {
return R.string.license_name_cc_by_sa;
} else if(license.equals(Prefs.Licenses.CC0)) {
return R.string.license_name_cc0;
}
throw new RuntimeException("Unrecognized license value");
}
public static String licenseUrlFor(String license) {
if(license.equals(Prefs.Licenses.CC_BY)) {
return "https://creativecommons.org/licenses/by/3.0/";
} else if(license.equals(Prefs.Licenses.CC_BY_SA)) {
return "https://creativecommons.org/licenses/by-sa/3.0/";
} else if(license.equals(Prefs.Licenses.CC0)) {
return "https://creativecommons.org/publicdomain/zero/1.0/";
}
throw new RuntimeException("Unrecognized license value");
}
public static String implode(String glue, Iterable<String> pieces) {
StringBuffer buffer = new StringBuffer();
boolean first = true;
for (String piece : pieces) {
if (first) {
first = false;
} else {
buffer.append(glue);
}
buffer.append(pieces);
}
return buffer.toString();
}
public static Uri uriForWikiPage(String name) {
String underscored = name.trim().replace(" ", "_");
String uriStr = CommonsApplication.HOME_URL + urlEncode(underscored);
return Uri.parse(uriStr);
}
/**
* Fast-forward an XmlPullParser to the next instance of the given element
* in the input stream (namespaced).
*
* @param parser
* @param namespace
* @param element
* @return true on match, false on failure
*/
public static boolean xmlFastForward(XmlPullParser parser, String namespace, String element) {
try {
while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (parser.getEventType() == XmlPullParser.START_TAG &&
parser.getNamespace().equals(namespace) &&
parser.getName().equals(element)) {
// We found it!
return true;
}
}
return false;
} catch (XmlPullParserException e) {
e.printStackTrace();
return false;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
}

View file

@ -0,0 +1,68 @@
package fr.free.nrw.commons;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import com.viewpagerindicator.CirclePageIndicator;
public class WelcomeActivity extends Activity {
static final int PAGE_WIKIPEDIA = 0,
PAGE_COPYRIGHT = 1,
PAGE_FINAL = 2;
static final int[] pageLayouts = new int[] {
R.layout.welcome_wikipedia,
R.layout.welcome_copyright,
R.layout.welcome_final
};
private ViewPager pager;
private Button yesButton;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
pager = (ViewPager)findViewById(R.id.welcomePager);
pager.setAdapter(new PagerAdapter() {
@Override
public int getCount() {
return pageLayouts.length;
}
@Override
public boolean isViewFromObject(View view, Object o) {
return (view == o);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View view = getLayoutInflater().inflate(pageLayouts[position], null);
container.addView(view);
if (position == PAGE_FINAL) {
yesButton = (Button)view.findViewById(R.id.welcomeYesButton);
yesButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
finish();
}
});
}
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object obj) {
yesButton = null;
container.removeView((View)obj);
}
});
CirclePageIndicator indicator = (CirclePageIndicator)findViewById(R.id.welcomePagerIndicator);
indicator.setViewPager(pager);
}
}

View file

@ -0,0 +1,8 @@
package fr.free.nrw.commons.api;
import com.android.volley.RequestQueue;
public class MWApi {
private RequestQueue queue;
}

View file

@ -0,0 +1,152 @@
package fr.free.nrw.commons.auth;
import java.io.IOException;
import android.accounts.OperationCanceledException;
import android.accounts.*;
import android.os.*;
import android.support.v7.app.AppCompatActivity;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils;
public class AuthenticatedActivity extends AppCompatActivity {
String accountType;
CommonsApplication app;
private String authCookie;
public AuthenticatedActivity(String accountType) {
this.accountType = accountType;
}
private class GetAuthCookieTask extends AsyncTask<Void, String, String> {
private Account account;
private AccountManager accountManager;
public GetAuthCookieTask(Account account, AccountManager accountManager) {
this.account = account;
this.accountManager = accountManager;
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
if(result != null) {
authCookie = result;
onAuthCookieAcquired(result);
} else {
onAuthFailure();
}
}
@Override
protected String doInBackground(Void... params) {
try {
return accountManager.blockingGetAuthToken(account, "", false);
} catch (OperationCanceledException e) {
e.printStackTrace();
return null;
} catch (AuthenticatorException e) {
e.printStackTrace();
return null;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
private class AddAccountTask extends AsyncTask<Void, String, String> {
private AccountManager accountManager;
public AddAccountTask(AccountManager accountManager) {
this.accountManager = accountManager;
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
if(result != null) {
Account[] allAccounts =accountManager.getAccountsByType(accountType);
Account curAccount = allAccounts[0];
GetAuthCookieTask getCookieTask = new GetAuthCookieTask(curAccount, accountManager);
getCookieTask.execute();
} else {
onAuthFailure();
}
}
@Override
protected String doInBackground(Void... params) {
AccountManagerFuture<Bundle> resultFuture = accountManager.addAccount(accountType, null, null, null, AuthenticatedActivity.this, null, null);
Bundle result;
try {
result = resultFuture.getResult();
} catch (OperationCanceledException e) {
e.printStackTrace();
return null;
} catch (AuthenticatorException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
if(result.containsKey(AccountManager.KEY_ACCOUNT_NAME)) {
return result.getString(AccountManager.KEY_ACCOUNT_NAME);
} else {
return null;
}
}
}
protected void requestAuthToken() {
if(authCookie != null) {
onAuthCookieAcquired(authCookie);
return;
}
AccountManager accountManager = AccountManager.get(this);
Account curAccount = app.getCurrentAccount();
if(curAccount == null) {
AddAccountTask addAccountTask = new AddAccountTask(accountManager);
// This AsyncTask blocks until the Login Activity returns
// And since in Android 4.x+ only one background thread runs all AsyncTasks
// And since LoginActivity can't return until it's own AsyncTask (that does the login)
// returns, we have a deadlock!
// Fixed by explicitly asking this to be executed in parallel
// See: https://groups.google.com/forum/?fromgroups=#!topic/android-developers/8M0RTFfO7-M
Utils.executeAsyncTask(addAccountTask);
} else {
GetAuthCookieTask task = new GetAuthCookieTask(curAccount, accountManager);
task.execute();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
app = (CommonsApplication)this.getApplicationContext();
if(savedInstanceState != null) {
authCookie = savedInstanceState.getString("authCookie");
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("authCookie", authCookie);
}
protected void onAuthCookieAcquired(String authCookie) {
}
protected void onAuthFailure() {
}
}

View file

@ -0,0 +1,230 @@
package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v4.app.NavUtils;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.EventLog;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
public class LoginActivity extends AccountAuthenticatorActivity {
public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username";
private CommonsApplication app;
Button loginButton;
Button signupButton;
EditText usernameEdit;
EditText passwordEdit;
private class LoginTask extends AsyncTask<String, String, String> {
Activity context;
ProgressDialog dialog;
String username;
String password;
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
Log.d("Commons", "Login done!");
EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT)
.param("username", username)
.param("result", result)
.log();
if (result.equals("Success")) {
dialog.dismiss();
Toast successToast = Toast.makeText(context, R.string.login_success, Toast.LENGTH_SHORT);
successToast.show();
Account account = new Account(username, WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE);
boolean accountCreated = AccountManager.get(context).addAccountExplicitly(account, password, null);
Bundle extras = context.getIntent().getExtras();
if (extras != null) {
if (accountCreated) { // Pass the new account back to the account manager
AccountAuthenticatorResponse response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
Bundle authResult = new Bundle();
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username);
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE);
response.onResult(authResult);
}
}
// FIXME: If the user turns it off, it shouldn't be auto turned back on
ContentResolver.setSyncAutomatically(account, ContributionsContentProvider.AUTHORITY, true); // Enable sync by default!
ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
context.finish();
} else {
int response;
if(result.equals("NetworkFailure")) {
response = R.string.login_failed_network;
} else if(result.equals("NotExists") || result.equals("Illegal") || result.equals("NotExists")) {
response = R.string.login_failed_username;
passwordEdit.setText("");
} else if(result.equals("EmptyPass") || result.equals("WrongPass") || result.equals("WrongPluginPass")) {
response = R.string.login_failed_password;
passwordEdit.setText("");
} else if(result.equals("Throttled")) {
response = R.string.login_failed_throttled;
} else if(result.equals("Blocked")) {
response = R.string.login_failed_blocked;
} else {
// Should never really happen
Log.d("Commons", "Login failed with reason: " + result);
response = R.string.login_failed_generic;
}
Toast.makeText(getApplicationContext(), response, Toast.LENGTH_LONG).show();
dialog.cancel();
}
}
@Override
protected void onPreExecute() {
super.onPreExecute();
dialog = new ProgressDialog(context);
dialog.setIndeterminate(true);
dialog.setTitle(getString(R.string.logging_in_title));
dialog.setMessage(getString(R.string.logging_in_message));
dialog.setCanceledOnTouchOutside(false);
dialog.show();
}
LoginTask(Activity context) {
this.context = context;
}
@Override
protected String doInBackground(String... params) {
username = params[0];
password = params[1];
try {
return app.getApi().login(username, password);
} catch (IOException e) {
// Do something better!
return "NetworkFailure";
}
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
app = (CommonsApplication) this.getApplicationContext();
setContentView(R.layout.activity_login);
loginButton = (Button) findViewById(R.id.loginButton);
signupButton = (Button) findViewById(R.id.signupButton);
usernameEdit = (EditText) findViewById(R.id.loginUsername);
passwordEdit = (EditText) findViewById(R.id.loginPassword);
final LoginActivity that = this;
TextWatcher loginEnabler = new TextWatcher() {
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { }
public void onTextChanged(CharSequence charSequence, int start, int count, int after) { }
public void afterTextChanged(Editable editable) {
if(usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0) {
loginButton.setEnabled(true);
} else {
loginButton.setEnabled(false);
}
}
};
usernameEdit.addTextChangedListener(loginEnabler);
passwordEdit.addTextChangedListener(loginEnabler);
passwordEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() {
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
if (loginButton.isEnabled()) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
performLogin();
return true;
} else if ((keyEvent != null) && keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
performLogin();
return true;
}
}
return false;
}
});
signupButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse("https://commons.wikimedia.org/wiki/Special:UserLogin/signup")));
}
});
loginButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
that.performLogin();
}
});
if (savedInstanceState == null) {
Intent welcomeIntent = new Intent(this, WelcomeActivity.class);
startActivity(welcomeIntent);
}
}
private void performLogin() {
String username = usernameEdit.getText().toString();
// Because Mediawiki is upercase-first-char-then-case-sensitive :)
String canonicalUsername = username.substring(0,1).toUpperCase() + username.substring(1);
String password = passwordEdit.getText().toString();
Log.d("Commons", "Login to start!");
LoginTask task = new LoginTask(this);
task.execute(canonicalUsername, password);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_login, menu);
return true;
}
@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,106 @@
package fr.free.nrw.commons.auth;
import java.io.IOException;
import android.accounts.*;
import android.content.*;
import android.os.*;
import org.mediawiki.api.*;
import fr.free.nrw.commons.CommonsApplication;
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
public static final String COMMONS_ACCOUNT_TYPE = "fr.free.nrw.commons";
private Context context;
public WikiAccountAuthenticator(Context context) {
super(context);
this.context = context;
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
// TODO Auto-generated method stub
final Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
// TODO Auto-generated method stub
return null;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
// TODO Auto-generated method stub
return null;
}
private String getAuthCookie(String username, String password) throws IOException {
MWApi api = CommonsApplication.createMWApi();
String result = api.login(username, password);
if(result.equals("Success")) {
return api.getAuthCookie();
} else {
return null;
}
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
// Extract the username and password from the Account Manager, and ask
// the server for an appropriate AuthToken.
final AccountManager am = AccountManager.get(context);
final String password = am.getPassword(account);
if (password != null) {
String authCookie;
try {
authCookie = getAuthCookie(account.name, password);
} catch (IOException e) {
// Network error!
e.printStackTrace();
throw new NetworkErrorException(e);
}
if (authCookie != null) {
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
result.putString(AccountManager.KEY_ACCOUNT_TYPE, COMMONS_ACCOUNT_TYPE);
result.putString(AccountManager.KEY_AUTHTOKEN, authCookie);
return result;
}
}
// If we get here, then we couldn't access the user's password - so we
// need to re-prompt them for their credentials. We do that by creating
// an intent to display our AuthenticatorActivity panel.
final Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(LoginActivity.PARAM_USERNAME, account.name);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
@Override
public String getAuthTokenLabel(String authTokenType) {
// TODO Auto-generated method stub
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
final Bundle result = new Bundle();
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
return result;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
// TODO Auto-generated method stub
return null;
}
}

View file

@ -0,0 +1,23 @@
package fr.free.nrw.commons.auth;
import android.app.*;
import android.content.*;
import android.os.*;
public class WikiAccountAuthenticatorService extends Service{
private static WikiAccountAuthenticator wikiAccountAuthenticator = null;
@Override
public IBinder onBind(Intent intent) {
if (!intent.getAction().equals(android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT)) {
return null;
}
if(wikiAccountAuthenticator == null) {
wikiAccountAuthenticator = new WikiAccountAuthenticator(this);
}
return wikiAccountAuthenticator.getIBinder();
}
}

View file

@ -0,0 +1,88 @@
package fr.free.nrw.commons.caching;
import android.util.Log;
import com.github.varunpant.quadtree.Point;
import com.github.varunpant.quadtree.QuadTree;
import java.util.ArrayList;
import java.util.List;
import fr.free.nrw.commons.upload.MwVolleyApi;
public class CacheController {
private double x, y;
private QuadTree quadTree;
private Point[] pointsFound;
private double xMinus, xPlus, yMinus, yPlus;
private static final String TAG = CacheController.class.getName();
private static final int EARTH_RADIUS = 6378137;
public CacheController() {
quadTree = new QuadTree(-180, -90, +180, +90);
}
public void setQtPoint(double decLongitude, double decLatitude) {
x = decLongitude;
y = decLatitude;
Log.d(TAG, "New QuadTree created");
Log.d(TAG, "X (longitude) value: " + x + ", Y (latitude) value: " + y);
}
public void cacheCategory() {
List<String> pointCatList = new ArrayList<String>();
if (MwVolleyApi.GpsCatExists.getGpsCatExists() == true) {
pointCatList.addAll(MwVolleyApi.getGpsCat());
Log.d(TAG, "Categories being cached: " + pointCatList);
} else {
Log.d(TAG, "No categories found, so no categories cached");
}
quadTree.set(x, y, pointCatList);
}
public List findCategory() {
//Convert decLatitude and decLongitude to a coordinate offset range
convertCoordRange();
pointsFound = quadTree.searchWithin(xMinus, yMinus, xPlus, yPlus);
List<String> displayCatList = new ArrayList<String>();
Log.d(TAG, "Points found in quadtree: " + pointsFound);
if (pointsFound.length != 0) {
Log.d(TAG, "Entering for loop");
for (Point point : pointsFound) {
Log.d(TAG, "Nearby point: " + point.toString());
displayCatList = (List<String>)point.getValue();
Log.d(TAG, "Nearby cat: " + point.getValue());
}
Log.d(TAG, "Categories found in cache: " + displayCatList.toString());
} else {
Log.d(TAG, "No categories found in cache");
}
return displayCatList;
}
//Based on algorithm at http://gis.stackexchange.com/questions/2951/algorithm-for-offsetting-a-latitude-longitude-by-some-amount-of-meters
public void convertCoordRange() {
//Position, decimal degrees
double lat = y;
double lon = x;
//offsets in meters
double offset = 100;
//Coordinate offsets in radians
double dLat = offset/EARTH_RADIUS;
double dLon = offset/(EARTH_RADIUS*Math.cos(Math.PI*lat/180));
//OffsetPosition, decimal degrees
yPlus = lat + dLat * 180/Math.PI;
yMinus = lat - dLat * 180/Math.PI;
xPlus = lon + dLon * 180/Math.PI;
xMinus = lon - dLon * 180/Math.PI;
Log.d(TAG, "Search within: xMinus=" + xMinus + ", yMinus=" + yMinus + ", xPlus=" + xPlus + ", yPlus=" + yPlus);
}
}

View file

@ -0,0 +1,479 @@
package fr.free.nrw.commons.category;
import android.app.Activity;
import android.content.ContentProviderClient;
import android.content.Context;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.support.v4.app.Fragment;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.CheckedTextView;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.upload.MwVolleyApi;
/**
* Displays the category suggestion and selection screen. Category search is initiated here.
*/
public class CategorizationFragment extends Fragment {
public static interface OnCategoriesSaveHandler {
public void onCategoriesSave(ArrayList<String> categories);
}
ListView categoriesList;
protected EditText categoriesFilter;
ProgressBar categoriesSearchInProgress;
TextView categoriesNotFoundView;
TextView categoriesSkip;
CategoriesAdapter categoriesAdapter;
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
private OnCategoriesSaveHandler onCategoriesSaveHandler;
protected HashMap<String, ArrayList<String>> categoriesCache;
// LHS guarantees ordered insertions, allowing for prioritized method A results
private final Set<String> results = new LinkedHashSet<String>();
fr.free.nrw.commons.category.PrefixUpdater prefixUpdaterSub;
fr.free.nrw.commons.category.MethodAUpdater methodAUpdaterSub;
private ContentProviderClient client;
protected final static int SEARCH_CATS_LIMIT = 25;
private static final String TAG = CategorizationFragment.class.getName();
public static class CategoryItem implements Parcelable {
public String name;
public boolean selected;
public static Creator<CategoryItem> CREATOR = new Creator<CategoryItem>() {
public CategoryItem createFromParcel(Parcel parcel) {
return new CategoryItem(parcel);
}
public CategoryItem[] newArray(int i) {
return new CategoryItem[0];
}
};
public CategoryItem(String name, boolean selected) {
this.name = name;
this.selected = selected;
}
public CategoryItem(Parcel in) {
name = in.readString();
selected = in.readInt() == 1;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(name);
parcel.writeInt(selected ? 1 : 0);
}
}
/**
* Retrieves recently-used categories and nearby categories, and merges them without duplicates.
* @return a list containing these categories
*/
protected ArrayList<String> recentCatQuery() {
ArrayList<String> items = new ArrayList<String>();
Set<String> mergedItems = new LinkedHashSet<String>();
try {
Cursor cursor = client.query(
fr.free.nrw.commons.category.CategoryContentProvider.BASE_URI,
fr.free.nrw.commons.category.Category.Table.ALL_FIELDS,
null,
new String[]{},
fr.free.nrw.commons.category.Category.Table.COLUMN_LAST_USED + " DESC");
// fixme add a limit on the original query instead of falling out of the loop?
while (cursor.moveToNext() && cursor.getPosition() < SEARCH_CATS_LIMIT) {
fr.free.nrw.commons.category.Category cat = fr.free.nrw.commons.category.Category.fromCursor(cursor);
items.add(cat.getName());
}
cursor.close();
if (MwVolleyApi.GpsCatExists.getGpsCatExists() == true) {
//Log.d(TAG, "GPS cats found in CategorizationFragment.java" + MwVolleyApi.getGpsCat().toString());
List<String> gpsItems = new ArrayList<String>(MwVolleyApi.getGpsCat());
//Log.d(TAG, "GPS items: " + gpsItems.toString());
mergedItems.addAll(gpsItems);
}
mergedItems.addAll(items);
}
catch (RemoteException e) {
throw new RuntimeException(e);
}
//Needs to be an ArrayList and not a List unless we want to modify a big portion of preexisting code
ArrayList<String> mergedItemsList = new ArrayList<String>(mergedItems);
return mergedItemsList;
}
/**
* Displays categories found to the user as they type in the search box
* @param categories a list of all categories found for the search string
* @param filter the search string
*/
protected void setCatsAfterAsync(ArrayList<String> categories, String filter) {
if (getActivity() != null) {
ArrayList<CategoryItem> items = new ArrayList<CategoryItem>();
HashSet<String> existingKeys = new HashSet<String>();
for (CategoryItem item : categoriesAdapter.getItems()) {
if (item.selected) {
items.add(item);
existingKeys.add(item.name);
}
}
for (String category : categories) {
if (!existingKeys.contains(category)) {
items.add(new CategoryItem(category, false));
}
}
categoriesAdapter.setItems(items);
categoriesAdapter.notifyDataSetInvalidated();
categoriesSearchInProgress.setVisibility(View.GONE);
if (categories.isEmpty()) {
if (TextUtils.isEmpty(filter)) {
// If we found no recent cats, show the skip message!
categoriesSkip.setVisibility(View.VISIBLE);
} else {
categoriesNotFoundView.setText(getString(R.string.categories_not_found, filter));
categoriesNotFoundView.setVisibility(View.VISIBLE);
}
} else {
categoriesList.smoothScrollToPosition(existingKeys.size());
}
}
else {
Log.e(TAG, "Error: Fragment is null");
}
}
private class CategoriesAdapter extends BaseAdapter {
private Context context;
private ArrayList<CategoryItem> items;
private CategoriesAdapter(Context context, ArrayList<CategoryItem> items) {
this.context = context;
this.items = items;
}
public int getCount() {
return items.size();
}
public Object getItem(int i) {
return items.get(i);
}
public ArrayList<CategoryItem> getItems() {
return items;
}
public void setItems(ArrayList<CategoryItem> items) {
this.items = items;
}
public long getItemId(int i) {
return i;
}
public View getView(int i, View view, ViewGroup viewGroup) {
CheckedTextView checkedView;
if(view == null) {
checkedView = (CheckedTextView) getActivity().getLayoutInflater().inflate(R.layout.layout_categories_item, null);
} else {
checkedView = (CheckedTextView) view;
}
CategoryItem item = (CategoryItem) this.getItem(i);
checkedView.setChecked(item.selected);
checkedView.setText(item.name);
checkedView.setTag(i);
return checkedView;
}
}
public int getCurrentSelectedCount() {
int count = 0;
for(CategoryItem item: categoriesAdapter.getItems()) {
if(item.selected) {
count++;
}
}
return count;
}
private fr.free.nrw.commons.category.Category lookupCategory(String name) {
try {
Cursor cursor = client.query(
fr.free.nrw.commons.category.CategoryContentProvider.BASE_URI,
fr.free.nrw.commons.category.Category.Table.ALL_FIELDS,
fr.free.nrw.commons.category.Category.Table.COLUMN_NAME + "=?",
new String[] {name},
null);
if (cursor.moveToFirst()) {
fr.free.nrw.commons.category.Category cat = fr.free.nrw.commons.category.Category.fromCursor(cursor);
return cat;
}
} catch (RemoteException e) {
// This feels lazy, but to hell with checked exceptions. :)
throw new RuntimeException(e);
}
// Newly used category...
fr.free.nrw.commons.category.Category cat = new fr.free.nrw.commons.category.Category();
cat.setName(name);
cat.setLastUsed(new Date());
cat.setTimesUsed(0);
return cat;
}
private class CategoryCountUpdater extends AsyncTask<Void, Void, Void> {
private String name;
public CategoryCountUpdater(String name) {
this.name = name;
}
@Override
protected Void doInBackground(Void... voids) {
fr.free.nrw.commons.category.Category cat = lookupCategory(name);
cat.incTimesUsed();
cat.setContentProviderClient(client);
cat.save();
return null; // Make the compiler happy.
}
}
private void updateCategoryCount(String name) {
Utils.executeAsyncTask(new CategoryCountUpdater(name), executor);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_categorization, null);
categoriesList = (ListView) rootView.findViewById(R.id.categoriesListBox);
categoriesFilter = (EditText) rootView.findViewById(R.id.categoriesSearchBox);
categoriesSearchInProgress = (ProgressBar) rootView.findViewById(R.id.categoriesSearchInProgress);
categoriesNotFoundView = (TextView) rootView.findViewById(R.id.categoriesNotFound);
categoriesSkip = (TextView) rootView.findViewById(R.id.categoriesExplanation);
categoriesSkip.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
getActivity().onBackPressed();
getActivity().finish();
}
});
ArrayList<CategoryItem> items;
if(savedInstanceState == null) {
items = new ArrayList<CategoryItem>();
categoriesCache = new HashMap<String, ArrayList<String>>();
} else {
items = savedInstanceState.getParcelableArrayList("currentCategories");
categoriesCache = (HashMap<String, ArrayList<String>>) savedInstanceState.getSerializable("categoriesCache");
}
categoriesAdapter = new CategoriesAdapter(getActivity(), items);
categoriesList.setAdapter(categoriesAdapter);
categoriesList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> adapterView, View view, int index, long id) {
CheckedTextView checkedView = (CheckedTextView) view;
CategoryItem item = (CategoryItem) adapterView.getAdapter().getItem(index);
item.selected = !item.selected;
checkedView.setChecked(item.selected);
if (item.selected) {
updateCategoryCount(item.name);
}
}
});
categoriesFilter.addTextChangedListener(new TextWatcher() {
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
startUpdatingCategoryList();
}
public void afterTextChanged(Editable editable) {
}
});
startUpdatingCategoryList();
return rootView;
}
/**
* Makes asynchronous calls to the Commons MediaWiki API via anonymous subclasses of
* 'MethodAUpdater' and 'PrefixUpdater'. Some of their methods are overridden in order to
* aggregate the results. A CountDownLatch is used to ensure that MethodA results are shown
* above Prefix results.
*/
private void requestSearchResults() {
final CountDownLatch latch = new CountDownLatch(1);
prefixUpdaterSub = new fr.free.nrw.commons.category.PrefixUpdater(this) {
@Override
protected ArrayList<String> doInBackground(Void... voids) {
ArrayList<String> result = new ArrayList<String>();
try {
result = super.doInBackground();
latch.await();
}
catch (InterruptedException e) {
Log.w(TAG, e);
//Thread.currentThread().interrupt();
}
return result;
}
@Override
protected void onPostExecute(ArrayList<String> result) {
super.onPostExecute(result);
results.addAll(result);
Log.d(TAG, "Prefix result: " + result);
String filter = categoriesFilter.getText().toString();
ArrayList<String> resultsList = new ArrayList<String>(results);
categoriesCache.put(filter, resultsList);
Log.d(TAG, "Final results List: " + resultsList);
categoriesAdapter.notifyDataSetChanged();
setCatsAfterAsync(resultsList, filter);
}
};
methodAUpdaterSub = new fr.free.nrw.commons.category.MethodAUpdater(this) {
@Override
protected void onPostExecute(ArrayList<String> result) {
results.clear();
super.onPostExecute(result);
results.addAll(result);
Log.d(TAG, "Method A result: " + result);
categoriesAdapter.notifyDataSetChanged();
latch.countDown();
}
};
Utils.executeAsyncTask(prefixUpdaterSub);
Utils.executeAsyncTask(methodAUpdaterSub);
}
private void startUpdatingCategoryList() {
if (prefixUpdaterSub != null) {
prefixUpdaterSub.cancel(true);
}
if (methodAUpdaterSub != null) {
methodAUpdaterSub.cancel(true);
}
requestSearchResults();
}
@Override
public void onCreateOptionsMenu(Menu menu, android.view.MenuInflater inflater) {
menu.clear();
inflater.inflate(R.menu.fragment_categorization, menu);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
getActivity().setTitle(R.string.categories_activity_title);
client = getActivity().getContentResolver().acquireContentProviderClient(fr.free.nrw.commons.category.CategoryContentProvider.AUTHORITY);
}
@Override
public void onDestroy() {
super.onDestroy();
client.release();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList("currentCategories", categoriesAdapter.getItems());
outState.putSerializable("categoriesCache", categoriesCache);
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
switch(menuItem.getItemId()) {
case R.id.menu_save_categories:
ArrayList<String> selectedCategories = new ArrayList<String>();
for(CategoryItem item: categoriesAdapter.getItems()) {
if(item.selected) {
selectedCategories.add(item.name);
}
}
onCategoriesSaveHandler.onCategoriesSave(selectedCategories);
return true;
}
return super.onOptionsItemSelected(menuItem);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
onCategoriesSaveHandler = (OnCategoriesSaveHandler) activity;
}
}

View file

@ -0,0 +1,144 @@
package fr.free.nrw.commons.category;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.RemoteException;
import java.util.Date;
public class Category {
private ContentProviderClient client;
private Uri contentUri;
private String name;
private Date lastUsed;
private int timesUsed;
// Getters/setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getLastUsed() {
// warning: Date objects are mutable.
return (Date)lastUsed.clone();
}
public void setLastUsed(Date lastUsed) {
// warning: Date objects are mutable.
this.lastUsed = (Date)lastUsed.clone();
}
public void touch() {
lastUsed = new Date();
}
public int getTimesUsed() {
return timesUsed;
}
public void setTimesUsed(int timesUsed) {
this.timesUsed = timesUsed;
}
public void incTimesUsed() {
timesUsed++;
touch();
}
// Database/content-provider stuff
public void setContentProviderClient(ContentProviderClient client) {
this.client = client;
}
public void save() {
try {
if(contentUri == null) {
contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues());
} else {
client.update(contentUri, toContentValues(), null, null);
}
} catch(RemoteException e) {
throw new RuntimeException(e);
}
}
public ContentValues toContentValues() {
ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_NAME, getName());
cv.put(Table.COLUMN_LAST_USED, getLastUsed().getTime());
cv.put(Table.COLUMN_TIMES_USED, getTimesUsed());
return cv;
}
public static Category fromCursor(Cursor cursor) {
// Hardcoding column positions!
Category c = new Category();
c.contentUri = CategoryContentProvider.uriForId(cursor.getInt(0));
c.name = cursor.getString(1);
c.lastUsed = new Date(cursor.getLong(2));
c.timesUsed = cursor.getInt(3);
return c;
}
public static class Table {
public static final String TABLE_NAME = "categories";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_NAME = "name";
public static final String COLUMN_LAST_USED = "last_used";
public static final String COLUMN_TIMES_USED = "times_used";
// 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_NAME,
COLUMN_LAST_USED,
COLUMN_TIMES_USED
};
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
+ COLUMN_NAME + " STRING,"
+ COLUMN_LAST_USED + " INTEGER,"
+ COLUMN_TIMES_USED + " INTEGER"
+ ");";
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 < 4) {
// doesn't exist yet
from++;
onUpdate(db, from, to);
return;
}
if(from == 4) {
// table added in version 5
onCreate(db);
from++;
onUpdate(db, from, to);
return;
}
if(from == 5) {
from++;
onUpdate(db, from, to);
return;
}
}
}
}

View file

@ -0,0 +1,157 @@
package fr.free.nrw.commons.category;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.data.DBOpenHelper;
public class CategoryContentProvider extends ContentProvider {
// For URI matcher
private static final int CATEGORIES = 1;
private static final int CATEGORIES_ID = 2;
public static final String AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
private static final String BASE_PATH = "categories";
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, CATEGORIES);
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CATEGORIES_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(Category.Table.TABLE_NAME);
int uriType = uriMatcher.match(uri);
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor;
switch(uriType) {
case CATEGORIES:
cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
break;
case CATEGORIES_ID:
cursor = queryBuilder.query(db,
Category.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 CATEGORIES:
id = sqlDB.insert(Category.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) {
return 0;
}
@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 CATEGORIES:
for(ContentValues value: values) {
Log.d("Commons", "Inserting! " + value.toString());
sqlDB.insert(Category.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 CATEGORIES_ID:
int id = Integer.valueOf(uri.getLastPathSegment());
if (TextUtils.isEmpty(selection)) {
rowsUpdated = sqlDB.update(Category.Table.TABLE_NAME,
contentValues,
Category.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,82 @@
package fr.free.nrw.commons.category;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import org.mediawiki.api.ApiResult;
import org.mediawiki.api.MWApi;
import java.io.IOException;
import java.util.ArrayList;
import fr.free.nrw.commons.CommonsApplication;
/**
* Sends asynchronous queries to the Commons MediaWiki API to retrieve categories that are close to
* the keyword typed in by the user. The 'srsearch' action-specific parameter is used for this
* purpose. This class should be subclassed in CategorizationFragment.java to aggregate the results.
*/
public class MethodAUpdater extends AsyncTask<Void, Void, ArrayList<String>> {
private String filter;
private static final String TAG = MethodAUpdater.class.getName();
CategorizationFragment catFragment;
public MethodAUpdater(CategorizationFragment catFragment) {
this.catFragment = catFragment;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
filter = catFragment.categoriesFilter.getText().toString();
catFragment.categoriesSearchInProgress.setVisibility(View.VISIBLE);
catFragment.categoriesNotFoundView.setVisibility(View.GONE);
catFragment.categoriesSkip.setVisibility(View.GONE);
}
@Override
protected ArrayList<String> doInBackground(Void... voids) {
//If user hasn't typed anything in yet, get GPS and recent items
if(TextUtils.isEmpty(filter)) {
return catFragment.recentCatQuery();
}
//if user types in something that is in cache, return cached category
if(catFragment.categoriesCache.containsKey(filter)) {
return catFragment.categoriesCache.get(filter);
}
//otherwise if user has typed something in that isn't in cache, search API for matching categories
MWApi api = CommonsApplication.createMWApi();
ApiResult result;
ArrayList<String> categories = new ArrayList<String>();
//URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch=
try {
result = api.action("query")
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", catFragment.SEARCH_CATS_LIMIT)
.param("srsearch", filter)
.get();
Log.d(TAG, "Method A URL filter" + result.toString());
} catch (IOException e) {
throw new RuntimeException(e);
}
ArrayList<ApiResult> categoryNodes = result.getNodes("/api/query/search/p/@title");
for(ApiResult categoryNode: categoryNodes) {
String cat = categoryNode.getDocument().getTextContent();
String catString = cat.replace("Category:", "");
categories.add(catString);
}
return categories;
}
}

View file

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

View file

@ -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;
}
}

View file

@ -0,0 +1,40 @@
package fr.free.nrw.commons.data;
import android.content.*;
import android.database.sqlite.*;
import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.category.Category;
import fr.free.nrw.commons.contributions.*;
public class DBOpenHelper extends SQLiteOpenHelper{
private static final String DATABASE_NAME = "commons.db";
private static final int DATABASE_VERSION = 6;
private static DBOpenHelper singleton = null;
public static synchronized DBOpenHelper getInstance(Context context) {
if ( singleton == null ) {
singleton = new DBOpenHelper(context);
}
return singleton;
}
private DBOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
Contribution.Table.onCreate(sqLiteDatabase);
ModifierSequence.Table.onCreate(sqLiteDatabase);
Category.Table.onCreate(sqLiteDatabase);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
Contribution.Table.onUpdate(sqLiteDatabase, from, to);
ModifierSequence.Table.onUpdate(sqLiteDatabase, from, to);
Category.Table.onUpdate(sqLiteDatabase, from, to);
}
}

View file

@ -0,0 +1,57 @@
package fr.free.nrw.commons.media;
import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;
import android.util.Log;
import org.mediawiki.api.ApiResult;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.Utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class CategoryImagesLoader extends AsyncTaskLoader<List<Media>>{
private final CommonsApplication app;
private final String category;
public CategoryImagesLoader(Context context, String category) {
super(context);
this.app = (CommonsApplication) context.getApplicationContext();
this.category = category;
}
@Override
protected void onStartLoading() {
super.onStartLoading();
super.forceLoad();
}
@Override
public List<Media> loadInBackground() {
ArrayList<Media> mediaList = new ArrayList<Media>();
ApiResult result;
try {
result = app.getApi().action("query")
.param("list", "categorymembers")
.param("cmtitle", "Category:" + category)
.param("cmprop", "title|timestamp")
.param("cmtype", "file")
.param("cmsort", "timestamp")
.param("cmdir", "descending")
.param("cmlimit", 50)
.get();
} catch (IOException e) {
throw new RuntimeException(e);
}
Log.d("Commons", Utils.getStringFromDOM(result.getDocument()));
List<ApiResult> members = result.getNodes("/api/query/categorymembers/cm");
for(ApiResult member : members) {
mediaList.add(new Media(member.getString("@title")));
}
return mediaList;
}
}

View file

@ -0,0 +1,367 @@
package fr.free.nrw.commons.media;
import android.content.Intent;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import com.android.volley.toolbox.ImageLoader;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.assist.ImageLoadingListener;
import java.io.IOException;
import java.util.ArrayList;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.License;
import fr.free.nrw.commons.LicenseList;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
public class MediaDetailFragment extends Fragment {
private boolean editable;
private DisplayImageOptions displayOptions;
private fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider detailProvider;
private int index;
public static MediaDetailFragment forMedia(int index) {
return forMedia(index, false);
}
public static MediaDetailFragment forMedia(int index, boolean editable) {
MediaDetailFragment mf = new MediaDetailFragment();
Bundle state = new Bundle();
state.putBoolean("editable", editable);
state.putInt("index", index);
state.putInt("listIndex", 0);
state.putInt("listTop", 0);
mf.setArguments(state);
return mf;
}
private ImageView image;
//private EditText title;
private ProgressBar loadingProgress;
private ImageView loadingFailed;
private fr.free.nrw.commons.media.MediaDetailSpacer spacer;
private int initialListTop = 0;
private TextView title;
private TextView desc;
private TextView license;
private LinearLayout categoryContainer;
private ScrollView scrollView;
private ArrayList<String> categoryNames;
private boolean categoriesLoaded = false;
private boolean categoriesPresent = false;
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
private ViewTreeObserver.OnScrollChangedListener scrollListener;
DataSetObserver dataObserver;
private AsyncTask<Void,Void,Boolean> detailFetchTask;
private LicenseList licenseList;
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("index", index);
outState.putBoolean("editable", editable);
getScrollPosition();
outState.putInt("listTop", initialListTop);
}
private void getScrollPosition() {
initialListTop = scrollView.getScrollY();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
detailProvider = (fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider)getActivity();
if(savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable");
index = savedInstanceState.getInt("index");
initialListTop = savedInstanceState.getInt("listTop");
} else {
editable = getArguments().getBoolean("editable");
index = getArguments().getInt("index");
initialListTop = 0;
}
categoryNames = new ArrayList<String>();
categoryNames.add(getString(R.string.detail_panel_cats_loading));
final View view = inflater.inflate(R.layout.fragment_media_detail, container, false);
image = (ImageView) view.findViewById(R.id.mediaDetailImage);
loadingProgress = (ProgressBar) view.findViewById(R.id.mediaDetailImageLoading);
loadingFailed = (ImageView) view.findViewById(R.id.mediaDetailImageFailed);
scrollView = (ScrollView) view.findViewById(R.id.mediaDetailScrollView);
// Detail consists of a list view with main pane in header view, plus category list.
spacer = (fr.free.nrw.commons.media.MediaDetailSpacer) view.findViewById(R.id.mediaDetailSpacer);
title = (TextView) view.findViewById(R.id.mediaDetailTitle);
desc = (TextView) view.findViewById(R.id.mediaDetailDesc);
license = (TextView) view.findViewById(R.id.mediaDetailLicense);
categoryContainer = (LinearLayout) view.findViewById(R.id.mediaDetailCategoryContainer);
licenseList = new LicenseList(getActivity());
Media media = detailProvider.getMediaAtPosition(index);
if (media == null) {
// Ask the detail provider to ping us when we're ready
Log.d("Commons", "MediaDetailFragment not yet ready to display details; registering observer");
dataObserver = new DataSetObserver() {
public void onChanged() {
Log.d("Commons", "MediaDetailFragment ready to display delayed details!");
detailProvider.unregisterDataSetObserver(dataObserver);
dataObserver = null;
displayMediaDetails(detailProvider.getMediaAtPosition(index));
}
};
detailProvider.registerDataSetObserver(dataObserver);
} else {
Log.d("Commons", "MediaDetailFragment ready to display details");
displayMediaDetails(media);
}
// Progressively darken the image in the background when we scroll detail pane up
scrollListener = new ViewTreeObserver.OnScrollChangedListener() {
public void onScrollChanged() {
updateTheDarkness();
}
};
view.getViewTreeObserver().addOnScrollChangedListener(scrollListener);
// Layout layoutListener to size the spacer item relative to the available space.
// There may be a .... better way to do this.
layoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
private int currentHeight = -1;
public void onGlobalLayout() {
int viewHeight = view.getHeight();
//int textHeight = title.getLineHeight();
int paddingDp = 112;
float paddingPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingDp, getResources().getDisplayMetrics());
int newHeight = viewHeight - Math.round(paddingPx);
if (newHeight != currentHeight) {
currentHeight = newHeight;
ViewGroup.LayoutParams params = spacer.getLayoutParams();
params.height = newHeight;
spacer.setLayoutParams(params);
scrollView.scrollTo(0, initialListTop);
}
}
};
view.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener);
return view;
}
private void displayMediaDetails(final Media media) {
String actualUrl = (media.getLocalUri() != null && !TextUtils.isEmpty(media.getLocalUri().toString())) ? media.getLocalUri().toString() : media.getThumbnailUrl(640);
if(actualUrl.startsWith("http")) {
ImageLoader loader = ((CommonsApplication)getActivity().getApplicationContext()).getImageLoader();
MediaWikiImageView mwImage = (MediaWikiImageView)image;
mwImage.setLoadingView(loadingProgress); //FIXME: Set this as an attribute
mwImage.setMedia(media, loader);
Log.d("Volley", actualUrl);
// FIXME: For transparent images
// Load image metadata: desc, license, categories
// FIXME: keep the spinner going while we load data
// FIXME: cache this data
detailFetchTask = new AsyncTask<Void, Void, Boolean>() {
private MediaDataExtractor extractor;
@Override
protected void onPreExecute() {
extractor = new MediaDataExtractor(media.getFilename(), licenseList);
}
@Override
protected Boolean doInBackground(Void... voids) {
try {
extractor.fetch();
return Boolean.TRUE;
} catch (IOException e) {
e.printStackTrace();
}
return Boolean.FALSE;
}
@Override
protected void onPostExecute(Boolean success) {
detailFetchTask = null;
if (success.booleanValue()) {
extractor.fill(media);
// Fill some fields
desc.setText(prettyDescription(media));
license.setText(prettyLicense(media));
categoryNames.removeAll(categoryNames);
categoryNames.addAll(media.getCategories());
categoriesLoaded = true;
categoriesPresent = (categoryNames.size() > 0);
if (!categoriesPresent) {
// Stick in a filler element.
categoryNames.add(getString(R.string.detail_panel_cats_none));
}
rebuildCatList();
} else {
Log.d("Commons", "Failed to load photo details.");
}
}
};
Utils.executeAsyncTask(detailFetchTask);
} else {
com.nostra13.universalimageloader.core.ImageLoader.getInstance().displayImage(actualUrl, image, displayOptions, new ImageLoadingListener() {
public void onLoadingStarted(String s, View view) {
loadingProgress.setVisibility(View.VISIBLE);
}
public void onLoadingFailed(String s, View view, FailReason failReason) {
loadingProgress.setVisibility(View.GONE);
loadingFailed.setVisibility(View.VISIBLE);
}
public void onLoadingComplete(String s, View view, Bitmap bitmap) {
loadingProgress.setVisibility(View.GONE);
loadingFailed.setVisibility(View.GONE);
image.setVisibility(View.VISIBLE);
if(bitmap.hasAlpha()) {
image.setBackgroundResource(android.R.color.white);
}
}
public void onLoadingCancelled(String s, View view) {
throw new RuntimeException("Image loading cancelled. But why?");
}
});
}
title.setText(media.getDisplayTitle());
desc.setText(""); // fill in from network...
license.setText(""); // fill in from network...
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
displayOptions = Utils.getGenericDisplayOptions().build();
}
@Override
public void onDestroyView() {
if (detailFetchTask != null) {
detailFetchTask.cancel(true);
detailFetchTask = null;
}
if (layoutListener != null) {
getView().getViewTreeObserver().removeGlobalOnLayoutListener(layoutListener); // old Android was on crack. CRACK IS WHACK
layoutListener = null;
}
if (scrollListener != null) {
getView().getViewTreeObserver().removeOnScrollChangedListener(scrollListener);
scrollListener = null;
}
if (dataObserver != null) {
detailProvider.unregisterDataSetObserver(dataObserver);
dataObserver = null;
}
super.onDestroyView();
}
private void rebuildCatList() {
// @fixme add the category items
for (String cat : categoryNames) {
View catLabel = buildCatLabel(cat);
categoryContainer.addView(catLabel);
}
}
private View buildCatLabel(String cat) {
final String catName = cat;
final View item = getLayoutInflater(null).inflate(R.layout.detail_category_item, null, false);
final TextView textView = (TextView)item.findViewById(R.id.mediaDetailCategoryItemText);
textView.setText(cat);
if (categoriesLoaded && categoriesPresent) {
textView.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
String selectedCategoryTitle = "Category:" + catName;
Intent viewIntent = new Intent();
viewIntent.setAction(Intent.ACTION_VIEW);
viewIntent.setData(Utils.uriForWikiPage(selectedCategoryTitle));
startActivity(viewIntent);
}
});
}
return item;
}
private void updateTheDarkness() {
// You must face the darkness alone
int scrollY = scrollView.getScrollY();
int scrollMax = getView().getHeight();
float scrollPercentage = (float)scrollY / (float)scrollMax;
final float transparencyMax = 0.75f;
if (scrollPercentage > transparencyMax) {
scrollPercentage = transparencyMax;
}
image.setAlpha(1.0f - scrollPercentage);
}
private String prettyDescription(Media media) {
// @todo use UI language when multilingual descs are available
String desc = media.getDescription("en").trim();
if (desc.equals("")) {
return getString(R.string.detail_description_empty);
} else {
return desc;
}
}
private String prettyLicense(Media media) {
String licenseKey = media.getLicense();
Log.d("Commons", "Media license is: " + licenseKey);
if (licenseKey == null || licenseKey.equals("")) {
return getString(R.string.detail_license_empty);
}
License licenseObj = licenseList.get(licenseKey);
if (licenseObj == null) {
return licenseKey;
} else {
return licenseObj.getName();
}
}
}

View file

@ -0,0 +1,287 @@
package fr.free.nrw.commons.media;
import android.annotation.SuppressLint;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.EventLog;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity;
public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPageChangeListener {
private ViewPager pager;
private Boolean editable;
private CommonsApplication app;
public void onPageScrolled(int i, float v, int i2) {
getActivity().supportInvalidateOptionsMenu();
}
public void onPageSelected(int i) {
}
public void onPageScrollStateChanged(int i) {
}
public interface MediaDetailProvider {
public Media getMediaAtPosition(int i);
public int getTotalMediaCount();
public void notifyDatasetChanged();
public void registerDataSetObserver(DataSetObserver observer);
public void unregisterDataSetObserver(DataSetObserver observer);
}
private class MediaDetailAdapter extends FragmentStatePagerAdapter {
public MediaDetailAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int i) {
if(i == 0) {
// See bug https://code.google.com/p/android/issues/detail?id=27526
pager.postDelayed(new Runnable() {
public void run() {
getActivity().supportInvalidateOptionsMenu();
}
}, 5);
}
return fr.free.nrw.commons.media.MediaDetailFragment.forMedia(i, editable);
}
@Override
public int getCount() {
return ((MediaDetailProvider)getActivity()).getTotalMediaCount();
}
}
public MediaDetailPagerFragment() {
this(false);
}
@SuppressLint("ValidFragment")
public MediaDetailPagerFragment(Boolean editable) {
this.editable = editable;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_media_detail_pager, container, false);
pager = (ViewPager) view.findViewById(R.id.mediaDetailsPager);
pager.setOnPageChangeListener(this);
if(savedInstanceState != null) {
final int pageNumber = savedInstanceState.getInt("current-page");
// Adapter doesn't seem to be loading immediately.
// Dear God, please forgive us for our sins
view.postDelayed(new Runnable() {
public void run() {
pager.setAdapter(new MediaDetailAdapter(getChildFragmentManager()));
pager.setCurrentItem(pageNumber, false);
getActivity().supportInvalidateOptionsMenu();
}
}, 100);
} else {
pager.setAdapter(new MediaDetailAdapter(getChildFragmentManager()));
}
return view;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("current-page", pager.getCurrentItem());
outState.putBoolean("editable", editable);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable");
}
app = (CommonsApplication)getActivity().getApplicationContext();
setHasOptionsMenu(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
MediaDetailProvider provider = (MediaDetailProvider)getActivity();
Media m = provider.getMediaAtPosition(pager.getCurrentItem());
switch(item.getItemId()) {
case R.id.menu_share_current_image:
EventLog.schema(CommonsApplication.EVENT_SHARE_ATTEMPT)
.param("username", app.getCurrentAccount().name)
.param("filename", m.getFilename())
.log();
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_TEXT, m.getDisplayTitle() + " " + m.getDescriptionUrl());
startActivity(shareIntent);
return true;
case R.id.menu_browser_current_image:
Intent viewIntent = new Intent();
viewIntent.setAction(Intent.ACTION_VIEW);
viewIntent.setData(Uri.parse(m.getDescriptionUrl()));
startActivity(viewIntent);
return true;
case R.id.menu_download_current_image:
downloadMedia(m);
return true;
case R.id.menu_retry_current_image:
// Is this... sane? :)
((ContributionsActivity)getActivity()).retryUpload(pager.getCurrentItem());
getActivity().getSupportFragmentManager().popBackStack();
return true;
case R.id.menu_cancel_current_image:
// todo: delete image
((ContributionsActivity)getActivity()).deleteUpload(pager.getCurrentItem());
getActivity().getSupportFragmentManager().popBackStack();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* Start the media file downloading to the local SD card/storage.
* The file can then be opened in Gallery or other apps.
*
* @param m
*/
private void downloadMedia(Media m) {
String imageUrl = m.getImageUrl(),
fileName = m.getFilename();
// Strip 'File:' from beginning of filename, we really shouldn't store it
fileName = fileName.replaceFirst("^File:", "");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
// Gingerbread DownloadManager has no HTTPS support...
// Download file over HTTP, there'll be no credentials
// sent so it should be safe-ish.
imageUrl = imageUrl.replaceFirst("^https://", "http://");
}
Uri imageUri = Uri.parse(imageUrl);
DownloadManager.Request req = new DownloadManager.Request(imageUri);
req.setDescription(getString(R.string.app_name));
req.setTitle(m.getDisplayTitle());
req.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
// Modern Android updates the gallery automatically. Yay!
req.allowScanningByMediaScanner();
// On HC/ICS/JB we can leave the download notification up when complete.
// This allows folks to open the file directly in gallery viewer.
// But for some reason it fails on Honeycomb (Google TV). Sigh.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
}
}
final DownloadManager manager = (DownloadManager)getActivity().getSystemService(Context.DOWNLOAD_SERVICE);
final long downloadId = manager.enqueue(req);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
// For Gingerbread compatibility...
BroadcastReceiver onComplete = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// Check if the download has completed...
Cursor c = manager.query(new DownloadManager.Query()
.setFilterById(downloadId)
.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL | DownloadManager.STATUS_FAILED)
);
if (c.moveToFirst()) {
int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
Log.d("Commons", "Download completed with status " + status);
if (status == DownloadManager.STATUS_SUCCESSFUL) {
// Force Gallery to index the new file
Uri mediaUri = Uri.parse("file://" + Environment.getExternalStorageDirectory());
getActivity().sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, mediaUri));
// todo: show a persistent notification?
}
} else {
Log.d("Commons", "Couldn't get download status for some reason");
}
getActivity().unregisterReceiver(this);
}
};
getActivity().registerReceiver(onComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if(!editable) { // Disable menu options for editable views
menu.clear(); // see http://stackoverflow.com/a/8495697/17865
inflater.inflate(R.menu.fragment_image_detail, menu);
if(pager != null) {
MediaDetailProvider provider = (MediaDetailProvider)getActivity();
Media m = provider.getMediaAtPosition(pager.getCurrentItem());
if(m != null) {
// Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
menu.findItem(R.id.menu_retry_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_cancel_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_download_current_image).setEnabled(true).setVisible(true);
if(m instanceof Contribution) {
Contribution c = (Contribution)m;
switch(c.getState()) {
case Contribution.STATE_FAILED:
menu.findItem(R.id.menu_retry_current_image).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_cancel_current_image).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_browser_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_share_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_download_current_image).setEnabled(false).setVisible(false);
break;
case Contribution.STATE_IN_PROGRESS:
case Contribution.STATE_QUEUED:
menu.findItem(R.id.menu_retry_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_cancel_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_browser_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_share_current_image).setEnabled(false).setVisible(false);
menu.findItem(R.id.menu_download_current_image).setEnabled(false).setVisible(false);
break;
case Contribution.STATE_COMPLETED:
// Default set of menu items works fine. Treat same as regular media object
break;
}
}
return;
}
}
}
}
public void showImage(int i) {
pager.setCurrentItem(i);
}
}

View file

@ -0,0 +1,19 @@
package fr.free.nrw.commons.media;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
public class MediaDetailSpacer extends View {
public MediaDetailSpacer(Context context) {
super(context);
}
public MediaDetailSpacer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MediaDetailSpacer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
}

View file

@ -0,0 +1,49 @@
package fr.free.nrw.commons.modifications;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class CategoryModifier extends PageModifier {
public static String PARAM_CATEGORIES = "categories";
public static String MODIFIER_NAME = "CategoriesModifier";
public CategoryModifier(String... categories) {
super(MODIFIER_NAME);
JSONArray categoriesArray = new JSONArray();
for(String category: categories) {
categoriesArray.put(category);
}
try {
params.putOpt(PARAM_CATEGORIES, categoriesArray);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public CategoryModifier(JSONObject data) {
super(MODIFIER_NAME);
this.params = data;
}
@Override
public String doModification(String pageName, String pageContents) {
JSONArray categories;
categories = params.optJSONArray(PARAM_CATEGORIES);
StringBuffer categoriesString = new StringBuffer();
for(int i=0; i < categories.length(); i++) {
String category = categories.optString(i);
categoriesString.append("\n[[Category:").append(category).append("]]");
}
return pageContents + categoriesString.toString();
}
@Override
public String getEditSumary() {
return String.format("Added " + params.optJSONArray(PARAM_CATEGORIES).length() + " categories.");
}
}

View file

@ -0,0 +1,160 @@
package fr.free.nrw.commons.modifications;
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 ModificationsContentProvider extends ContentProvider{
private static final int MODIFICATIONS = 1;
private static final int MODIFICATIONS_ID = 2;
public static final String AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider";
private static final String BASE_PATH = "modifications";
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, MODIFICATIONS);
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_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(ModifierSequence.Table.TABLE_NAME);
int uriType = uriMatcher.match(uri);
switch(uriType) {
case MODIFICATIONS:
break;
default:
throw new IllegalArgumentException("Unknown URI" + uri);
}
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
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 MODIFICATIONS:
id = sqlDB.insert(ModifierSequence.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 uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
switch (uriType) {
case MODIFICATIONS_ID:
String id = uri.getLastPathSegment();
sqlDB.delete(ModifierSequence.Table.TABLE_NAME,
"_id = ?",
new String[] { id }
);
return 1;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
@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 MODIFICATIONS:
for(ContentValues value: values) {
Log.d("Commons", "Inserting! " + value.toString());
sqlDB.insert(ModifierSequence.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 MODIFICATIONS:
rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME,
contentValues,
selection,
selectionArgs);
break;
case MODIFICATIONS_ID:
int id = Integer.valueOf(uri.getLastPathSegment());
if (TextUtils.isEmpty(selection)) {
rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME,
contentValues,
ModifierSequence.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,141 @@
package fr.free.nrw.commons.modifications;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.*;
import android.database.Cursor;
import android.os.RemoteException;
import android.util.Log;
import android.accounts.Account;
import android.os.Bundle;
import java.io.*;
import fr.free.nrw.commons.contributions.Contribution;
import org.mediawiki.api.*;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
}
@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!
Cursor allModifications;
try {
allModifications = contentProviderClient.query(ModificationsContentProvider.BASE_URI, null, null, null, null);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
// Exit early if nothing to do
if(allModifications == null || allModifications.getCount() == 0) {
Log.d("Commons", "No modifications to perform");
return;
}
String authCookie;
try {
authCookie = AccountManager.get(getContext()).blockingGetAuthToken(account, "", false);
} catch (OperationCanceledException e) {
throw new RuntimeException(e);
} catch (IOException e) {
Log.d("Commons", "Could not authenticate :(");
return;
} catch (AuthenticatorException e) {
throw new RuntimeException(e);
}
MWApi api = CommonsApplication.createMWApi();
api.setAuthCookie(authCookie);
String editToken;
ApiResult requestResult, responseResult;
try {
editToken = api.getEditToken();
} catch (IOException e) {
Log.d("Commons", "Can not retreive edit token!");
return;
}
allModifications.moveToFirst();
Log.d("Commons", "Found " + allModifications.getCount() + " modifications to execute");
ContentProviderClient contributionsClient = null;
try {
contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY);
while(!allModifications.isAfterLast()) {
ModifierSequence sequence = ModifierSequence.fromCursor(allModifications);
sequence.setContentProviderClient(contentProviderClient);
Contribution contrib;
Cursor contributionCursor;
try {
contributionCursor = contributionsClient.query(sequence.getMediaUri(), null, null, null, null);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
contributionCursor.moveToFirst();
contrib = Contribution.fromCursor(contributionCursor);
if(contrib.getState() == Contribution.STATE_COMPLETED) {
try {
requestResult = api.action("query")
.param("prop", "revisions")
.param("rvprop", "timestamp|content")
.param("titles", contrib.getFilename())
.get();
} catch (IOException e) {
Log.d("Commons", "Network fuckup on modifications sync!");
continue;
}
Log.d("Commons", "Page content is " + Utils.getStringFromDOM(requestResult.getDocument()));
String pageContent = requestResult.getString("/api/query/pages/page/revisions/rev");
String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent);
try {
responseResult = api.action("edit")
.param("title", contrib.getFilename())
.param("token", editToken)
.param("text", processedPageContent)
.param("summary", sequence.getEditSummary())
.post();
} catch (IOException e) {
Log.d("Commons", "Network fuckup on modifications sync!");
continue;
}
Log.d("Commons", "Response is" + Utils.getStringFromDOM(responseResult.getDocument()));
String result = responseResult.getString("/api/edit/@result");
if(!result.equals("Success")) {
// FIXME: Log this somewhere else
Log.d("Commons", "Non success result!" + result);
} else {
sequence.delete();
}
}
allModifications.moveToNext();
}
} finally {
if(contributionsClient != null) {
contributionsClient.release();
}
}
}
}

View file

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

View file

@ -0,0 +1,147 @@
package fr.free.nrw.commons.modifications;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.RemoteException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
public class ModifierSequence {
private Uri mediaUri;
private ArrayList<PageModifier> modifiers;
private Uri contentUri;
private ContentProviderClient client;
public ModifierSequence(Uri mediaUri) {
this.mediaUri = mediaUri;
modifiers = new ArrayList<PageModifier>();
}
public ModifierSequence(Uri mediaUri, JSONObject data) {
this(mediaUri);
JSONArray modifiersJSON = data.optJSONArray("modifiers");
for(int i=0; i< modifiersJSON.length(); i++) {
modifiers.add(PageModifier.fromJSON(modifiersJSON.optJSONObject(i)));
}
}
public Uri getMediaUri() {
return mediaUri;
}
public void queueModifier(PageModifier modifier) {
modifiers.add(modifier);
}
public String executeModifications(String pageName, String pageContents) {
for(PageModifier modifier: modifiers) {
pageContents = modifier.doModification(pageName, pageContents);
}
return pageContents;
}
public String getEditSummary() {
StringBuffer editSummary = new StringBuffer();
for(PageModifier modifier: modifiers) {
editSummary.append(modifier.getEditSumary()).append(" ");
}
editSummary.append("Via Commons Mobile App");
return editSummary.toString();
}
public JSONObject toJSON() {
JSONObject data = new JSONObject();
try {
JSONArray modifiersJSON = new JSONArray();
for(PageModifier modifier: modifiers) {
modifiersJSON.put(modifier.toJSON());
}
data.put("modifiers", modifiersJSON);
return data;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public ContentValues toContentValues() {
ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_MEDIA_URI, mediaUri.toString());
cv.put(Table.COLUMN_DATA, toJSON().toString());
return cv;
}
public static ModifierSequence fromCursor(Cursor cursor) {
// Hardcoding column positions!
ModifierSequence ms = null;
try {
ms = new ModifierSequence(Uri.parse(cursor.getString(1)), new JSONObject(cursor.getString(2)));
} catch (JSONException e) {
throw new RuntimeException(e);
}
ms.contentUri = ModificationsContentProvider.uriForId(cursor.getInt(0));
return ms;
}
public void save() {
try {
if(contentUri == null) {
contentUri = client.insert(ModificationsContentProvider.BASE_URI, this.toContentValues());
} else {
client.update(contentUri, toContentValues(), null, null);
}
} catch(RemoteException e) {
throw new RuntimeException(e);
}
}
public void delete() {
try {
client.delete(contentUri, null, null);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
public void setContentProviderClient(ContentProviderClient client) {
this.client = client;
}
public static class Table {
public static final String TABLE_NAME = "modifications";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_MEDIA_URI = "mediauri";
public static final String COLUMN_DATA = "data";
// 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_MEDIA_URI,
COLUMN_DATA
};
private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ "_id INTEGER PRIMARY KEY,"
+ "mediauri STRING,"
+ "data STRING"
+ ");";
public static void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
}
public static void onUpdate(SQLiteDatabase db, int from, int to) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
}
}

View file

@ -0,0 +1,41 @@
package fr.free.nrw.commons.modifications;
import org.json.JSONException;
import org.json.JSONObject;
public abstract class PageModifier {
public static PageModifier fromJSON(JSONObject data) {
String name = data.optString("name");
if(name.equals(CategoryModifier.MODIFIER_NAME)) {
return new CategoryModifier(data.optJSONObject("data"));
} else if(name.equals(TemplateRemoveModifier.MODIFIER_NAME)) {
return new TemplateRemoveModifier(data.optJSONObject("data"));
}
return null;
}
protected String name;
protected JSONObject params;
protected PageModifier(String name) {
this.name = name;
params = new JSONObject();
}
public abstract String doModification(String pageName, String pageContents);
public abstract String getEditSumary();
public JSONObject toJSON() {
JSONObject data = new JSONObject();
try {
data.putOpt("name", name);
data.put("data", params);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return data;
}
}

View file

@ -0,0 +1,96 @@
package fr.free.nrw.commons.modifications;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TemplateRemoveModifier extends PageModifier {
public static final String MODIFIER_NAME = "TemplateRemoverModifier";
public static final String PARAM_TEMPLATE_NAME = "template";
public static final Pattern PATTERN_TEMPLATE_OPEN = Pattern.compile("\\{\\{");
public static final Pattern PATTERN_TEMPLATE_CLOSE = Pattern.compile("\\}\\}");
public TemplateRemoveModifier(String templateName) {
super(MODIFIER_NAME);
try {
params.putOpt(PARAM_TEMPLATE_NAME, templateName);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public TemplateRemoveModifier(JSONObject data) {
super(MODIFIER_NAME);
this.params = data;
}
@Override
public String doModification(String pageName, String pageContents) {
String templateRawName = params.optString(PARAM_TEMPLATE_NAME);
// Wikitext title normalizing rules. Spaces and _ equivalent
// They also 'condense' - any number of them reduce to just one (just like HTML)
String templateNormalized = templateRawName.trim().replaceAll("(\\s|_)+", "(\\s|_)+");
// Not supporting {{ inside <nowiki> and HTML comments yet
// (Thanks to marktraceur for reminding me of the HTML comments exception)
Pattern templateStartPattern = Pattern.compile("\\{\\{" + templateNormalized, Pattern.CASE_INSENSITIVE);
Matcher matcher = templateStartPattern.matcher(pageContents);
while(matcher.find()) {
int braceCount = 1;
int startIndex = matcher.start();
int curIndex = matcher.end();
Matcher openMatch = PATTERN_TEMPLATE_OPEN.matcher(pageContents);
Matcher closeMatch = PATTERN_TEMPLATE_CLOSE.matcher(pageContents);
while(curIndex < pageContents.length()) {
boolean openFound = openMatch.find(curIndex);
boolean closeFound = closeMatch.find(curIndex);
if(openFound && (!closeFound || openMatch.start() < closeMatch.start())) {
braceCount++;
curIndex = openMatch.end();
} else if (closeFound) {
braceCount--;
curIndex = closeMatch.end();
} else if (braceCount > 0) {
// The template never closes, so...remove nothing
curIndex = startIndex;
break;
}
if (braceCount == 0) {
// The braces have all been closed!
break;
}
}
// Strip trailing whitespace
while(curIndex < pageContents.length()) {
if(pageContents.charAt(curIndex) == ' ' || pageContents.charAt(curIndex) == '\n') {
curIndex++;
} else {
break;
}
}
// I am so going to hell for this, sigh
pageContents = pageContents.substring(0, startIndex) + pageContents.substring(curIndex);
matcher = templateStartPattern.matcher(pageContents);
}
return pageContents;
}
@Override
public String getEditSumary() {
return "Removed template " + params.optString(PARAM_TEMPLATE_NAME) + ".";
}
}

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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient android:angle="90" android:startColor="#60ffffff" android:endColor="#40ffffff"/>
</shape>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="#70000000"
android:endColor="#00000000"
android:angle="270"
>
</gradient>
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="#00000000"
android:endColor="#ff000000"
android:angle="270"
>
</gradient>
</shape>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="#ffffffff"
android:endColor="#f4f4f4ff"
android:type="linear"
/>
</shape>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0c609c"
android:gravity="center"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/welcome_copyright"
android:adjustViewBounds="true"
android:layout_gravity="center"
/>
<LinearLayout android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="240dp"
android:text="@string/welcome_copyright_text"
android:layout_gravity="center"
android:textStyle="bold"
android:textAlignment="center"
android:paddingTop="24dp"
android:gravity="center_horizontal"
android:textColor="@android:color/white"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="240dp"
android:text="@string/welcome_copyright_subtext"
android:layout_gravity="center"
android:textAlignment="center"
android:paddingTop="16dp"
android:gravity="center_horizontal"/>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0c609c"
android:gravity="center"
>
<!-- Sorry about the hardcoded sizes here. They're image-related. -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="180dp"
android:gravity="center_horizontal">
<ImageView
android:layout_width="150dp"
android:layout_height="180dp"
android:src="@drawable/welcome_wikipedia"/>
<ImageView
android:layout_width="160dp"
android:layout_height="120dp"
android:layout_gravity="center"
android:src="@drawable/welcome_copyright"/>
</LinearLayout>
<LinearLayout android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="240dp"
android:text="@string/welcome_final_text"
android:layout_gravity="center"
android:textStyle="bold"
android:textAlignment="center"
android:gravity="center_horizontal"
android:textColor="@android:color/white"
android:singleLine="false"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="240dp"
android:layout_gravity="center"
android:textAlignment="center"
android:paddingTop="16dp"
android:gravity="center_horizontal"/>
<Button
android:layout_width="120dp"
android:layout_height="40dp"
android:text="@string/welcome_final_button_text"
android:id="@+id/welcomeYesButton"
android:layout_gravity="center"
android:background="@android:color/white"
android:textColor="#0c609c"
android:textStyle="bold"
/>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0c609c"
android:gravity="center"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/welcome_wikipedia"
android:adjustViewBounds="true"/>
<LinearLayout android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="240dp"
android:text="@string/welcome_wikipedia_text"
android:layout_gravity="center"
android:textStyle="bold"
android:textAlignment="center"
android:paddingTop="24dp"
android:gravity="center_horizontal"
android:textColor="@android:color/white"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="240dp"
android:text="@string/welcome_wikipedia_subtext"
android:layout_gravity="center"
android:textAlignment="center"
android:paddingTop="16dp"
android:gravity="center_horizontal"/>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:src="@drawable/ic_launcher"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
/>
<TextView
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="@string/app_name"
style="?android:textAppearanceLarge"
/>
<TextView
android:id="@+id/about_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/about_license"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
style="?android:textAppearanceSmall"
android:gravity="center"
android:text="@string/about_license"
/>
<TextView
android:id="@+id/about_improve"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
style="?android:textAppearanceSmall"
android:gravity="center"
android:text="@string/about_improve"
/>
<TextView
android:id="@+id/about_privacy_policy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
style="?android:textAppearanceSmall"
android:gravity="center"
android:text="@string/about_privacy_policy"
/>
<TextView
android:id="@+id/about_uploads_to"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
style="?android:textAppearanceSmall"
android:gravity="center"
android:alpha="0.2"
/>
</LinearLayout>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/campaignsList"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</FrameLayout>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:id="@+id/contributionsFragmentContainer"
android:background="#000000"
>
<fragment
android:name="fr.free.nrw.commons.contributions.ContributionsListFragment"
android:id="@+id/contributionsListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</FrameLayout>

View file

@ -0,0 +1,76 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical" >
<ImageView
android:id="@+id/commonsLogo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:src="@drawable/commons_logo_large" />
<TextView
android:id="@+id/loginSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/app_name"
android:textAppearance="?android:attr/textAppearanceMedium" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_margin="16dip"
android:gravity="center"
android:orientation="vertical"
>
<FrameLayout
android:id="@+id/loginErrors"
android:layout_width="fill_parent"
android:layout_height="48dp"
/>
<EditText
android:id="@+id/loginUsername"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:hint="@string/username"
android:inputType="textNoSuggestions"
android:imeOptions="flagNoExtractUi"
>
<requestFocus />
</EditText>
<EditText
android:id="@+id/loginPassword"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
android:inputType="textPassword"
android:imeOptions="flagNoExtractUi" />
<Button
android:id="@+id/loginButton"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:text="@string/login" />
<Button
android:id="@+id/signupButton"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/signup"
android:layout_gravity="center_horizontal"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/uploadsFragmentContainer"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>

View file

@ -0,0 +1,21 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#000"
>
<ImageView
android:id="@+id/backgroundImage"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:scaleType="centerCrop" />
<FrameLayout
android:id="@+id/single_upload_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</FrameLayout>

Some files were not shown because too many files have changed in this diff Show more