Merge branch 'master' into removeCsv

This commit is contained in:
Vivek Maskara 2017-12-05 12:54:29 +05:30 committed by GitHub
commit 169a4edc46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
130 changed files with 1805 additions and 1107 deletions

View file

@ -1,6 +1,32 @@
# Wikimedia Commons for Android # Wikimedia Commons for Android
## v2.5.0 beta ## v2.6.4 beta
- Excluded httpclient and commons-logging to fix release build errors
- Fixed crashes caused by Fresco and Dagger
## v2.6.3 beta
- Same as 2.6.2 except with localizations added for Google Code-In
## v2.6.2 beta
- Reverted temporarily to last stable version while working on crash fix
## v2.6.1 beta
- Failed attempt to fix crashes in release build with the previous beta release
## v2.6.0 beta
- Multiple bugfixes for location updates and list/map loading in Nearby
- Multiple fixes for various crashes and memory leaks
- Added several unit tests
- Modified About page to include WMF disclaimer and modified Privacy Policy link to point to our individual privacy policy
- Added option for users to send logs to developers (has to be manually activated by user)
- Converted PNGs to WebPs
- Improved login screen with new design and privacy policy link
- Improved category display, if a category has an exact name entered, it will be shown first
- New UI for Nearby list
- Added product flavors for production and the beta-cluster Wikimedia servers
- Various improvements to navigation flow and backstack
## v2.5.0 beta
- Added one-time popup for beta users to provide feedback on IEG renewal proposal - Added one-time popup for beta users to provide feedback on IEG renewal proposal
- Added link to Commons policies in ShareActivity - Added link to Commons policies in ShareActivity
- Various string fixes - Various string fixes

View file

