Merge "commons" into the project root directory
47
app/build.gradle
Normal 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'
|
||||
}
|
||||
}
|
||||
BIN
app/libs/Quadtree-1.0-gitbb3cc76.jar
Normal file
3
app/proguard-rules.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-dontobfuscate
|
||||
-keep class org.apache.http.** { *; }
|
||||
-dontwarn org.apache.http.**
|
||||
150
app/src/main/AndroidManifest.xml
Normal 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>
|
||||
126
app/src/main/assets/fontconfig/fonts.conf
Normal 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>
|
||||
|
||||
BIN
app/src/main/assets/fontconfig/fonts/truetype/Ubuntu-R.ttf
Normal file
44
app/src/main/java/fr/free/nrw/commons/AboutActivity.java
Normal 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)));
|
||||
}
|
||||
}
|
||||
217
app/src/main/java/fr/free/nrw/commons/CommonsApplication.java
Normal 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);
|
||||
}
|
||||
}
|
||||
126
app/src/main/java/fr/free/nrw/commons/EventLog.java
Normal 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]);
|
||||
}
|
||||
}
|
||||
68
app/src/main/java/fr/free/nrw/commons/HandlerService.java
Normal 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);
|
||||
}
|
||||
46
app/src/main/java/fr/free/nrw/commons/License.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
79
app/src/main/java/fr/free/nrw/commons/LicenseList.java
Normal 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;
|
||||
}
|
||||
}
|
||||
245
app/src/main/java/fr/free/nrw/commons/Media.java
Normal 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;
|
||||
}
|
||||
}
|
||||
296
app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java
Normal 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
|
||||
}
|
||||
}
|
||||
228
app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java
Normal 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();
|
||||
}
|
||||
}
|
||||
15
app/src/main/java/fr/free/nrw/commons/Prefs.java
Normal 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";
|
||||
}
|
||||
}
|
||||
144
app/src/main/java/fr/free/nrw/commons/SettingsActivity.java
Normal 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;
|
||||
}
|
||||
}
|
||||
243
app/src/main/java/fr/free/nrw/commons/Utils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java
Normal 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);
|
||||
}
|
||||
}
|
||||
8
app/src/main/java/fr/free/nrw/commons/api/MWApi.java
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package fr.free.nrw.commons.api;
|
||||
|
||||
import com.android.volley.RequestQueue;
|
||||
|
||||
public class MWApi {
|
||||
private RequestQueue queue;
|
||||
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
230
app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
144
app/src/main/java/fr/free/nrw/commons/category/Category.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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!");
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
40
app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) + ".";
|
||||
}
|
||||
}
|
||||
138
app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java
Normal 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());
|
||||
}
|
||||
}
|
||||
202
app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
257
app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
394
app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
325
app/src/main/java/fr/free/nrw/commons/upload/UploadService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable-hdpi/commons_logo_large.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_action_search.png
Normal file
|
After Width: | Height: | Size: 3 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_menu_download.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/drawable-hdpi/social_send_now.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/drawable-hdpi/welcome_copyright.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
app/src/main/res/drawable-hdpi/welcome_wikipedia.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
app/src/main/res/drawable-ldpi/welcome_copyright.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
app/src/main/res/drawable-ldpi/welcome_wikipedia.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
app/src/main/res/drawable-ldrtl-hdpi/social_send_now.png
Normal file
|
After Width: | Height: | Size: 640 B |
BIN
app/src/main/res/drawable-ldrtl-mdpi/social_send_now.png
Normal file
|
After Width: | Height: | Size: 489 B |
BIN
app/src/main/res/drawable-ldrtl-xhdpi/social_send_now.png
Normal file
|
After Width: | Height: | Size: 905 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_search.png
Normal file
|
After Width: | Height: | Size: 3 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_menu_download.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
app/src/main/res/drawable-mdpi/social_send_now.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/drawable-mdpi/welcome_copyright.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
app/src/main/res/drawable-mdpi/welcome_wikipedia.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_action_search.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_menu_download.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/main/res/drawable-xhdpi/social_send_now.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/drawable-xhdpi/welcome_copyright.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
app/src/main/res/drawable-xhdpi/welcome_wikipedia.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
4
app/src/main/res/drawable/action_bar_translucent.xml
Normal 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>
|
||||
10
app/src/main/res/drawable/actionbar_top_shadow.xml
Normal 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>
|
||||
BIN
app/src/main/res/drawable/ic_download.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
10
app/src/main/res/drawable/media_info_shadow.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
46
app/src/main/res/layout-land/welcome_copyright.xml
Normal 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>
|
||||
68
app/src/main/res/layout-land/welcome_final.xml
Normal 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>
|
||||
45
app/src/main/res/layout-land/welcome_wikipedia.xml
Normal 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>
|
||||
64
app/src/main/res/layout/activity_about.xml
Normal 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>
|
||||
13
app/src/main/res/layout/activity_campaigns.xml
Normal 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>
|
||||
16
app/src/main/res/layout/activity_contributions.xml
Normal 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>
|
||||
76
app/src/main/res/layout/activity_login.xml
Normal 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>
|
||||
8
app/src/main/res/layout/activity_multiple_uploads.xml
Normal 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>
|
||||
8
app/src/main/res/layout/activity_post_upload.xml
Normal 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>
|
||||
21
app/src/main/res/layout/activity_share.xml
Normal 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>
|
||||