@ -58,6 +58,7 @@ dependencies {
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:3.4' testImplementation 'org.robolectric:robolectric:3.4'
testImplementation 'org.mockito:mockito-all:1.10.19'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
@ -67,6 +68,11 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.1' debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.1'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
implementation 'com.google.dagger:dagger:2.11'
implementation 'com.google.dagger:dagger-android-support:2.11'
annotationProcessor 'com.google.dagger:dagger-compiler:2.11'
annotationProcessor 'com.google.dagger:dagger-android-processor:2.11'
} }
android { android {
@ -77,9 +83,10 @@ android {
defaultConfig { defaultConfig {
applicationId 'fr.free.nrw.commons' applicationId 'fr.free.nrw.commons'
versionCode 74 versionCode 79
versionName '2.5.0' versionName '2.6.4'
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
minSdkVersion project.minSdkVersion minSdkVersion project.minSdkVersion
targetSdkVersion project.targetSdkVersion targetSdkVersion project.targetSdkVersion
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@ -96,6 +103,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
} }
debug { debug {
applicationIdSuffix ".debug"
testCoverageEnabled true testCoverageEnabled true
versionNameSuffix "-debug-" + getBranchName() + "~" + getBuildVersion() versionNameSuffix "-debug-" + getBranchName() + "~" + getBuildVersion()
} }
@ -145,6 +153,8 @@ android {
//FIXME: Temporary fix for https://github.com/commons-app/apps-android-commons/issues/709 //FIXME: Temporary fix for https://github.com/commons-app/apps-android-commons/issues/709
configurations.all { configurations.all {
resolutionStrategy.force 'com.android.support:support-annotations:25.2.0' resolutionStrategy.force 'com.android.support:support-annotations:25.2.0'
exclude module: 'httpclient'
exclude module: 'commons-logging'
} }
buildToolsVersion buildToolsVersion buildToolsVersion buildToolsVersion
} }

View file

@ -1,30 +0,0 @@
package fr.free.nrw.commons;
import android.support.test.espresso.assertion.ViewAssertions;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import fr.free.nrw.commons.nearby.NearbyActivity;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class NearbyActivityTest {
@Rule
public final ActivityTestRule<NearbyActivity> nearby =
new ActivityTestRule<>(NearbyActivity.class);
@Test
public void testActivityLaunch() {
onView(withText(R.string.title_activity_nearby))
.check(ViewAssertions.matches(isDisplayed()));
}
}

View file

@ -0,0 +1,28 @@
package fr.free.nrw.commons.upload;
import android.net.Uri;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
@RunWith(AndroidJUnit4.class)
public class FileUtilsTest {
@Test
public void isSelfOwned() throws Exception {
Uri uri = Uri.parse("content://fr.free.nrw.commons.provider/document/1");
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
assertThat(selfOwned, is(true));
}
@Test
public void isNotSelfOwned() throws Exception {
Uri uri = Uri.parse("content://com.android.providers.media.documents/document/1");
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
assertThat(selfOwned, is(false));
}
}

View file

@ -2,20 +2,23 @@
package="fr.free.nrw.commons"> package="fr.free.nrw.commons">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <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_SETTINGS" />
<uses-permission android:name="android.permission.READ_SYNC_STATS"/> <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_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/> <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS"/> <uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS"/> <uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/> <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS"/> <uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS"/> <uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
<uses-permission android:name="android.permission.READ_LOGS"/> <uses-permission android:name="android.permission.READ_LOGS"/>
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
<uses-feature android:name="android.hardware.location.gps" />
<application <application
android:name=".CommonsApplication" android:name=".CommonsApplication"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
@ -28,23 +31,19 @@
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:finishOnTaskLaunch="true" /> android:finishOnTaskLaunch="true" />
<activity <activity android:name=".auth.LoginActivity">
android:name=".auth.LoginActivity"
>
<intent-filter> <intent-filter>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".WelcomeActivity" <activity android:name=".WelcomeActivity" />
>
</activity>
<activity <activity
android:name=".upload.ShareActivity" android:name=".upload.ShareActivity"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name">
>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -52,11 +51,11 @@
<data android:mimeType="audio/ogg" /> <data android:mimeType="audio/ogg" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".upload.MultipleShareActivity" android:name=".upload.MultipleShareActivity"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name">
>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" /> <action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -66,33 +65,34 @@
</activity> </activity>
<activity <activity
android:name=".contributions.ContributionsActivity" android:name=".contributions.ContributionsActivity"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name" />
>
</activity>
<activity <activity
android:name=".settings.SettingsActivity" android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings" android:label="@string/title_activity_settings" />
/>
<activity <activity
android:name=".AboutActivity" android:name=".AboutActivity"
android:label="@string/title_activity_about" android:label="@string/title_activity_about"
android:parentActivityName=".contributions.ContributionsActivity" /> android:parentActivityName=".contributions.ContributionsActivity" />
<activity <activity
android:name=".auth.SignupActivity" android:name=".auth.SignupActivity"
android:label="@string/title_activity_signup"/> android:label="@string/title_activity_signup" />
<activity <activity
android:name=".nearby.NearbyActivity" android:name=".nearby.NearbyActivity"
android:label="@string/title_activity_nearby" android:label="@string/title_activity_nearby"
android:parentActivityName=".contributions.ContributionsActivity" /> android:parentActivityName=".contributions.ContributionsActivity" />
<service android:name=".upload.UploadService" > <service android:name=".upload.UploadService" />
</service>
<service <service
android:name=".auth.WikiAccountAuthenticatorService" android:name=".auth.WikiAccountAuthenticatorService"
android:exported="true" android:exported="true"
android:process=":auth" > android:process=":auth">
<intent-filter> <intent-filter>
<action android:name="android.accounts.AccountAuthenticator" /> <action android:name="android.accounts.AccountAuthenticator" />
</intent-filter> </intent-filter>
@ -103,27 +103,25 @@
</service> </service>
<service <service
android:name=".contributions.ContributionsSyncService" android:name=".contributions.ContributionsSyncService"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action <action android:name="android.content.SyncAdapter" />
android:name="android.content.SyncAdapter" />
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.content.SyncAdapter" android:name="android.content.SyncAdapter"
android:resource="@xml/contributions_sync_adapter" /> android:resource="@xml/contributions_sync_adapter" />
</service> </service>
<service <service
android:name=".modifications.ModificationsSyncService" android:name=".modifications.ModificationsSyncService"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action <action android:name="android.content.SyncAdapter" />
android:name="android.content.SyncAdapter" />
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.content.SyncAdapter" android:name="android.content.SyncAdapter"
android:resource="@xml/modifications_sync_adapter" /> android:resource="@xml/modifications_sync_adapter" />
</service> </service>
<provider <provider
@ -133,31 +131,29 @@
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data <meta-data
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/> android:resource="@xml/provider_paths" />
</provider> </provider>
<provider <provider
android:name=".contributions.ContributionsContentProvider" android:name=".contributions.ContributionsContentProvider"
android:label="@string/provider_contributions" android:authorities="fr.free.nrw.commons.contributions.contentprovider"
android:syncable="true" android:exported="false"
android:authorities="fr.free.nrw.commons.contributions.contentprovider" android:label="@string/provider_contributions"
android:exported="false"> android:syncable="true" />
</provider>
<provider <provider
android:name=".modifications.ModificationsContentProvider" android:name=".modifications.ModificationsContentProvider"
android:label="@string/provider_modifications" android:authorities="fr.free.nrw.commons.modifications.contentprovider"
android:syncable="true" android:exported="false"
android:authorities="fr.free.nrw.commons.modifications.contentprovider" android:label="@string/provider_modifications"
android:exported="false"> android:syncable="true" />
</provider>
<provider <provider
android:name=".category.CategoryContentProvider" android:name=".category.CategoryContentProvider"
android:label="@string/provider_categories" android:authorities="fr.free.nrw.commons.categories.contentprovider"
android:syncable="false" android:exported="false"
android:authorities="fr.free.nrw.commons.categories.contentprovider" android:label="@string/provider_categories"
android:exported="false"> android:syncable="false" />
</provider>
</application> </application>

View file

@ -1,7 +1,5 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.widget.TextView; import android.widget.TextView;

View file

@ -1,19 +1,8 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.app.Application;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.preference.PreferenceManager;
import android.support.v4.util.LruCache;
import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.stetho.Stetho; import com.facebook.stetho.Stetho;
@ -25,23 +14,23 @@ import org.acra.ReportingInteractionMode;
import org.acra.annotation.ReportsCrashes; import org.acra.annotation.ReportsCrashes;
import java.io.File; import java.io.File;
import java.io.IOException;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import dagger.android.DispatchingAndroidInjector; import dagger.android.AndroidInjector;
import dagger.android.HasActivityInjector; import dagger.android.DaggerApplication;
import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.data.Category; import fr.free.nrw.commons.data.Category;
import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.DaggerAppComponent; import fr.free.nrw.commons.di.CommonsApplicationComponent;
import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.di.DaggerCommonsApplicationComponent;
import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.utils.FileUtils; import fr.free.nrw.commons.utils.FileUtils;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
// TODO: Use ProGuard to rip out reporting when publishing // TODO: Use ProGuard to rip out reporting when publishing
@ -53,9 +42,13 @@ import timber.log.Timber;
resDialogCommentPrompt = R.string.crash_dialog_comment_prompt, resDialogCommentPrompt = R.string.crash_dialog_comment_prompt,
resDialogOkToast = R.string.crash_dialog_ok_toast resDialogOkToast = R.string.crash_dialog_ok_toast
) )
public class CommonsApplication extends Application implements HasActivityInjector { public class CommonsApplication extends DaggerApplication {
private Account currentAccount = null; // Unlike a savings account... @Inject SessionManager sessionManager;
@Inject DBOpenHelper dbOpenHelper;
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
@Inject @Named("application_preferences") SharedPreferences applicationPrefs;
@Inject @Named("prefs") SharedPreferences otherPrefs;
public static final Object[] EVENT_UPLOAD_ATTEMPT = {"MobileAppUploadAttempts", 5334329L}; public static final Object[] EVENT_UPLOAD_ATTEMPT = {"MobileAppUploadAttempts", 5334329L};
public static final Object[] EVENT_LOGIN_ATTEMPT = {"MobileAppLoginAttempts", 5257721L}; public static final Object[] EVENT_LOGIN_ATTEMPT = {"MobileAppLoginAttempts", 5257721L};
@ -64,84 +57,23 @@ public class CommonsApplication extends Application implements HasActivityInject
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using Android Commons app"; public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using Android Commons app";
public static final String FEEDBACK_EMAIL = "commons-app-android-private@googlegroups.com"; public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback"; public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback";
@Inject DispatchingAndroidInjector<Activity> dispatchingActivityInjector; private CommonsApplicationComponent component;
@Inject MediaWikiApi mediaWikiApi;
private static CommonsApplication instance = null;
private MediaWikiApi api = null;
private LruCache<String, String> thumbnailUrlCache = new LruCache<>(1024);
private CacheController cacheData = null;
private DBOpenHelper dbOpenHelper = null;
private NearbyPlaces nearbyPlaces = null;
private RefWatcher refWatcher; private RefWatcher refWatcher;
/**
* This should not be called by ANY application code (other than the magic Android glue)
* Use CommonsApplication.getInstance() instead to get the singleton.
*/
public CommonsApplication() {
CommonsApplication.instance = this;
}
public static CommonsApplication getInstance() {
if (instance == null) {
instance = new CommonsApplication();
}
return instance;
}
public MediaWikiApi getMWApi() {
if (api == null) {
api = new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
}
return api;
}
public CacheController getCacheData() {
if (cacheData == null) {
cacheData = new CacheController();
}
return cacheData;
}
public LruCache<String, String> getThumbnailUrlCache() {
return thumbnailUrlCache;
}
public synchronized DBOpenHelper getDBOpenHelper() {
if (dbOpenHelper == null) {
dbOpenHelper = new DBOpenHelper(this);
}
return dbOpenHelper;
}
public synchronized NearbyPlaces getNearbyPlaces() {
if (nearbyPlaces == null) {
nearbyPlaces = new NearbyPlaces();
}
return nearbyPlaces;
}
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
Fresco.initialize(this);
if (setupLeakCanary() == RefWatcher.DISABLED) { if (setupLeakCanary() == RefWatcher.DISABLED) {
return; return;
} }
Timber.plant(new Timber.DebugTree()); Timber.plant(new Timber.DebugTree());
DaggerAppComponent
.builder()
.application(this)
.build()
.inject(this);
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
ACRA.init(this); ACRA.init(this);
} else { } else {
@ -150,11 +82,6 @@ public class CommonsApplication extends Application implements HasActivityInject
// Fire progress callbacks for every 3% of uploaded content // Fire progress callbacks for every 3% of uploaded content
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0"); System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
Fresco.initialize(this);
//For caching area -> categories
cacheData = new CacheController();
} }
protected RefWatcher setupLeakCanary() { protected RefWatcher setupLeakCanary() {
@ -169,43 +96,18 @@ public class CommonsApplication extends Application implements HasActivityInject
return application.refWatcher; return application.refWatcher;
} }
/** @Override
* @return Account|null protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
*/ return injector();
public Account getCurrentAccount() {
if (currentAccount == null) {
AccountManager accountManager = AccountManager.get(this);
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType());
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(AccountUtil.accountType(), mediaWikiApi.getAuthCookie());
try {
String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false);
mediaWikiApi.setAuthCookie(authCookie);
return true;
} catch (OperationCanceledException | NullPointerException | IOException | AuthenticatorException e) {
e.printStackTrace();
return false;
}
} }
public boolean deviceHasCamera() { public CommonsApplicationComponent injector() {
PackageManager pm = getPackageManager(); if (component == null) {
return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) component = DaggerCommonsApplicationComponent.builder()
|| pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); .appModule(new CommonsApplicationModule(this))
.build();
}
return component;
} }
public void clearApplicationData(Context context, LogoutListener logoutListener) { public void clearApplicationData(Context context, LogoutListener logoutListener) {
@ -220,67 +122,25 @@ public class CommonsApplication extends Application implements HasActivityInject
} }
} }
AccountManager accountManager = AccountManager.get(this); sessionManager.clearAllAccounts()
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType()); .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
AccountManagerCallback<Boolean> amCallback = new AccountManagerCallback<Boolean>() { .subscribe(() -> {
private int index = 0;
void setIndex(int index) {
this.index = index;
}
int getIndex() {
return index;
}
@Override
public void run(AccountManagerFuture<Boolean> accountManagerFuture) {
setIndex(getIndex() + 1);
try {
if (accountManagerFuture != null && accountManagerFuture.getResult()) {
Timber.d("Account removed successfully.");
}
} catch (OperationCanceledException | IOException | AuthenticatorException e) {
e.printStackTrace();
}
if (getIndex() == allAccounts.length) {
Timber.d("All accounts have been removed"); Timber.d("All accounts have been removed");
//TODO: fix preference manager //TODO: fix preference manager
PreferenceManager.getDefaultSharedPreferences(getInstance()) defaultPrefs.edit().clear().commit();
.edit().clear().commit(); applicationPrefs.edit().clear().commit();
SharedPreferences preferences = context applicationPrefs.edit().putBoolean("firstrun", false).apply();otherPrefs.edit().clear().commit();
.getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
preferences.edit().clear().commit();
context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
.edit().clear().commit();
preferences.edit().putBoolean("firstrun", false).apply();
updateAllDatabases(); updateAllDatabases();
currentAccount = null;
logoutListener.onLogoutComplete(); logoutListener.onLogoutComplete();
} });
}
};
for (Account account : allAccounts) {
accountManager.removeAccount(account, amCallback, null);
}
}
@Override
public DispatchingAndroidInjector<Activity> activityInjector() {
return dispatchingActivityInjector;
} }
/** /**
* Deletes all tables and re-creates them. * Deletes all tables and re-creates them.
*/ */
public void updateAllDatabases() { private void updateAllDatabases() {
DBOpenHelper dbOpenHelper = CommonsApplication.getInstance().getDBOpenHelper();
dbOpenHelper.getReadableDatabase().close(); dbOpenHelper.getReadableDatabase().close();
SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); SQLiteDatabase db = dbOpenHelper.getWritableDatabase();

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import android.app.Service;
import android.content.Intent; import android.content.Intent;
import android.os.Binder; import android.os.Binder;
import android.os.Handler; import android.os.Handler;
@ -9,7 +8,9 @@ import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
public abstract class HandlerService<T> extends Service { import dagger.android.DaggerService;
public abstract class HandlerService<T> extends DaggerService {
private volatile Looper threadLooper; private volatile Looper threadLooper;
private volatile ServiceHandler threadHandler; private volatile ServiceHandler threadHandler;
private String serviceName; private String serviceName;

View file

@ -5,7 +5,9 @@ import android.content.res.Resources;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
@ -19,7 +21,7 @@ public class LicenseList {
res = activity.getResources(); res = activity.getResources();
XmlPullParser parser = res.getXml(R.xml.wikimedia_licenses); XmlPullParser parser = res.getXml(R.xml.wikimedia_licenses);
String namespace = "https://www.mediawiki.org/wiki/Extension:UploadWizard/xmlns/licenses"; String namespace = "https://www.mediawiki.org/wiki/Extension:UploadWizard/xmlns/licenses";
while (Utils.xmlFastForward(parser, namespace, "license")) { while (xmlFastForward(parser, namespace, "license")) {
String id = parser.getAttributeValue(null, "id"); String id = parser.getAttributeValue(null, "id");
String template = parser.getAttributeValue(null, "template"); String template = parser.getAttributeValue(null, "template");
String url = parser.getAttributeValue(null, "url"); String url = parser.getAttributeValue(null, "url");
@ -60,4 +62,34 @@ public class LicenseList {
+ nameIdForTemplate(template), null, null); + nameIdForTemplate(template), null, null);
return (nameId != 0) ? res.getString(nameId) : template; return (nameId != 0) ? res.getString(nameId) : template;
} }
/**
* 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
*/
private boolean xmlFastForward(XmlPullParser parser, String namespace, String element) {
try {
while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (parser.getEventType() == XmlPullParser.START_TAG &&
parser.getNamespace().equals(namespace) &&
parser.getName().equals(element)) {
// We found it!
return true;
}
}
return false;
} catch (XmlPullParserException e) {
e.printStackTrace();
return false;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
} }

View file

@ -11,12 +11,12 @@ import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
@ -33,45 +33,39 @@ import timber.log.Timber;
* which are not intrinsic to the media and may change due to editing. * which are not intrinsic to the media and may change due to editing.
*/ */
public class MediaDataExtractor { public class MediaDataExtractor {
private final MediaWikiApi mediaWikiApi;
private boolean fetched; private boolean fetched;
private String filename;
private ArrayList<String> categories; private ArrayList<String> categories;
private Map<String, String> descriptions; private Map<String, String> descriptions;
private String license; private String license;
private @Nullable LatLng coordinates; private @Nullable LatLng coordinates;
private LicenseList licenseList;
/** @Inject
* @param filename of the target media object, should include 'File:' prefix public MediaDataExtractor(MediaWikiApi mwApi) {
*/ this.categories = new ArrayList<>();
public MediaDataExtractor(String filename, LicenseList licenseList) { this.descriptions = new HashMap<>();
this.filename = filename; this.fetched = false;
categories = new ArrayList<>(); this.mediaWikiApi = mwApi;
descriptions = new HashMap<>();
fetched = false;
this.licenseList = licenseList;
} }
/** /*
* Actually fetch the data over the network. * Actually fetch the data over the network.
* todo: use local caching? * todo: use local caching?
* *
* Warning: synchronous i/o, call on a background thread * Warning: synchronous i/o, call on a background thread
*/ */
public void fetch() throws IOException { public void fetch(String filename, LicenseList licenseList) throws IOException {
if (fetched) { if (fetched) {
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again."); throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
} }
MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); MediaResult result = mediaWikiApi.fetchMediaByFilename(filename);
MediaResult result = api.fetchMediaByFilename(filename);
// In-page category links are extracted from source, as XML doesn't cover [[links]] // In-page category links are extracted from source, as XML doesn't cover [[links]]
extractCategories(result.getWikiSource()); extractCategories(result.getWikiSource());
// Description template info is extracted from preprocessor XML // Description template info is extracted from preprocessor XML
processWikiParseTree(result.getParseTreeXmlSource()); processWikiParseTree(result.getParseTreeXmlSource(), licenseList);
fetched = true; fetched = true;
} }
@ -90,7 +84,7 @@ public class MediaDataExtractor {
} }
} }
private void processWikiParseTree(String source) throws IOException { private void processWikiParseTree(String source, LicenseList licenseList) throws IOException {
Document doc; Document doc;
try { try {
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();

View file

@ -7,16 +7,17 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
class MediaThumbnailFetchTask extends AsyncTask<String, String, String> { class MediaThumbnailFetchTask extends AsyncTask<String, String, String> {
protected final Media media; protected final Media media;
private MediaWikiApi mediaWikiApi;
public MediaThumbnailFetchTask(@NonNull Media media) { public MediaThumbnailFetchTask(@NonNull Media media, MediaWikiApi mwApi) {
this.media = media; this.media = media;
this.mediaWikiApi = mwApi;
} }
@Override @Override
protected String doInBackground(String... params) { protected String doInBackground(String... params) {
try { try {
MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); return mediaWikiApi.findThumbnailByFilename(params[0]);
return api.findThumbnailByFilename(params[0]);
} catch (Exception e) { } catch (Exception e) {
// Do something better! // Do something better!
} }

View file

@ -4,6 +4,7 @@ import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.graphics.drawable.VectorDrawableCompat; import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.util.LruCache;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.Toast; import android.widget.Toast;
@ -11,9 +12,15 @@ import android.widget.Toast;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.view.SimpleDraweeView; import com.facebook.drawee.view.SimpleDraweeView;
import javax.inject.Inject;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
public class MediaWikiImageView extends SimpleDraweeView { public class MediaWikiImageView extends SimpleDraweeView {
@Inject MediaWikiApi mwApi;
@Inject LruCache<String, String> thumbnailUrlCache;
private ThumbnailFetchTask currentThumbnailTask; private ThumbnailFetchTask currentThumbnailTask;
public MediaWikiImageView(Context context) { public MediaWikiImageView(Context context) {
@ -39,11 +46,11 @@ public class MediaWikiImageView extends SimpleDraweeView {
return; return;
} }
if (CommonsApplication.getInstance().getThumbnailUrlCache().get(media.getFilename()) != null) { if (thumbnailUrlCache.get(media.getFilename()) != null) {
setImageUrl(CommonsApplication.getInstance().getThumbnailUrlCache().get(media.getFilename())); setImageUrl(thumbnailUrlCache.get(media.getFilename()));
} else { } else {
setImageUrl(null); setImageUrl(null);
currentThumbnailTask = new ThumbnailFetchTask(media); currentThumbnailTask = new ThumbnailFetchTask(media, mwApi);
currentThumbnailTask.execute(media.getFilename()); currentThumbnailTask.execute(media.getFilename());
} }
} }
@ -57,6 +64,7 @@ public class MediaWikiImageView extends SimpleDraweeView {
} }
private void init() { private void init() {
((CommonsApplication) getContext().getApplicationContext()).injector().inject(this);
setHierarchy(GenericDraweeHierarchyBuilder setHierarchy(GenericDraweeHierarchyBuilder
.newInstance(getResources()) .newInstance(getResources())
.setPlaceholderImage(VectorDrawableCompat.create(getResources(), .setPlaceholderImage(VectorDrawableCompat.create(getResources(),
@ -71,8 +79,8 @@ public class MediaWikiImageView extends SimpleDraweeView {
} }
private class ThumbnailFetchTask extends MediaThumbnailFetchTask { private class ThumbnailFetchTask extends MediaThumbnailFetchTask {
ThumbnailFetchTask(@NonNull Media media) { ThumbnailFetchTask(@NonNull Media media, @NonNull MediaWikiApi mwApi) {
super(media); super(media, mwApi);
} }
@Override @Override
@ -85,7 +93,7 @@ public class MediaWikiImageView extends SimpleDraweeView {
} else { } else {
// only cache meaningful thumbnails received from network. // only cache meaningful thumbnails received from network.
try { try {
CommonsApplication.getInstance().getThumbnailUrlCache().put(media.getFilename(), result); thumbnailUrlCache.put(media.getFilename(), result);
} catch (NullPointerException npe) { } catch (NullPointerException npe) {
Timber.e("error when adding pic to cache " + npe); Timber.e("error when adding pic to cache " + npe);

View file

@ -1,102 +1,26 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import android.content.Context; import android.content.Context;
import android.os.Build;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.text.Html;
import android.text.Spanned;
import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.w3c.dom.Node;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.BufferedInputStream;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale; import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
import timber.log.Timber; import timber.log.Timber;
public class Utils { public class Utils {
// Get SHA1 of file from input stream
public static String getSHA1(InputStream is) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
Timber.e(e, "Exception while getting Digest");
return "";
}
byte[] buffer = new byte[8192];
int read;
try {
while ((read = is.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
byte[] md5sum = digest.digest();
BigInteger bigInt = new BigInteger(1, md5sum);
String output = bigInt.toString(16);
// Fill to 40 chars
output = String.format("%40s", output).replace(' ', '0');
Timber.i("File SHA1: %s", output);
return output;
} catch (IOException e) {
Timber.e(e, "IO Exception");
return "";
} finally {
try {
is.close();
} catch (IOException e) {
Timber.e(e, "Exception on closing MD5 input stream");
}
}
}
/**
* Fix Html.fromHtml is deprecated problem
*
* @param source provided Html string
* @return returned Spanned of appropriate method according to version check
*/
public static Spanned fromHtml(String source) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY);
} else {
//noinspection deprecation
return Html.fromHtml(source);
}
}
/** /**
* Strips localization symbols from a string. * Strips localization symbols from a string.
* Removes the suffix after "@" and quotes. * Removes the suffix after "@" and quotes.
@ -113,49 +37,12 @@ public class Utils {
} }
} }
public static Date parseMWDate(String mwDate) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // 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'", Locale.ENGLISH); // Assuming MW always gives me UTC
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return isoFormat.format(date);
}
public static String makeThumbBaseUrl(@NonNull String filename) { public static String makeThumbBaseUrl(@NonNull String filename) {
String name = new PageTitle(filename).getPrefixedText(); String name = new PageTitle(filename).getPrefixedText();
String sha = new String(Hex.encodeHex(DigestUtils.md5(name))); String sha = new String(Hex.encodeHex(DigestUtils.md5(name)));
return String.format("%s/%s/%s/%s", BuildConfig.IMAGE_URL_BASE, sha.substring(0, 1), sha.substring(0, 2), urlEncode(name)); return String.format("%s/%s/%s/%s", BuildConfig.IMAGE_URL_BASE, sha.substring(0, 1), sha.substring(0, 2), urlEncode(name));
} }
public static String getStringFromDOM(Node dom) {
Transformer transformer = null;
try {
transformer = TransformerFactory.newInstance().newTransformer();
} catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
StringWriter outputStream = new StringWriter();
DOMSource domSource = new DOMSource(dom);
StreamResult strResult = new StreamResult(outputStream);
try {
transformer.transform(domSource, strResult);
} catch (TransformerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return outputStream.toString();
}
public static String urlEncode(String url) { public static String urlEncode(String url) {
try { try {
return URLEncoder.encode(url, "utf-8"); return URLEncoder.encode(url, "utf-8");
@ -164,39 +51,10 @@ public class Utils {
} }
} }
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 capitalize(String string) { public static String capitalize(String string) {
return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1); return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1);
} }
public static String licenseTemplateFor(String license) {
switch (license) {
case Prefs.Licenses.CC_BY_3:
return "{{self|cc-by-3.0}}";
case Prefs.Licenses.CC_BY_4:
return "{{self|cc-by-4.0}}";
case Prefs.Licenses.CC_BY_SA_3:
return "{{self|cc-by-sa-3.0}}";
case Prefs.Licenses.CC_BY_SA_4:
return "{{self|cc-by-sa-4.0}}";
case Prefs.Licenses.CC0:
return "{{self|cc-zero}}";
case Prefs.Licenses.CC_BY:
return "{{self|cc-by-3.0}}";
case Prefs.Licenses.CC_BY_SA:
return "{{self|cc-by-sa-3.0}}";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
public static int licenseNameFor(String license) { public static int licenseNameFor(String license) {
switch (license) { switch (license) {
case Prefs.Licenses.CC_BY_3: case Prefs.Licenses.CC_BY_3:
@ -217,51 +75,6 @@ public class Utils {
throw new RuntimeException("Unrecognized license value: " + license); throw new RuntimeException("Unrecognized license value: " + license);
} }
public static String licenseUrlFor(String license) {
switch (license) {
case Prefs.Licenses.CC_BY_3:
return "https://creativecommons.org/licenses/by/3.0/";
case Prefs.Licenses.CC_BY_4:
return "https://creativecommons.org/licenses/by/4.0/";
case Prefs.Licenses.CC_BY_SA_3:
return "https://creativecommons.org/licenses/by-sa/3.0/";
case Prefs.Licenses.CC_BY_SA_4:
return "https://creativecommons.org/licenses/by-sa/4.0/";
case Prefs.Licenses.CC0:
return "https://creativecommons.org/publicdomain/zero/1.0/";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
/**
* 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;
}
}
public static String fixExtension(String title, String extension) { public static String fixExtension(String title, String extension) {
Pattern jpegPattern = Pattern.compile("\\.jpeg$", Pattern.CASE_INSENSITIVE); Pattern jpegPattern = Pattern.compile("\\.jpeg$", Pattern.CASE_INSENSITIVE);
@ -277,10 +90,6 @@ public class Utils {
return title; return title;
} }
public static boolean isNullOrWhiteSpace(String value) {
return value == null || value.trim().isEmpty();
}
public static boolean isDarkTheme(Context context) { public static boolean isDarkTheme(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("theme", false); return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("theme", false);
} }

View file

@ -4,21 +4,31 @@ import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse; import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager; import android.accounts.AccountManager;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import timber.log.Timber; import timber.log.Timber;
import static android.accounts.AccountManager.ERROR_CODE_REMOTE_EXCEPTION;
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
public class AccountUtil { public class AccountUtil {
public static void createAccount(@Nullable AccountAuthenticatorResponse response, public static final String ACCOUNT_TYPE = "fr.free.nrw.commons";
String username, String password) { private final Context context;
Account account = new Account(username, accountType()); public AccountUtil(Context context) {
this.context = context;
}
public void createAccount(@Nullable AccountAuthenticatorResponse response,
String username, String password) {
Account account = new Account(username, ACCOUNT_TYPE);
boolean created = accountManager().addAccountExplicitly(account, password, null); boolean created = accountManager().addAccountExplicitly(account, password, null);
Timber.d("account creation " + (created ? "successful" : "failure")); Timber.d("account creation " + (created ? "successful" : "failure"));
@ -26,8 +36,8 @@ public class AccountUtil {
if (created) { if (created) {
if (response != null) { if (response != null) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString(AccountManager.KEY_ACCOUNT_NAME, username); bundle.putString(KEY_ACCOUNT_NAME, username);
bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType()); bundle.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
response.onResult(bundle); response.onResult(bundle);
@ -35,7 +45,7 @@ public class AccountUtil {
} else { } else {
if (response != null) { if (response != null) {
response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, ""); response.onError(ERROR_CODE_REMOTE_EXCEPTION, "");
} }
Timber.d("account creation failure"); Timber.d("account creation failure");
} }
@ -45,18 +55,8 @@ public class AccountUtil {
ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default! ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
} }
@NonNull private AccountManager accountManager() {
public static String accountType() { return AccountManager.get(context);
return "fr.free.nrw.commons";
}
private static AccountManager accountManager() {
return AccountManager.get(app());
}
@NonNull
private static CommonsApplication app() {
return CommonsApplication.getInstance();
} }
} }

View file

@ -5,47 +5,50 @@ import android.accounts.AccountManager;
import android.accounts.AccountManagerFuture; import android.accounts.AccountManagerFuture;
import android.os.Bundle; import android.os.Bundle;
import fr.free.nrw.commons.CommonsApplication; import javax.inject.Inject;
import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
public abstract class AuthenticatedActivity extends NavigationBaseActivity { public abstract class AuthenticatedActivity extends NavigationBaseActivity {
private String accountType; @Inject SessionManager sessionManager;
CommonsApplication app;
private String authCookie; private String authCookie;
public AuthenticatedActivity() {
this.accountType = AccountUtil.accountType();
}
private void getAuthCookie(Account account, AccountManager accountManager) { private void getAuthCookie(Account account, AccountManager accountManager) {
Single.fromCallable(() -> accountManager.blockingGetAuthToken(account, "", false)) Single.fromCallable(() -> accountManager.blockingGetAuthToken(account, "", false))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.doOnError(Timber::e) .doOnError(Timber::e)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onAuthCookieAcquired, throwable -> onAuthFailure()); .subscribe(
this:: onAuthCookieAcquired,
throwable -> onAuthFailure());
} }
private void addAccount(AccountManager accountManager) { private void addAccount(AccountManager accountManager) {
Single.just(accountManager.addAccount(accountType, null, null, null, AuthenticatedActivity.this, null, null)) Single.just(accountManager.addAccount(ACCOUNT_TYPE, null, null,
null, AuthenticatedActivity.this, null, null))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.map(AccountManagerFuture::getResult) .map(AccountManagerFuture::getResult)
.doOnEvent((bundle, throwable) -> { .doOnEvent((bundle, throwable) -> {
if (!bundle.containsKey(AccountManager.KEY_ACCOUNT_NAME)) { if (!bundle.containsKey(KEY_ACCOUNT_NAME)) {
throw new RuntimeException("Bundle doesn't contain account-name key: " throw new RuntimeException("Bundle doesn't contain account-name key: "
+ AccountManager.KEY_ACCOUNT_NAME); + KEY_ACCOUNT_NAME);
} }
}) })
.map(bundle -> bundle.getString(AccountManager.KEY_ACCOUNT_NAME)) .map(bundle -> bundle.getString(KEY_ACCOUNT_NAME))
.doOnError(Timber::e) .doOnError(Timber::e)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> { .subscribe(s -> {
Account[] allAccounts = accountManager.getAccountsByType(accountType); Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
Account curAccount = allAccounts[0]; Account curAccount = allAccounts[0];
getAuthCookie(curAccount, accountManager); getAuthCookie(curAccount, accountManager);
}, },
@ -58,7 +61,7 @@ public abstract class AuthenticatedActivity extends NavigationBaseActivity {
return; return;
} }
AccountManager accountManager = AccountManager.get(this); AccountManager accountManager = AccountManager.get(this);
Account curAccount = app.getCurrentAccount(); Account curAccount = sessionManager.getCurrentAccount();
if (curAccount == null) { if (curAccount == null) {
addAccount(accountManager); addAccount(accountManager);
} else { } else {
@ -69,7 +72,7 @@ public abstract class AuthenticatedActivity extends NavigationBaseActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
app = CommonsApplication.getInstance();
if (savedInstanceState != null) { if (savedInstanceState != null) {
authCookie = savedInstanceState.getString("authCookie"); authCookie = savedInstanceState.getString("authCookie");
} }

View file

@ -21,15 +21,19 @@ import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.TextView; import android.widget.TextView;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import dagger.android.AndroidInjection;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.PageTitle; import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity;
import timber.log.Timber; import timber.log.Timber;
@ -40,6 +44,12 @@ public class LoginActivity extends AccountAuthenticatorActivity {
public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username"; public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username";
@Inject MediaWikiApi mwApi;
@Inject AccountUtil accountUtil;
@Inject SessionManager sessionManager;
@Inject @Named("application_preferences") SharedPreferences prefs;
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
@BindView(R.id.loginButton) Button loginButton; @BindView(R.id.loginButton) Button loginButton;
@BindView(R.id.signupButton) Button signupButton; @BindView(R.id.signupButton) Button signupButton;
@BindView(R.id.loginUsername) EditText usernameEdit; @BindView(R.id.loginUsername) EditText usernameEdit;
@ -47,11 +57,8 @@ public class LoginActivity extends AccountAuthenticatorActivity {
@BindView(R.id.loginTwoFactor) EditText twoFactorEdit; @BindView(R.id.loginTwoFactor) EditText twoFactorEdit;
@BindView(R.id.error_message_container) ViewGroup errorMessageContainer; @BindView(R.id.error_message_container) ViewGroup errorMessageContainer;
@BindView(R.id.error_message) TextView errorMessage; @BindView(R.id.error_message) TextView errorMessage;
private CommonsApplication app;
ProgressDialog progressDialog; ProgressDialog progressDialog;
private AppCompatDelegate delegate; private AppCompatDelegate delegate;
private SharedPreferences prefs = null;
private LoginTextWatcher textWatcher = new LoginTextWatcher(); private LoginTextWatcher textWatcher = new LoginTextWatcher();
@Override @Override
@ -59,16 +66,13 @@ public class LoginActivity extends AccountAuthenticatorActivity {
setTheme(Utils.isDarkTheme(this) ? R.style.DarkAppTheme : R.style.LightAppTheme); setTheme(Utils.isDarkTheme(this) ? R.style.DarkAppTheme : R.style.LightAppTheme);
getDelegate().installViewFactory(); getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState); getDelegate().onCreate(savedInstanceState);
AndroidInjection.inject(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
app = CommonsApplication.getInstance();
setContentView(R.layout.activity_login); setContentView(R.layout.activity_login);
ButterKnife.bind(this); ButterKnife.bind(this);
prefs = getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
usernameEdit.addTextChangedListener(textWatcher); usernameEdit.addTextChangedListener(textWatcher);
passwordEdit.addTextChangedListener(textWatcher); passwordEdit.addTextChangedListener(textWatcher);
twoFactorEdit.addTextChangedListener(textWatcher); twoFactorEdit.addTextChangedListener(textWatcher);
@ -91,7 +95,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
WelcomeActivity.startYourself(this); WelcomeActivity.startYourself(this);
prefs.edit().putBoolean("firstrun", false).apply(); prefs.edit().putBoolean("firstrun", false).apply();
} }
if (app.getCurrentAccount() != null) { if (sessionManager.getCurrentAccount() != null) {
startMainActivity(); startMainActivity();
} }
} }
@ -113,6 +117,25 @@ public class LoginActivity extends AccountAuthenticatorActivity {
super.onDestroy(); super.onDestroy();
} }
private LoginTask getLoginTask() {
return new LoginTask(
this,
canonicializeUsername(usernameEdit.getText().toString()),
passwordEdit.getText().toString(),
twoFactorEdit.getText().toString(),
accountUtil, mwApi, defaultPrefs
);
}
/**
* Because Mediawiki is upercase-first-char-then-case-sensitive :)
* @param username String
* @return String canonicial username
*/
private String canonicializeUsername(String username) {
return new PageTitle(username).getText();
}
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
@ -207,24 +230,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}; };
} }
private LoginTask getLoginTask() {
return new LoginTask(
this,
canonicializeUsername(usernameEdit.getText().toString()),
passwordEdit.getText().toString(),
twoFactorEdit.getText().toString()
);
}
/**
* Because Mediawiki is upercase-first-char-then-case-sensitive :)
* @param username String
* @return String canonicial username
*/
private String canonicializeUsername(String username) {
return new PageTitle(username).getText();
}
private void showMessage(@StringRes int resId, @ColorRes int colorResId) { private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
errorMessage.setText(getString(resId)); errorMessage.setText(getString(resId));
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));

View file

@ -1,8 +1,8 @@
package fr.free.nrw.commons.auth; package fr.free.nrw.commons.auth;
import android.accounts.AccountAuthenticatorResponse; import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.SharedPreferences;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
@ -11,22 +11,34 @@ import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.mwapi.EventLog; import fr.free.nrw.commons.mwapi.EventLog;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
import static android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE;
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
class LoginTask extends AsyncTask<String, String, String> { class LoginTask extends AsyncTask<String, String, String> {
private LoginActivity loginActivity; private LoginActivity loginActivity;
private String username; private String username;
private String password; private String password;
private String twoFactorCode = ""; private String twoFactorCode = "";
private CommonsApplication app; private AccountUtil accountUtil;
private MediaWikiApi mwApi;
private SharedPreferences prefs;
public LoginTask(LoginActivity loginActivity, String username, String password, String twoFactorCode) { public LoginTask(LoginActivity loginActivity, String username, String password,
String twoFactorCode, AccountUtil accountUtil,
MediaWikiApi mwApi, SharedPreferences prefs) {
this.loginActivity = loginActivity; this.loginActivity = loginActivity;
this.username = username; this.username = username;
this.password = password; this.password = password;
this.twoFactorCode = twoFactorCode; this.twoFactorCode = twoFactorCode;
app = CommonsApplication.getInstance(); this.accountUtil = accountUtil;
this.mwApi = mwApi;
this.prefs = prefs;
} }
@Override @Override
@ -44,9 +56,9 @@ class LoginTask extends AsyncTask<String, String, String> {
protected String doInBackground(String... params) { protected String doInBackground(String... params) {
try { try {
if (twoFactorCode.isEmpty()) { if (twoFactorCode.isEmpty()) {
return app.getMWApi().login(username, password); return mwApi.login(username, password);
} else { } else {
return app.getMWApi().login(username, password, twoFactorCode); return mwApi.login(username, password, twoFactorCode);
} }
} catch (IOException e) { } catch (IOException e) {
// Do something better! // Do something better!
@ -59,7 +71,7 @@ class LoginTask extends AsyncTask<String, String, String> {
super.onPostExecute(result); super.onPostExecute(result);
Timber.d("Login done!"); Timber.d("Login done!");
EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT) EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT, mwApi, prefs)
.param("username", username) .param("username", username)
.param("result", result) .param("result", result)
.log(); .log();
@ -79,16 +91,16 @@ class LoginTask extends AsyncTask<String, String, String> {
Bundle extras = loginActivity.getIntent().getExtras(); Bundle extras = loginActivity.getIntent().getExtras();
if (extras != null) { if (extras != null) {
Timber.d("Bundle of extras: %s", extras); Timber.d("Bundle of extras: %s", extras);
response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE); response = extras.getParcelable(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
if (response != null) { if (response != null) {
Bundle authResult = new Bundle(); Bundle authResult = new Bundle();
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username); authResult.putString(KEY_ACCOUNT_NAME, username);
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, AccountUtil.accountType()); authResult.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
response.onResult(authResult); response.onResult(authResult);
} }
} }
AccountUtil.createAccount(response, username, password); accountUtil.createAccount(response, username, password);
loginActivity.startMainActivity(); loginActivity.startMainActivity();
} }

View file

@ -0,0 +1,71 @@
package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context;
import java.io.IOException;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import io.reactivex.Completable;
import io.reactivex.Observable;
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
/**
* Manage the current logged in user session.
*/
public class SessionManager {
private final Context context;
private final MediaWikiApi mediaWikiApi;
private Account currentAccount; // Unlike a savings account... ;-)
public SessionManager(Context context, MediaWikiApi mediaWikiApi) {
this.context = context;
this.mediaWikiApi = mediaWikiApi;
this.currentAccount = null;
}
/**
* @return Account|null
*/
public Account getCurrentAccount() {
if (currentAccount == null) {
AccountManager accountManager = AccountManager.get(context);
Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
if (allAccounts.length != 0) {
currentAccount = allAccounts[0];
}
}
return currentAccount;
}
public Boolean revalidateAuthToken() {
AccountManager accountManager = AccountManager.get(context);
Account curAccount = getCurrentAccount();
if (curAccount == null) {
return false; // This should never happen
}
accountManager.invalidateAuthToken(ACCOUNT_TYPE, mediaWikiApi.getAuthCookie());
try {
String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false);
mediaWikiApi.setAuthCookie(authCookie);
return true;
} catch (OperationCanceledException | NullPointerException | IOException | AuthenticatorException e) {
e.printStackTrace();
return false;
}
}
public Completable clearAllAccounts() {
AccountManager accountManager = AccountManager.get(context);
Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
return Completable.fromObservable(Observable.fromArray(allAccounts)
.map(a -> accountManager.removeAccount(a, null, null).getResult()))
.doOnComplete(() -> currentAccount = null);
}
}

View file

@ -7,7 +7,6 @@ import android.webkit.WebViewClient;
import android.widget.Toast; import android.widget.Toast;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.theme.BaseActivity; import fr.free.nrw.commons.theme.BaseActivity;
import timber.log.Timber; import timber.log.Timber;
@ -39,11 +38,8 @@ public class SignupActivity extends BaseActivity {
//Signup success, so clear cookies, notify user, and load LoginActivity again //Signup success, so clear cookies, notify user, and load LoginActivity again
Timber.d("Overriding URL %s", url); Timber.d("Overriding URL %s", url);
Toast toast = Toast.makeText( Toast toast = Toast.makeText(SignupActivity.this,
CommonsApplication.getInstance(), "Account created!", Toast.LENGTH_LONG);
"Account created!",
Toast.LENGTH_LONG
);
toast.show(); toast.show();
// terminate on task completion. // terminate on task completion.
finish(); finish();

View file

@ -13,7 +13,6 @@ import android.support.annotation.Nullable;
import java.io.IOException; import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import static android.accounts.AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION; import static android.accounts.AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION;
@ -25,15 +24,18 @@ import static android.accounts.AccountManager.KEY_BOOLEAN_RESULT;
import static android.accounts.AccountManager.KEY_ERROR_CODE; import static android.accounts.AccountManager.KEY_ERROR_CODE;
import static android.accounts.AccountManager.KEY_ERROR_MESSAGE; import static android.accounts.AccountManager.KEY_ERROR_MESSAGE;
import static android.accounts.AccountManager.KEY_INTENT; import static android.accounts.AccountManager.KEY_INTENT;
import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE;
import static fr.free.nrw.commons.auth.LoginActivity.PARAM_USERNAME; import static fr.free.nrw.commons.auth.LoginActivity.PARAM_USERNAME;
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
private Context context; private final Context context;
private MediaWikiApi mediaWikiApi;
WikiAccountAuthenticator(Context context) { WikiAccountAuthenticator(Context context, MediaWikiApi mwApi) {
super(context); super(context);
this.context = context; this.context = context;
this.mediaWikiApi = mwApi;
} }
private Bundle unsupportedOperation() { private Bundle unsupportedOperation() {
@ -47,7 +49,7 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
} }
private boolean supportedAccountType(@Nullable String type) { private boolean supportedAccountType(@Nullable String type) {
return AccountUtil.accountType().equals(type); return ACCOUNT_TYPE.equals(type);
} }
@Override @Override
@ -86,11 +88,10 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
} }
private String getAuthCookie(String username, String password) throws IOException { private String getAuthCookie(String username, String password) throws IOException {
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
//TODO add 2fa support here //TODO add 2fa support here
String result = api.login(username, password); String result = mediaWikiApi.login(username, password);
if (result.equals("PASS")) { if (result.equals("PASS")) {
return api.getAuthCookie(); return mediaWikiApi.getAuthCookie();
} else { } else {
return null; return null;
} }
@ -115,7 +116,7 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
if (authCookie != null) { if (authCookie != null) {
final Bundle result = new Bundle(); final Bundle result = new Bundle();
result.putString(KEY_ACCOUNT_NAME, account.name); result.putString(KEY_ACCOUNT_NAME, account.name);
result.putString(KEY_ACCOUNT_TYPE, AccountUtil.accountType()); result.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
result.putString(KEY_AUTHTOKEN, authCookie); result.putString(KEY_AUTHTOKEN, authCookie);
return result; return result;
} }

View file

@ -1,22 +1,28 @@
package fr.free.nrw.commons.auth; package fr.free.nrw.commons.auth;
import android.accounts.AccountManager;
import android.app.Service;
import android.content.Intent; import android.content.Intent;
import android.os.IBinder; import android.os.IBinder;
public class WikiAccountAuthenticatorService extends Service { import javax.inject.Inject;
import dagger.android.DaggerService;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import static android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT;
public class WikiAccountAuthenticatorService extends DaggerService {
@Inject MediaWikiApi mwApi;
private WikiAccountAuthenticator wikiAccountAuthenticator = null;
private static WikiAccountAuthenticator wikiAccountAuthenticator = null;
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
if (!intent.getAction().equals(AccountManager.ACTION_AUTHENTICATOR_INTENT)) { if (!intent.getAction().equals(ACTION_AUTHENTICATOR_INTENT)) {
return null; return null;
} }
if (wikiAccountAuthenticator == null) { if (wikiAccountAuthenticator == null) {
wikiAccountAuthenticator = new WikiAccountAuthenticator(this); wikiAccountAuthenticator = new WikiAccountAuthenticator(this, mwApi);
} }
return wikiAccountAuthenticator.getIBinder(); return wikiAccountAuthenticator.getIBinder();
} }

View file

@ -3,8 +3,6 @@ package fr.free.nrw.commons.category;
import android.content.ContentProviderClient; import android.content.ContentProviderClient;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
@ -31,11 +29,15 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication; import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.data.Category; import fr.free.nrw.commons.data.Category;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.upload.MwVolleyApi; import fr.free.nrw.commons.upload.MwVolleyApi;
import fr.free.nrw.commons.utils.StringSortingUtils; import fr.free.nrw.commons.utils.StringSortingUtils;
import io.reactivex.Observable; import io.reactivex.Observable;
@ -50,7 +52,7 @@ import static fr.free.nrw.commons.category.CategoryContentProvider.AUTHORITY;
/** /**
* Displays the category suggestion and selection screen. Category search is initiated here. * Displays the category suggestion and selection screen. Category search is initiated here.
*/ */
public class CategorizationFragment extends Fragment { public class CategorizationFragment extends DaggerFragment {
public static final int SEARCH_CATS_LIMIT = 25; public static final int SEARCH_CATS_LIMIT = 25;
@ -65,6 +67,9 @@ public class CategorizationFragment extends Fragment {
@BindView(R.id.categoriesExplanation) @BindView(R.id.categoriesExplanation)
TextView categoriesSkip; TextView categoriesSkip;
@Inject MediaWikiApi mwApi;
@Inject @Named("default_preferences") SharedPreferences prefs;
private RVRendererAdapter<CategoryItem> categoriesAdapter; private RVRendererAdapter<CategoryItem> categoriesAdapter;
private OnCategoriesSaveHandler onCategoriesSaveHandler; private OnCategoriesSaveHandler onCategoriesSaveHandler;
private HashMap<String, ArrayList<String>> categoriesCache; private HashMap<String, ArrayList<String>> categoriesCache;
@ -205,7 +210,9 @@ public class CategorizationFragment extends Fragment {
.sorted(sortBySimilarity(filter)) .sorted(sortBySimilarity(filter))
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
s -> categoriesAdapter.add(s), Timber::e, () -> { s -> categoriesAdapter.add(s),
Timber::e,
() -> {
categoriesAdapter.notifyDataSetChanged(); categoriesAdapter.notifyDataSetChanged();
categoriesSearchInProgress.setVisibility(View.GONE); categoriesSearchInProgress.setVisibility(View.GONE);
@ -253,10 +260,9 @@ public class CategorizationFragment extends Fragment {
private Observable<CategoryItem> titleCategories() { private Observable<CategoryItem> titleCategories() {
//Retrieve the title that was saved when user tapped submit icon //Retrieve the title that was saved when user tapped submit icon
SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity()); String title = prefs.getString("Title", "");
String title = titleDesc.getString("Title", "");
return CommonsApplication.getInstance().getMWApi() return mwApi
.searchTitles(title, SEARCH_CATS_LIMIT) .searchTitles(title, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false)); .map(name -> new CategoryItem(name, false));
} }
@ -279,7 +285,7 @@ public class CategorizationFragment extends Fragment {
} }
//otherwise, search API for matching categories //otherwise, search API for matching categories
return CommonsApplication.getInstance().getMWApi() return mwApi
.allCategories(term, SEARCH_CATS_LIMIT) .allCategories(term, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false)); .map(name -> new CategoryItem(name, false));
} }
@ -290,7 +296,7 @@ public class CategorizationFragment extends Fragment {
return Observable.empty(); return Observable.empty();
} }
return CommonsApplication.getInstance().getMWApi() return mwApi
.searchCategories(term, SEARCH_CATS_LIMIT) .searchCategories(term, SEARCH_CATS_LIMIT)
.map(s -> new CategoryItem(s, false)); .map(s -> new CategoryItem(s, false));
} }

View file

@ -10,7 +10,9 @@ import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.text.TextUtils; import android.text.TextUtils;
import fr.free.nrw.commons.CommonsApplication; import javax.inject.Inject;
import dagger.android.AndroidInjection;
import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.data.DBOpenHelper;
import timber.log.Timber; import timber.log.Timber;
@ -36,17 +38,15 @@ public class CategoryContentProvider extends ContentProvider {
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CATEGORIES_ID); uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CATEGORIES_ID);
} }
private DBOpenHelper dbOpenHelper;
public static Uri uriForId(int id) { public static Uri uriForId(int id) {
return Uri.parse(BASE_URI.toString() + "/" + id); return Uri.parse(BASE_URI.toString() + "/" + id);
} }
@SuppressWarnings("ConstantConditions") @Inject DBOpenHelper dbOpenHelper;
@Override @Override
public boolean onCreate() { public boolean onCreate() {
CommonsApplication app = ((CommonsApplication) getContext().getApplicationContext()); AndroidInjection.inject(this);
dbOpenHelper = app.getDBOpenHelper();
return false; return false;
} }

View file

@ -7,6 +7,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.net.Uri; import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import android.os.RemoteException; import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.text.TextUtils; import android.text.TextUtils;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@ -16,7 +17,6 @@ import java.util.Locale;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
public class Contribution extends Media { public class Contribution extends Media {
@ -149,7 +149,7 @@ public class Contribution extends Media {
} }
buffer.append("== {{int:license-header}} ==\n") buffer.append("== {{int:license-header}} ==\n")
.append(Utils.licenseTemplateFor(getLicense())).append("\n\n") .append(licenseTemplateFor(getLicense())).append("\n\n")
.append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n") .append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n")
.append(getTrackingTemplates()); .append(getTrackingTemplates());
return buffer.toString(); return buffer.toString();
@ -377,4 +377,25 @@ public class Contribution extends Media {
} }
} }
} }
@NonNull
private String licenseTemplateFor(String license) {
switch (license) {
case Prefs.Licenses.CC_BY_3:
return "{{self|cc-by-3.0}}";
case Prefs.Licenses.CC_BY_4:
return "{{self|cc-by-4.0}}";
case Prefs.Licenses.CC_BY_SA_3:
return "{{self|cc-by-sa-3.0}}";
case Prefs.Licenses.CC_BY_SA_4:
return "{{self|cc-by-sa-4.0}}";
case Prefs.Licenses.CC0:
return "{{self|cc-zero}}";
case Prefs.Licenses.CC_BY:
return "{{self|cc-by-3.0}}";
case Prefs.Licenses.CC_BY_SA:
return "{{self|cc-by-sa-3.0}}";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
} }

View file

@ -9,7 +9,6 @@ import android.database.Cursor;
import android.database.DataSetObserver; import android.database.DataSetObserver;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader; import android.support.v4.content.CursorLoader;
@ -24,21 +23,20 @@ import android.widget.AdapterView;
import java.util.ArrayList; import java.util.ArrayList;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import dagger.android.AndroidInjection;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity; import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.UploadService; import fr.free.nrw.commons.upload.UploadService;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
@ -49,10 +47,17 @@ import static fr.free.nrw.commons.contributions.ContributionsContentProvider.AUT
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING; import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING;
public class ContributionsActivity extends AuthenticatedActivity public class ContributionsActivity
implements LoaderManager.LoaderCallbacks<Cursor>, AdapterView.OnItemClickListener, extends AuthenticatedActivity
MediaDetailPagerFragment.MediaDetailProvider, FragmentManager.OnBackStackChangedListener, implements LoaderManager.LoaderCallbacks<Cursor>,
ContributionsListFragment.SourceRefresher { AdapterView.OnItemClickListener,
MediaDetailPagerFragment.MediaDetailProvider,
FragmentManager.OnBackStackChangedListener,
ContributionsListFragment.SourceRefresher {
@Inject MediaWikiApi mediaWikiApi;
@Inject SessionManager sessionManager;
@Inject @Named("default_preferences") SharedPreferences prefs;
private Cursor allContributions; private Cursor allContributions;
private ContributionsListFragment contributionsList; private ContributionsListFragment contributionsList;
@ -62,9 +67,6 @@ public class ContributionsActivity extends AuthenticatedActivity
private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<>(); private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<>();
private String CONTRIBUTION_SELECTION = ""; private String CONTRIBUTION_SELECTION = "";
@Inject
MediaWikiApi mediaWikiApi;
/* /*
This sorts in the following order: This sorts in the following order:
Currently Uploading Currently Uploading
@ -109,12 +111,8 @@ public class ContributionsActivity extends AuthenticatedActivity
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); boolean isSettingsChanged = prefs.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
boolean isSettingsChanged = prefs.edit().putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false).apply();
sharedPreferences.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
editor.apply();
if (isSettingsChanged) { if (isSettingsChanged) {
refreshSource(); refreshSource();
} }
@ -122,9 +120,8 @@ public class ContributionsActivity extends AuthenticatedActivity
@Override @Override
protected void onAuthCookieAcquired(String authCookie) { protected void onAuthCookieAcquired(String authCookie) {
// Do a sync every time we get here! // Do a sync everytime we get here!
CommonsApplication app = ((CommonsApplication) getApplication()); requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.AUTHORITY, new Bundle());
requestSync(app.getCurrentAccount(), AUTHORITY, new Bundle());
Intent uploadServiceIntent = new Intent(this, UploadService.class); Intent uploadServiceIntent = new Intent(this, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
startService(uploadServiceIntent); startService(uploadServiceIntent);
@ -139,19 +136,18 @@ public class ContributionsActivity extends AuthenticatedActivity
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
AndroidInjection.inject(this);
setContentView(R.layout.activity_contributions); setContentView(R.layout.activity_contributions);
ButterKnife.bind(this); ButterKnife.bind(this);
// Activity can call methods in the fragment by acquiring a // Activity can call methods in the fragment by acquiring a
// reference to the Fragment from FragmentManager, using findFragmentById() // reference to the Fragment from FragmentManager, using findFragmentById()
FragmentManager supportFragmentManager = getSupportFragmentManager(); FragmentManager supportFragmentManager = getSupportFragmentManager();
contributionsList = (ContributionsListFragment) supportFragmentManager contributionsList = (ContributionsListFragment)supportFragmentManager
.findFragmentById(R.id.contributionsListFragment); .findFragmentById(R.id.contributionsListFragment);
supportFragmentManager.addOnBackStackChangedListener(this); supportFragmentManager.addOnBackStackChangedListener(this);
if (savedInstanceState != null) { if (savedInstanceState != null) {
mediaDetails = (MediaDetailPagerFragment) supportFragmentManager mediaDetails = (MediaDetailPagerFragment)supportFragmentManager
.findFragmentById(R.id.contributionsFragmentContainer); .findFragmentById(R.id.contributionsFragmentContainer);
getSupportLoaderManager().initLoader(0, null, this); getSupportLoaderManager().initLoader(0, null, this);
@ -241,8 +237,7 @@ public class ContributionsActivity extends AuthenticatedActivity
@Override @Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) { public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); int uploads = prefs.getInt(UPLOADS_SHOWING, 100);
int uploads = sharedPref.getInt(UPLOADS_SHOWING, 100);
return new CursorLoader(this, BASE_URI, return new CursorLoader(this, BASE_URI,
ALL_FIELDS, CONTRIBUTION_SELECTION, null, ALL_FIELDS, CONTRIBUTION_SELECTION, null,
CONTRIBUTION_SORT + "LIMIT " + uploads); CONTRIBUTION_SORT + "LIMIT " + uploads);
@ -289,9 +284,8 @@ public class ContributionsActivity extends AuthenticatedActivity
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
private void setUploadCount() { private void setUploadCount() {
CommonsApplication app = ((CommonsApplication) getApplication()); compositeDisposable.add(mediaWikiApi
Disposable uploadCountDisposable = mediaWikiApi .getUploadCount(sessionManager.getCurrentAccount().name)
.getUploadCount(app.getCurrentAccount().name)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
@ -299,8 +293,7 @@ public class ContributionsActivity extends AuthenticatedActivity
.getQuantityString(R.plurals.contributions_subtitle, .getQuantityString(R.plurals.contributions_subtitle,
uploadCount, uploadCount)), uploadCount, uploadCount)),
t -> Timber.e(t, "Fetching upload count failed") t -> Timber.e(t, "Fetching upload count failed")
); ));
compositeDisposable.add(uploadCountDisposable);
} }
@Override @Override

View file

@ -10,7 +10,10 @@ import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.text.TextUtils; import android.text.TextUtils;
import fr.free.nrw.commons.CommonsApplication; import javax.inject.Inject;
import dagger.android.AndroidInjection;
import fr.free.nrw.commons.data.DBOpenHelper;
import timber.log.Timber; import timber.log.Timber;
import static android.content.UriMatcher.NO_MATCH; import static android.content.UriMatcher.NO_MATCH;
@ -36,9 +39,12 @@ public class ContributionsContentProvider extends ContentProvider {
return Uri.parse(BASE_URI.toString() + "/" + id); return Uri.parse(BASE_URI.toString() + "/" + id);
} }
@Inject DBOpenHelper dbOpenHelper;
@Override @Override
public boolean onCreate() { public boolean onCreate() {
return false; AndroidInjection.inject(this);
return true;
} }
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
@ -50,8 +56,7 @@ public class ContributionsContentProvider extends ContentProvider {
int uriType = uriMatcher.match(uri); int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext(); SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
SQLiteDatabase db = app.getDBOpenHelper().getReadableDatabase();
Cursor cursor; Cursor cursor;
switch (uriType) { switch (uriType) {
@ -87,9 +92,8 @@ public class ContributionsContentProvider extends ContentProvider {
@Override @Override
public Uri insert(@NonNull Uri uri, ContentValues contentValues) { public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri); int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext(); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase(); long id = 0;
long id;
switch (uriType) { switch (uriType) {
case CONTRIBUTIONS: case CONTRIBUTIONS:
id = sqlDB.insert(TABLE_NAME, null, contentValues); id = sqlDB.insert(TABLE_NAME, null, contentValues);
@ -107,13 +111,12 @@ public class ContributionsContentProvider extends ContentProvider {
int rows; int rows;
int uriType = uriMatcher.match(uri); int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext(); SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
switch (uriType) { switch (uriType) {
case CONTRIBUTIONS_ID: case CONTRIBUTIONS_ID:
Timber.d("Deleting contribution id %s", uri.getLastPathSegment()); Timber.d("Deleting contribution id %s", uri.getLastPathSegment());
rows = sqlDB.delete(TABLE_NAME, rows = db.delete(TABLE_NAME,
"_id = ?", "_id = ?",
new String[]{uri.getLastPathSegment()} new String[]{uri.getLastPathSegment()}
); );
@ -130,8 +133,7 @@ public class ContributionsContentProvider extends ContentProvider {
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) { public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (ContributionsContentProvider)"); Timber.d("Hello, bulk insert! (ContributionsContentProvider)");
int uriType = uriMatcher.match(uri); int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext(); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
sqlDB.beginTransaction(); sqlDB.beginTransaction();
switch (uriType) { switch (uriType) {
case CONTRIBUTIONS: case CONTRIBUTIONS:
@ -162,9 +164,8 @@ public class ContributionsContentProvider extends ContentProvider {
error out otherwise. error out otherwise.
*/ */
int uriType = uriMatcher.match(uri); int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext(); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase(); int rowsUpdated = 0;
int rowsUpdated;
switch (uriType) { switch (uriType) {
case CONTRIBUTIONS: case CONTRIBUTIONS:
rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, selection, selectionArgs); rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, selection, selectionArgs);

View file

@ -5,9 +5,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -22,9 +20,12 @@ import android.widget.ListAdapter;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication; import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.nearby.NearbyActivity; import fr.free.nrw.commons.nearby.NearbyActivity;
import timber.log.Timber; import timber.log.Timber;
@ -32,11 +33,10 @@ import timber.log.Timber;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
import static android.content.Context.MODE_PRIVATE;
import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.view.View.GONE; import static android.view.View.GONE;
public class ContributionsListFragment extends Fragment { public class ContributionsListFragment extends DaggerFragment {
@BindView(R.id.contributionsList) @BindView(R.id.contributionsList)
GridView contributionsList; GridView contributionsList;
@ -45,6 +45,9 @@ public class ContributionsListFragment extends Fragment {
@BindView(R.id.loadingContributionsProgressBar) @BindView(R.id.loadingContributionsProgressBar)
ProgressBar progressBar; ProgressBar progressBar;
@Inject @Named("prefs") SharedPreferences prefs;
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
private ContributionController controller; private ContributionController controller;
@Override @Override
@ -59,7 +62,6 @@ public class ContributionsListFragment extends Fragment {
} }
//TODO: Should this be in onResume? //TODO: Should this be in onResume?
SharedPreferences prefs = getActivity().getSharedPreferences("prefs", MODE_PRIVATE);
String lastModified = prefs.getString("lastSyncTimestamp", ""); String lastModified = prefs.getString("lastSyncTimestamp", "");
Timber.d("Last Sync Timestamp: %s", lastModified); Timber.d("Last Sync Timestamp: %s", lastModified);
@ -162,9 +164,7 @@ public class ContributionsListFragment extends Fragment {
return true; return true;
case R.id.menu_from_camera: case R.id.menu_from_camera:
SharedPreferences sharedPref = PreferenceManager boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true);
.getDefaultSharedPreferences(CommonsApplication.getInstance());
boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) {
// Here, thisActivity is the current activity // Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE) if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE)
@ -242,12 +242,17 @@ public class ContributionsListFragment extends Fragment {
menu.clear(); // See http://stackoverflow.com/a/8495697/17865 menu.clear(); // See http://stackoverflow.com/a/8495697/17865
inflater.inflate(R.menu.fragment_contributions_list, menu); inflater.inflate(R.menu.fragment_contributions_list, menu);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext(); if (!deviceHasCamera()) {
if (!app.deviceHasCamera()) {
menu.findItem(R.id.menu_from_camera).setEnabled(false); menu.findItem(R.id.menu_from_camera).setEnabled(false);
} }
} }
public boolean deviceHasCamera() {
PackageManager pm = getContext().getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) ||
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
}
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);

View file

@ -13,9 +13,15 @@ import android.os.RemoteException;
import android.text.TextUtils; import android.text.TextUtils;
import java.io.IOException; import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
@ -23,11 +29,11 @@ import fr.free.nrw.commons.mwapi.LogEventResult;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
import static android.content.Context.MODE_PRIVATE;
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED; import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
import static fr.free.nrw.commons.contributions.Contribution.Table.COLUMN_FILENAME; import static fr.free.nrw.commons.contributions.Contribution.Table.COLUMN_FILENAME;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
@SuppressWarnings("WeakerAccess")
public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
private static final String[] existsQuery = {COLUMN_FILENAME}; private static final String[] existsQuery = {COLUMN_FILENAME};
@ -35,6 +41,10 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
private static final ContentValues[] EMPTY = {}; private static final ContentValues[] EMPTY = {};
private static int COMMIT_THRESHOLD = 10; private static int COMMIT_THRESHOLD = 10;
@SuppressWarnings("WeakerAccess")
@Inject MediaWikiApi mwApi;
@Inject @Named("prefs") SharedPreferences prefs;
public ContributionsSyncAdapter(Context context, boolean autoInitialize) { public ContributionsSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize); super(context, autoInitialize);
} }
@ -71,10 +81,9 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
@Override @Override
public void onPerformSync(Account account, Bundle bundle, String authority, public void onPerformSync(Account account, Bundle bundle, String authority,
ContentProviderClient contentProviderClient, SyncResult syncResult) { ContentProviderClient contentProviderClient, SyncResult syncResult) {
((CommonsApplication) getContext().getApplicationContext()).injector().inject(this);
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you! // This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
String user = account.name; String user = account.name;
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
SharedPreferences prefs = getContext().getSharedPreferences("prefs", MODE_PRIVATE);
String lastModified = prefs.getString("lastSyncTimestamp", ""); String lastModified = prefs.getString("lastSyncTimestamp", "");
Date curTime = new Date(); Date curTime = new Date();
LogEventResult result; LogEventResult result;
@ -83,7 +92,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
while (!done) { while (!done) {
try { try {
result = api.logEvents(user, lastModified, queryContinue, getLimit()); result = mwApi.logEvents(user, lastModified, queryContinue, getLimit());
} catch (IOException e) { } catch (IOException e) {
// There isn't really much we can do, eh? // There isn't really much we can do, eh?
// FIXME: Perhaps add EventLogging? // FIXME: Perhaps add EventLogging?
@ -137,8 +146,13 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
done = true; done = true;
} }
} }
prefs.edit().putString("lastSyncTimestamp", Utils.toMWDate(curTime)).apply(); prefs.edit().putString("lastSyncTimestamp", toMWDate(curTime)).apply();
Timber.d("Oh hai, everyone! Look, a kitty!"); Timber.d("Oh hai, everyone! Look, a kitty!");
} }
private String toMWDate(Date date) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return isoFormat.format(date);
}
} }

View file

@ -1,16 +0,0 @@
package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
@Module
public abstract class ActivityBuilder {
@ContributesAndroidInjector()
abstract ContributionsActivity bindSplashScreenActivity();
@ContributesAndroidInjector()
abstract NearbyActivity bindNearbyActivity();
}

View file

@ -0,0 +1,46 @@
package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.AboutActivity;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
import fr.free.nrw.commons.upload.MultipleShareActivity;
import fr.free.nrw.commons.upload.ShareActivity;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract LoginActivity bindLoginActivity();
@ContributesAndroidInjector
abstract WelcomeActivity bindWelcomeActivity();
@ContributesAndroidInjector
abstract ShareActivity bindShareActivity();
@ContributesAndroidInjector
abstract MultipleShareActivity bindMultipleShareActivity();
@ContributesAndroidInjector
abstract ContributionsActivity bindContributionsActivity();
@ContributesAndroidInjector
abstract SettingsActivity bindSettingsActivity();
@ContributesAndroidInjector
abstract AboutActivity bindAboutActivity();
@ContributesAndroidInjector
abstract SignupActivity bindSignupActivity();
@ContributesAndroidInjector
abstract NearbyActivity bindNearbyActivity();
}

View file

@ -1,29 +0,0 @@
package fr.free.nrw.commons.di;
import android.app.Application;
import javax.inject.Singleton;
import dagger.BindsInstance;
import dagger.Component;
import dagger.android.support.AndroidSupportInjectionModule;
import fr.free.nrw.commons.CommonsApplication;
@Singleton
@Component(modules = {
AndroidSupportInjectionModule.class,
AppModule.class,
ActivityBuilder.class
})
public interface AppComponent {
@Component.Builder
interface Builder {
@BindsInstance
Builder application(Application application);
AppComponent build();
}
void inject(CommonsApplication application);
}

View file

@ -1,29 +0,0 @@
package fr.free.nrw.commons.di;
import android.app.Application;
import android.content.Context;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
@Module
public class AppModule {
@Provides
@Singleton
Context provideContext(Application application) {
return application;
}
@Provides
@Singleton
public MediaWikiApi getMWApi() {
return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
}
}

View file

@ -0,0 +1,40 @@
package fr.free.nrw.commons.di;
import javax.inject.Singleton;
import dagger.Component;
import dagger.android.AndroidInjectionModule;
import dagger.android.AndroidInjector;
import dagger.android.support.AndroidSupportInjectionModule;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
@Singleton
@Component(modules = {
CommonsApplicationModule.class,
AndroidInjectionModule.class,
AndroidSupportInjectionModule.class,
ActivityBuilderModule.class,
FragmentBuilderModule.class,
ServiceBuilderModule.class,
ContentProviderBuilderModule.class
})
public interface CommonsApplicationComponent extends AndroidInjector<CommonsApplication> {
void inject(CommonsApplication application);
void inject(ContributionsSyncAdapter syncAdapter);
void inject(ModificationsSyncAdapter syncAdapter);
void inject(MediaWikiImageView mediaWikiImageView);
@Component.Builder
@SuppressWarnings({"WeakerAccess", "unused"})
interface Builder {
Builder appModule(CommonsApplicationModule applicationModule);
CommonsApplicationComponent build();
}
}

View file

@ -0,0 +1,104 @@
package fr.free.nrw.commons.di;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v4.util.LruCache;
import javax.inject.Named;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.upload.UploadController;
import static android.content.Context.MODE_PRIVATE;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public class CommonsApplicationModule {
private CommonsApplication application;
public CommonsApplicationModule(CommonsApplication application) {
this.application = application;
}
@Provides
public AccountUtil providesAccountUtil() {
return new AccountUtil(application);
}
@Provides
@Named("application_preferences")
public SharedPreferences providesApplicationSharedPreferences() {
return application.getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
}
@Provides
@Named("default_preferences")
public SharedPreferences providesDefaultSharedPreferences() {
return PreferenceManager.getDefaultSharedPreferences(application);
}
@Provides
@Named("prefs")
public SharedPreferences providesOtherSharedPreferences() {
return application.getSharedPreferences("prefs", MODE_PRIVATE);
}
@Provides
public UploadController providesUploadController(SessionManager sessionManager, @Named("default_preferences") SharedPreferences sharedPreferences) {
return new UploadController(sessionManager, application, sharedPreferences);
}
@Provides
@Singleton
public SessionManager providesSessionManager(MediaWikiApi mediaWikiApi) {
return new SessionManager(application, mediaWikiApi);
}
@Provides
@Singleton
public MediaWikiApi provideMediaWikiApi() {
return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
}
@Provides
@Singleton
public LocationServiceManager provideLocationServiceManager() {
return new LocationServiceManager(application);
}
@Provides
@Singleton
public CacheController provideCacheController() {
return new CacheController();
}
@Provides
@Singleton
public DBOpenHelper provideDBOpenHelper() {
return new DBOpenHelper(application);
}
@Provides
@Singleton
public NearbyPlaces provideNearbyPlaces() {
return new NearbyPlaces();
}
@Provides
@Singleton
public LruCache<String, String> provideLruCache() {
return new LruCache<>(1024);
}
}

View file

@ -0,0 +1,22 @@
package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.category.CategoryContentProvider;
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class ContentProviderBuilderModule {
@ContributesAndroidInjector
abstract ContributionsContentProvider bindContributionsContentProvider();
@ContributesAndroidInjector
abstract ModificationsContentProvider bindModificationsContentProvider();
@ContributesAndroidInjector
abstract CategoryContentProvider bindCategoryContentProvider();
}

View file

@ -0,0 +1,46 @@
package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.contributions.ContributionsListFragment;
import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.nearby.NearbyListFragment;
import fr.free.nrw.commons.nearby.NoPermissionsFragment;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.upload.MultipleUploadListFragment;
import fr.free.nrw.commons.upload.SingleUploadFragment;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract CategorizationFragment bindCategorizationFragment();
@ContributesAndroidInjector
abstract ContributionsListFragment bindContributionsListFragment();
@ContributesAndroidInjector
abstract MediaDetailFragment bindMediaDetailFragment();
@ContributesAndroidInjector
abstract MediaDetailPagerFragment bindMediaDetailPagerFragment();
@ContributesAndroidInjector
abstract NearbyListFragment bindNearbyListFragment();
@ContributesAndroidInjector
abstract NoPermissionsFragment bindNoPermissionsFragment();
@ContributesAndroidInjector
abstract SettingsFragment bindSettingsFragment();
@ContributesAndroidInjector
abstract MultipleUploadListFragment bindMultipleUploadListFragment();
@ContributesAndroidInjector
abstract SingleUploadFragment bindSingleUploadFragment();
}

View file

@ -0,0 +1,18 @@
package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.auth.WikiAccountAuthenticatorService;
import fr.free.nrw.commons.upload.UploadService;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class ServiceBuilderModule {
@ContributesAndroidInjector
abstract UploadService bindUploadService();
@ContributesAndroidInjector
abstract WikiAccountAuthenticatorService bindWikiAccountAuthenticatorService();
}

View file

@ -1,5 +1,8 @@
package fr.free.nrw.commons.location; package fr.free.nrw.commons.location;
import android.location.Location;
import android.support.annotation.NonNull;
public class LatLng { public class LatLng {
private final double latitude; private final double latitude;
@ -22,6 +25,10 @@ public class LatLng {
this.accuracy = accuracy; this.accuracy = accuracy;
} }
public static LatLng from(@NonNull Location location) {
return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
}
public int hashCode() { public int hashCode() {
boolean var1 = true; boolean var1 = true;
byte var2 = 1; byte var2 = 1;

View file

@ -1,11 +1,15 @@
package fr.free.nrw.commons.location; package fr.free.nrw.commons.location;
import android.Manifest;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.location.Criteria; import android.content.pm.PackageManager;
import android.location.Location; import android.location.Location;
import android.location.LocationListener; import android.location.LocationListener;
import android.location.LocationManager; import android.location.LocationManager;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
@ -15,61 +19,136 @@ import javax.inject.Singleton;
import timber.log.Timber; import timber.log.Timber;
@Singleton
public class LocationServiceManager implements LocationListener { public class LocationServiceManager implements LocationListener {
public static final int LOCATION_REQUEST = 1;
private String provider; private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 2 * 60 * 1000;
private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 10;
private Context context;
private LocationManager locationManager; private LocationManager locationManager;
private LatLng lastLocation; private Location lastLocation;
private Float latestLocationAccuracy;
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>(); private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
private boolean isLocationManagerRegistered = false;
@Inject
public LocationServiceManager(Context context) { public LocationServiceManager(Context context) {
this.context = context;
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
provider = locationManager.getBestProvider(new Criteria(), true);
} }
public boolean isProviderEnabled() { public boolean isProviderEnabled() {
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
} }
public LatLng getLastLocation() { public boolean isLocationPermissionGranted() {
return lastLocation; return ContextCompat.checkSelfPermission(context,
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
} }
/** public void requestPermissions(Activity activity) {
* Returns the accuracy of the location. The measurement is if (activity.isFinishing()) {
* given as a radius in meter of 68 % confidence. return;
* }
* @return Float ActivityCompat.requestPermissions(activity,
*/ new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
public Float getLatestLocationAccuracy() { LOCATION_REQUEST);
return latestLocationAccuracy; }
public boolean isPermissionExplanationRequired(Activity activity) {
if (activity.isFinishing()) {
return false;
}
return ActivityCompat.shouldShowRequestPermissionRationale(activity,
Manifest.permission.ACCESS_FINE_LOCATION);
}
public LatLng getLastLocation() {
if (lastLocation == null) {
return null;
}
return LatLng.from(lastLocation);
} }
/** Registers a LocationManager to listen for current location. /** Registers a LocationManager to listen for current location.
*/ */
public void registerLocationManager() { public void registerLocationManager() {
if (!isLocationManagerRegistered)
isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
&& requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
}
private boolean requestLocationUpdatesFromProvider(String locationProvider) {
try { try {
locationManager.requestLocationUpdates(provider, 400, 1, this); locationManager.requestLocationUpdates(locationProvider,
Location location = locationManager.getLastKnownLocation(provider); MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS,
//Location works, just need to 'send' GPS coords MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS,
// via emulator extended controls if testing on emulator this);
Timber.d("Checking for location..."); return true;
if (location != null) {
this.onLocationChanged(location);
}
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Timber.e(e, "Illegal argument exception"); Timber.e(e, "Illegal argument exception");
return false;
} catch (SecurityException e) { } catch (SecurityException e) {
Timber.e(e, "Security exception"); Timber.e(e, "Security exception");
return false;
} }
} }
protected boolean isBetterLocation(Location location, Location currentBestLocation) {
if (currentBestLocation == null) {
// A new location is always better than no location
return true;
}
// Check whether the new location fix is newer or older
long timeDelta = location.getTime() - currentBestLocation.getTime();
boolean isSignificantlyNewer = timeDelta > MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS;
boolean isSignificantlyOlder = timeDelta < -MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS;
boolean isNewer = timeDelta > 0;
// If it's been more than two minutes since the current location, use the new location
// because the user has likely moved
if (isSignificantlyNewer) {
return true;
// If the new location is more than two minutes older, it must be worse
} else if (isSignificantlyOlder) {
return false;
}
// Check whether the new location fix is more or less accurate
int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
boolean isLessAccurate = accuracyDelta > 0;
boolean isMoreAccurate = accuracyDelta < 0;
boolean isSignificantlyLessAccurate = accuracyDelta > 200;
// Check if the old and new location are from the same provider
boolean isFromSameProvider = isSameProvider(location.getProvider(),
currentBestLocation.getProvider());
// Determine location quality using a combination of timeliness and accuracy
if (isMoreAccurate) {
return true;
} else if (isNewer && !isLessAccurate) {
return true;
} else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
return true;
}
return false;
}
/**
* Checks whether two providers are the same
*/
private boolean isSameProvider(String provider1, String provider2) {
if (provider1 == null) {
return provider2 == null;
}
return provider1.equals(provider2);
}
/** Unregisters location manager. /** Unregisters location manager.
*/ */
public void unregisterLocationManager() { public void unregisterLocationManager() {
isLocationManagerRegistered = false;
try { try {
locationManager.removeUpdates(this); locationManager.removeUpdates(this);
} catch (SecurityException e) { } catch (SecurityException e) {
@ -89,15 +168,11 @@ public class LocationServiceManager implements LocationListener {
@Override @Override
public void onLocationChanged(Location location) { public void onLocationChanged(Location location) {
double currentLatitude = location.getLatitude(); if (isBetterLocation(location, lastLocation)) {
double currentLongitude = location.getLongitude(); lastLocation = location;
latestLocationAccuracy = location.getAccuracy(); for (LocationUpdateListener listener : locationListeners) {
Timber.d("Latitude: %f Longitude: %f Accuracy %f", listener.onLocationChanged(LatLng.from(lastLocation));
currentLatitude, currentLongitude, latestLocationAccuracy); }
lastLocation = new LatLng(currentLatitude, currentLongitude, latestLocationAccuracy);
for (LocationUpdateListener listener : locationListeners) {
listener.onLocationChanged(lastLocation);
} }
} }

View file

@ -6,7 +6,6 @@ import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -22,6 +21,10 @@ import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.Locale; import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Provider;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.License; import fr.free.nrw.commons.License;
import fr.free.nrw.commons.LicenseList; import fr.free.nrw.commons.LicenseList;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
@ -30,10 +33,11 @@ import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.PageTitle; import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.ui.widget.CompatTextView; import fr.free.nrw.commons.ui.widget.CompatTextView;
import timber.log.Timber; import timber.log.Timber;
public class MediaDetailFragment extends Fragment { public class MediaDetailFragment extends DaggerFragment {
private boolean editable; private boolean editable;
private MediaDetailPagerFragment.MediaDetailProvider detailProvider; private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
@ -53,6 +57,9 @@ public class MediaDetailFragment extends Fragment {
return mf; return mf;
} }
@Inject
Provider<MediaDataExtractor> mediaDataExtractorProvider;
private MediaWikiImageView image; private MediaWikiImageView image;
private MediaDetailSpacer spacer; private MediaDetailSpacer spacer;
private int initialListTop = 0; private int initialListTop = 0;
@ -69,7 +76,7 @@ public class MediaDetailFragment extends Fragment {
private boolean categoriesPresent = false; private boolean categoriesPresent = false;
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once! private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
private ViewTreeObserver.OnScrollChangedListener scrollListener; private ViewTreeObserver.OnScrollChangedListener scrollListener;
DataSetObserver dataObserver; private DataSetObserver dataObserver;
private AsyncTask<Void,Void,Boolean> detailFetchTask; private AsyncTask<Void,Void,Boolean> detailFetchTask;
private LicenseList licenseList; private LicenseList licenseList;
@ -188,13 +195,13 @@ public class MediaDetailFragment extends Fragment {
@Override @Override
protected void onPreExecute() { protected void onPreExecute() {
extractor = new MediaDataExtractor(media.getFilename(), licenseList); extractor = mediaDataExtractorProvider.get();
} }
@Override @Override
protected Boolean doInBackground(Void... voids) { protected Boolean doInBackground(Void... voids) {
try { try {
extractor.fetch(); extractor.fetch(media.getFilename(), licenseList);
return Boolean.TRUE; return Boolean.TRUE;
} catch (IOException e) { } catch (IOException e) {
Timber.d(e); Timber.d(e);

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.media;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.DownloadManager; import android.app.DownloadManager;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.database.DataSetObserver; import android.database.DataSetObserver;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@ -24,12 +25,18 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.mwapi.EventLog; import fr.free.nrw.commons.mwapi.EventLog;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.content.Context.DOWNLOAD_SERVICE; import static android.content.Context.DOWNLOAD_SERVICE;
@ -37,7 +44,11 @@ import static android.content.Intent.ACTION_VIEW;
import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static fr.free.nrw.commons.CommonsApplication.EVENT_SHARE_ATTEMPT; import static fr.free.nrw.commons.CommonsApplication.EVENT_SHARE_ATTEMPT;
public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPageChangeListener { public class MediaDetailPagerFragment extends DaggerFragment implements ViewPager.OnPageChangeListener {
@Inject MediaWikiApi mwApi;
@Inject SessionManager sessionManager;
@Inject @Named("default_preferences") SharedPreferences prefs;
private ViewPager pager; private ViewPager pager;
private Boolean editable; private Boolean editable;
@ -101,8 +112,8 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
case R.id.menu_share_current_image: case R.id.menu_share_current_image:
// Share - this is just logs it, intent set in onCreateOptionsMenu, around line 252 // Share - this is just logs it, intent set in onCreateOptionsMenu, around line 252
CommonsApplication app = (CommonsApplication) getActivity().getApplication(); CommonsApplication app = (CommonsApplication) getActivity().getApplication();
EventLog.schema(EVENT_SHARE_ATTEMPT) EventLog.schema(EVENT_SHARE_ATTEMPT, mwApi, prefs)
.param("username", app.getCurrentAccount().name) .param("username", sessionManager.getCurrentAccount().name)
.param("filename", m.getFilename()) .param("filename", m.getFilename())
.log(); .log();
return true; return true;
@ -161,9 +172,7 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
req.allowScanningByMediaScanner(); req.allowScanningByMediaScanner();
req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !(ContextCompat.checkSelfPermission(getContext(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) {
&& !(ContextCompat.checkSelfPermission(getContext(),
READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) {
Snackbar.make(getView(), R.string.read_storage_permission_rationale, Snackbar.make(getView(), R.string.read_storage_permission_rationale,
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok, Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
view -> ActivityCompat.requestPermissions(getActivity(), view -> ActivityCompat.requestPermissions(getActivity(),

View file

@ -10,7 +10,10 @@ import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.text.TextUtils; import android.text.TextUtils;
import fr.free.nrw.commons.CommonsApplication; import javax.inject.Inject;
import dagger.android.AndroidInjection;
import fr.free.nrw.commons.data.DBOpenHelper;
import timber.log.Timber; import timber.log.Timber;
public class ModificationsContentProvider extends ContentProvider { public class ModificationsContentProvider extends ContentProvider {
@ -33,10 +36,12 @@ public class ModificationsContentProvider extends ContentProvider {
return Uri.parse(BASE_URI.toString() + "/" + id); return Uri.parse(BASE_URI.toString() + "/" + id);
} }
@Inject DBOpenHelper dbOpenHelper;
@Override @Override
public boolean onCreate() { public boolean onCreate() {
return false; AndroidInjection.inject(this);
return true;
} }
@Override @Override
@ -53,7 +58,7 @@ public class ModificationsContentProvider extends ContentProvider {
throw new IllegalArgumentException("Unknown URI" + uri); throw new IllegalArgumentException("Unknown URI" + uri);
} }
SQLiteDatabase db = CommonsApplication.getInstance().getDBOpenHelper().getReadableDatabase(); SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri); cursor.setNotificationUri(getContext().getContentResolver(), uri);
@ -69,7 +74,7 @@ public class ModificationsContentProvider extends ContentProvider {
@Override @Override
public Uri insert(@NonNull Uri uri, ContentValues contentValues) { public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri); int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase(); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id = 0; long id = 0;
switch (uriType) { switch (uriType) {
case MODIFICATIONS: case MODIFICATIONS:
@ -85,7 +90,7 @@ public class ModificationsContentProvider extends ContentProvider {
@Override @Override
public int delete(@NonNull Uri uri, String s, String[] strings) { public int delete(@NonNull Uri uri, String s, String[] strings) {
int uriType = uriMatcher.match(uri); int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase(); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
switch (uriType) { switch (uriType) {
case MODIFICATIONS_ID: case MODIFICATIONS_ID:
String id = uri.getLastPathSegment(); String id = uri.getLastPathSegment();
@ -103,7 +108,7 @@ public class ModificationsContentProvider extends ContentProvider {
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) { public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (ModificationsContentProvider)"); Timber.d("Hello, bulk insert! (ModificationsContentProvider)");
int uriType = uriMatcher.match(uri); int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase(); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
sqlDB.beginTransaction(); sqlDB.beginTransaction();
switch (uriType) { switch (uriType) {
case MODIFICATIONS: case MODIFICATIONS:
@ -131,7 +136,7 @@ public class ModificationsContentProvider extends ContentProvider {
In here, the only concat created argument is for id. It is cast to an int, and will error out otherwise. 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); int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase(); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated = 0; int rowsUpdated = 0;
switch (uriType) { switch (uriType) {
case MODIFICATIONS: case MODIFICATIONS:

View file

@ -14,8 +14,9 @@ import android.os.RemoteException;
import java.io.IOException; import java.io.IOException;
import javax.inject.Inject;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
@ -23,6 +24,8 @@ import timber.log.Timber;
public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
@Inject MediaWikiApi mwApi;
public ModificationsSyncAdapter(Context context, boolean autoInitialize) { public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize); super(context, autoInitialize);
} }
@ -30,6 +33,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
@Override @Override
public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) { 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! // This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
((CommonsApplication)getContext().getApplicationContext()).injector().inject(this);
Cursor allModifications; Cursor allModifications;
try { try {
@ -54,17 +58,16 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
return; return;
} }
if (Utils.isNullOrWhiteSpace(authCookie)) { if (isNullOrWhiteSpace(authCookie)) {
Timber.d("Could not authenticate :("); Timber.d("Could not authenticate :(");
return; return;
} }
MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); mwApi.setAuthCookie(authCookie);
api.setAuthCookie(authCookie);
String editToken; String editToken;
try { try {
editToken = api.getEditToken(); editToken = mwApi.getEditToken();
} catch (IOException e) { } catch (IOException e) {
Timber.d("Can not retreive edit token!"); Timber.d("Can not retreive edit token!");
return; return;
@ -95,7 +98,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
if (contrib.getState() == Contribution.STATE_COMPLETED) { if (contrib.getState() == Contribution.STATE_COMPLETED) {
String pageContent; String pageContent;
try { try {
pageContent = api.revisionsByFilename(contrib.getFilename()); pageContent = mwApi.revisionsByFilename(contrib.getFilename());
} catch (IOException e) { } catch (IOException e) {
Timber.d("Network fuckup on modifications sync!"); Timber.d("Network fuckup on modifications sync!");
continue; continue;
@ -106,7 +109,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
String editResult; String editResult;
try { try {
editResult = api.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary()); editResult = mwApi.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
} catch (IOException e) { } catch (IOException e) {
Timber.d("Network fuckup on modifications sync!"); Timber.d("Network fuckup on modifications sync!");
continue; continue;
@ -129,4 +132,8 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
} }
} }
} }
private boolean isNullOrWhiteSpace(String value) {
return value == null || value.trim().isEmpty();
}
} }

View file

@ -25,6 +25,8 @@ import org.mediawiki.api.MWApi;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
@ -34,7 +36,6 @@ import java.util.concurrent.Callable;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.PageTitle; import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.Utils;
import in.yuvi.http.fluent.Http; import in.yuvi.http.fluent.Http;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
@ -335,7 +336,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
logEvents.add(new LogEventResult.LogEvent( logEvents.add(new LogEventResult.LogEvent(
image.getString("@pageid"), image.getString("@pageid"),
image.getString("@title"), image.getString("@title"),
Utils.parseMWDate(image.getString("@timestamp"))) parseMWDate(image.getString("@timestamp")))
); );
} }
return logEvents; return logEvents;
@ -402,7 +403,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
String errorCode = result.getString("/api/error/@code"); String errorCode = result.getString("/api/error/@code");
return new UploadResult(resultStatus, errorCode); return new UploadResult(resultStatus, errorCode);
} else { } else {
Date dateUploaded = Utils.parseMWDate(result.getString("/api/upload/imageinfo/@timestamp")); Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename
String imageUrl = result.getString("/api/upload/imageinfo/@url"); String imageUrl = result.getString("/api/upload/imageinfo/@url");
return new UploadResult(resultStatus, dateUploaded, canonicalFilename, imageUrl); return new UploadResult(resultStatus, dateUploaded, canonicalFilename, imageUrl);
@ -428,4 +429,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return Integer.parseInt(uploadCount); return Integer.parseInt(uploadCount);
}); });
} }
private Date parseMWDate(String mwDate) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
try {
return isoFormat.parse(mwDate);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
} }

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.mwapi; package fr.free.nrw.commons.mwapi;
import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
@ -15,14 +16,14 @@ public class EventLog {
} }
} }
private static LogBuilder schema(String schema, long revision) { private static LogBuilder schema(String schema, long revision, MediaWikiApi mwApi, SharedPreferences prefs) {
return new LogBuilder(schema, revision); return new LogBuilder(schema, revision, mwApi, prefs);
} }
public static LogBuilder schema(Object[] scid) { public static LogBuilder schema(Object[] scid, MediaWikiApi mwApi, SharedPreferences prefs) {
if (scid.length != 2) { if (scid.length != 2) {
throw new IllegalArgumentException("Needs an object array with schema as first param and revision as second"); throw new IllegalArgumentException("Needs an object array with schema as first param and revision as second");
} }
return schema((String) scid[0], (Long) scid[1]); return schema((String) scid[0], (Long) scid[1], mwApi, prefs);
} }
} }

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.mwapi;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.preference.PreferenceManager;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@ -12,19 +11,23 @@ import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
@SuppressWarnings("WeakerAccess")
public class LogBuilder { public class LogBuilder {
private JSONObject data; private final MediaWikiApi mwApi;
private long rev; private final JSONObject data;
private String schema; private final long rev;
private final String schema;
private final SharedPreferences prefs;
LogBuilder(String schema, long revision) { LogBuilder(String schema, long revision, MediaWikiApi mwApi, SharedPreferences prefs) {
data = new JSONObject(); this.prefs = prefs;
this.data = new JSONObject();
this.schema = schema; this.schema = schema;
this.rev = revision; this.rev = revision;
this.mwApi = mwApi;
} }
public LogBuilder param(String key, Object value) { public LogBuilder param(String key, Object value) {
@ -56,11 +59,10 @@ public class LogBuilder {
// Use *only* for tracking the user preference change for EventLogging // Use *only* for tracking the user preference change for EventLogging
// Attempting to use anywhere else will cause kitten explosions // Attempting to use anywhere else will cause kitten explosions
public void log(boolean force) { public void log(boolean force) {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(CommonsApplication.getInstance()); if (!prefs.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
if (!settings.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
return; // User has disabled tracking return; // User has disabled tracking
} }
LogTask logTask = new LogTask(); LogTask logTask = new LogTask(mwApi);
logTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this); logTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this);
} }

View file

@ -2,11 +2,16 @@ package fr.free.nrw.commons.mwapi;
import android.os.AsyncTask; import android.os.AsyncTask;
import fr.free.nrw.commons.CommonsApplication;
class LogTask extends AsyncTask<LogBuilder, Void, Boolean> { class LogTask extends AsyncTask<LogBuilder, Void, Boolean> {
private final MediaWikiApi mwApi;
public LogTask(MediaWikiApi mwApi) {
this.mwApi = mwApi;
}
@Override @Override
protected Boolean doInBackground(LogBuilder... logBuilders) { protected Boolean doInBackground(LogBuilder... logBuilders) {
return CommonsApplication.getInstance().getMWApi().logEvents(logBuilders); return mwApi.logEvents(logBuilders);
} }
} }

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons.nearby; package fr.free.nrw.commons.nearby;
import android.Manifest;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -10,10 +9,8 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -31,8 +28,6 @@ import javax.inject.Inject;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import dagger.android.AndroidInjection;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.location.LocationServiceManager;
@ -46,16 +41,22 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; import timber.log.Timber;
import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUEST;
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener { public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener {
@BindView(R.id.progressBar)
ProgressBar progressBar;
private static final int LOCATION_REQUEST = 1; private static final int LOCATION_REQUEST = 1;
private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed"; private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed";
@BindView(R.id.progressBar)
ProgressBar progressBar;
@Inject @Inject
LocationServiceManager locationManager; LocationServiceManager locationManager;
@Inject
NearbyController nearbyController;
private LatLng curLatLang; private LatLng curLatLang;
private Bundle bundle; private Bundle bundle;
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
@ -66,11 +67,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
AndroidInjection.inject(this);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
setContentView(R.layout.activity_nearby); setContentView(R.layout.activity_nearby);
ButterKnife.bind(this); ButterKnife.bind(this);
checkLocationPermission();
bundle = new Bundle(); bundle = new Bundle();
initDrawer(); initDrawer();
initViewState(); initViewState();
@ -102,7 +101,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
// Handle item selection // Handle item selection
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.action_refresh: case R.id.action_refresh:
lockNearbyView = false; lockNearbyView(false);
refreshView(true); refreshView(true);
return true; return true;
case R.id.action_toggle_view: case R.id.action_toggle_view:
@ -115,52 +114,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
} }
} }
private void checkLocationPermission() { private void requestLocationPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!isFinishing()) {
if (ContextCompat.checkSelfPermission(this, locationManager.requestPermissions(this);
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
refreshView(false);
} else {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.ACCESS_FINE_LOCATION)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
new AlertDialog.Builder(this)
.setMessage(getString(R.string.location_permission_rationale))
.setPositiveButton("OK", (dialog, which) -> {
ActivityCompat.requestPermissions(NearbyActivity.this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
LOCATION_REQUEST);
dialog.dismiss();
})
.setNegativeButton("Cancel", null)
.create()
.show();
} else {
// No explanation needed, we can request the permission.
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
LOCATION_REQUEST);
// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
// app-defined int constant. The callback method gets the
// result of the request.
}
}
}
} else {
refreshView(false);
} }
} }
@ -185,7 +141,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
.setCancelable(false) .setCancelable(false)
.setPositiveButton(R.string.give_permission, (dialog, which) -> { .setPositiveButton(R.string.give_permission, (dialog, which) -> {
//will ask for the location permission again //will ask for the location permission again
checkLocationPermission(); checkGps();
}) })
.setNegativeButton(R.string.cancel, (dialog, which) -> { .setNegativeButton(R.string.cancel, (dialog, which) -> {
//dismiss dialog and finish activity //dismiss dialog and finish activity
@ -209,11 +165,48 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
Timber.d("Loaded settings page"); Timber.d("Loaded settings page");
startActivityForResult(callGPSSettingIntent, 1); startActivityForResult(callGPSSettingIntent, 1);
}) })
.setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> dialog.cancel()) .setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> {
showLocationPermissionDeniedErrorDialog();
dialog.cancel();
})
.create() .create()
.show(); .show();
} else { } else {
Timber.d("GPS is enabled"); Timber.d("GPS is enabled");
checkLocationPermission();
}
}
private void checkLocationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (locationManager.isLocationPermissionGranted()) {
refreshView(false);
} else {
// Should we show an explanation?
if (locationManager.isPermissionExplanationRequired(this)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
new AlertDialog.Builder(this)
.setMessage(getString(R.string.location_permission_rationale_nearby))
.setPositiveButton("OK", (dialog, which) -> {
requestLocationPermissions();
dialog.dismiss();
})
.setNegativeButton("Cancel", (dialog, id) -> {
showLocationPermissionDeniedErrorDialog();
dialog.cancel();
})
.create()
.show();
} else {
// No explanation needed, we can request the permission.
requestLocationPermissions();
}
}
} else {
refreshView(false);
} }
} }
@ -238,7 +231,6 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
locationManager.registerLocationManager();
locationManager.addLocationListener(this); locationManager.addLocationListener(this);
} }
@ -262,13 +254,18 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
super.onResume(); super.onResume();
lockNearbyView = false; lockNearbyView = false;
checkGps(); checkGps();
refreshView(false);
} }
/**
* This method should be the single point to load/refresh nearby places
*
* @param isHardRefresh
*/
private void refreshView(boolean isHardRefresh) { private void refreshView(boolean isHardRefresh) {
if (lockNearbyView) { if (lockNearbyView) {
return; return;
} }
locationManager.registerLocationManager();
LatLng lastLocation = locationManager.getLastLocation(); LatLng lastLocation = locationManager.getLastLocation();
if (curLatLang != null && curLatLang.equals(lastLocation)) { //refresh view only if location has changed if (curLatLang != null && curLatLang.equals(lastLocation)) { //refresh view only if location has changed
if (isHardRefresh) { if (isHardRefresh) {
@ -284,20 +281,14 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
} }
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
setupPlaceList(this); placesDisposable = Observable.fromCallable(() -> nearbyController
} .loadAttractionsFromLocation(curLatLang, this))
private void setupPlaceList(Context context) {
placesDisposable = Observable.fromCallable(() -> NearbyController
.loadAttractionsFromLocation(curLatLang, CommonsApplication.getInstance()))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe((result) -> { .subscribe(this::populatePlaces);
populatePlaces(context, result);
});
} }
private void populatePlaces(Context context, List<Place> placeList) { private void populatePlaces(List<Place> placeList) {
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriSerializer()) .registerTypeAdapter(Uri.class, new UriSerializer())
.create(); .create();
@ -306,7 +297,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
if (placeList.size() == 0) { if (placeList.size() == 0) {
int duration = Toast.LENGTH_SHORT; int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(context, R.string.no_nearby, duration); Toast toast = Toast.makeText(this, R.string.no_nearby, duration);
toast.show(); toast.show();
} }
@ -314,7 +305,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
bundle.putString("PlaceList", gsonPlaceList); bundle.putString("PlaceList", gsonPlaceList);
bundle.putString("CurLatLng", gsonCurLatLng); bundle.putString("CurLatLng", gsonCurLatLng);
lockNearbyView = true; lockNearbyView(true);
// Begin the transaction // Begin the transaction
if (viewMode.isMap()) { if (viewMode.isMap()) {
setMapFragment(); setMapFragment();
@ -325,6 +316,18 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
hideProgressBar(); hideProgressBar();
} }
private void lockNearbyView(boolean lock) {
if (lock) {
lockNearbyView = true;
locationManager.unregisterLocationManager();
locationManager.removeLocationListener(this);
} else {
lockNearbyView = false;
locationManager.registerLocationManager();
locationManager.addLocationListener(this);
}
}
private void hideProgressBar() { private void hideProgressBar() {
if (progressBar != null) { if (progressBar != null) {
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
@ -338,7 +341,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
Fragment fragment = new NearbyMapFragment(); Fragment fragment = new NearbyMapFragment();
fragment.setArguments(bundle); fragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container, fragment); fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName());
fragmentTransaction.commitAllowingStateLoss(); fragmentTransaction.commitAllowingStateLoss();
} }
@ -349,15 +352,10 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
Fragment fragment = new NearbyListFragment(); Fragment fragment = new NearbyListFragment();
fragment.setArguments(bundle); fragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container, fragment); fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName());
fragmentTransaction.commitAllowingStateLoss(); fragmentTransaction.commitAllowingStateLoss();
} }
public static void startYourself(Context context) {
Intent settingsIntent = new Intent(context, NearbyActivity.class);
context.startActivity(settingsIntent);
}
@Override @Override
public void onLocationChanged(LatLng latLng) { public void onLocationChanged(LatLng latLng) {
refreshView(false); refreshView(false);

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.nearby;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.preference.PreferenceManager;
import android.support.graphics.drawable.VectorDrawableCompat; import android.support.graphics.drawable.VectorDrawableCompat;
import com.mapbox.mapboxsdk.annotations.IconFactory; import com.mapbox.mapboxsdk.annotations.IconFactory;
@ -15,7 +14,9 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import fr.free.nrw.commons.CommonsApplication; import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.UiUtils; import fr.free.nrw.commons.utils.UiUtils;
@ -24,9 +25,17 @@ import timber.log.Timber;
import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween; import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
public class NearbyController { public class NearbyController {
private static final int MAX_RESULTS = 1000; private static final int MAX_RESULTS = 1000;
private final NearbyPlaces nearbyPlaces;
private final SharedPreferences prefs;
@Inject
public NearbyController(NearbyPlaces nearbyPlaces,
@Named("default_preferences") SharedPreferences prefs) {
this.nearbyPlaces = nearbyPlaces;
this.prefs = prefs;
}
/** /**
* Prepares Place list to make their distance information update later. * Prepares Place list to make their distance information update later.
@ -34,13 +43,11 @@ public class NearbyController {
* @param context context * @param context context
* @return Place list without distance information * @return Place list without distance information
*/ */
public static List<Place> loadAttractionsFromLocation(LatLng curLatLng, Context context) { public List<Place> loadAttractionsFromLocation(LatLng curLatLng, Context context) {
Timber.d("Loading attractions near %s", curLatLng); Timber.d("Loading attractions near %s", curLatLng);
if (curLatLng == null) { if (curLatLng == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
NearbyPlaces nearbyPlaces = CommonsApplication.getInstance().getNearbyPlaces();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
List<Place> places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage()); List<Place> places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage());
if (curLatLng != null) { if (curLatLng != null) {
Timber.d("Sorting places by distance..."); Timber.d("Sorting places by distance...");

View file

@ -2,7 +2,6 @@ package fr.free.nrw.commons.nearby;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -17,12 +16,13 @@ import java.lang.reflect.Type;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.UriDeserializer; import fr.free.nrw.commons.utils.UriDeserializer;
import timber.log.Timber; import timber.log.Timber;
public class NearbyListFragment extends Fragment { public class NearbyListFragment extends DaggerFragment {
private static final Type LIST_TYPE = new TypeToken<List<Place>>() { private static final Type LIST_TYPE = new TypeToken<List<Place>>() {
}.getType(); }.getType();
private static final Type CUR_LAT_LNG_TYPE = new TypeToken<LatLng>() { private static final Type CUR_LAT_LNG_TYPE = new TypeToken<LatLng>() {

View file

@ -1,19 +1,19 @@
package fr.free.nrw.commons.nearby; package fr.free.nrw.commons.nearby;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import timber.log.Timber; import timber.log.Timber;
/** /**
* Tells user that Nearby Places cannot be displayed if location permissions are denied * Tells user that Nearby Places cannot be displayed if location permissions are denied
*/ */
public class NoPermissionsFragment extends Fragment { public class NoPermissionsFragment extends DaggerFragment {
public NoPermissionsFragment() { public NoPermissionsFragment() {
} }

View file

@ -1,7 +1,5 @@
package fr.free.nrw.commons.settings; package fr.free.nrw.commons.settings;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatDelegate; import android.support.v7.app.AppCompatDelegate;

View file

@ -15,15 +15,17 @@ import android.preference.EditTextPreference;
import android.preference.ListPreference; import android.preference.ListPreference;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider; import android.support.v4.content.FileProvider;
import android.widget.Toast; import android.widget.Toast;
import java.io.File; import java.io.File;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.android.AndroidInjection;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
@ -34,8 +36,11 @@ public class SettingsFragment extends PreferenceFragment {
private static final int REQUEST_CODE_WRITE_EXTERNAL_STORAGE = 100; private static final int REQUEST_CODE_WRITE_EXTERNAL_STORAGE = 100;
@Inject @Named("default_preferences") SharedPreferences prefs;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
AndroidInjection.inject(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Load the preferences from an XML resource // Load the preferences from an XML resource
@ -58,14 +63,12 @@ public class SettingsFragment extends PreferenceFragment {
}); });
final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads"); final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads");
final SharedPreferences sharedPref = PreferenceManager int uploads = prefs.getInt(Prefs.UPLOADS_SHOWING, 100);
.getDefaultSharedPreferences(CommonsApplication.getInstance());
int uploads = sharedPref.getInt(Prefs.UPLOADS_SHOWING, 100);
uploadLimit.setText(uploads + ""); uploadLimit.setText(uploads + "");
uploadLimit.setSummary(uploads + ""); uploadLimit.setSummary(uploads + "");
uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> { uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> {
int value = Integer.parseInt(newValue.toString()); int value = Integer.parseInt(newValue.toString());
final SharedPreferences.Editor editor = sharedPref.edit(); final SharedPreferences.Editor editor = prefs.edit();
if (value > 500) { if (value > 500) {
new AlertDialog.Builder(getActivity()) new AlertDialog.Builder(getActivity())
.setTitle(R.string.maximum_limit) .setTitle(R.string.maximum_limit)

View file

@ -3,18 +3,17 @@ package fr.free.nrw.commons.theme;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
import dagger.android.support.DaggerAppCompatActivity;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
public class BaseActivity extends AppCompatActivity { public abstract class BaseActivity extends DaggerAppCompatActivity {
boolean currentTheme; boolean currentTheme;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
if (Utils.isDarkTheme(this)) { boolean currentThemeIsDark = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false);
if (currentThemeIsDark){
currentTheme = true; currentTheme = true;
setTheme(R.style.DarkAppTheme); setTheme(R.style.DarkAppTheme);
} else { } else {
@ -27,7 +26,7 @@ public class BaseActivity extends AppCompatActivity {
@Override @Override
protected void onResume() { protected void onResume() {
// Restart activity if theme is changed // Restart activity if theme is changed
boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false); boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false);
if (currentTheme != newTheme) { //is activity theme changed if (currentTheme != newTheme) { //is activity theme changed
Intent intent = getIntent(); Intent intent = getIntent();
finish(); finish();

View file

@ -62,10 +62,10 @@ public abstract class NavigationBaseActivity extends BaseActivity
private void setUserName() { private void setUserName() {
View navHeaderView = navigationView.getHeaderView(0); View navHeaderView = navigationView.getHeaderView(0);
TextView username = (TextView) navHeaderView.findViewById(R.id.username); TextView username = navHeaderView.findViewById(R.id.username);
AccountManager accountManager = AccountManager.get(this); AccountManager accountManager = AccountManager.get(this);
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType()); Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.ACCOUNT_TYPE);
if (allAccounts.length != 0) { if (allAccounts.length != 0) {
username.setText(allAccounts[0].name); username.setText(allAccounts[0].name);
} }

View file

@ -1,12 +1,13 @@
package fr.free.nrw.commons.ui.widget; package fr.free.nrw.commons.ui.widget;
import android.content.Context; import android.content.Context;
import android.os.Build;
import android.support.v7.widget.AppCompatTextView; import android.support.v7.widget.AppCompatTextView;
import android.text.Html;
import android.text.Spanned;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.util.AttributeSet; import android.util.AttributeSet;
import fr.free.nrw.commons.Utils;
/** /**
* An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any * An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any
* links clickable. * links clickable.
@ -17,10 +18,25 @@ public class HtmlTextView extends AppCompatTextView {
super(context, attrs); super(context, attrs);
setMovementMethod(LinkMovementMethod.getInstance()); setMovementMethod(LinkMovementMethod.getInstance());
setText(Utils.fromHtml(getText().toString())); setText(fromHtml(getText().toString()));
} }
public void setHtmlText(String newText) { public void setHtmlText(String newText) {
setText(Utils.fromHtml(newText)); setText(fromHtml(newText));
}
/**
* Fix Html.fromHtml is deprecated problem
*
* @param source provided Html string
* @return returned Spanned of appropriate method according to version check
*/
private static Spanned fromHtml(String source) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY);
} else {
//noinspection deprecation
return Html.fromHtml(source);
}
} }
} }

View file

@ -7,7 +7,6 @@ import android.support.v7.app.AlertDialog;
import java.io.IOException; import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
@ -18,6 +17,7 @@ import timber.log.Timber;
* Displays a warning to the user if the file already exists on Commons * Displays a warning to the user if the file already exists on Commons
*/ */
public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> { public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
interface Callback { interface Callback {
void onResult(Result result); void onResult(Result result);
} }
@ -28,14 +28,16 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
DUPLICATE_CANCELLED DUPLICATE_CANCELLED
} }
private final MediaWikiApi api;
private final String fileSha1; private final String fileSha1;
private final Context context; private final Context context;
private final Callback callback; private final Callback callback;
public ExistingFileAsync(String fileSha1, Context context, Callback callback) { public ExistingFileAsync(String fileSha1, Context context, Callback callback, MediaWikiApi mwApi) {
this.fileSha1 = fileSha1; this.fileSha1 = fileSha1;
this.context = context; this.context = context;
this.callback = callback; this.callback = callback;
this.api = mwApi;
} }
@Override @Override
@ -45,7 +47,6 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
@Override @Override
protected Boolean doInBackground(Void... voids) { protected Boolean doInBackground(Void... voids) {
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
// https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba // https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba
boolean fileExists; boolean fileExists;

View file

@ -8,7 +8,6 @@ import android.location.LocationListener;
import android.location.LocationManager; import android.location.LocationManager;
import android.media.ExifInterface; import android.media.ExifInterface;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi; import android.support.annotation.RequiresApi;
@ -16,7 +15,6 @@ import android.support.annotation.RequiresApi;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -26,6 +24,8 @@ import timber.log.Timber;
*/ */
public class GPSExtractor { public class GPSExtractor {
private final Context context;
private SharedPreferences prefs;
private ExifInterface exif; private ExifInterface exif;
private double decLatitude; private double decLatitude;
private double decLongitude; private double decLongitude;
@ -38,9 +38,12 @@ public class GPSExtractor {
/** /**
* Construct from the file descriptor of the image (only for API 24 or newer). * Construct from the file descriptor of the image (only for API 24 or newer).
* @param fileDescriptor the file descriptor of the image * @param fileDescriptor the file descriptor of the image
* @param context the context
*/ */
@RequiresApi(24) @RequiresApi(24)
public GPSExtractor(@NonNull FileDescriptor fileDescriptor) { public GPSExtractor(@NonNull FileDescriptor fileDescriptor, Context context, SharedPreferences prefs) {
this.context = context;
this.prefs = prefs;
try { try {
exif = new ExifInterface(fileDescriptor); exif = new ExifInterface(fileDescriptor);
} catch (IOException | IllegalArgumentException e) { } catch (IOException | IllegalArgumentException e) {
@ -51,13 +54,16 @@ public class GPSExtractor {
/** /**
* Construct from the file path of the image. * Construct from the file path of the image.
* @param path file path of the image * @param path file path of the image
* @param context the context
*/ */
public GPSExtractor(@NonNull String path) { public GPSExtractor(@NonNull String path, Context context, SharedPreferences prefs) {
this.prefs = prefs;
try { try {
exif = new ExifInterface(path); exif = new ExifInterface(path);
} catch (IOException | IllegalArgumentException e) { } catch (IOException | IllegalArgumentException e) {
Timber.w(e); Timber.w(e);
} }
this.context = context;
} }
/** /**
@ -65,9 +71,7 @@ public class GPSExtractor {
* @return true if enabled, false if disabled * @return true if enabled, false if disabled
*/ */
private boolean gpsPreferenceEnabled() { private boolean gpsPreferenceEnabled() {
SharedPreferences sharedPref boolean gpsPref = prefs.getBoolean("allowGps", false);
= PreferenceManager.getDefaultSharedPreferences(CommonsApplication.getInstance());
boolean gpsPref = sharedPref.getBoolean("allowGps", false);
Timber.d("Gps pref set to: %b", gpsPref); Timber.d("Gps pref set to: %b", gpsPref);
return gpsPref; return gpsPref;
} }
@ -76,8 +80,7 @@ public class GPSExtractor {
* Registers a LocationManager to listen for current location * Registers a LocationManager to listen for current location
*/ */
protected void registerLocationManager() { protected void registerLocationManager() {
locationManager = (LocationManager) CommonsApplication.getInstance() locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
.getSystemService(Context.LOCATION_SERVICE);
Criteria criteria = new Criteria(); Criteria criteria = new Criteria();
String provider = locationManager.getBestProvider(criteria, true); String provider = locationManager.getBestProvider(criteria, true);
myLocationListener = new MyLocationListener(); myLocationListener = new MyLocationListener();

View file

@ -6,6 +6,7 @@ import android.content.ContentProviderClient;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.database.DataSetObserver; import android.database.DataSetObserver;
import android.net.Uri; import android.net.Uri;
@ -24,11 +25,15 @@ import android.widget.Toast;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity; import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.category.OnCategoriesSaveHandler; import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
@ -38,24 +43,27 @@ import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.modifications.TemplateRemoveModifier; import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.EventLog; import fr.free.nrw.commons.mwapi.EventLog;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
public class MultipleShareActivity public class MultipleShareActivity extends AuthenticatedActivity
extends AuthenticatedActivity implements MediaDetailPagerFragment.MediaDetailProvider,
implements MediaDetailPagerFragment.MediaDetailProvider, AdapterView.OnItemClickListener,
AdapterView.OnItemClickListener, FragmentManager.OnBackStackChangedListener,
FragmentManager.OnBackStackChangedListener, MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
OnCategoriesSaveHandler { OnCategoriesSaveHandler {
private CommonsApplication app;
@Inject MediaWikiApi mwApi;
@Inject SessionManager sessionManager;
@Inject UploadController uploadController;
@Inject @Named("default_preferences") SharedPreferences prefs;
private ArrayList<Contribution> photosList = null; private ArrayList<Contribution> photosList = null;
private MultipleUploadListFragment uploadsList; private MultipleUploadListFragment uploadsList;
private MediaDetailPagerFragment mediaDetails; private MediaDetailPagerFragment mediaDetails;
private CategorizationFragment categorizationFragment; private CategorizationFragment categorizationFragment;
private UploadController uploadController;
@Override @Override
public Media getMediaAtPosition(int i) { public Media getMediaAtPosition(int i) {
return photosList.get(i); return photosList.get(i);
@ -132,11 +140,7 @@ public class MultipleShareActivity
dialog.setProgress(uploadCount); dialog.setProgress(uploadCount);
if (uploadCount == photosList.size()) { if (uploadCount == photosList.size()) {
dialog.dismiss(); dialog.dismiss();
Toast startingToast = Toast.makeText( Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG);
CommonsApplication.getInstance(),
R.string.uploading_started,
Toast.LENGTH_LONG
);
startingToast.show(); startingToast.show();
} }
}); });
@ -163,8 +167,8 @@ public class MultipleShareActivity
@Override @Override
public void onCategoriesSave(List<String> categories) { public void onCategoriesSave(List<String> categories) {
if (categories.size() > 0) { if (categories.size() > 0) {
ContentProviderClient client = getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY); ContentProviderClient client = getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY);
for (Contribution contribution: photosList) { for (Contribution contribution : photosList) {
ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri()); ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{}))); categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
@ -176,9 +180,9 @@ public class MultipleShareActivity
} }
// FIXME: Make sure that the content provider is up // 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 // 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! ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT) EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT, mwApi, prefs)
.param("username", app.getCurrentAccount().name) .param("username", sessionManager.getCurrentAccount().name)
.param("categories-count", categories.size()) .param("categories-count", categories.size())
.param("files-count", photosList.size()) .param("files-count", photosList.size())
.param("source", Contribution.SOURCE_EXTERNAL) .param("source", Contribution.SOURCE_EXTERNAL)
@ -202,10 +206,8 @@ public class MultipleShareActivity
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
uploadController = new UploadController();
setContentView(R.layout.activity_multiple_uploads); setContentView(R.layout.activity_multiple_uploads);
app = CommonsApplication.getInstance();
ButterKnife.bind(this); ButterKnife.bind(this);
initDrawer(); initDrawer();
@ -245,7 +247,7 @@ public class MultipleShareActivity
@Override @Override
protected void onAuthCookieAcquired(String authCookie) { protected void onAuthCookieAcquired(String authCookie) {
app.getMWApi().setAuthCookie(authCookie); mwApi.setAuthCookie(authCookie);
Intent intent = getIntent(); Intent intent = getIntent();
if (intent.getAction().equals(Intent.ACTION_SEND_MULTIPLE)) { if (intent.getAction().equals(Intent.ACTION_SEND_MULTIPLE)) {
@ -266,7 +268,7 @@ public class MultipleShareActivity
uploadsList = (MultipleUploadListFragment) getSupportFragmentManager().findFragmentByTag("uploadsList"); uploadsList = (MultipleUploadListFragment) getSupportFragmentManager().findFragmentByTag("uploadsList");
if (uploadsList == null) { if (uploadsList == null) {
uploadsList = new MultipleUploadListFragment(); uploadsList = new MultipleUploadListFragment();
getSupportFragmentManager() getSupportFragmentManager()
.beginTransaction() .beginTransaction()
.add(R.id.uploadsFragmentContainer, uploadsList, "uploadsList") .add(R.id.uploadsFragmentContainer, uploadsList, "uploadsList")
@ -288,16 +290,16 @@ public class MultipleShareActivity
public void onBackPressed() { public void onBackPressed() {
super.onBackPressed(); super.onBackPressed();
if (categorizationFragment != null && categorizationFragment.isVisible()) { if (categorizationFragment != null && categorizationFragment.isVisible()) {
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT) EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT, mwApi, prefs)
.param("username", app.getCurrentAccount().name) .param("username", sessionManager.getCurrentAccount().name)
.param("categories-count", categorizationFragment.getCurrentSelectedCount()) .param("categories-count", categorizationFragment.getCurrentSelectedCount())
.param("files-count", photosList.size()) .param("files-count", photosList.size())
.param("source", Contribution.SOURCE_EXTERNAL) .param("source", Contribution.SOURCE_EXTERNAL)
.param("result", "cancelled") .param("result", "cancelled")
.log(); .log();
} else { } else {
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT) EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT, mwApi, prefs)
.param("username", app.getCurrentAccount().name) .param("username", sessionManager.getCurrentAccount().name)
.param("source", getIntent().getStringExtra(UploadService.EXTRA_SOURCE)) .param("source", getIntent().getStringExtra(UploadService.EXTRA_SOURCE))
.param("multiple", true) .param("multiple", true)
.param("result", "cancelled") .param("result", "cancelled")
@ -307,11 +309,7 @@ public class MultipleShareActivity
@Override @Override
public void onBackStackChanged() { public void onBackStackChanged() {
if (mediaDetails != null && mediaDetails.isVisible()) { getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible()) ;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
} else {
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
} }
} }

View file

@ -5,7 +5,6 @@ import android.graphics.Point;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.graphics.drawable.VectorDrawableCompat; import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.app.Fragment;
import android.text.Editable; import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
@ -28,11 +27,12 @@ import android.widget.TextView;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.view.SimpleDraweeView; import com.facebook.drawee.view.SimpleDraweeView;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment;
public class MultipleUploadListFragment extends Fragment { public class MultipleUploadListFragment extends DaggerFragment {
public interface OnMultipleUploadInitiatedHandler { public interface OnMultipleUploadInitiatedHandler {
void OnMultipleUploadInitiated(); void OnMultipleUploadInitiated();

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.content.Context;
import android.net.Uri; import android.net.Uri;
import com.android.volley.Cache; import com.android.volley.Cache;
@ -20,7 +21,6 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import fr.free.nrw.commons.CommonsApplication;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -33,12 +33,14 @@ public class MwVolleyApi {
private static RequestQueue REQUEST_QUEUE; private static RequestQueue REQUEST_QUEUE;
private static final Gson GSON = new GsonBuilder().create(); private static final Gson GSON = new GsonBuilder().create();
protected static Set<String> categorySet; private static Set<String> categorySet;
private static List<String> categoryList; private static List<String> categoryList;
private static final String MWURL = "https://commons.wikimedia.org/"; private static final String MWURL = "https://commons.wikimedia.org/";
private final Context context;
public MwVolleyApi() { public MwVolleyApi(Context context) {
this.context = context;
categorySet = new HashSet<>(); categorySet = new HashSet<>();
} }
@ -93,7 +95,7 @@ public class MwVolleyApi {
private synchronized RequestQueue getQueue() { private synchronized RequestQueue getQueue() {
if (REQUEST_QUEUE == null) { if (REQUEST_QUEUE == null) {
REQUEST_QUEUE = Volley.newRequestQueue(CommonsApplication.getInstance()); REQUEST_QUEUE = Volley.newRequestQueue(context);
} }
return REQUEST_QUEUE; return REQUEST_QUEUE;
} }

View file

@ -30,14 +30,21 @@ import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.AuthenticatedActivity; import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.category.OnCategoriesSaveHandler; import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
@ -46,6 +53,7 @@ import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.modifications.TemplateRemoveModifier; import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.EventLog; import fr.free.nrw.commons.mwapi.EventLog;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber; import timber.log.Timber;
import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.DUPLICATE_PROCEED; import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.DUPLICATE_PROCEED;
@ -66,7 +74,11 @@ public class ShareActivity
private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4; private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4;
private CategorizationFragment categorizationFragment; private CategorizationFragment categorizationFragment;
private CommonsApplication app; @Inject MediaWikiApi mwApi;
@Inject CacheController cacheController;
@Inject SessionManager sessionManager;
@Inject UploadController uploadController;
@Inject @Named("default_preferences") SharedPreferences prefs;
private String source; private String source;
private String mimeType; private String mimeType;
@ -75,8 +87,6 @@ public class ShareActivity
private Contribution contribution; private Contribution contribution;
private SimpleDraweeView backgroundImageView; private SimpleDraweeView backgroundImageView;
private UploadController uploadController;
private boolean cacheFound; private boolean cacheFound;
private GPSExtractor imageObj; private GPSExtractor imageObj;
@ -117,7 +127,7 @@ public class ShareActivity
@RequiresApi(16) @RequiresApi(16)
private boolean needsToRequestStoragePermission() { private boolean needsToRequestStoragePermission() {
// We need to ask storage permission when // We need to ask storage permission when
// the file is not owned by this app, (e.g. shared from the Gallery) // the file is not owned by this application, (e.g. shared from the Gallery)
// and permission is not obtained. // and permission is not obtained.
return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri) return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri)
&& (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) && (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
@ -127,16 +137,12 @@ public class ShareActivity
private void uploadBegins() { private void uploadBegins() {
getFileMetadata(locationPermitted); getFileMetadata(locationPermitted);
Toast startingToast = Toast.makeText( Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG);
CommonsApplication.getInstance(),
R.string.uploading_started,
Toast.LENGTH_LONG
);
startingToast.show(); startingToast.show();
if (!cacheFound) { if (!cacheFound) {
//Has to be called after apiCall.request() //Has to be called after apiCall.request()
app.getCacheData().cacheCategory(); cacheController.cacheCategory();
Timber.d("Cache the categories found"); Timber.d("Cache the categories found");
} }
@ -168,10 +174,10 @@ public class ShareActivity
// FIXME: Make sure that the content provider is up // 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 // 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! ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT) EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT, mwApi, prefs)
.param("username", app.getCurrentAccount().name) .param("username", sessionManager.getCurrentAccount().name)
.param("categories-count", categories.size()) .param("categories-count", categories.size())
.param("files-count", 1) .param("files-count", 1)
.param("source", contribution.getSource()) .param("source", contribution.getSource())
@ -192,16 +198,16 @@ public class ShareActivity
public void onBackPressed() { public void onBackPressed() {
super.onBackPressed(); super.onBackPressed();
if (categorizationFragment != null && categorizationFragment.isVisible()) { if (categorizationFragment != null && categorizationFragment.isVisible()) {
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT) EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT, mwApi, prefs)
.param("username", app.getCurrentAccount().name) .param("username", sessionManager.getCurrentAccount().name)
.param("categories-count", categorizationFragment.getCurrentSelectedCount()) .param("categories-count", categorizationFragment.getCurrentSelectedCount())
.param("files-count", 1) .param("files-count", 1)
.param("source", contribution.getSource()) .param("source", contribution.getSource())
.param("result", "cancelled") .param("result", "cancelled")
.log(); .log();
} else { } else {
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT) EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT, mwApi, prefs)
.param("username", app.getCurrentAccount().name) .param("username", sessionManager.getCurrentAccount().name)
.param("source", getIntent().getStringExtra(UploadService.EXTRA_SOURCE)) .param("source", getIntent().getStringExtra(UploadService.EXTRA_SOURCE))
.param("multiple", true) .param("multiple", true)
.param("result", "cancelled") .param("result", "cancelled")
@ -211,8 +217,7 @@ public class ShareActivity
@Override @Override
protected void onAuthCookieAcquired(String authCookie) { protected void onAuthCookieAcquired(String authCookie) {
app.getMWApi().setAuthCookie(authCookie); mwApi.setAuthCookie(authCookie);
} }
@Override @Override
@ -225,11 +230,10 @@ public class ShareActivity
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
uploadController = new UploadController();
setContentView(R.layout.activity_share); setContentView(R.layout.activity_share);
ButterKnife.bind(this); ButterKnife.bind(this);
initBack(); initBack();
app = CommonsApplication.getInstance();
backgroundImageView = (SimpleDraweeView) findViewById(R.id.backgroundImage); backgroundImageView = (SimpleDraweeView) findViewById(R.id.backgroundImage);
backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder
.newInstance(getResources()) .newInstance(getResources())
@ -379,7 +383,7 @@ public class ShareActivity
try { try {
InputStream inputStream = getContentResolver().openInputStream(mediaUri); InputStream inputStream = getContentResolver().openInputStream(mediaUri);
Timber.d("Input stream created from %s", mediaUri.toString()); Timber.d("Input stream created from %s", mediaUri.toString());
String fileSHA1 = Utils.getSHA1(inputStream); String fileSHA1 = getSHA1(inputStream);
Timber.d("File SHA1 is: %s", fileSHA1); Timber.d("File SHA1 is: %s", fileSHA1);
ExistingFileAsync fileAsyncTask = ExistingFileAsync fileAsyncTask =
@ -387,7 +391,7 @@ public class ShareActivity
Timber.d("%s duplicate check: %s", mediaUri.toString(), result); Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
duplicateCheckPassed = (result == DUPLICATE_PROCEED duplicateCheckPassed = (result == DUPLICATE_PROCEED
|| result == NO_DUPLICATE); || result == NO_DUPLICATE);
}); }, mwApi);
fileAsyncTask.execute(); fileAsyncTask.execute();
} catch (IOException e) { } catch (IOException e) {
Timber.d(e, "IO Exception: "); Timber.d(e, "IO Exception: ");
@ -424,9 +428,7 @@ public class ShareActivity
ParcelFileDescriptor descriptor ParcelFileDescriptor descriptor
= getContentResolver().openFileDescriptor(mediaUri, "r"); = getContentResolver().openFileDescriptor(mediaUri, "r");
if (descriptor != null) { if (descriptor != null) {
SharedPreferences sharedPref = PreferenceManager boolean useExtStorage = prefs.getBoolean("useExternalStorage", true);
.getDefaultSharedPreferences(CommonsApplication.getInstance());
boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true);
if (useExtStorage) { if (useExtStorage) {
copyPath = Environment.getExternalStorageDirectory().toString() copyPath = Environment.getExternalStorageDirectory().toString()
+ "/CommonsApp/" + new Date().getTime() + ".jpg"; + "/CommonsApp/" + new Date().getTime() + ".jpg";
@ -467,12 +469,12 @@ public class ShareActivity
= getContentResolver().openFileDescriptor(mediaUri, "r"); = getContentResolver().openFileDescriptor(mediaUri, "r");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (descriptor != null) { if (descriptor != null) {
imageObj = new GPSExtractor(descriptor.getFileDescriptor()); imageObj = new GPSExtractor(descriptor.getFileDescriptor(), this, prefs);
} }
} else { } else {
String filePath = getPathOfMediaOrCopy(); String filePath = getPathOfMediaOrCopy();
if (filePath != null) { if (filePath != null) {
imageObj = new GPSExtractor(filePath); imageObj = new GPSExtractor(filePath, this, prefs);
} }
} }
} }
@ -499,12 +501,12 @@ public class ShareActivity
if (imageObj.imageCoordsExists) { if (imageObj.imageCoordsExists) {
double decLongitude = imageObj.getDecLongitude(); double decLongitude = imageObj.getDecLongitude();
double decLatitude = imageObj.getDecLatitude(); double decLatitude = imageObj.getDecLatitude();
app.getCacheData().setQtPoint(decLongitude, decLatitude); cacheController.setQtPoint(decLongitude, decLatitude);
} }
MwVolleyApi apiCall = new MwVolleyApi(); MwVolleyApi apiCall = new MwVolleyApi(this);
List<String> displayCatList = app.getCacheData().findCategory(); List<String> displayCatList = cacheController.findCategory();
boolean catListEmpty = displayCatList.isEmpty(); boolean catListEmpty = displayCatList.isEmpty();
// If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories // If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories
@ -550,4 +552,41 @@ public class ShareActivity
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
// Get SHA1 of file from input stream
private String getSHA1(InputStream is) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
Timber.e(e, "Exception while getting Digest");
return "";
}
byte[] buffer = new byte[8192];
int read;
try {
while ((read = is.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
byte[] md5sum = digest.digest();
BigInteger bigInt = new BigInteger(1, md5sum);
String output = bigInt.toString(16);
// Fill to 40 chars
output = String.format("%40s", output).replace(' ', '0');
Timber.i("File SHA1: %s", output);
return output;
} catch (IOException e) {
Timber.e(e, "IO Exception");
return "";
} finally {
try {
is.close();
} catch (IOException e) {
Timber.e(e, "Exception on closing MD5 input stream");
}
}
}
} }

View file

@ -7,7 +7,7 @@ import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v4.app.Fragment; import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
@ -28,11 +28,15 @@ import android.widget.TextView;
import java.util.ArrayList; import java.util.ArrayList;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
import butterknife.OnItemSelected; import butterknife.OnItemSelected;
import butterknife.OnTouch; import butterknife.OnTouch;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
@ -41,7 +45,7 @@ import timber.log.Timber;
import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP; import static android.view.MotionEvent.ACTION_UP;
public class SingleUploadFragment extends Fragment { public class SingleUploadFragment extends DaggerFragment {
@BindView(R.id.titleEdit) EditText titleEdit; @BindView(R.id.titleEdit) EditText titleEdit;
@BindView(R.id.descEdit) EditText descEdit; @BindView(R.id.descEdit) EditText descEdit;
@ -49,7 +53,8 @@ public class SingleUploadFragment extends Fragment {
@BindView(R.id.share_license_summary) TextView licenseSummaryView; @BindView(R.id.share_license_summary) TextView licenseSummaryView;
@BindView(R.id.licenseSpinner) Spinner licenseSpinner; @BindView(R.id.licenseSpinner) Spinner licenseSpinner;
private SharedPreferences prefs; @Inject @Named("default_preferences") SharedPreferences prefs;
private String license; private String license;
private OnUploadActionInitiated uploadActionInitiatedHandler; private OnUploadActionInitiated uploadActionInitiatedHandler;
private TitleTextWatcher textWatcher = new TitleTextWatcher(); private TitleTextWatcher textWatcher = new TitleTextWatcher();
@ -72,11 +77,10 @@ public class SingleUploadFragment extends Fragment {
String desc = descEdit.getText().toString(); String desc = descEdit.getText().toString();
//Save the title/desc in short-lived cache so next time this fragment is loaded, we can access these //Save the title/desc in short-lived cache so next time this fragment is loaded, we can access these
SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity()); prefs.edit()
SharedPreferences.Editor editor = titleDesc.edit(); .putString("Title", title)
editor.putString("Title", title); .putString("Desc", desc)
editor.putString("Desc", desc); .apply();
editor.apply();
uploadActionInitiatedHandler.uploadActionInitiated(title, desc); uploadActionInitiatedHandler.uploadActionInitiated(title, desc);
return true; return true;
@ -91,7 +95,6 @@ public class SingleUploadFragment extends Fragment {
View rootView = inflater.inflate(R.layout.fragment_single_upload, container, false); View rootView = inflater.inflate(R.layout.fragment_single_upload, container, false);
ButterKnife.bind(this, rootView); ButterKnife.bind(this, rootView);
ArrayList<String> licenseItems = new ArrayList<>(); ArrayList<String> licenseItems = new ArrayList<>();
licenseItems.add(getString(R.string.license_name_cc0)); licenseItems.add(getString(R.string.license_name_cc0));
licenseItems.add(getString(R.string.license_name_cc_by)); licenseItems.add(getString(R.string.license_name_cc_by));
@ -99,7 +102,6 @@ public class SingleUploadFragment extends Fragment {
licenseItems.add(getString(R.string.license_name_cc_by_four)); licenseItems.add(getString(R.string.license_name_cc_by_four));
licenseItems.add(getString(R.string.license_name_cc_by_sa_four)); licenseItems.add(getString(R.string.license_name_cc_by_sa_four));
prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
// check if this is the first time we have uploaded // check if this is the first time we have uploaded
@ -172,9 +174,9 @@ public class SingleUploadFragment extends Fragment {
} }
setLicenseSummary(license); setLicenseSummary(license);
SharedPreferences.Editor editor = prefs.edit(); prefs.edit()
editor.putString(Prefs.DEFAULT_LICENSE, license); .putString(Prefs.DEFAULT_LICENSE, license)
editor.commit(); .commit();
} }
@OnTouch(R.id.share_license_summary) @OnTouch(R.id.share_license_summary)
@ -182,7 +184,7 @@ public class SingleUploadFragment extends Fragment {
if (motionEvent.getActionMasked() == ACTION_DOWN) { if (motionEvent.getActionMasked() == ACTION_DOWN) {
Intent intent = new Intent(); Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW); intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(Utils.licenseUrlFor(license))); intent.setData(Uri.parse(licenseUrlFor(license)));
startActivity(intent); startActivity(intent);
return true; return true;
} else { } else {
@ -193,9 +195,8 @@ public class SingleUploadFragment extends Fragment {
@OnClick(R.id.titleDescButton) @OnClick(R.id.titleDescButton)
void setTitleDescButton() { void setTitleDescButton() {
//Retrieve last title and desc entered //Retrieve last title and desc entered
SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity()); String title = prefs.getString("Title", "");
String title = titleDesc.getString("Title", ""); String desc = prefs.getString("Desc", "");
String desc = titleDesc.getString("Desc", "");
Timber.d("Title: %s, Desc: %s", title, desc); Timber.d("Title: %s, Desc: %s", title, desc);
titleEdit.setText(title); titleEdit.setText(title);
@ -263,6 +264,23 @@ public class SingleUploadFragment extends Fragment {
} }
} }
@NonNull
private String licenseUrlFor(String license) {
switch (license) {
case Prefs.Licenses.CC_BY_3:
return "https://creativecommons.org/licenses/by/3.0/";
case Prefs.Licenses.CC_BY_4:
return "https://creativecommons.org/licenses/by/4.0/";
case Prefs.Licenses.CC_BY_SA_3:
return "https://creativecommons.org/licenses/by-sa/3.0/";
case Prefs.Licenses.CC_BY_SA_4:
return "https://creativecommons.org/licenses/by-sa/4.0/";
case Prefs.Licenses.CC0:
return "https://creativecommons.org/publicdomain/zero/1.0/";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
public interface OnUploadActionInitiated { public interface OnUploadActionInitiated {
void uploadActionInitiated(String title, String description); void uploadActionInitiated(String title, String description);
} }

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
@ -9,31 +10,36 @@ import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.IBinder; import android.os.IBinder;
import android.preference.PreferenceManager;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.text.TextUtils; import android.text.TextUtils;
import java.io.BufferedInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.Date; import java.util.Date;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
import timber.log.Timber; import timber.log.Timber;
public class UploadController { public class UploadController {
private UploadService uploadService; private UploadService uploadService;
private final CommonsApplication app; private SessionManager sessionManager;
private Context context;
private SharedPreferences prefs;
public interface ContributionUploadProgress { public interface ContributionUploadProgress {
void onUploadStarted(Contribution contribution); void onUploadStarted(Contribution contribution);
} }
public UploadController() { public UploadController(SessionManager sessionManager, Context context, SharedPreferences sharedPreferences) {
app = CommonsApplication.getInstance(); this.sessionManager = sessionManager;
this.context = context;
this.prefs = sharedPreferences;
} }
private boolean isUploadServiceConnected; private boolean isUploadServiceConnected;
@ -52,15 +58,15 @@ public class UploadController {
}; };
public void prepareService() { public void prepareService() {
Intent uploadServiceIntent = new Intent(app, UploadService.class); Intent uploadServiceIntent = new Intent(context, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
app.startService(uploadServiceIntent); context.startService(uploadServiceIntent);
app.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE); context.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
} }
public void cleanup() { public void cleanup() {
if (isUploadServiceConnected) { if (isUploadServiceConnected) {
app.unbindService(uploadServiceConnection); context.unbindService(uploadServiceConnection);
} }
} }
@ -68,7 +74,9 @@ public class UploadController {
Contribution contribution; Contribution contribution;
//TODO: Modify this to include coords //TODO: Modify this to include coords
contribution = new Contribution(mediaUri, null, title, description, -1, null, null, app.getCurrentAccount().name, CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords); contribution = new Contribution(mediaUri, null, title, description, -1,
null, null, sessionManager.getCurrentAccount().name,
CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords);
contribution.setTag("mimeType", mimeType); contribution.setTag("mimeType", mimeType);
contribution.setSource(source); contribution.setSource(source);
@ -78,12 +86,9 @@ public class UploadController {
} }
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) { public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
//Set creator, desc, and license //Set creator, desc, and license
if (TextUtils.isEmpty(contribution.getCreator())) { if (TextUtils.isEmpty(contribution.getCreator())) {
contribution.setCreator(app.getCurrentAccount().name); contribution.setCreator(sessionManager.getCurrentAccount().name);
} }
if (contribution.getDescription() == null) { if (contribution.getDescription() == null) {
@ -102,14 +107,15 @@ public class UploadController {
@Override @Override
protected Contribution doInBackground(Void... voids /* stare into you */) { protected Contribution doInBackground(Void... voids /* stare into you */) {
long length; long length;
ContentResolver contentResolver = context.getContentResolver();
try { try {
if (contribution.getDataLength() <= 0) { if (contribution.getDataLength() <= 0) {
length = app.getContentResolver() length = contentResolver
.openAssetFileDescriptor(contribution.getLocalUri(), "r") .openAssetFileDescriptor(contribution.getLocalUri(), "r")
.getLength(); .getLength();
if (length == -1) { if (length == -1) {
// Let us find out the long way! // Let us find out the long way!
length = Utils.countBytes(app.getContentResolver() length = countBytes(contentResolver
.openInputStream(contribution.getLocalUri())); .openInputStream(contribution.getLocalUri()));
} }
contribution.setDataLength(length); contribution.setDataLength(length);
@ -126,7 +132,7 @@ public class UploadController {
Boolean imagePrefix = false; Boolean imagePrefix = false;
if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) { if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
mimeType = app.getContentResolver().getType(contribution.getLocalUri()); mimeType = contentResolver.getType(contribution.getLocalUri());
} }
if (mimeType != null) { if (mimeType != null) {
@ -137,7 +143,7 @@ public class UploadController {
if (imagePrefix && contribution.getDateCreated() == null) { if (imagePrefix && contribution.getDateCreated() == null) {
Timber.d("local uri " + contribution.getLocalUri()); Timber.d("local uri " + contribution.getLocalUri());
Cursor cursor = app.getContentResolver().query(contribution.getLocalUri(), Cursor cursor = contentResolver.query(contribution.getLocalUri(),
new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null); new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null);
if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) { if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) {
cursor.moveToFirst(); cursor.moveToFirst();
@ -165,4 +171,14 @@ public class UploadController {
} }
}.executeOnExecutor(Executors.newFixedThreadPool(1)); // TODO remove this by using a sensible thread handling strategy }.executeOnExecutor(Executors.newFixedThreadPool(1)); // TODO remove this by using a sensible thread handling strategy
} }
private long countBytes(InputStream stream) throws IOException {
long count = 0;
BufferedInputStream bis = new BufferedInputStream(stream);
while (bis.read() != -1) {
count++;
}
return count;
}
} }

View file

@ -8,6 +8,7 @@ import android.content.ContentProviderClient;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
@ -22,10 +23,14 @@ import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.contributions.ContributionsContentProvider;
@ -45,12 +50,13 @@ public class UploadService extends HandlerService<Contribution> {
public static final String EXTRA_SOURCE = EXTRA_PREFIX + ".source"; public static final String EXTRA_SOURCE = EXTRA_PREFIX + ".source";
public static final String EXTRA_CAMPAIGN = EXTRA_PREFIX + ".campaign"; public static final String EXTRA_CAMPAIGN = EXTRA_PREFIX + ".campaign";
@Inject MediaWikiApi mwApi;
@Inject SessionManager sessionManager;
@Inject @Named("default_preferences") SharedPreferences prefs;
private NotificationManager notificationManager; private NotificationManager notificationManager;
private ContentProviderClient contributionsProviderClient; private ContentProviderClient contributionsProviderClient;
private CommonsApplication app;
private NotificationCompat.Builder curProgressNotification; private NotificationCompat.Builder curProgressNotification;
private int toUpload; private int toUpload;
// The file names of unfinished uploads, used to prevent overwriting // The file names of unfinished uploads, used to prevent overwriting
@ -118,7 +124,6 @@ public class UploadService extends HandlerService<Contribution> {
super.onCreate(); super.onCreate();
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
app = CommonsApplication.getInstance();
contributionsProviderClient = this.getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY); contributionsProviderClient = this.getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY);
} }
@ -180,8 +185,6 @@ public class UploadService extends HandlerService<Contribution> {
@SuppressLint("StringFormatInvalid") @SuppressLint("StringFormatInvalid")
private void uploadContribution(Contribution contribution) { private void uploadContribution(Contribution contribution) {
MediaWikiApi api = app.getMWApi();
InputStream file; InputStream file;
String notificationTag = contribution.getLocalUri().toString(); String notificationTag = contribution.getLocalUri().toString();
@ -228,9 +231,9 @@ public class UploadService extends HandlerService<Contribution> {
filename = findUniqueFilename(filename); filename = findUniqueFilename(filename);
unfinishedUploads.add(filename); unfinishedUploads.add(filename);
} }
if (!api.validateLogin()) { if (!mwApi.validateLogin()) {
// Need to revalidate! // Need to revalidate!
if (app.revalidateAuthToken()) { if (sessionManager.revalidateAuthToken()) {
Timber.d("Successfully revalidated token!"); Timber.d("Successfully revalidated token!");
} else { } else {
Timber.d("Unable to revalidate :("); Timber.d("Unable to revalidate :(");
@ -246,7 +249,7 @@ public class UploadService extends HandlerService<Contribution> {
getString(R.string.upload_progress_notification_title_finishing, contribution.getDisplayTitle()), getString(R.string.upload_progress_notification_title_finishing, contribution.getDisplayTitle()),
contribution contribution
); );
UploadResult uploadResult = api.uploadFile(filename, file, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), notificationUpdater); UploadResult uploadResult = mwApi.uploadFile(filename, file, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), notificationUpdater);
Timber.d("Response is %s", uploadResult.toString()); Timber.d("Response is %s", uploadResult.toString());
@ -255,8 +258,8 @@ public class UploadService extends HandlerService<Contribution> {
String resultStatus = uploadResult.getResultStatus(); String resultStatus = uploadResult.getResultStatus();
if (!resultStatus.equals("Success")) { if (!resultStatus.equals("Success")) {
showFailedNotification(contribution); showFailedNotification(contribution);
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT) EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT, mwApi, prefs)
.param("username", app.getCurrentAccount().name) .param("username", sessionManager.getCurrentAccount().name)
.param("source", contribution.getSource()) .param("source", contribution.getSource())
.param("multiple", contribution.getMultiple()) .param("multiple", contribution.getMultiple())
.param("result", uploadResult.getErrorCode()) .param("result", uploadResult.getErrorCode())
@ -269,8 +272,8 @@ public class UploadService extends HandlerService<Contribution> {
contribution.setDateUploaded(uploadResult.getDateUploaded()); contribution.setDateUploaded(uploadResult.getDateUploaded());
contribution.save(); contribution.save();
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT) EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT, mwApi, prefs)
.param("username", app.getCurrentAccount().name) .param("username", sessionManager.getCurrentAccount().name)
.param("source", contribution.getSource()) //FIXME .param("source", contribution.getSource()) //FIXME
.param("filename", contribution.getFilename()) .param("filename", contribution.getFilename())
.param("multiple", contribution.getMultiple()) .param("multiple", contribution.getMultiple())
@ -287,7 +290,7 @@ public class UploadService extends HandlerService<Contribution> {
toUpload--; toUpload--;
if (toUpload == 0) { if (toUpload == 0) {
// Sync modifications right after all uplaods are processed // Sync modifications right after all uplaods are processed
ContentResolver.requestSync((CommonsApplication.getInstance()).getCurrentAccount(), ModificationsContentProvider.AUTHORITY, new Bundle()); ContentResolver.requestSync(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, new Bundle());
stopForeground(true); stopForeground(true);
} }
} }
@ -310,7 +313,6 @@ public class UploadService extends HandlerService<Contribution> {
} }
private String findUniqueFilename(String fileName) throws IOException { private String findUniqueFilename(String fileName) throws IOException {
MediaWikiApi api = app.getMWApi();
String sequenceFileName; String sequenceFileName;
for (int sequenceNumber = 1; true; sequenceNumber++) { for (int sequenceNumber = 1; true; sequenceNumber++) {
if (sequenceNumber == 1) { if (sequenceNumber == 1) {
@ -326,7 +328,7 @@ public class UploadService extends HandlerService<Contribution> {
sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2"); sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2");
} }
} }
if (!api.fileExistsWithName(sequenceFileName) if (!mwApi.fileExistsWithName(sequenceFileName)
&& !unfinishedUploads.contains(sequenceFileName)) { && !unfinishedUploads.contains(sequenceFileName)) {
break; break;
} }

View file

@ -7,7 +7,6 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;

View file

@ -201,4 +201,6 @@
<string name="send_log_file">Unviar ficheru de rexistru</string> <string name="send_log_file">Unviar ficheru de rexistru</string>
<string name="send_log_file_description">Unviar ficheru de rexistru a los desendolcadores per corréu electrónicu</string> <string name="send_log_file_description">Unviar ficheru de rexistru a los desendolcadores per corréu electrónicu</string>
<string name="login_to_your_account">Anicia sesión na to cuenta</string> <string name="login_to_your_account">Anicia sesión na to cuenta</string>
<string name="nearby_location_has_not_changed">L\'allugamientu nun camudó.</string>
<string name="nearby_location_not_available">L\'allugamientu nun ta disponible.</string>
</resources> </resources>

View file

@ -201,4 +201,7 @@
<string name="send_log_file">লগ ফাইল পাঠান</string> <string name="send_log_file">লগ ফাইল পাঠান</string>
<string name="send_log_file_description">ইমেইলের মাধ্যমে উন্নয়নকারীর কাছে লগ ফাইল পাঠান</string> <string name="send_log_file_description">ইমেইলের মাধ্যমে উন্নয়নকারীর কাছে লগ ফাইল পাঠান</string>
<string name="login_to_your_account">আপনার অ্যাকাউন্টে প্রবেশ করুন</string> <string name="login_to_your_account">আপনার অ্যাকাউন্টে প্রবেশ করুন</string>
<string name="nearby_location_has_not_changed">অবস্থান পরিবর্তন হয়নি।</string>
<string name="nearby_location_not_available">অবস্থান উপলব্ধ নয়।</string>
<string name="location_permission_rationale_nearby">কাছাকাছি স্থানসমূহের একটি তালিকা প্রদর্শন করতে অনুমতি প্রয়োজন</string>
</resources> </resources>

View file

@ -201,4 +201,6 @@
<string name="send_log_file">Send logfil</string> <string name="send_log_file">Send logfil</string>
<string name="send_log_file_description">Send logfil til udviklerne via e-post</string> <string name="send_log_file_description">Send logfil til udviklerne via e-post</string>
<string name="login_to_your_account">Log ind på din konto</string> <string name="login_to_your_account">Log ind på din konto</string>
<string name="nearby_location_has_not_changed">Sted er ikke ændret.</string>
<string name="nearby_location_not_available">Sted ikke tilgængeligt.</string>
</resources> </resources>

View file

@ -201,4 +201,7 @@
<string name="send_log_file">Logdatei senden</string> <string name="send_log_file">Logdatei senden</string>
<string name="send_log_file_description">Logdatei an die Entwickler per E-Mail senden</string> <string name="send_log_file_description">Logdatei an die Entwickler per E-Mail senden</string>
<string name="login_to_your_account">Bei deinem Benutzerkonto anmelden</string> <string name="login_to_your_account">Bei deinem Benutzerkonto anmelden</string>
<string name="nearby_location_has_not_changed">Der Standort hat sich nicht geändert.</string>
<string name="nearby_location_not_available">Der Standort ist nicht verfügbar.</string>
<string name="location_permission_rationale_nearby">Berechtigung zur Anzeige einer Liste mit Orten in der Nähe erforderlich</string>
</resources> </resources>

View file

@ -24,7 +24,7 @@
<item quantity="one">1 dosye selagnayış</item> <item quantity="one">1 dosye selagnayış</item>
<item quantity="other">%d dosye Selagnayışi</item> <item quantity="other">%d dosye Selagnayışi</item>
</plurals> </plurals>
<string name="title_activity_contributions" fuzzy="true">Barkerdışê mı</string> <string name="title_activity_contributions">Barkerdışê mınê peyêni</string>
<string name="contribution_state_queued">Ratneya</string> <string name="contribution_state_queued">Ratneya</string>
<string name="contribution_state_failed">Nêbı</string> <string name="contribution_state_failed">Nêbı</string>
<string name="contribution_state_in_progress">%1$d%% temamya</string> <string name="contribution_state_in_progress">%1$d%% temamya</string>
@ -118,4 +118,5 @@
<string name="navigation_drawer_open">Ake</string> <string name="navigation_drawer_open">Ake</string>
<string name="navigation_item_home">Keye</string> <string name="navigation_item_home">Keye</string>
<string name="navigation_item_upload">Bar ke</string> <string name="navigation_item_upload">Bar ke</string>
<string name="navigation_item_logout">Veciyayış</string>
</resources> </resources>

View file

@ -199,4 +199,6 @@
<string name="use_external_storage_summary">Αποθηκεύσετε εικόνες που παίρνονται στην κάμερα εφαρμογής στην συσκευή σας</string> <string name="use_external_storage_summary">Αποθηκεύσετε εικόνες που παίρνονται στην κάμερα εφαρμογής στην συσκευή σας</string>
<string name="send_log_file">Αποστείλατε τον φάκελλο σύνδεσης</string> <string name="send_log_file">Αποστείλατε τον φάκελλο σύνδεσης</string>
<string name="send_log_file_description">Στείλατε τον φάκελλο σύνδεσης στους δημιουργούς μέσω email</string> <string name="send_log_file_description">Στείλατε τον φάκελλο σύνδεσης στους δημιουργούς μέσω email</string>
<string name="nearby_location_has_not_changed">Ο εντοπισμός δεν έχει αλλάξει.</string>
<string name="nearby_location_not_available">Ο τόπος δεν είναι διαθέσιμος.</string>
</resources> </resources>

View file

@ -12,7 +12,7 @@
<string name="login_failed">Acceso fallido.</string> <string name="login_failed">Acceso fallido.</string>
<string name="upload_failed">No se encontró el archivo. Prueba con otro.</string> <string name="upload_failed">No se encontró el archivo. Prueba con otro.</string>
<string name="authentication_failed">Falló la autenticación.</string> <string name="authentication_failed">Falló la autenticación.</string>
<string name="uploading_started">¡Empenzando a subir!</string> <string name="uploading_started">Ha comenzado la carga.</string>
<string name="upload_completed_notification_title">¡Se subieron %1$s!</string> <string name="upload_completed_notification_title">¡Se subieron %1$s!</string>
<string name="upload_completed_notification_text">Pulsa para ver tu subida</string> <string name="upload_completed_notification_text">Pulsa para ver tu subida</string>
<string name="upload_progress_notification_title_start">Empezando la subida de %1$s</string> <string name="upload_progress_notification_title_start">Empezando la subida de %1$s</string>
@ -201,4 +201,7 @@
<string name="send_log_file">Enviar archivo de registro</string> <string name="send_log_file">Enviar archivo de registro</string>
<string name="send_log_file_description">Enviar archivo de registro a los desarrolladores por correo electrónico</string> <string name="send_log_file_description">Enviar archivo de registro a los desarrolladores por correo electrónico</string>
<string name="login_to_your_account">Accede a tu cuenta</string> <string name="login_to_your_account">Accede a tu cuenta</string>
<string name="nearby_location_has_not_changed">La ubicación no ha cambiado.</string>
<string name="nearby_location_not_available">La ubicación no está disponible.</string>
<string name="location_permission_rationale_nearby">Se necesita permiso para mostrar una lista de lugares cercanos</string>
</resources> </resources>

View file

@ -201,4 +201,7 @@
<string name="send_log_file">Envoyer le journal</string> <string name="send_log_file">Envoyer le journal</string>
<string name="send_log_file_description">Envoyer le journal aux développeurs par courriel</string> <string name="send_log_file_description">Envoyer le journal aux développeurs par courriel</string>
<string name="login_to_your_account">Connectez-vous à votre compte</string> <string name="login_to_your_account">Connectez-vous à votre compte</string>
<string name="nearby_location_has_not_changed">L\'emplacement n\'a pas changé.</string>
<string name="nearby_location_not_available">Emplacement non disponible.</string>
<string name="location_permission_rationale_nearby">Une permission est requise pour afficher une liste de lieux relatifs</string>
</resources> </resources>

View file

@ -201,4 +201,6 @@
<string name="send_log_file">Enviar ficheiro de rexistro</string> <string name="send_log_file">Enviar ficheiro de rexistro</string>
<string name="send_log_file_description">Enviar ficheiro de rexistro ós desenvolvedores por correo electrónico</string> <string name="send_log_file_description">Enviar ficheiro de rexistro ós desenvolvedores por correo electrónico</string>
<string name="login_to_your_account">Comezar sesión na súa conta</string> <string name="login_to_your_account">Comezar sesión na súa conta</string>
<string name="nearby_location_has_not_changed">A localización non cambiou.</string>
<string name="nearby_location_not_available">A localización non está dispoñible.</string>
</resources> </resources>

View file

@ -11,9 +11,9 @@
<string name="login_success">Sikeres bejelentkezés</string> <string name="login_success">Sikeres bejelentkezés</string>
<string name="login_failed">A bejelentkezés nem sikerült.</string> <string name="login_failed">A bejelentkezés nem sikerült.</string>
<string name="upload_failed">A fájl nem található. Próbálkozz másik fájllal.</string> <string name="upload_failed">A fájl nem található. Próbálkozz másik fájllal.</string>
<string name="authentication_failed">Sikertelen azonosítás.</string> <string name="authentication_failed">Sikertelen hitelesítés.</string>
<string name="uploading_started">Feltöltés indul.</string> <string name="uploading_started">Feltöltés elindult.</string>
<string name="upload_completed_notification_title">%1$s feltöltve!</string> <string name="upload_completed_notification_title">%1$s feltöltve.</string>
<string name="upload_completed_notification_text">Feltöltés megtekintése</string> <string name="upload_completed_notification_text">Feltöltés megtekintése</string>
<string name="upload_progress_notification_title_start">Feltöltés indul: %1$s</string> <string name="upload_progress_notification_title_start">Feltöltés indul: %1$s</string>
<string name="upload_progress_notification_title_in_progress">%1$s feltöltése</string> <string name="upload_progress_notification_title_in_progress">%1$s feltöltése</string>
@ -105,7 +105,7 @@
<string name="license_name_cc_by_sa_3_0_ee">CC BY-SA 3.0 (Észtország)</string> <string name="license_name_cc_by_sa_3_0_ee">CC BY-SA 3.0 (Észtország)</string>
<string name="license_name_cc_by_sa_3_0_es">CC BY-SA 3.0 (Spanyolország)</string> <string name="license_name_cc_by_sa_3_0_es">CC BY-SA 3.0 (Spanyolország)</string>
<string name="license_name_cc_by_sa_3_0_hr">CC BY-SA 3.0 (Horvátország)</string> <string name="license_name_cc_by_sa_3_0_hr">CC BY-SA 3.0 (Horvátország)</string>
<string name="license_name_cc_by_sa_3_0_lu">CC BY-SA 3.0 (Luxembourg)</string> <string name="license_name_cc_by_sa_3_0_lu">CC BY-SA 3.0 (Luxemburg)</string>
<string name="license_name_cc_by_sa_3_0_nl">CC BY-SA 3.0 (Hollandia)</string> <string name="license_name_cc_by_sa_3_0_nl">CC BY-SA 3.0 (Hollandia)</string>
<string name="license_name_cc_by_sa_3_0_no">CC BY-SA 3.0 (Norvégia)</string> <string name="license_name_cc_by_sa_3_0_no">CC BY-SA 3.0 (Norvégia)</string>
<string name="license_name_cc_by_sa_3_0_pl">CC BY-SA 3.0 (Lengyelország)</string> <string name="license_name_cc_by_sa_3_0_pl">CC BY-SA 3.0 (Lengyelország)</string>
@ -190,9 +190,15 @@
<string name="no_description_found">nincs leírás</string> <string name="no_description_found">nincs leírás</string>
<string name="nearby_info_menu_commons_article">Commons leírólap</string> <string name="nearby_info_menu_commons_article">Commons leírólap</string>
<string name="nearby_info_menu_wikidata_article">Wikidata-elem</string> <string name="nearby_info_menu_wikidata_article">Wikidata-elem</string>
<string name="error_while_cache">Hiba a képek gyorsítótárazásakor</string>
<string name="title_info">Egy egyedi, leíró cím a fájlnak, ami fájlnévként fog szolgálni. Egyszerű nyelvezetet használhatsz szóközökkel. Ne tedd bele a kiterjesztést.</string> <string name="title_info">Egy egyedi, leíró cím a fájlnak, ami fájlnévként fog szolgálni. Egyszerű nyelvezetet használhatsz szóközökkel. Ne tedd bele a kiterjesztést.</string>
<string name="description_info">Kérlek a lehető legteljesebb módon írd le a fájlt: hol készült, mit ábrázol, mi a kontextus? Kérlek add meg az objektumokat vagy személyeket a képen, valamint a nehezen kitalálható információkat (például a kép készítésének dátumát, ha az egy tájkép). Amennyiben a média valami szokatlant ábrázol, kérlek fejtsd ki, hogy mi teszi szokatlanná.</string> <string name="description_info">Kérlek a lehető legteljesebb módon írd le a fájlt: hol készült, mit ábrázol, mi a kontextus? Kérlek add meg az objektumokat vagy személyeket a képen, valamint a nehezen kitalálható információkat (például a kép készítésének dátumát, ha az egy tájkép). Amennyiben a média valami szokatlant ábrázol, kérlek fejtsd ki, hogy mi teszi szokatlanná.</string>
<string name="give_permission">Engedély adása</string> <string name="give_permission">Engedély adása</string>
<string name="use_external_storage">Külső tárhely használata</string> <string name="use_external_storage">Külső tárhely használata</string>
<string name="use_external_storage_summary">Az alkalmazáson belüli kamerával készült képek mentése az eszközre</string>
<string name="send_log_file">Naplófájlok küldése</string>
<string name="send_log_file_description">Naplófájlok küldése e-mailben a fejlesztőknek</string>
<string name="login_to_your_account">Bejelentkezés a fiókodba</string> <string name="login_to_your_account">Bejelentkezés a fiókodba</string>
<string name="nearby_location_has_not_changed">A hely nem változott.</string>
<string name="nearby_location_not_available">A hely nem érhető el.</string>
</resources> </resources>

View file

@ -167,4 +167,6 @@
<string name="nearby_info_menu_commons_article">Pagina di Commons del file</string> <string name="nearby_info_menu_commons_article">Pagina di Commons del file</string>
<string name="nearby_info_menu_wikidata_article">Elemento Wikidata</string> <string name="nearby_info_menu_wikidata_article">Elemento Wikidata</string>
<string name="login_to_your_account">Accedi alla tua utenza</string> <string name="login_to_your_account">Accedi alla tua utenza</string>
<string name="nearby_location_has_not_changed">La posizione non è cambiata.</string>
<string name="nearby_location_not_available">Posizione non disponibile.</string>
</resources> </resources>

View file

@ -65,7 +65,7 @@
<string name="title_activity_settings">設定</string> <string name="title_activity_settings">設定</string>
<string name="title_activity_signup">利用者登録</string> <string name="title_activity_signup">利用者登録</string>
<string name="menu_about">このアプリについて</string> <string name="menu_about">このアプリについて</string>
<string name="about_license" fuzzy="true">&lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\"&gt;Apache ライセンス v2&lt;/a&gt; のもとで公開されているオープン ソース ソフトウェアです。Wikimedia Commons ならびにそのロゴはウィキメディア財団の商標であり、ウィキメディア財団の許可により使用しています。このサイトはウィキメディア財団の公認3でも提携先でもありません。</string> <string name="about_license">ウィキメディア・コモンズ・アプリはウィキメディア・コミュニティの助成金受給者とボランティアによって製作・メンテナンスされているオープンソースソフトウェアです。ウィキメディア財団はこのアプリの製作・開発・メンテナンスに関与していません。</string>
<string name="about_improve">ソースは &lt;a href=\"https://github.com/commons-app/apps-android-commons\"&gt;GitHub&lt;/a&gt; にあります。バグとアイディアは &lt;a href=\"https://github.com/commons-app/apps-android-commons/issues\"&gt;Github&lt;/a&gt; へ。</string> <string name="about_improve">ソースは &lt;a href=\"https://github.com/commons-app/apps-android-commons\"&gt;GitHub&lt;/a&gt; にあります。バグとアイディアは &lt;a href=\"https://github.com/commons-app/apps-android-commons/issues\"&gt;Github&lt;/a&gt; へ。</string>
<string name="about_privacy_policy" fuzzy="true">&lt;a href=\"https://wikimediafoundation.org/wiki/プライバシー・ポリシー\"&gt;プライバシー・ポリシー&lt;/a&gt;</string> <string name="about_privacy_policy" fuzzy="true">&lt;a href=\"https://wikimediafoundation.org/wiki/プライバシー・ポリシー\"&gt;プライバシー・ポリシー&lt;/a&gt;</string>
<string name="about_credits">&lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\"&gt;クレジット&lt;/a&gt;</string> <string name="about_credits">&lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\"&gt;クレジット&lt;/a&gt;</string>
@ -168,4 +168,5 @@
<string name="no_description_found">説明がありません</string> <string name="no_description_found">説明がありません</string>
<string name="nearby_info_menu_wikidata_article">ウィキデータ項目</string> <string name="nearby_info_menu_wikidata_article">ウィキデータ項目</string>
<string name="use_external_storage">外部ストレージを使用</string> <string name="use_external_storage">外部ストレージを使用</string>
<string name="nearby_location_not_available">位置が無効です。</string>
</resources> </resources>

View file

@ -192,10 +192,14 @@
<string name="nearby_info_menu_commons_article">공용 파일 문서</string> <string name="nearby_info_menu_commons_article">공용 파일 문서</string>
<string name="nearby_info_menu_wikidata_article">위키데이터 항목</string> <string name="nearby_info_menu_wikidata_article">위키데이터 항목</string>
<string name="error_while_cache">그림 캐시 처리 오류</string> <string name="error_while_cache">그림 캐시 처리 오류</string>
<string name="title_info">이 파일을 설명할 수 있는 제목으로, 파일 이름으로 사용됩니다. 띄어쓰기를 포함한 일반적인 단어를 사용할 수 있습니다. 파일 확장자는 포함하지 마세요</string>
<string name="give_permission">권한 부여</string> <string name="give_permission">권한 부여</string>
<string name="use_external_storage">외부 저장소 사용하기</string> <string name="use_external_storage">외부 저장소 사용하기</string>
<string name="use_external_storage_summary">장치의 인앱 카메라로 찍은 사진 저장하기</string> <string name="use_external_storage_summary">장치의 인앱 카메라로 찍은 사진 저장하기</string>
<string name="send_log_file">로그 파일 보내기</string> <string name="send_log_file">로그 파일 보내기</string>
<string name="send_log_file_description">이메일로 개발자에게 로그 파일 보내기</string> <string name="send_log_file_description">이메일로 개발자에게 로그 파일 보내기</string>
<string name="login_to_your_account">자신의 계정으로 로그인</string> <string name="login_to_your_account">자신의 계정으로 로그인</string>
<string name="nearby_location_has_not_changed">위치가 변경되지 않았습니다.</string>
<string name="nearby_location_not_available">위치를 사용할 수 없습니다.</string>
<string name="location_permission_rationale_nearby">주변 장소의 목록을 표시하기 위한 권한이 필요합니다.</string>
</resources> </resources>

View file

@ -191,4 +191,6 @@
<string name="send_log_file">Log-Fichier schécken</string> <string name="send_log_file">Log-Fichier schécken</string>
<string name="send_log_file_description">Log-Fichier per E-Mail un d\'Entwéckler schécken</string> <string name="send_log_file_description">Log-Fichier per E-Mail un d\'Entwéckler schécken</string>
<string name="login_to_your_account">An Äre Benotzerkont aloggen</string> <string name="login_to_your_account">An Äre Benotzerkont aloggen</string>
<string name="nearby_location_has_not_changed">De Plaz huet net geännert.</string>
<string name="nearby_location_not_available">Plaz ass net disponibel.</string>
</resources> </resources>

View file

@ -17,7 +17,7 @@
<string name="upload_completed_notification_text">Допрете за да го погледате подигањето</string> <string name="upload_completed_notification_text">Допрете за да го погледате подигањето</string>
<string name="upload_progress_notification_title_start">Почнувам со подигањето на „%1$s“</string> <string name="upload_progress_notification_title_start">Почнувам со подигањето на „%1$s“</string>
<string name="upload_progress_notification_title_in_progress">Подигање на „%1$s“</string> <string name="upload_progress_notification_title_in_progress">Подигање на „%1$s“</string>
<string name="upload_progress_notification_title_finishing">Заврпувам со подигање на „%1$s“</string> <string name="upload_progress_notification_title_finishing">Завршувам со подигање на „%1$s“</string>
<string name="upload_failed_notification_title">Подигањето на „%1$s“ не успеа</string> <string name="upload_failed_notification_title">Подигањето на „%1$s“ не успеа</string>
<string name="upload_failed_notification_subtitle">Допрете за да погледате</string> <string name="upload_failed_notification_subtitle">Допрете за да погледате</string>
<plurals name="uploads_pending_notification_indicator"> <plurals name="uploads_pending_notification_indicator">
@ -73,7 +73,7 @@
<string name="title_activity_settings">Нагодувања</string> <string name="title_activity_settings">Нагодувања</string>
<string name="title_activity_signup">Регистрација</string> <string name="title_activity_signup">Регистрација</string>
<string name="menu_about">За извршникот</string> <string name="menu_about">За извршникот</string>
<string name="about_license">Прилогот на Ризницата има отворен код. Негови творци и оддржувачи се примателите на наменските средства од Викимедиината заедница како и членовите на заедницата. Фондацијата Викимедија нема учество во нејзиното создавање, разработка и одржување.</string> <string name="about_license">Прилогот на Ризницата има отворен код. Негови творци и одржувачи се примателите на наменските средства од Викимедиината заедница како и членовите на заедницата. Фондацијата Викимедија нема учество во нејзиното создавање, разработка и одржување.</string>
<string name="about_improve">&lt;a href=\"https://github.com/commons-app/apps-android-commons\"&gt;Извор&lt;/a&gt; и &lt;a href=\"https://commons-app.github.io/\"&gt;мреж. место&lt;/a&gt; на GitHub&lt;/a&gt;. Создајте нов &lt;a href=\"https://github.com/commons-app/apps-android-commons/issues\"&gt;случај на GitHub&lt;/a&gt; за пријавување грешки и давање предлози.</string> <string name="about_improve">&lt;a href=\"https://github.com/commons-app/apps-android-commons\"&gt;Извор&lt;/a&gt; и &lt;a href=\"https://commons-app.github.io/\"&gt;мреж. место&lt;/a&gt; на GitHub&lt;/a&gt;. Создајте нов &lt;a href=\"https://github.com/commons-app/apps-android-commons/issues\"&gt;случај на GitHub&lt;/a&gt; за пријавување грешки и давање предлози.</string>
<string name="about_privacy_policy">&lt;a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\"&gt;Заштита на личните податоци&lt;/a&gt;</string> <string name="about_privacy_policy">&lt;a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\"&gt;Заштита на личните податоци&lt;/a&gt;</string>
<string name="about_credits">&lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\"&gt;Заслуги&lt;/a&gt;</string> <string name="about_credits">&lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\"&gt;Заслуги&lt;/a&gt;</string>
@ -201,4 +201,7 @@
<string name="send_log_file">Испрати дневничка податотека</string> <string name="send_log_file">Испрати дневничка податотека</string>
<string name="send_log_file_description">Испрати дневничка податотека на разработувачите по е-пошта</string> <string name="send_log_file_description">Испрати дневничка податотека на разработувачите по е-пошта</string>
<string name="login_to_your_account">Најавете се со вашата сметка</string> <string name="login_to_your_account">Најавете се со вашата сметка</string>
<string name="nearby_location_has_not_changed">Местоположбата не е сменета.</string>
<string name="nearby_location_not_available">Местоположбата е недостапна.</string>
<string name="location_permission_rationale_nearby">Се бара дозвола за приказ на список на околни места</string>
</resources> </resources>

View file

@ -201,4 +201,7 @@
<string name="send_log_file">Send loggfil</string> <string name="send_log_file">Send loggfil</string>
<string name="send_log_file_description">Send loggfil til utviklerne via epost</string> <string name="send_log_file_description">Send loggfil til utviklerne via epost</string>
<string name="login_to_your_account">Logg inn med kontoen din</string> <string name="login_to_your_account">Logg inn med kontoen din</string>
<string name="nearby_location_has_not_changed">Stedet har ikke blitt endret.</string>
<string name="nearby_location_not_available">Sted ikke tilgjengelig.</string>
<string name="location_permission_rationale_nearby">Tillatelse kreves for å vise listen over steder i nærheten</string>
</resources> </resources>

View file

@ -201,4 +201,7 @@
<string name="send_log_file">Mandé l\'archivi d\'argistr</string> <string name="send_log_file">Mandé l\'archivi d\'argistr</string>
<string name="send_log_file_description">Mandé l\'archivi d\'argistr ai dësvlupator për pòsta eletrònica</string> <string name="send_log_file_description">Mandé l\'archivi d\'argistr ai dësvlupator për pòsta eletrònica</string>
<string name="login_to_your_account">Ch\'as colega a sò cont</string> <string name="login_to_your_account">Ch\'as colega a sò cont</string>
<string name="nearby_location_has_not_changed">Ël leu a l\'é nen cangià.</string>
<string name="nearby_location_not_available">Leu nen disponìbil.</string>
<string name="location_permission_rationale_nearby">A-i é da manca dël përmess pr\'ësmon-e na lista dij pòst davzin</string>
</resources> </resources>

View file

@ -201,4 +201,6 @@
<string name="send_log_file">Enviar arquivo de registro</string> <string name="send_log_file">Enviar arquivo de registro</string>
<string name="send_log_file_description">Enviar arquivo de log para desenvolvedores por e-mail</string> <string name="send_log_file_description">Enviar arquivo de log para desenvolvedores por e-mail</string>
<string name="login_to_your_account">Faça login na sua conta</string> <string name="login_to_your_account">Faça login na sua conta</string>
<string name="nearby_location_has_not_changed">O local não mudou.</string>
<string name="nearby_location_not_available">Localização não disponível.</string>
</resources> </resources>

View file

@ -10,6 +10,7 @@
<string name="logging_in_message">Aguarde, por favor…</string> <string name="logging_in_message">Aguarde, por favor…</string>
<string name="login_success">Inicio de sessão bem sucedido</string> <string name="login_success">Inicio de sessão bem sucedido</string>
<string name="login_failed">O início de sessão falhou!</string> <string name="login_failed">O início de sessão falhou!</string>
<string name="upload_failed">Ficheiro não encontrado. Por favor, tente outro ficheiro.</string>
<string name="authentication_failed">Falha de autenticação!</string> <string name="authentication_failed">Falha de autenticação!</string>
<string name="uploading_started">Iniciado o carregamento!</string> <string name="uploading_started">Iniciado o carregamento!</string>
<string name="upload_completed_notification_title">%1$s enviado!</string> <string name="upload_completed_notification_title">%1$s enviado!</string>
@ -19,11 +20,11 @@
<string name="upload_progress_notification_title_finishing">A terminar o carregamento de %1$s</string> <string name="upload_progress_notification_title_finishing">A terminar o carregamento de %1$s</string>
<string name="upload_failed_notification_title">O carregamento de %1$s falhou</string> <string name="upload_failed_notification_title">O carregamento de %1$s falhou</string>
<string name="upload_failed_notification_subtitle">Toque para ver</string> <string name="upload_failed_notification_subtitle">Toque para ver</string>
<plurals name="uploads_pending_notification_indicator" fuzzy="true"> <plurals name="uploads_pending_notification_indicator">
<item quantity="one">A carregar um ficheiro</item> <item quantity="one">%d a carregar um ficheiro</item>
<item quantity="other">A carregar %d ficheiros</item> <item quantity="other">a carregar %d ficheiros</item>
</plurals> </plurals>
<string name="title_activity_contributions" fuzzy="true">Meus carregamentos</string> <string name="title_activity_contributions">Meus carregamentos recentes</string>
<string name="contribution_state_queued">Em espera</string> <string name="contribution_state_queued">Em espera</string>
<string name="contribution_state_failed">Falhou</string> <string name="contribution_state_failed">Falhou</string>
<string name="contribution_state_in_progress">%1$d%% concluído</string> <string name="contribution_state_in_progress">%1$d%% concluído</string>
@ -39,8 +40,9 @@
<string name="login_failed_network">Não foi possível iniciar sessão - falha de rede</string> <string name="login_failed_network">Não foi possível iniciar sessão - falha de rede</string>
<string name="login_failed_username">Não foi possível iniciar sessão - verifique o seu nome de utilizador(a)</string> <string name="login_failed_username">Não foi possível iniciar sessão - verifique o seu nome de utilizador(a)</string>
<string name="login_failed_password">Não foi possível iniciar sessão - verifique a sua palavra-passe</string> <string name="login_failed_password">Não foi possível iniciar sessão - verifique a sua palavra-passe</string>
<string name="login_failed_throttled" fuzzy="true">Demasiadas tentativas mal sucedidas. Por favor, tente de novo dentro de alguns minutos</string> <string name="login_failed_throttled">Demasiadas tentativas malsucedidas. Por favor, tente de novo dentro de alguns minutos.</string>
<string name="login_failed_blocked">Desculpe, este utilizador foi bloqueado no Commons</string> <string name="login_failed_blocked">Desculpe, este utilizador foi bloqueado no Commons</string>
<string name="login_failed_2fa_needed">Precisa fornecer o seu código de ativação de dois fatores.</string>
<string name="login_failed_generic">Falha ao iniciar sessão</string> <string name="login_failed_generic">Falha ao iniciar sessão</string>
<string name="share_upload_button">Carregar</string> <string name="share_upload_button">Carregar</string>
<string name="multiple_share_base_title">Nomeie este conjunto</string> <string name="multiple_share_base_title">Nomeie este conjunto</string>
@ -48,42 +50,54 @@
<string name="menu_upload_single">Enviar</string> <string name="menu_upload_single">Enviar</string>
<string name="categories_search_text_hint">Pesquisar categorias</string> <string name="categories_search_text_hint">Pesquisar categorias</string>
<string name="menu_save_categories">Gravar</string> <string name="menu_save_categories">Gravar</string>
<plurals name="contributions_subtitle" fuzzy="true"> <string name="refresh_button">Atualizar</string>
<item quantity="zero">Sem carregamentos ainda</item> <string name="gps_disabled">O GPS está desativado no seu dispositivo. Gostarias de ativá-lo?</string>
<item quantity="one">1 carregamento</item> <string name="enable_gps">Ativar GPS</string>
<string name="contributions_subtitle_zero">Ainda não foram enviados ficheiros</string>
<plurals name="contributions_subtitle">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">%d carregamento</item>
<item quantity="other">%d carregamentos</item> <item quantity="other">%d carregamentos</item>
</plurals> </plurals>
<plurals name="starting_multiple_uploads" fuzzy="true"> <plurals name="starting_multiple_uploads">
<item quantity="one">A iniciar um carregamento</item> <item quantity="one">A iniciar %d carregamento</item>
<item quantity="other">A iniciar %d carregamentos</item> <item quantity="other">A iniciar %d carregamentos</item>
</plurals> </plurals>
<plurals name="multiple_uploads_title" fuzzy="true"> <plurals name="multiple_uploads_title">
<item quantity="one">1 carregamento</item> <item quantity="one">%d carregamento</item>
<item quantity="other">%d carregamentos</item> <item quantity="other">%d carregamentos</item>
</plurals> </plurals>
<string name="categories_not_found">Nenhuma categoria correspondente %1$s encontrada</string> <string name="categories_not_found">Nenhuma categoria correspondente %1$s encontrada</string>
<string name="categories_skip_explanation">Adicione categorias para tornar as suas imagens mais fáceis de encontrar no Wikimedia Commons. \n\nComece a digitar para adicionar categorias.\nCarregue nesta mensagem (ou carregue para voltar) para saltar este passo</string> <string name="categories_skip_explanation">Adicione categorias para tornar as suas imagens mais fáceis de encontrar no Wikimedia Commons. \n\nComece a digitar para adicionar categorias.\nCarregue nesta mensagem (ou carregue para voltar) para saltar este passo</string>
<string name="categories_activity_title">Categorias</string> <string name="categories_activity_title">Categorias</string>
<string name="title_activity_settings">Configurações</string> <string name="title_activity_settings">Configurações</string>
<string name="title_activity_signup">Registar-se</string>
<string name="menu_about">Sobre</string> <string name="menu_about">Sobre</string>
<string name="about_license" fuzzy="true">Software em código aberto distribuído sob &lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\"&gt;Apache License v2&lt;/a&gt;. O Wikimedia Commons e o seu logótipo são marcas registadas da Wikimedia Foundation e são usadas com permissão da Wikimedia Foundation. Não somos endossados ou afiliados à Wikimedia Foundation.</string> <string name="about_license">A aplicação do Wikimedia Commons é uma aplicação de código aberto criada e mantida por beneficiários e voluntários da comunidade Wikimedia. A Fundação Wikimedia não está envolvida na criação, programação ou manutenção da aplicação.</string>
<string name="about_improve" fuzzy="true">Código no &lt;a href=\"https://github.com/commons-app/android-commons\"&gt;GitHub&lt;/a&gt;. Erros no &lt;a href=\" https://github.com/commons-app/apps-android-commons/issues\"&gt;Github&lt;/a&gt;.</string> <string name="about_improve">&lt;a href=\"https://github.com/commons-app/apps-android-commons\"&gt;Fonte&lt;/a&gt; e &lt;a href=\"https://commons-app.github.io/\"&gt;sítio&lt;/a&gt; no GitHub. Criar uma nova &lt;a href=\"https://github.com/commons-app/apps-android-commons/issues\"&gt;publicação no GitHub&lt;/a&gt; para informar erros e sugestões.</string>
<string name="about_privacy_policy" fuzzy="true">&lt;a href=\"https://wikimediafoundation.org/wiki/Privacy_policy\"&gt;Política de privacidade&lt;/a&gt;</string> <string name="about_privacy_policy">&lt;a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\"&gt;Política de privacidade&lt;/a&gt;</string>
<string name="about_credits">&lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\"&gt;Créditos&lt;/a&gt;</string>
<string name="title_activity_about">Sobre</string> <string name="title_activity_about">Sobre</string>
<string name="menu_feedback">Enviar comentários (por e-mail)</string> <string name="menu_feedback">Enviar comentários (por e-mail)</string>
<string name="no_email_client">Não foi instalado nenhum cliente de correio eletrónico</string>
<string name="provider_categories">Categorias usadas recentemente</string> <string name="provider_categories">Categorias usadas recentemente</string>
<string name="waiting_first_sync">A aguardar pela primeira sincronização…</string> <string name="waiting_first_sync">A aguardar pela primeira sincronização…</string>
<string name="no_uploads_yet">Não carregou ainda nenhuma foto.</string> <string name="no_uploads_yet">Não carregou ainda nenhuma foto.</string>
<string name="menu_retry_upload">Tente novamente</string> <string name="menu_retry_upload">Tente novamente</string>
<string name="menu_cancel_upload">Cancelar</string> <string name="menu_cancel_upload">Cancelar</string>
<string name="share_license_summary">Essa imagem será licenciada sob %1$s</string> <string name="share_license_summary">Essa imagem será licenciada sob %1$s</string>
<string name="media_upload_policy">Ao carregar esta imagem, declaro que esta é a minha própria obra, que não contém material protegido ou selfies, e que adere às &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines/pt\"&gt;políticas do Wikimedia Commons&lt;/a&gt;.</string>
<string name="menu_download">Descarregar</string> <string name="menu_download">Descarregar</string>
<string name="preference_license">Licença</string> <string name="preference_license">Licença</string>
<string name="use_previous">Usar título/descrição anteriores</string> <string name="use_previous">Usar título/descrição anteriores</string>
<string name="allow_gps">Obter automaticamente a localização atual</string> <string name="allow_gps">Obter automaticamente a localização atual</string>
<string name="allow_gps_summary">Recuperar localização atual para oferecer sugestões da categoria se a imagem não é georreferenciada</string> <string name="allow_gps_summary">Recuperar localização atual para oferecer sugestões da categoria se a imagem não é georreferenciada</string>
<string name="preference_theme">Modo noturno</string>
<string name="preference_theme_summary">Utilizar tema escuro</string>
<string name="license_name_cc_by_sa_four">Atribuição-CompartilhaIgual 4.0</string>
<string name="license_name_cc_by_four">Atribuição 4.0</string>
<string name="license_name_cc_by_sa"> Atribuição Compartilhamento pela mesma Licença</string> <string name="license_name_cc_by_sa"> Atribuição Compartilhamento pela mesma Licença</string>
<string name="license_name_cc_by" fuzzy="true">CC Atribuição 3.0</string> <string name="license_name_cc_by">Atribuição 3.0</string>
<string name="license_name_cc0">CC0</string> <string name="license_name_cc0">CC0</string>
<string name="license_name_cc_by_sa_3_0">CC BY-SA 3.0</string> <string name="license_name_cc_by_sa_3_0">CC BY-SA 3.0</string>
<string name="license_name_cc_by_sa_3_0_at">CC BY-SA 3.0 (Áustria)</string> <string name="license_name_cc_by_sa_3_0_at">CC BY-SA 3.0 (Áustria)</string>
@ -98,10 +112,16 @@
<string name="license_name_cc_by_sa_3_0_ro">CC BY-SA 3.0 (Roménia)</string> <string name="license_name_cc_by_sa_3_0_ro">CC BY-SA 3.0 (Roménia)</string>
<string name="license_name_cc_by_3_0">CC BY 3.0</string> <string name="license_name_cc_by_3_0">CC BY 3.0</string>
<string name="license_name_cc_by_sa_4_0">CC-BY-SA 4.0</string> <string name="license_name_cc_by_sa_4_0">CC-BY-SA 4.0</string>
<string name="license_name_cc_by_4_0">CC BY 4.0</string>
<string name="license_name_cc_zero">CC Zero</string> <string name="license_name_cc_zero">CC Zero</string>
<string name="tutorial_1_text">Wikimedia Commons armazena a maioria das imagens que são usadas na Wikipédia.</string> <string name="tutorial_1_text">Wikimedia Commons armazena a maioria das imagens que são usadas na Wikipédia.</string>
<string name="tutorial_1_subtext">As suas imagens ajudam a educar pessoas em todo o mundo!</string> <string name="tutorial_1_subtext">As suas imagens ajudam a educar pessoas em todo o mundo!</string>
<string name="tutorial_2_text">Por favor, carregue apenas as imagens tiradas ou criadas por ti:</string>
<string name="tutorial_2_subtext">- Objetos naturais (flores, animais, montanhas)\n- Objetos úteis (bicicletas, estações de comboio)\n- Pessoas famosas (o seu presidente da câmara, atletas olímpicos que conheça)</string>
<string name="tutorial_3_text">Por favor, NÃO carregue:</string>
<string name="tutorial_3_subtext">- Autorretratos ou imagens dos seus amigos\n- Imagens descarregadas da internet\n- Capturas de ecrã de aplicações com direitos de autor</string>
<string name="tutorial_4_text">Exemplo de carregamento:</string> <string name="tutorial_4_text">Exemplo de carregamento:</string>
<string name="tutorial_4_subtext">- Título: Ópera de Sydney\n- Descrição: A Ópera de Sydney vista em toda a baía\n- Categorias: Sydney Opera House, Sydney Opera House from the west, Sydney Opera House remote views</string>
<string name="welcome_wikipedia_text">Contribua com as suas imagens. Ajude os artigos da Wikipédia a ganhar vida!</string> <string name="welcome_wikipedia_text">Contribua com as suas imagens. Ajude os artigos da Wikipédia a ganhar vida!</string>
<string name="welcome_wikipedia_subtext">As imagens na Wikipédia provêm do Wikimedia Commons.</string> <string name="welcome_wikipedia_subtext">As imagens na Wikipédia provêm do Wikimedia Commons.</string>
<string name="welcome_copyright_text">As suas imagens ajudam a educar as pessoas em todo o mundo.</string> <string name="welcome_copyright_text">As suas imagens ajudam a educar as pessoas em todo o mundo.</string>
@ -109,13 +129,78 @@
<string name="welcome_final_text">Acha que conseguiu?</string> <string name="welcome_final_text">Acha que conseguiu?</string>
<string name="welcome_final_button_text">Sim!</string> <string name="welcome_final_button_text">Sim!</string>
<string name="detail_panel_cats_label">Categorias</string> <string name="detail_panel_cats_label">Categorias</string>
<string name="detail_panel_cats_loading" fuzzy="true">A carregar…</string> <string name="detail_panel_cats_loading">A carregar…</string>
<string name="detail_panel_cats_none">Nenhuma selecionada</string> <string name="detail_panel_cats_none">Nenhuma selecionada</string>
<string name="detail_description_empty">Sem descrição</string> <string name="detail_description_empty">Sem descrição</string>
<string name="detail_license_empty">Licença desconhecida</string> <string name="detail_license_empty">Licença desconhecida</string>
<string name="menu_refresh">Actualizar</string> <string name="menu_refresh">Actualizar</string>
<string name="read_storage_permission_rationale">Permissão necessária: Ler a armazenagem externa. A aplicação não pode funcionar sem isso.</string>
<string name="write_storage_permission_rationale">Permissão necessária: Escrever na armazenagem externa. A aplicação não pode funcionar sem isso.</string>
<string name="location_permission_rationale">Permissão opcional: Obter a localização atual para sugerir categorias</string>
<string name="ok">OK</string> <string name="ok">OK</string>
<string name="title_activity_nearby">Locais Próximos</string> <string name="title_activity_nearby">Locais Próximos</string>
<string name="no_nearby">Não foram encontrados locais próximos.</string>
<string name="warning">Aviso</string>
<string name="file_exists">Este ficheiro já existe no Commons. Tem certeza de que deseja continuar?</string>
<string name="yes">Sim</string> <string name="yes">Sim</string>
<string name="no">Não</string> <string name="no">Não</string>
<string name="media_detail_title">Título</string>
<string name="media_detail_media_title">Título do ficheiro multimédia</string>
<string name="media_detail_description">Descrição</string>
<string name="media_detail_description_explanation">Aqui vai a descrição do ficheiro multimédia. Potencialmente, pode ser demasiado longo, e precisará ser agrupado em várias linhas. Esperamos que seja agradável.</string>
<string name="media_detail_uploaded_date">Data de carregamento</string>
<string name="media_detail_license">Licença</string>
<string name="media_detail_coordinates">Coordenadas</string>
<string name="media_detail_coordinates_empty">Não fornecido</string>
<string name="become_a_tester_title">Torne-se um Testador Beta</string>
<string name="become_a_tester_description">Entre no nosso canal beta na Google Play e obtenha acesso rápido a novas funcionalidades e correções de erros</string>
<string name="use_wikidata">Utilizar o Wikidata</string>
<string name="use_wikidata_summary">(Aviso: desabilitar isso pode causar um grande consumo de dados móveis)</string>
<string name="_2fa_code">Código de autenticação de dois fatores</string>
<string name="number_of_uploads">Meu limite de carregamentos recentes</string>
<string name="maximum_limit">Limite máximo</string>
<string name="maximum_limit_alert">Não é possível visualizar mais de 500.</string>
<string name="set_limit">Definir o limite de carregamentos recentes</string>
<string name="login_failed_2fa_not_supported">Atualmente, a autenticação de dois fatores não é suportada.</string>
<string name="logout_verification">Deseja realmente sair?</string>
<string name="commons_logo">Logótipo do Commons</string>
<string name="background_image">Imagem de fundo</string>
<string name="mediaimage_failed">Falha na imagem de média</string>
<string name="no_image_found">Nenhuma imagem encontrada</string>
<string name="upload_image">Carregar imagem</string>
<string name="welcome_image_mount_zao">Monte Zao</string>
<string name="welcome_image_llamas">Lamas</string>
<string name="welcome_image_rainbow_bridge">Ponte de Arco-Íris</string>
<string name="welcome_image_tulip">Túlipa</string>
<string name="welcome_image_no_selfies">Nada de autorretratos</string>
<string name="welcome_image_proprietary">Imagem proprietária</string>
<string name="welcome_image_welcome_wikipedia">Bem-vindo(a) à Wikipédia</string>
<string name="welcome_image_welcome_copyright">Direitos de autor de boas-vindas</string>
<string name="welcome_image_sydney_opera_house">Ópera de Sydney</string>
<string name="cancel">Cancelar</string>
<string name="navigation_drawer_open">Abrir</string>
<string name="navigation_drawer_close">Fechar</string>
<string name="navigation_item_home">Início</string>
<string name="navigation_item_upload">Carregar</string>
<string name="navigation_item_nearby">Próximo</string>
<string name="navigation_item_about">Acerca</string>
<string name="navigation_item_settings">Configurações</string>
<string name="navigation_item_feedback">Comentários</string>
<string name="navigation_item_logout">Sair</string>
<string name="navigation_item_info">Tutorial</string>
<string name="nearby_needs_permissions">Os sítios próximos não podem ser visualizados sem permissões de localização</string>
<string name="no_description_found">descrição não encontrada</string>
<string name="nearby_info_menu_commons_article">Página do ficheiro no Commons</string>
<string name="nearby_info_menu_wikidata_article">Item do Wikidata</string>
<string name="error_while_cache">Erro durante a cache de imagens</string>
<string name="title_info">Um título descritivo exclusivo para o ficheiro, que servirá como um nome de ficheiro. Pode utilizar uma linguagem simples com espaços. Não inclua a extensão do ficheiro</string>
<string name="description_info">Por favor, descreva o ficheiro da melhor forma possível: Onde foi tirado? O que isso mostra? Qual é o contexto? Por favor, descreva os objetos ou as pessoas. Indique as informações que não podem ser facilmente adivinhadas, por exemplo, a hora do dia, se for uma paisagem. Se o ficheiro mostrar algo incomum, explique o que torna incomum.</string>
<string name="give_permission">Permitir</string>
<string name="use_external_storage">Utilizar a armazenagem externa</string>
<string name="use_external_storage_summary">Gravar as fotografias tiradas com a câmara na aplicação do seu dispositivo</string>
<string name="send_log_file">Carregar ficheiro de registo</string>
<string name="send_log_file_description">Carregar ficheiro de registo para programadores por correio eletrónico</string>
<string name="login_to_your_account">Inicie sessão na sua conta</string>
<string name="nearby_location_has_not_changed">A localização não foi alterada.</string>
<string name="nearby_location_not_available">A localização não está disponível.</string>
</resources> </resources>

View file

@ -205,4 +205,7 @@
<string name="send_log_file">Выслать лог-файл</string> <string name="send_log_file">Выслать лог-файл</string>
<string name="send_log_file_description">Выслать лог-файл разработчикам по е-мейлу</string> <string name="send_log_file_description">Выслать лог-файл разработчикам по е-мейлу</string>
<string name="login_to_your_account">Войдите в свою учётную запись</string> <string name="login_to_your_account">Войдите в свою учётную запись</string>
<string name="nearby_location_has_not_changed">Местоположение не изменено.</string>
<string name="nearby_location_not_available">Местоположение недоступно.</string>
<string name="location_permission_rationale_nearby">Необходимо разрешение для отображения списка ближайших мест</string>
</resources> </resources>

View file

@ -201,4 +201,7 @@
<string name="send_log_file">Skicka loggfil</string> <string name="send_log_file">Skicka loggfil</string>
<string name="send_log_file_description">Skicka loggfilen till utvecklarna via e-post</string> <string name="send_log_file_description">Skicka loggfilen till utvecklarna via e-post</string>
<string name="login_to_your_account">Logga in på ditt konto</string> <string name="login_to_your_account">Logga in på ditt konto</string>
<string name="nearby_location_has_not_changed">Platsen har inte ändrats.</string>
<string name="nearby_location_not_available">Platsen är inte tillgänglig.</string>
<string name="location_permission_rationale_nearby">Behörighet krävs för att visa en lista över platser i närheten</string>
</resources> </resources>

View file

@ -201,4 +201,7 @@
<string name="send_log_file">寄送日誌檔案</string> <string name="send_log_file">寄送日誌檔案</string>
<string name="send_log_file_description">經由電子郵件寄送日誌檔案給開發人員</string> <string name="send_log_file_description">經由電子郵件寄送日誌檔案給開發人員</string>
<string name="login_to_your_account">登入您的帳號</string> <string name="login_to_your_account">登入您的帳號</string>
<string name="nearby_location_has_not_changed">位置無法更改。</string>
<string name="nearby_location_not_available">位置無效。</string>
<string name="location_permission_rationale_nearby">需權限來顯示附近地點清單</string>
</resources> </resources>

View file

@ -201,4 +201,7 @@
<string name="send_log_file">发送日志文件</string> <string name="send_log_file">发送日志文件</string>
<string name="send_log_file_description">通过电子邮件将日志文件发送给开发人员</string> <string name="send_log_file_description">通过电子邮件将日志文件发送给开发人员</string>
<string name="login_to_your_account">登录您的账户</string> <string name="login_to_your_account">登录您的账户</string>
<string name="nearby_location_has_not_changed">位置没有更新。</string>
<string name="nearby_location_not_available">位置不可用。</string>
<string name="location_permission_rationale_nearby">需要权限以显示附近地点列表</string>
</resources> </resources>

View file

@ -213,4 +213,5 @@ Tap this message (or hit back) to skip this step.</string>
<string name="nearby_location_has_not_changed">Location has not changed.</string> <string name="nearby_location_has_not_changed">Location has not changed.</string>
<string name="nearby_location_not_available">Location not available.</string> <string name="nearby_location_not_available">Location not available.</string>
<string name="location_permission_rationale_nearby">Permission required to display a list of nearby places</string>
</resources> </resources>

View file

@ -9,7 +9,7 @@ import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21) @Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class)
public class MediaTest { public class MediaTest {
@Test @Test
public void displayTitleShouldStripExtension() { public void displayTitleShouldStripExtension() {

View file

@ -18,7 +18,7 @@ import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21) @Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class)
public class NearbyControllerTest { public class NearbyControllerTest {
@Test @Test

View file

@ -11,7 +11,7 @@ import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21) @Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class)
public class PageTitleTest { public class PageTitleTest {
@Test @Test
public void displayTextShouldNotBeUnderscored() { public void displayTextShouldNotBeUnderscored() {

View file

@ -1,12 +1,182 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import android.content.SharedPreferences;
import android.support.v4.util.LruCache;
import com.squareup.leakcanary.RefWatcher; import com.squareup.leakcanary.RefWatcher;
// This class is automatically discovered by Robolectric import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.CommonsApplicationComponent;
import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.di.DaggerCommonsApplicationComponent;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.upload.UploadController;
public class TestCommonsApplication extends CommonsApplication { public class TestCommonsApplication extends CommonsApplication {
CommonsApplicationComponent mockApplicationComponent;
@Mock
CommonsApplicationModule commonsApplicationModule;
@Mock
AccountUtil accountUtil;
@Mock
SharedPreferences appSharedPreferences;
@Mock
SharedPreferences defaultSharedPreferences;
@Mock
SharedPreferences otherSharedPreferences;
@Mock
UploadController uploadController;
@Mock
SessionManager sessionManager;
@Mock
MediaWikiApi mediaWikiApi;
@Mock
LocationServiceManager locationServiceManager;
@Mock
CacheController cacheController;
@Mock
DBOpenHelper dbOpenHelper;
@Mock
NearbyPlaces nearbyPlaces;
@Mock
LruCache<String, String> lruCache;
@Override
public void onCreate() {
MockitoAnnotations.initMocks(this);
super.onCreate();
}
@Override @Override
protected RefWatcher setupLeakCanary() { protected RefWatcher setupLeakCanary() {
// No leakcanary in unit tests. // No leakcanary in unit tests.
return RefWatcher.DISABLED; return RefWatcher.DISABLED;
} }
@Override
public CommonsApplicationComponent injector() {
if (mockApplicationComponent == null) {
mockApplicationComponent = DaggerCommonsApplicationComponent.builder()
.appModule(new CommonsApplicationModule(this) {
@Override
public AccountUtil providesAccountUtil() {
return accountUtil;
}
@Override
public SharedPreferences providesApplicationSharedPreferences() {
return appSharedPreferences;
}
@Override
public SharedPreferences providesDefaultSharedPreferences() {
return defaultSharedPreferences;
}
@Override
public SharedPreferences providesOtherSharedPreferences() {
return otherSharedPreferences;
}
@Override
public UploadController providesUploadController(SessionManager sessionManager, SharedPreferences sharedPreferences) {
return uploadController;
}
@Override
public SessionManager providesSessionManager(MediaWikiApi mediaWikiApi) {
return sessionManager;
}
@Override
public MediaWikiApi provideMediaWikiApi() {
return mediaWikiApi;
}
@Override
public LocationServiceManager provideLocationServiceManager() {
return locationServiceManager;
}
@Override
public CacheController provideCacheController() {
return cacheController;
}
@Override
public DBOpenHelper provideDBOpenHelper() {
return dbOpenHelper;
}
@Override
public NearbyPlaces provideNearbyPlaces() {
return nearbyPlaces;
}
@Override
public LruCache<String, String> provideLruCache() {
return lruCache;
}
}).build();
}
return mockApplicationComponent;
}
public AccountUtil getAccountUtil() {
return accountUtil;
}
public SharedPreferences getAppSharedPreferences() {
return appSharedPreferences;
}
public SharedPreferences getDefaultSharedPreferences() {
return defaultSharedPreferences;
}
public SharedPreferences getOtherSharedPreferences() {
return otherSharedPreferences;
}
public UploadController getUploadController() {
return uploadController;
}
public SessionManager getSessionManager() {
return sessionManager;
}
public MediaWikiApi getMediaWikiApi() {
return mediaWikiApi;
}
public LocationServiceManager getLocationServiceManager() {
return locationServiceManager;
}
public CacheController getCacheController() {
return cacheController;
}
public DBOpenHelper getDbOpenHelper() {
return dbOpenHelper;
}
public NearbyPlaces getNearbyPlaces() {
return nearbyPlaces;
}
public LruCache<String, String> getLruCache() {
return lruCache;
}
} }

View file

@ -17,6 +17,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.TestCommonsApplication;
import io.reactivex.observers.TestObserver; import io.reactivex.observers.TestObserver;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockResponse;
@ -28,7 +29,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21) @Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class)
public class ApacheHttpClientMediaWikiApiTest { public class ApacheHttpClientMediaWikiApiTest {
private ApacheHttpClientMediaWikiApi testObject; private ApacheHttpClientMediaWikiApi testObject;

View file

@ -0,0 +1,47 @@
package fr.free.nrw.commons.nearby;
import android.app.Activity;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.TestCommonsApplication;
import fr.free.nrw.commons.location.LatLng;
import static junit.framework.Assert.assertNotNull;
import static org.mockito.Mockito.when;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class)
public class NearbyActivityTest {
private static final LatLng ST_LOUIS_MO_LAT_LNG = new LatLng(38.627003, -90.199402, 0);
private ActivityController<NearbyActivity> activityController;
private NearbyActivity nearbyActivity;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
TestCommonsApplication application = (TestCommonsApplication) RuntimeEnvironment.application;
when(application.getLocationServiceManager().getLastLocation()).thenReturn(ST_LOUIS_MO_LAT_LNG);
activityController = Robolectric.buildActivity(NearbyActivity.class);
nearbyActivity = activityController.get();
}
@Test
public void activityLaunchesAndShowsList() {
activityController.create().resume().visible();
assertNotNull(nearbyActivity.getSupportFragmentManager().findFragmentByTag("NearbyListFragment"));
}
}

View file

@ -20,13 +20,14 @@ import java.util.Collections;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.TestCommonsApplication;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21) @Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class)
public class NearbyAdapterFactoryTest { public class NearbyAdapterFactoryTest {
private static final Place PLACE = new Place("name", Place.Description.AIRPORT, private static final Place PLACE = new Place("name", Place.Description.AIRPORT,

View file

@ -1,9 +1,8 @@
package fr.free.nrw.commons package fr.free.nrw.commons
import org.hamcrest.CoreMatchers.`is` as _is
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.hamcrest.CoreMatchers.`is` as _is
class UtilsTest { class UtilsTest {
@Test fun `strip nothing from non-localized string`() { @Test fun `strip nothing from non-localized string`() {

View file

@ -10,7 +10,6 @@ buildscript {
classpath "com.android.tools.build:gradle:${project.gradleVersion}" classpath "com.android.tools.build:gradle:${project.gradleVersion}"
classpath 'com.dicedmelon.gradle:jacoco-android:0.1.1' classpath 'com.dicedmelon.gradle:jacoco-android:0.1.1'
classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.7.1' classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.7.1'
classpath 'me.tatarka:gradle-retrolambda:3.6.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

41
dependency-injection.md Normal file
View file

@ -0,0 +1,41 @@
## Overview
At its core, dependency injection is just the principle of `"tell, dont ask"` put into practice; for instance, if a class needs to use the `MediaWikiApi`, it should be handed an instance of the classs rather than reaching out to get it. This has the effect of decoupling code, making it easier to test and reuse.
## Dependency Injection in the Commons app
We use Dagger 2 as our dependency injection engine. Dagger is a fully static, compile-time dependency injection framework for both Java and Android. Dagger aims to address many of the development and performance issues that have plagued reflection-based solutions that came before it, but it does come at something of a cost in complexity.
For more information about Dagger, take a look at the [Dagger user guide](https://google.github.io/dagger/users-guide.html).
## Dagger configuration in the Commons app
The top level `CommonsApplicationComponent` pulls together configuration for injection across the app. The most important files to understand
- if you need to add a new Activity, look at `ActivityBuilderModule` and copy how injection is configured. The `BaseActivity` class will take care of the rest.
- if you are adding a new Fragment, look at `FragmentBuilderModule`
- if you are adding a new ContentProvider, look at `ContentProviderBuilderModule`
- if you are adding a new Service, look at `ServiceBuilderModule`
- other dependencies are configured in `CommonsApplicationModule`
## "Provider" methods
Dagger will resolve the method arguments on provider methods in a module (or the constructor arguments when annotated with `@Inject`) and build the objects accordingly - either by calling another provider method or by looking for a constructor on a class that has the `@Inject` annotation. Dagger takes care of managing singletons, just annotate with the `@Singleton` annotation. For instance,
```java
@Provides
@Singleton
public SessionManager providesSessionManager(MediaWikiApi mediaWikiApi) {
return new SessionManager(application, mediaWikiApi);
}
```
If your code injects an interface (in this case, `MediaWikiApi`) then Dagger needs to know which concrete class to use. This comes by way of a provider method:
```java
@Provides
@Singleton
public MediaWikiApi provideMediaWikiApi() {
return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST);
}
```

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