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,5 +1,31 @@
# Wikimedia Commons for Android
## 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 link to Commons policies in ShareActivity

View file

@ -58,6 +58,7 @@ dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:3.4'
testImplementation 'org.mockito:mockito-all:1.10.19'
testImplementation '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'
releaseImplementation '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 {
@ -77,9 +83,10 @@ android {
defaultConfig {
applicationId 'fr.free.nrw.commons'
versionCode 74
versionName '2.5.0'
versionCode 79
versionName '2.6.4'
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
minSdkVersion project.minSdkVersion
targetSdkVersion project.targetSdkVersion
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@ -96,6 +103,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
debug {
applicationIdSuffix ".debug"
testCoverageEnabled true
versionNameSuffix "-debug-" + getBranchName() + "~" + getBuildVersion()
}
@ -145,6 +153,8 @@ android {
//FIXME: Temporary fix for https://github.com/commons-app/apps-android-commons/issues/709
configurations.all {
resolutionStrategy.force 'com.android.support:support-annotations:25.2.0'
exclude module: 'httpclient'
exclude module: 'commons-logging'
}
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

@ -16,6 +16,9 @@
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
<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
android:name=".CommonsApplication"
android:icon="@drawable/ic_launcher"
@ -28,23 +31,19 @@
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true" />
<activity
android:name=".auth.LoginActivity"
>
<activity android:name=".auth.LoginActivity">
<intent-filter>
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<activity
android:name=".WelcomeActivity"
>
</activity>
<activity android:name=".WelcomeActivity" />
<activity
android:name=".upload.ShareActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
>
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
@ -52,11 +51,11 @@
<data android:mimeType="audio/ogg" />
</intent-filter>
</activity>
<activity
android:name=".upload.MultipleShareActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
>
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
@ -68,27 +67,28 @@
<activity
android:name=".contributions.ContributionsActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
>
</activity>
android:label="@string/app_name" />
<activity
android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings"
/>
android:label="@string/title_activity_settings" />
<activity
android:name=".AboutActivity"
android:label="@string/title_activity_about"
android:parentActivityName=".contributions.ContributionsActivity" />
<activity
android:name=".auth.SignupActivity"
android:label="@string/title_activity_signup" />
<activity
android:name=".nearby.NearbyActivity"
android:label="@string/title_activity_nearby"
android:parentActivityName=".contributions.ContributionsActivity" />
<service android:name=".upload.UploadService" >
</service>
<service android:name=".upload.UploadService" />
<service
android:name=".auth.WikiAccountAuthenticatorService"
android:exported="true"
@ -106,8 +106,7 @@
android:name=".contributions.ContributionsSyncService"
android:exported="true">
<intent-filter>
<action
android:name="android.content.SyncAdapter" />
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
@ -118,8 +117,7 @@
android:name=".modifications.ModificationsSyncService"
android:exported="true">
<intent-filter>
<action
android:name="android.content.SyncAdapter" />
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
@ -138,26 +136,24 @@
<provider
android:name=".contributions.ContributionsContentProvider"
android:label="@string/provider_contributions"
android:syncable="true"
android:authorities="fr.free.nrw.commons.contributions.contentprovider"
android:exported="false">
</provider>
android:exported="false"
android:label="@string/provider_contributions"
android:syncable="true" />
<provider
android:name=".modifications.ModificationsContentProvider"
android:label="@string/provider_modifications"
android:syncable="true"
android:authorities="fr.free.nrw.commons.modifications.contentprovider"
android:exported="false">
</provider>
android:exported="false"
android:label="@string/provider_modifications"
android:syncable="true" />
<provider
android:name=".category.CategoryContentProvider"
android:label="@string/provider_categories"
android:syncable="false"
android:authorities="fr.free.nrw.commons.categories.contentprovider"
android:exported="false">
</provider>
android:exported="false"
android:label="@string/provider_categories"
android:syncable="false" />
</application>

View file

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

View file

@ -1,19 +1,8 @@
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.SharedPreferences;
import android.content.pm.PackageManager;
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.stetho.Stetho;
@ -25,23 +14,23 @@ import org.acra.ReportingInteractionMode;
import org.acra.annotation.ReportsCrashes;
import java.io.File;
import java.io.IOException;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.HasActivityInjector;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.caching.CacheController;
import dagger.android.AndroidInjector;
import dagger.android.DaggerApplication;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.data.Category;
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.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.utils.FileUtils;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
// TODO: Use ProGuard to rip out reporting when publishing
@ -53,9 +42,13 @@ import timber.log.Timber;
resDialogCommentPrompt = R.string.crash_dialog_comment_prompt,
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_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 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";
@Inject DispatchingAndroidInjector<Activity> dispatchingActivityInjector;
@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 CommonsApplicationComponent component;
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
public void onCreate() {
super.onCreate();
Fresco.initialize(this);
if (setupLeakCanary() == RefWatcher.DISABLED) {
return;
}
Timber.plant(new Timber.DebugTree());
DaggerAppComponent
.builder()
.application(this)
.build()
.inject(this);
if (!BuildConfig.DEBUG) {
ACRA.init(this);
} else {
@ -150,11 +82,6 @@ public class CommonsApplication extends Application implements HasActivityInject
// Fire progress callbacks for every 3% of uploaded content
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
Fresco.initialize(this);
//For caching area -> categories
cacheData = new CacheController();
}
protected RefWatcher setupLeakCanary() {
@ -169,43 +96,18 @@ public class CommonsApplication extends Application implements HasActivityInject
return application.refWatcher;
}
/**
* @return Account|null
*/
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;
@Override
protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
return injector();
}
public Boolean revalidateAuthToken() {
AccountManager accountManager = AccountManager.get(this);
Account curAccount = getCurrentAccount();
if (curAccount == null) {
return false; // This should never happen
public CommonsApplicationComponent injector() {
if (component == null) {
component = DaggerCommonsApplicationComponent.builder()
.appModule(new CommonsApplicationModule(this))
.build();
}
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() {
PackageManager pm = getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)
|| pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
return component;
}
public void clearApplicationData(Context context, LogoutListener logoutListener) {
@ -220,67 +122,25 @@ public class CommonsApplication extends Application implements HasActivityInject
}
}
AccountManager accountManager = AccountManager.get(this);
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType());
AccountManagerCallback<Boolean> amCallback = new AccountManagerCallback<Boolean>() {
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) {
sessionManager.clearAllAccounts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
Timber.d("All accounts have been removed");
//TODO: fix preference manager
PreferenceManager.getDefaultSharedPreferences(getInstance())
.edit().clear().commit();
SharedPreferences preferences = context
.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();
defaultPrefs.edit().clear().commit();
applicationPrefs.edit().clear().commit();
applicationPrefs.edit().putBoolean("firstrun", false).apply();otherPrefs.edit().clear().commit();
updateAllDatabases();
currentAccount = null;
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.
*/
public void updateAllDatabases() {
DBOpenHelper dbOpenHelper = CommonsApplication.getInstance().getDBOpenHelper();
private void updateAllDatabases() {
dbOpenHelper.getReadableDatabase().close();
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Handler;
@ -9,7 +8,9 @@ import android.os.IBinder;
import android.os.Looper;
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 ServiceHandler threadHandler;
private String serviceName;

View file

@ -5,7 +5,9 @@ import android.content.res.Resources;
import android.support.annotation.Nullable;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
@ -19,7 +21,7 @@ public class LicenseList {
res = activity.getResources();
XmlPullParser parser = res.getXml(R.xml.wikimedia_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 template = parser.getAttributeValue(null, "template");
String url = parser.getAttributeValue(null, "url");
@ -60,4 +62,34 @@ public class LicenseList {
+ nameIdForTemplate(template), null, null);
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.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
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.
*/
public class MediaDataExtractor {
private final MediaWikiApi mediaWikiApi;
private boolean fetched;
private String filename;
private ArrayList<String> categories;
private Map<String, String> descriptions;
private String license;
private @Nullable LatLng coordinates;
private LicenseList licenseList;
/**
* @param filename of the target media object, should include 'File:' prefix
*/
public MediaDataExtractor(String filename, LicenseList licenseList) {
this.filename = filename;
categories = new ArrayList<>();
descriptions = new HashMap<>();
fetched = false;
this.licenseList = licenseList;
@Inject
public MediaDataExtractor(MediaWikiApi mwApi) {
this.categories = new ArrayList<>();
this.descriptions = new HashMap<>();
this.fetched = false;
this.mediaWikiApi = mwApi;
}
/**
/*
* Actually fetch the data over the network.
* todo: use local caching?
*
* Warning: synchronous i/o, call on a background thread
*/
public void fetch() throws IOException {
public void fetch(String filename, LicenseList licenseList) throws IOException {
if (fetched) {
throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again.");
}
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
MediaResult result = api.fetchMediaByFilename(filename);
MediaResult result = mediaWikiApi.fetchMediaByFilename(filename);
// In-page category links are extracted from source, as XML doesn't cover [[links]]
extractCategories(result.getWikiSource());
// Description template info is extracted from preprocessor XML
processWikiParseTree(result.getParseTreeXmlSource());
processWikiParseTree(result.getParseTreeXmlSource(), licenseList);
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;
try {
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> {
protected final Media media;
private MediaWikiApi mediaWikiApi;
public MediaThumbnailFetchTask(@NonNull Media media) {
public MediaThumbnailFetchTask(@NonNull Media media, MediaWikiApi mwApi) {
this.media = media;
this.mediaWikiApi = mwApi;
}
@Override
protected String doInBackground(String... params) {
try {
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
return api.findThumbnailByFilename(params[0]);
return mediaWikiApi.findThumbnailByFilename(params[0]);
} catch (Exception e) {
// Do something better!
}

View file

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

View file

@ -1,102 +1,26 @@
package fr.free.nrw.commons;
import android.content.Context;
import android.os.Build;
import android.preference.PreferenceManager;
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.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.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
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.TimeZone;
import java.util.regex.Matcher;
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 timber.log.Timber;
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.
* 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) {
String name = new PageTitle(filename).getPrefixedText();
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));
}
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) {
try {
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) {
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) {
switch (license) {
case Prefs.Licenses.CC_BY_3:
@ -217,51 +75,6 @@ public class Utils {
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) {
Pattern jpegPattern = Pattern.compile("\\.jpeg$", Pattern.CASE_INSENSITIVE);
@ -277,10 +90,6 @@ public class Utils {
return title;
}
public static boolean isNullOrWhiteSpace(String value) {
return value == null || value.trim().isEmpty();
}
public static boolean isDarkTheme(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("theme", false);
}

View file

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

View file

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

View file

@ -21,15 +21,19 @@ import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import dagger.android.AndroidInjection;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
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";
@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.signupButton) Button signupButton;
@BindView(R.id.loginUsername) EditText usernameEdit;
@ -47,11 +57,8 @@ public class LoginActivity extends AccountAuthenticatorActivity {
@BindView(R.id.loginTwoFactor) EditText twoFactorEdit;
@BindView(R.id.error_message_container) ViewGroup errorMessageContainer;
@BindView(R.id.error_message) TextView errorMessage;
private CommonsApplication app;
ProgressDialog progressDialog;
private AppCompatDelegate delegate;
private SharedPreferences prefs = null;
private LoginTextWatcher textWatcher = new LoginTextWatcher();
@Override
@ -59,16 +66,13 @@ public class LoginActivity extends AccountAuthenticatorActivity {
setTheme(Utils.isDarkTheme(this) ? R.style.DarkAppTheme : R.style.LightAppTheme);
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState);
AndroidInjection.inject(this);
super.onCreate(savedInstanceState);
app = CommonsApplication.getInstance();
setContentView(R.layout.activity_login);
ButterKnife.bind(this);
prefs = getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE);
usernameEdit.addTextChangedListener(textWatcher);
passwordEdit.addTextChangedListener(textWatcher);
twoFactorEdit.addTextChangedListener(textWatcher);
@ -91,7 +95,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
WelcomeActivity.startYourself(this);
prefs.edit().putBoolean("firstrun", false).apply();
}
if (app.getCurrentAccount() != null) {
if (sessionManager.getCurrentAccount() != null) {
startMainActivity();
}
}
@ -113,6 +117,25 @@ public class LoginActivity extends AccountAuthenticatorActivity {
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
protected void 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) {
errorMessage.setText(getString(resId));
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));

View file

@ -1,8 +1,8 @@
package fr.free.nrw.commons.auth;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.app.ProgressDialog;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
@ -11,22 +11,34 @@ import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.mwapi.EventLog;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
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> {
private LoginActivity loginActivity;
private String username;
private String password;
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.username = username;
this.password = password;
this.twoFactorCode = twoFactorCode;
app = CommonsApplication.getInstance();
this.accountUtil = accountUtil;
this.mwApi = mwApi;
this.prefs = prefs;
}
@Override
@ -44,9 +56,9 @@ class LoginTask extends AsyncTask<String, String, String> {
protected String doInBackground(String... params) {
try {
if (twoFactorCode.isEmpty()) {
return app.getMWApi().login(username, password);
return mwApi.login(username, password);
} else {
return app.getMWApi().login(username, password, twoFactorCode);
return mwApi.login(username, password, twoFactorCode);
}
} catch (IOException e) {
// Do something better!
@ -59,7 +71,7 @@ class LoginTask extends AsyncTask<String, String, String> {
super.onPostExecute(result);
Timber.d("Login done!");
EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT)
EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT, mwApi, prefs)
.param("username", username)
.param("result", result)
.log();
@ -79,16 +91,16 @@ class LoginTask extends AsyncTask<String, String, String> {
Bundle extras = loginActivity.getIntent().getExtras();
if (extras != null) {
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) {
Bundle authResult = new Bundle();
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username);
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, AccountUtil.accountType());
authResult.putString(KEY_ACCOUNT_NAME, username);
authResult.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
response.onResult(authResult);
}
}
AccountUtil.createAccount(response, username, password);
accountUtil.createAccount(response, username, password);
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 fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.theme.BaseActivity;
import timber.log.Timber;
@ -39,11 +38,8 @@ public class SignupActivity extends BaseActivity {
//Signup success, so clear cookies, notify user, and load LoginActivity again
Timber.d("Overriding URL %s", url);
Toast toast = Toast.makeText(
CommonsApplication.getInstance(),
"Account created!",
Toast.LENGTH_LONG
);
Toast toast = Toast.makeText(SignupActivity.this,
"Account created!", Toast.LENGTH_LONG);
toast.show();
// terminate on task completion.
finish();

View file

@ -13,7 +13,6 @@ import android.support.annotation.Nullable;
import java.io.IOException;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
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_MESSAGE;
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;
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);
this.context = context;
this.mediaWikiApi = mwApi;
}
private Bundle unsupportedOperation() {
@ -47,7 +49,7 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
}
private boolean supportedAccountType(@Nullable String type) {
return AccountUtil.accountType().equals(type);
return ACCOUNT_TYPE.equals(type);
}
@Override
@ -86,11 +88,10 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
}
private String getAuthCookie(String username, String password) throws IOException {
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
//TODO add 2fa support here
String result = api.login(username, password);
String result = mediaWikiApi.login(username, password);
if (result.equals("PASS")) {
return api.getAuthCookie();
return mediaWikiApi.getAuthCookie();
} else {
return null;
}
@ -115,7 +116,7 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
if (authCookie != null) {
final Bundle result = new Bundle();
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);
return result;
}

View file

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

View file

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

View file

@ -10,7 +10,9 @@ import android.net.Uri;
import android.support.annotation.NonNull;
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;
@ -36,17 +38,15 @@ public class CategoryContentProvider extends ContentProvider {
uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CATEGORIES_ID);
}
private DBOpenHelper dbOpenHelper;
public static Uri uriForId(int id) {
return Uri.parse(BASE_URI.toString() + "/" + id);
}
@SuppressWarnings("ConstantConditions")
@Inject DBOpenHelper dbOpenHelper;
@Override
public boolean onCreate() {
CommonsApplication app = ((CommonsApplication) getContext().getApplicationContext());
dbOpenHelper = app.getDBOpenHelper();
AndroidInjection.inject(this);
return false;
}

View file

@ -7,6 +7,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Parcel;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import java.text.SimpleDateFormat;
@ -16,7 +17,6 @@ import java.util.Locale;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.settings.Prefs;
public class Contribution extends Media {
@ -149,7 +149,7 @@ public class Contribution extends Media {
}
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(getTrackingTemplates());
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.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
@ -24,21 +23,20 @@ import android.widget.AdapterView;
import java.util.ArrayList;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.ButterKnife;
import dagger.android.AndroidInjection;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.UploadService;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
@ -49,11 +47,18 @@ 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.settings.Prefs.UPLOADS_SHOWING;
public class ContributionsActivity extends AuthenticatedActivity
implements LoaderManager.LoaderCallbacks<Cursor>, AdapterView.OnItemClickListener,
MediaDetailPagerFragment.MediaDetailProvider, FragmentManager.OnBackStackChangedListener,
public class ContributionsActivity
extends AuthenticatedActivity
implements LoaderManager.LoaderCallbacks<Cursor>,
AdapterView.OnItemClickListener,
MediaDetailPagerFragment.MediaDetailProvider,
FragmentManager.OnBackStackChangedListener,
ContributionsListFragment.SourceRefresher {
@Inject MediaWikiApi mediaWikiApi;
@Inject SessionManager sessionManager;
@Inject @Named("default_preferences") SharedPreferences prefs;
private Cursor allContributions;
private ContributionsListFragment contributionsList;
private MediaDetailPagerFragment mediaDetails;
@ -62,9 +67,6 @@ public class ContributionsActivity extends AuthenticatedActivity
private ArrayList<DataSetObserver> observersWaitingForLoad = new ArrayList<>();
private String CONTRIBUTION_SELECTION = "";
@Inject
MediaWikiApi mediaWikiApi;
/*
This sorts in the following order:
Currently Uploading
@ -109,12 +111,8 @@ public class ContributionsActivity extends AuthenticatedActivity
@Override
protected void onResume() {
super.onResume();
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
boolean isSettingsChanged =
sharedPreferences.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
editor.apply();
boolean isSettingsChanged = prefs.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
prefs.edit().putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false).apply();
if (isSettingsChanged) {
refreshSource();
}
@ -123,8 +121,7 @@ public class ContributionsActivity extends AuthenticatedActivity
@Override
protected void onAuthCookieAcquired(String authCookie) {
// Do a sync everytime we get here!
CommonsApplication app = ((CommonsApplication) getApplication());
requestSync(app.getCurrentAccount(), AUTHORITY, new Bundle());
requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.AUTHORITY, new Bundle());
Intent uploadServiceIntent = new Intent(this, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
startService(uploadServiceIntent);
@ -139,7 +136,6 @@ public class ContributionsActivity extends AuthenticatedActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AndroidInjection.inject(this);
setContentView(R.layout.activity_contributions);
ButterKnife.bind(this);
@ -241,8 +237,7 @@ public class ContributionsActivity extends AuthenticatedActivity
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
int uploads = sharedPref.getInt(UPLOADS_SHOWING, 100);
int uploads = prefs.getInt(UPLOADS_SHOWING, 100);
return new CursorLoader(this, BASE_URI,
ALL_FIELDS, CONTRIBUTION_SELECTION, null,
CONTRIBUTION_SORT + "LIMIT " + uploads);
@ -289,9 +284,8 @@ public class ContributionsActivity extends AuthenticatedActivity
@SuppressWarnings("ConstantConditions")
private void setUploadCount() {
CommonsApplication app = ((CommonsApplication) getApplication());
Disposable uploadCountDisposable = mediaWikiApi
.getUploadCount(app.getCurrentAccount().name)
compositeDisposable.add(mediaWikiApi
.getUploadCount(sessionManager.getCurrentAccount().name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
@ -299,8 +293,7 @@ public class ContributionsActivity extends AuthenticatedActivity
.getQuantityString(R.plurals.contributions_subtitle,
uploadCount, uploadCount)),
t -> Timber.e(t, "Fetching upload count failed")
);
compositeDisposable.add(uploadCountDisposable);
));
}
@Override

View file

@ -10,7 +10,10 @@ import android.net.Uri;
import android.support.annotation.NonNull;
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 static android.content.UriMatcher.NO_MATCH;
@ -36,9 +39,12 @@ public class ContributionsContentProvider extends ContentProvider {
return Uri.parse(BASE_URI.toString() + "/" + id);
}
@Inject DBOpenHelper dbOpenHelper;
@Override
public boolean onCreate() {
return false;
AndroidInjection.inject(this);
return true;
}
@SuppressWarnings("ConstantConditions")
@ -50,8 +56,7 @@ public class ContributionsContentProvider extends ContentProvider {
int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
SQLiteDatabase db = app.getDBOpenHelper().getReadableDatabase();
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor;
switch (uriType) {
@ -87,9 +92,8 @@ public class ContributionsContentProvider extends ContentProvider {
@Override
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
long id;
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id = 0;
switch (uriType) {
case CONTRIBUTIONS:
id = sqlDB.insert(TABLE_NAME, null, contentValues);
@ -107,13 +111,12 @@ public class ContributionsContentProvider extends ContentProvider {
int rows;
int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
switch (uriType) {
case CONTRIBUTIONS_ID:
Timber.d("Deleting contribution id %s", uri.getLastPathSegment());
rows = sqlDB.delete(TABLE_NAME,
rows = db.delete(TABLE_NAME,
"_id = ?",
new String[]{uri.getLastPathSegment()}
);
@ -130,8 +133,7 @@ public class ContributionsContentProvider extends ContentProvider {
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (ContributionsContentProvider)");
int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
sqlDB.beginTransaction();
switch (uriType) {
case CONTRIBUTIONS:
@ -162,9 +164,8 @@ public class ContributionsContentProvider extends ContentProvider {
error out otherwise.
*/
int uriType = uriMatcher.match(uri);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase();
int rowsUpdated;
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated = 0;
switch (uriType) {
case CONTRIBUTIONS:
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.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
@ -22,9 +20,12 @@ import android.widget.ListAdapter;
import android.widget.ProgressBar;
import android.widget.TextView;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
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.nearby.NearbyActivity;
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.WRITE_EXTERNAL_STORAGE;
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.view.View.GONE;
public class ContributionsListFragment extends Fragment {
public class ContributionsListFragment extends DaggerFragment {
@BindView(R.id.contributionsList)
GridView contributionsList;
@ -45,6 +45,9 @@ public class ContributionsListFragment extends Fragment {
@BindView(R.id.loadingContributionsProgressBar)
ProgressBar progressBar;
@Inject @Named("prefs") SharedPreferences prefs;
@Inject @Named("default_preferences") SharedPreferences defaultPrefs;
private ContributionController controller;
@Override
@ -59,7 +62,6 @@ public class ContributionsListFragment extends Fragment {
}
//TODO: Should this be in onResume?
SharedPreferences prefs = getActivity().getSharedPreferences("prefs", MODE_PRIVATE);
String lastModified = prefs.getString("lastSyncTimestamp", "");
Timber.d("Last Sync Timestamp: %s", lastModified);
@ -162,9 +164,7 @@ public class ContributionsListFragment extends Fragment {
return true;
case R.id.menu_from_camera:
SharedPreferences sharedPref = PreferenceManager
.getDefaultSharedPreferences(CommonsApplication.getInstance());
boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true);
boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) {
// Here, thisActivity is the current activity
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
inflater.inflate(R.menu.fragment_contributions_list, menu);
CommonsApplication app = (CommonsApplication) getContext().getApplicationContext();
if (!app.deviceHasCamera()) {
if (!deviceHasCamera()) {
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
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

View file

@ -13,9 +13,15 @@ import android.os.RemoteException;
import android.text.TextUtils;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
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.Utils;
@ -23,11 +29,11 @@ import fr.free.nrw.commons.mwapi.LogEventResult;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
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.Table.COLUMN_FILENAME;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
@SuppressWarnings("WeakerAccess")
public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
private static final String[] existsQuery = {COLUMN_FILENAME};
@ -35,6 +41,10 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
private static final ContentValues[] EMPTY = {};
private static int COMMIT_THRESHOLD = 10;
@SuppressWarnings("WeakerAccess")
@Inject MediaWikiApi mwApi;
@Inject @Named("prefs") SharedPreferences prefs;
public ContributionsSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
}
@ -71,10 +81,9 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
@Override
public void onPerformSync(Account account, Bundle bundle, String authority,
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!
String user = account.name;
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
SharedPreferences prefs = getContext().getSharedPreferences("prefs", MODE_PRIVATE);
String lastModified = prefs.getString("lastSyncTimestamp", "");
Date curTime = new Date();
LogEventResult result;
@ -83,7 +92,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
while (!done) {
try {
result = api.logEvents(user, lastModified, queryContinue, getLimit());
result = mwApi.logEvents(user, lastModified, queryContinue, getLimit());
} catch (IOException e) {
// There isn't really much we can do, eh?
// FIXME: Perhaps add EventLogging?
@ -137,8 +146,13 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
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!");
}
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;
import android.location.Location;
import android.support.annotation.NonNull;
public class LatLng {
private final double latitude;
@ -22,6 +25,10 @@ public class LatLng {
this.accuracy = accuracy;
}
public static LatLng from(@NonNull Location location) {
return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy());
}
public int hashCode() {
boolean var1 = true;
byte var2 = 1;

View file

@ -1,11 +1,15 @@
package fr.free.nrw.commons.location;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.location.Criteria;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@ -15,61 +19,136 @@ import javax.inject.Singleton;
import timber.log.Timber;
@Singleton
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 LatLng lastLocation;
private Float latestLocationAccuracy;
private Location lastLocation;
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
private boolean isLocationManagerRegistered = false;
@Inject
public LocationServiceManager(Context context) {
this.context = context;
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
provider = locationManager.getBestProvider(new Criteria(), true);
}
public boolean isProviderEnabled() {
return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
}
public LatLng getLastLocation() {
return lastLocation;
public boolean isLocationPermissionGranted() {
return ContextCompat.checkSelfPermission(context,
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}
/**
* Returns the accuracy of the location. The measurement is
* given as a radius in meter of 68 % confidence.
*
* @return Float
*/
public Float getLatestLocationAccuracy() {
return latestLocationAccuracy;
public void requestPermissions(Activity activity) {
if (activity.isFinishing()) {
return;
}
ActivityCompat.requestPermissions(activity,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
LOCATION_REQUEST);
}
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.
*/
public void registerLocationManager() {
try {
locationManager.requestLocationUpdates(provider, 400, 1, this);
Location location = locationManager.getLastKnownLocation(provider);
//Location works, just need to 'send' GPS coords
// via emulator extended controls if testing on emulator
Timber.d("Checking for location...");
if (location != null) {
this.onLocationChanged(location);
if (!isLocationManagerRegistered)
isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
&& requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
}
private boolean requestLocationUpdatesFromProvider(String locationProvider) {
try {
locationManager.requestLocationUpdates(locationProvider,
MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS,
MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS,
this);
return true;
} catch (IllegalArgumentException e) {
Timber.e(e, "Illegal argument exception");
return false;
} catch (SecurityException e) {
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.
*/
public void unregisterLocationManager() {
isLocationManagerRegistered = false;
try {
locationManager.removeUpdates(this);
} catch (SecurityException e) {
@ -89,15 +168,11 @@ public class LocationServiceManager implements LocationListener {
@Override
public void onLocationChanged(Location location) {
double currentLatitude = location.getLatitude();
double currentLongitude = location.getLongitude();
latestLocationAccuracy = location.getAccuracy();
Timber.d("Latitude: %f Longitude: %f Accuracy %f",
currentLatitude, currentLongitude, latestLocationAccuracy);
lastLocation = new LatLng(currentLatitude, currentLongitude, latestLocationAccuracy);
if (isBetterLocation(location, lastLocation)) {
lastLocation = location;
for (LocationUpdateListener listener : locationListeners) {
listener.onLocationChanged(lastLocation);
listener.onLocationChanged(LatLng.from(lastLocation));
}
}
}

View file

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

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.media;
import android.annotation.SuppressLint;
import android.app.DownloadManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Build;
@ -24,12 +25,18 @@ import android.view.MenuItem;
import android.view.View;
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.Media;
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.ContributionsActivity;
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.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 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 Boolean editable;
@ -101,8 +112,8 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
case R.id.menu_share_current_image:
// Share - this is just logs it, intent set in onCreateOptionsMenu, around line 252
CommonsApplication app = (CommonsApplication) getActivity().getApplication();
EventLog.schema(EVENT_SHARE_ATTEMPT)
.param("username", app.getCurrentAccount().name)
EventLog.schema(EVENT_SHARE_ATTEMPT, mwApi, prefs)
.param("username", sessionManager.getCurrentAccount().name)
.param("filename", m.getFilename())
.log();
return true;
@ -161,9 +172,7 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa
req.allowScanningByMediaScanner();
req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !(ContextCompat.checkSelfPermission(getContext(),
READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !(ContextCompat.checkSelfPermission(getContext(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) {
Snackbar.make(getView(), R.string.read_storage_permission_rationale,
Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok,
view -> ActivityCompat.requestPermissions(getActivity(),

View file

@ -10,7 +10,10 @@ import android.net.Uri;
import android.support.annotation.NonNull;
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;
public class ModificationsContentProvider extends ContentProvider {
@ -33,10 +36,12 @@ public class ModificationsContentProvider extends ContentProvider {
return Uri.parse(BASE_URI.toString() + "/" + id);
}
@Inject DBOpenHelper dbOpenHelper;
@Override
public boolean onCreate() {
return false;
AndroidInjection.inject(this);
return true;
}
@Override
@ -53,7 +58,7 @@ public class ModificationsContentProvider extends ContentProvider {
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.setNotificationUri(getContext().getContentResolver(), uri);
@ -69,7 +74,7 @@ public class ModificationsContentProvider extends ContentProvider {
@Override
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id = 0;
switch (uriType) {
case MODIFICATIONS:
@ -85,7 +90,7 @@ public class ModificationsContentProvider extends ContentProvider {
@Override
public int delete(@NonNull Uri uri, String s, String[] strings) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
switch (uriType) {
case MODIFICATIONS_ID:
String id = uri.getLastPathSegment();
@ -103,7 +108,7 @@ public class ModificationsContentProvider extends ContentProvider {
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (ModificationsContentProvider)");
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
sqlDB.beginTransaction();
switch (uriType) {
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.
*/
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase();
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated = 0;
switch (uriType) {
case MODIFICATIONS:

View file

@ -14,8 +14,9 @@ import android.os.RemoteException;
import java.io.IOException;
import javax.inject.Inject;
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.ContributionsContentProvider;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
@ -23,6 +24,8 @@ import timber.log.Timber;
public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
@Inject MediaWikiApi mwApi;
public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
}
@ -30,6 +33,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
@Override
public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) {
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
((CommonsApplication)getContext().getApplicationContext()).injector().inject(this);
Cursor allModifications;
try {
@ -54,17 +58,16 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
return;
}
if (Utils.isNullOrWhiteSpace(authCookie)) {
if (isNullOrWhiteSpace(authCookie)) {
Timber.d("Could not authenticate :(");
return;
}
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
api.setAuthCookie(authCookie);
mwApi.setAuthCookie(authCookie);
String editToken;
try {
editToken = api.getEditToken();
editToken = mwApi.getEditToken();
} catch (IOException e) {
Timber.d("Can not retreive edit token!");
return;
@ -95,7 +98,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
if (contrib.getState() == Contribution.STATE_COMPLETED) {
String pageContent;
try {
pageContent = api.revisionsByFilename(contrib.getFilename());
pageContent = mwApi.revisionsByFilename(contrib.getFilename());
} catch (IOException e) {
Timber.d("Network fuckup on modifications sync!");
continue;
@ -106,7 +109,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
String editResult;
try {
editResult = api.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
editResult = mwApi.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
} catch (IOException e) {
Timber.d("Network fuckup on modifications sync!");
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.InputStream;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@ -34,7 +36,6 @@ import java.util.concurrent.Callable;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.Utils;
import in.yuvi.http.fluent.Http;
import io.reactivex.Observable;
import io.reactivex.Single;
@ -335,7 +336,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
logEvents.add(new LogEventResult.LogEvent(
image.getString("@pageid"),
image.getString("@title"),
Utils.parseMWDate(image.getString("@timestamp")))
parseMWDate(image.getString("@timestamp")))
);
}
return logEvents;
@ -402,7 +403,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
String errorCode = result.getString("/api/error/@code");
return new UploadResult(resultStatus, errorCode);
} 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 imageUrl = result.getString("/api/upload/imageinfo/@url");
return new UploadResult(resultStatus, dateUploaded, canonicalFilename, imageUrl);
@ -428,4 +429,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
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;
import android.content.SharedPreferences;
import android.os.Build;
import fr.free.nrw.commons.Utils;
@ -15,14 +16,14 @@ public class EventLog {
}
}
private static LogBuilder schema(String schema, long revision) {
return new LogBuilder(schema, revision);
private static LogBuilder schema(String schema, long revision, MediaWikiApi mwApi, SharedPreferences prefs) {
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) {
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.os.AsyncTask;
import android.os.Build;
import android.preference.PreferenceManager;
import org.json.JSONException;
import org.json.JSONObject;
@ -12,19 +11,23 @@ import java.net.MalformedURLException;
import java.net.URL;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.settings.Prefs;
@SuppressWarnings("WeakerAccess")
public class LogBuilder {
private JSONObject data;
private long rev;
private String schema;
private final MediaWikiApi mwApi;
private final JSONObject data;
private final long rev;
private final String schema;
private final SharedPreferences prefs;
LogBuilder(String schema, long revision) {
data = new JSONObject();
LogBuilder(String schema, long revision, MediaWikiApi mwApi, SharedPreferences prefs) {
this.prefs = prefs;
this.data = new JSONObject();
this.schema = schema;
this.rev = revision;
this.mwApi = mwApi;
}
public LogBuilder param(String key, Object value) {
@ -56,11 +59,10 @@ public class LogBuilder {
// Use *only* for tracking the user preference change for EventLogging
// Attempting to use anywhere else will cause kitten explosions
public void log(boolean force) {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(CommonsApplication.getInstance());
if (!settings.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
if (!prefs.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) {
return; // User has disabled tracking
}
LogTask logTask = new LogTask();
LogTask logTask = new LogTask(mwApi);
logTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this);
}

View file

@ -2,11 +2,16 @@ package fr.free.nrw.commons.mwapi;
import android.os.AsyncTask;
import fr.free.nrw.commons.CommonsApplication;
class LogTask extends AsyncTask<LogBuilder, Void, Boolean> {
private final MediaWikiApi mwApi;
public LogTask(MediaWikiApi mwApi) {
this.mwApi = mwApi;
}
@Override
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;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@ -10,10 +9,8 @@ import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.view.Menu;
import android.view.MenuInflater;
@ -31,8 +28,6 @@ import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import dagger.android.AndroidInjection;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
@ -46,16 +41,22 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUEST;
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener {
@BindView(R.id.progressBar)
ProgressBar progressBar;
private static final int LOCATION_REQUEST = 1;
private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed";
@BindView(R.id.progressBar)
ProgressBar progressBar;
@Inject
LocationServiceManager locationManager;
@Inject
NearbyController nearbyController;
private LatLng curLatLang;
private Bundle bundle;
private SharedPreferences sharedPreferences;
@ -66,11 +67,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AndroidInjection.inject(this);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
setContentView(R.layout.activity_nearby);
ButterKnife.bind(this);
checkLocationPermission();
bundle = new Bundle();
initDrawer();
initViewState();
@ -102,7 +101,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
// Handle item selection
switch (item.getItemId()) {
case R.id.action_refresh:
lockNearbyView = false;
lockNearbyView(false);
refreshView(true);
return true;
case R.id.action_toggle_view:
@ -115,52 +114,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
}
}
private void checkLocationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(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);
private void requestLocationPermissions() {
if (!isFinishing()) {
locationManager.requestPermissions(this);
}
}
@ -185,7 +141,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
.setCancelable(false)
.setPositiveButton(R.string.give_permission, (dialog, which) -> {
//will ask for the location permission again
checkLocationPermission();
checkGps();
})
.setNegativeButton(R.string.cancel, (dialog, which) -> {
//dismiss dialog and finish activity
@ -209,11 +165,48 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
Timber.d("Loaded settings page");
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()
.show();
} else {
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
protected void onStart() {
super.onStart();
locationManager.registerLocationManager();
locationManager.addLocationListener(this);
}
@ -262,13 +254,18 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
super.onResume();
lockNearbyView = false;
checkGps();
refreshView(false);
}
/**
* This method should be the single point to load/refresh nearby places
*
* @param isHardRefresh
*/
private void refreshView(boolean isHardRefresh) {
if (lockNearbyView) {
return;
}
locationManager.registerLocationManager();
LatLng lastLocation = locationManager.getLastLocation();
if (curLatLang != null && curLatLang.equals(lastLocation)) { //refresh view only if location has changed
if (isHardRefresh) {
@ -284,20 +281,14 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
}
progressBar.setVisibility(View.VISIBLE);
setupPlaceList(this);
}
private void setupPlaceList(Context context) {
placesDisposable = Observable.fromCallable(() -> NearbyController
.loadAttractionsFromLocation(curLatLang, CommonsApplication.getInstance()))
placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLang, this))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((result) -> {
populatePlaces(context, result);
});
.subscribe(this::populatePlaces);
}
private void populatePlaces(Context context, List<Place> placeList) {
private void populatePlaces(List<Place> placeList) {
Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriSerializer())
.create();
@ -306,7 +297,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
if (placeList.size() == 0) {
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();
}
@ -314,7 +305,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
bundle.putString("PlaceList", gsonPlaceList);
bundle.putString("CurLatLng", gsonCurLatLng);
lockNearbyView = true;
lockNearbyView(true);
// Begin the transaction
if (viewMode.isMap()) {
setMapFragment();
@ -325,6 +316,18 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
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() {
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
@ -338,7 +341,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
Fragment fragment = new NearbyMapFragment();
fragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container, fragment);
fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName());
fragmentTransaction.commitAllowingStateLoss();
}
@ -349,15 +352,10 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
Fragment fragment = new NearbyListFragment();
fragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container, fragment);
fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName());
fragmentTransaction.commitAllowingStateLoss();
}
public static void startYourself(Context context) {
Intent settingsIntent = new Intent(context, NearbyActivity.class);
context.startActivity(settingsIntent);
}
@Override
public void onLocationChanged(LatLng latLng) {
refreshView(false);

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.nearby;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.preference.PreferenceManager;
import android.support.graphics.drawable.VectorDrawableCompat;
import com.mapbox.mapboxsdk.annotations.IconFactory;
@ -15,7 +14,9 @@ import java.util.List;
import java.util.Locale;
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.location.LatLng;
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.formatDistanceBetween;
public class NearbyController {
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.
@ -34,13 +43,11 @@ public class NearbyController {
* @param context context
* @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);
if (curLatLng == null) {
return Collections.emptyList();
}
NearbyPlaces nearbyPlaces = CommonsApplication.getInstance().getNearbyPlaces();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
List<Place> places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage());
if (curLatLng != null) {
Timber.d("Sorting places by distance...");

View file

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

View file

@ -1,19 +1,19 @@
package fr.free.nrw.commons.nearby;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import butterknife.ButterKnife;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R;
import timber.log.Timber;
/**
* 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() {
}

View file

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

View file

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

View file

@ -3,18 +3,17 @@ package fr.free.nrw.commons.theme;
import android.content.Intent;
import android.os.Bundle;
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.Utils;
public class BaseActivity extends AppCompatActivity {
public abstract class BaseActivity extends DaggerAppCompatActivity {
boolean currentTheme;
@Override
protected void onCreate(Bundle savedInstanceState) {
if (Utils.isDarkTheme(this)) {
boolean currentThemeIsDark = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false);
if (currentThemeIsDark){
currentTheme = true;
setTheme(R.style.DarkAppTheme);
} else {

View file

@ -62,10 +62,10 @@ public abstract class NavigationBaseActivity extends BaseActivity
private void setUserName() {
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);
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType());
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.ACCOUNT_TYPE);
if (allAccounts.length != 0) {
username.setText(allAccounts[0].name);
}

View file

@ -1,12 +1,13 @@
package fr.free.nrw.commons.ui.widget;
import android.content.Context;
import android.os.Build;
import android.support.v7.widget.AppCompatTextView;
import android.text.Html;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.util.AttributeSet;
import fr.free.nrw.commons.Utils;
/**
* An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any
* links clickable.
@ -17,10 +18,25 @@ public class HtmlTextView extends AppCompatTextView {
super(context, attrs);
setMovementMethod(LinkMovementMethod.getInstance());
setText(Utils.fromHtml(getText().toString()));
setText(fromHtml(getText().toString()));
}
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 fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsActivity;
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
*/
public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
interface Callback {
void onResult(Result result);
}
@ -28,14 +28,16 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
DUPLICATE_CANCELLED
}
private final MediaWikiApi api;
private final String fileSha1;
private final Context context;
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.context = context;
this.callback = callback;
this.api = mwApi;
}
@Override
@ -45,7 +47,6 @@ public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
@Override
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
boolean fileExists;

View file

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

View file

@ -6,6 +6,7 @@ import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.DataSetObserver;
import android.net.Uri;
@ -24,11 +25,15 @@ import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
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.OnCategoriesSaveHandler;
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.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.EventLog;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber;
public class MultipleShareActivity
extends AuthenticatedActivity
public class MultipleShareActivity extends AuthenticatedActivity
implements MediaDetailPagerFragment.MediaDetailProvider,
AdapterView.OnItemClickListener,
FragmentManager.OnBackStackChangedListener,
MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
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 MultipleUploadListFragment uploadsList;
private MediaDetailPagerFragment mediaDetails;
private CategorizationFragment categorizationFragment;
private UploadController uploadController;
@Override
public Media getMediaAtPosition(int i) {
return photosList.get(i);
@ -132,11 +140,7 @@ public class MultipleShareActivity
dialog.setProgress(uploadCount);
if (uploadCount == photosList.size()) {
dialog.dismiss();
Toast startingToast = Toast.makeText(
CommonsApplication.getInstance(),
R.string.uploading_started,
Toast.LENGTH_LONG
);
Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG);
startingToast.show();
}
});
@ -176,9 +180,9 @@ public class MultipleShareActivity
}
// FIXME: Make sure that the content provider is up
// This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
ContentResolver.setSyncAutomatically(app.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT)
.param("username", app.getCurrentAccount().name)
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT, mwApi, prefs)
.param("username", sessionManager.getCurrentAccount().name)
.param("categories-count", categories.size())
.param("files-count", photosList.size())
.param("source", Contribution.SOURCE_EXTERNAL)
@ -202,10 +206,8 @@ public class MultipleShareActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
uploadController = new UploadController();
setContentView(R.layout.activity_multiple_uploads);
app = CommonsApplication.getInstance();
ButterKnife.bind(this);
initDrawer();
@ -245,7 +247,7 @@ public class MultipleShareActivity
@Override
protected void onAuthCookieAcquired(String authCookie) {
app.getMWApi().setAuthCookie(authCookie);
mwApi.setAuthCookie(authCookie);
Intent intent = getIntent();
if (intent.getAction().equals(Intent.ACTION_SEND_MULTIPLE)) {
@ -288,16 +290,16 @@ public class MultipleShareActivity
public void onBackPressed() {
super.onBackPressed();
if (categorizationFragment != null && categorizationFragment.isVisible()) {
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT)
.param("username", app.getCurrentAccount().name)
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT, mwApi, prefs)
.param("username", sessionManager.getCurrentAccount().name)
.param("categories-count", categorizationFragment.getCurrentSelectedCount())
.param("files-count", photosList.size())
.param("source", Contribution.SOURCE_EXTERNAL)
.param("result", "cancelled")
.log();
} else {
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT)
.param("username", app.getCurrentAccount().name)
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT, mwApi, prefs)
.param("username", sessionManager.getCurrentAccount().name)
.param("source", getIntent().getStringExtra(UploadService.EXTRA_SOURCE))
.param("multiple", true)
.param("result", "cancelled")
@ -307,11 +309,7 @@ public class MultipleShareActivity
@Override
public void onBackStackChanged() {
if (mediaDetails != null && mediaDetails.isVisible()) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
} else {
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible()) ;
}
}

View file

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

View file

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

View file

@ -30,14 +30,21 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
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.SessionManager;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
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.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.EventLog;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber;
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 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 mimeType;
@ -75,8 +87,6 @@ public class ShareActivity
private Contribution contribution;
private SimpleDraweeView backgroundImageView;
private UploadController uploadController;
private boolean cacheFound;
private GPSExtractor imageObj;
@ -117,7 +127,7 @@ public class ShareActivity
@RequiresApi(16)
private boolean needsToRequestStoragePermission() {
// 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.
return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri)
&& (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
@ -127,16 +137,12 @@ public class ShareActivity
private void uploadBegins() {
getFileMetadata(locationPermitted);
Toast startingToast = Toast.makeText(
CommonsApplication.getInstance(),
R.string.uploading_started,
Toast.LENGTH_LONG
);
Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG);
startingToast.show();
if (!cacheFound) {
//Has to be called after apiCall.request()
app.getCacheData().cacheCategory();
cacheController.cacheCategory();
Timber.d("Cache the categories found");
}
@ -168,10 +174,10 @@ public class ShareActivity
// FIXME: Make sure that the content provider is up
// This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
ContentResolver.setSyncAutomatically(app.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default!
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT)
.param("username", app.getCurrentAccount().name)
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT, mwApi, prefs)
.param("username", sessionManager.getCurrentAccount().name)
.param("categories-count", categories.size())
.param("files-count", 1)
.param("source", contribution.getSource())
@ -192,16 +198,16 @@ public class ShareActivity
public void onBackPressed() {
super.onBackPressed();
if (categorizationFragment != null && categorizationFragment.isVisible()) {
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT)
.param("username", app.getCurrentAccount().name)
EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT, mwApi, prefs)
.param("username", sessionManager.getCurrentAccount().name)
.param("categories-count", categorizationFragment.getCurrentSelectedCount())
.param("files-count", 1)
.param("source", contribution.getSource())
.param("result", "cancelled")
.log();
} else {
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT)
.param("username", app.getCurrentAccount().name)
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT, mwApi, prefs)
.param("username", sessionManager.getCurrentAccount().name)
.param("source", getIntent().getStringExtra(UploadService.EXTRA_SOURCE))
.param("multiple", true)
.param("result", "cancelled")
@ -211,8 +217,7 @@ public class ShareActivity
@Override
protected void onAuthCookieAcquired(String authCookie) {
app.getMWApi().setAuthCookie(authCookie);
mwApi.setAuthCookie(authCookie);
}
@Override
@ -225,11 +230,10 @@ public class ShareActivity
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
uploadController = new UploadController();
setContentView(R.layout.activity_share);
ButterKnife.bind(this);
initBack();
app = CommonsApplication.getInstance();
backgroundImageView = (SimpleDraweeView) findViewById(R.id.backgroundImage);
backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder
.newInstance(getResources())
@ -379,7 +383,7 @@ public class ShareActivity
try {
InputStream inputStream = getContentResolver().openInputStream(mediaUri);
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);
ExistingFileAsync fileAsyncTask =
@ -387,7 +391,7 @@ public class ShareActivity
Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
duplicateCheckPassed = (result == DUPLICATE_PROCEED
|| result == NO_DUPLICATE);
});
}, mwApi);
fileAsyncTask.execute();
} catch (IOException e) {
Timber.d(e, "IO Exception: ");
@ -424,9 +428,7 @@ public class ShareActivity
ParcelFileDescriptor descriptor
= getContentResolver().openFileDescriptor(mediaUri, "r");
if (descriptor != null) {
SharedPreferences sharedPref = PreferenceManager
.getDefaultSharedPreferences(CommonsApplication.getInstance());
boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true);
boolean useExtStorage = prefs.getBoolean("useExternalStorage", true);
if (useExtStorage) {
copyPath = Environment.getExternalStorageDirectory().toString()
+ "/CommonsApp/" + new Date().getTime() + ".jpg";
@ -467,12 +469,12 @@ public class ShareActivity
= getContentResolver().openFileDescriptor(mediaUri, "r");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (descriptor != null) {
imageObj = new GPSExtractor(descriptor.getFileDescriptor());
imageObj = new GPSExtractor(descriptor.getFileDescriptor(), this, prefs);
}
} else {
String filePath = getPathOfMediaOrCopy();
if (filePath != null) {
imageObj = new GPSExtractor(filePath);
imageObj = new GPSExtractor(filePath, this, prefs);
}
}
}
@ -499,12 +501,12 @@ public class ShareActivity
if (imageObj.imageCoordsExists) {
double decLongitude = imageObj.getDecLongitude();
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();
// 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);
}
// 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.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
import android.text.TextWatcher;
@ -28,11 +28,15 @@ import android.widget.TextView;
import java.util.ArrayList;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.OnItemSelected;
import butterknife.OnTouch;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
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_UP;
public class SingleUploadFragment extends Fragment {
public class SingleUploadFragment extends DaggerFragment {
@BindView(R.id.titleEdit) EditText titleEdit;
@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.licenseSpinner) Spinner licenseSpinner;
private SharedPreferences prefs;
@Inject @Named("default_preferences") SharedPreferences prefs;
private String license;
private OnUploadActionInitiated uploadActionInitiatedHandler;
private TitleTextWatcher textWatcher = new TitleTextWatcher();
@ -72,11 +77,10 @@ public class SingleUploadFragment extends Fragment {
String desc = descEdit.getText().toString();
//Save the title/desc in short-lived cache so next time this fragment is loaded, we can access these
SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity());
SharedPreferences.Editor editor = titleDesc.edit();
editor.putString("Title", title);
editor.putString("Desc", desc);
editor.apply();
prefs.edit()
.putString("Title", title)
.putString("Desc", desc)
.apply();
uploadActionInitiatedHandler.uploadActionInitiated(title, desc);
return true;
@ -91,7 +95,6 @@ public class SingleUploadFragment extends Fragment {
View rootView = inflater.inflate(R.layout.fragment_single_upload, container, false);
ButterKnife.bind(this, rootView);
ArrayList<String> licenseItems = new ArrayList<>();
licenseItems.add(getString(R.string.license_name_cc0));
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_sa_four));
prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
// check if this is the first time we have uploaded
@ -172,9 +174,9 @@ public class SingleUploadFragment extends Fragment {
}
setLicenseSummary(license);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(Prefs.DEFAULT_LICENSE, license);
editor.commit();
prefs.edit()
.putString(Prefs.DEFAULT_LICENSE, license)
.commit();
}
@OnTouch(R.id.share_license_summary)
@ -182,7 +184,7 @@ public class SingleUploadFragment extends Fragment {
if (motionEvent.getActionMasked() == ACTION_DOWN) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(Utils.licenseUrlFor(license)));
intent.setData(Uri.parse(licenseUrlFor(license)));
startActivity(intent);
return true;
} else {
@ -193,9 +195,8 @@ public class SingleUploadFragment extends Fragment {
@OnClick(R.id.titleDescButton)
void setTitleDescButton() {
//Retrieve last title and desc entered
SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity());
String title = titleDesc.getString("Title", "");
String desc = titleDesc.getString("Desc", "");
String title = prefs.getString("Title", "");
String desc = prefs.getString("Desc", "");
Timber.d("Title: %s, Desc: %s", title, desc);
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 {
void uploadActionInitiated(String title, String description);
}

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.upload;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
@ -9,31 +10,36 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.text.TextUtils;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.concurrent.Executors;
import fr.free.nrw.commons.CommonsApplication;
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.settings.Prefs;
import timber.log.Timber;
public class UploadController {
private UploadService uploadService;
private final CommonsApplication app;
private SessionManager sessionManager;
private Context context;
private SharedPreferences prefs;
public interface ContributionUploadProgress {
void onUploadStarted(Contribution contribution);
}
public UploadController() {
app = CommonsApplication.getInstance();
public UploadController(SessionManager sessionManager, Context context, SharedPreferences sharedPreferences) {
this.sessionManager = sessionManager;
this.context = context;
this.prefs = sharedPreferences;
}
private boolean isUploadServiceConnected;
@ -52,15 +58,15 @@ public class UploadController {
};
public void prepareService() {
Intent uploadServiceIntent = new Intent(app, UploadService.class);
Intent uploadServiceIntent = new Intent(context, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
app.startService(uploadServiceIntent);
app.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
context.startService(uploadServiceIntent);
context.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
}
public void cleanup() {
if (isUploadServiceConnected) {
app.unbindService(uploadServiceConnection);
context.unbindService(uploadServiceConnection);
}
}
@ -68,7 +74,9 @@ public class UploadController {
Contribution contribution;
//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.setSource(source);
@ -78,12 +86,9 @@ public class UploadController {
}
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
//Set creator, desc, and license
if (TextUtils.isEmpty(contribution.getCreator())) {
contribution.setCreator(app.getCurrentAccount().name);
contribution.setCreator(sessionManager.getCurrentAccount().name);
}
if (contribution.getDescription() == null) {
@ -102,14 +107,15 @@ public class UploadController {
@Override
protected Contribution doInBackground(Void... voids /* stare into you */) {
long length;
ContentResolver contentResolver = context.getContentResolver();
try {
if (contribution.getDataLength() <= 0) {
length = app.getContentResolver()
length = contentResolver
.openAssetFileDescriptor(contribution.getLocalUri(), "r")
.getLength();
if (length == -1) {
// Let us find out the long way!
length = Utils.countBytes(app.getContentResolver()
length = countBytes(contentResolver
.openInputStream(contribution.getLocalUri()));
}
contribution.setDataLength(length);
@ -126,7 +132,7 @@ public class UploadController {
Boolean imagePrefix = false;
if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
mimeType = app.getContentResolver().getType(contribution.getLocalUri());
mimeType = contentResolver.getType(contribution.getLocalUri());
}
if (mimeType != null) {
@ -137,7 +143,7 @@ public class UploadController {
if (imagePrefix && contribution.getDateCreated() == null) {
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);
if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) {
cursor.moveToFirst();
@ -165,4 +171,14 @@ public class UploadController {
}
}.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.ContentValues;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
@ -22,10 +23,14 @@ import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.R;
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.ContributionsActivity;
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_CAMPAIGN = EXTRA_PREFIX + ".campaign";
@Inject MediaWikiApi mwApi;
@Inject SessionManager sessionManager;
@Inject @Named("default_preferences") SharedPreferences prefs;
private NotificationManager notificationManager;
private ContentProviderClient contributionsProviderClient;
private CommonsApplication app;
private NotificationCompat.Builder curProgressNotification;
private int toUpload;
// The file names of unfinished uploads, used to prevent overwriting
@ -118,7 +124,6 @@ public class UploadService extends HandlerService<Contribution> {
super.onCreate();
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
app = CommonsApplication.getInstance();
contributionsProviderClient = this.getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY);
}
@ -180,8 +185,6 @@ public class UploadService extends HandlerService<Contribution> {
@SuppressLint("StringFormatInvalid")
private void uploadContribution(Contribution contribution) {
MediaWikiApi api = app.getMWApi();
InputStream file;
String notificationTag = contribution.getLocalUri().toString();
@ -228,9 +231,9 @@ public class UploadService extends HandlerService<Contribution> {
filename = findUniqueFilename(filename);
unfinishedUploads.add(filename);
}
if (!api.validateLogin()) {
if (!mwApi.validateLogin()) {
// Need to revalidate!
if (app.revalidateAuthToken()) {
if (sessionManager.revalidateAuthToken()) {
Timber.d("Successfully revalidated token!");
} else {
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()),
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());
@ -255,8 +258,8 @@ public class UploadService extends HandlerService<Contribution> {
String resultStatus = uploadResult.getResultStatus();
if (!resultStatus.equals("Success")) {
showFailedNotification(contribution);
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT)
.param("username", app.getCurrentAccount().name)
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT, mwApi, prefs)
.param("username", sessionManager.getCurrentAccount().name)
.param("source", contribution.getSource())
.param("multiple", contribution.getMultiple())
.param("result", uploadResult.getErrorCode())
@ -269,8 +272,8 @@ public class UploadService extends HandlerService<Contribution> {
contribution.setDateUploaded(uploadResult.getDateUploaded());
contribution.save();
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT)
.param("username", app.getCurrentAccount().name)
EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT, mwApi, prefs)
.param("username", sessionManager.getCurrentAccount().name)
.param("source", contribution.getSource()) //FIXME
.param("filename", contribution.getFilename())
.param("multiple", contribution.getMultiple())
@ -287,7 +290,7 @@ public class UploadService extends HandlerService<Contribution> {
toUpload--;
if (toUpload == 0) {
// 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);
}
}
@ -310,7 +313,6 @@ public class UploadService extends HandlerService<Contribution> {
}
private String findUniqueFilename(String fileName) throws IOException {
MediaWikiApi api = app.getMWApi();
String sequenceFileName;
for (int sequenceNumber = 1; true; sequenceNumber++) {
if (sequenceNumber == 1) {
@ -326,7 +328,7 @@ public class UploadService extends HandlerService<Contribution> {
sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2");
}
}
if (!api.fileExistsWithName(sequenceFileName)
if (!mwApi.fileExistsWithName(sequenceFileName)
&& !unfinishedUploads.contains(sequenceFileName)) {
break;
}

View file

@ -7,7 +7,6 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
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_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="nearby_location_has_not_changed">L\'allugamientu nun camudó.</string>
<string name="nearby_location_not_available">L\'allugamientu nun ta disponible.</string>
</resources>

View file

@ -201,4 +201,7 @@
<string name="send_log_file">লগ ফাইল পাঠান</string>
<string name="send_log_file_description">ইমেইলের মাধ্যমে উন্নয়নকারীর কাছে লগ ফাইল পাঠান</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>

View file

@ -201,4 +201,6 @@
<string name="send_log_file">Send logfil</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="nearby_location_has_not_changed">Sted er ikke ændret.</string>
<string name="nearby_location_not_available">Sted ikke tilgængeligt.</string>
</resources>

View file

@ -201,4 +201,7 @@
<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="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>

View file

@ -24,7 +24,7 @@
<item quantity="one">1 dosye selagnayış</item>
<item quantity="other">%d dosye Selagnayışi</item>
</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_failed">Nêbı</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_item_home">Keye</string>
<string name="navigation_item_upload">Bar ke</string>
<string name="navigation_item_logout">Veciyayış</string>
</resources>

View file

@ -199,4 +199,6 @@
<string name="use_external_storage_summary">Αποθηκεύσετε εικόνες που παίρνονται στην κάμερα εφαρμογής στην συσκευή σας</string>
<string name="send_log_file">Αποστείλατε τον φάκελλο σύνδεσης</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>

View file

@ -12,7 +12,7 @@
<string name="login_failed">Acceso fallido.</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="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_text">Pulsa para ver tu subida</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_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="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>

View file

@ -201,4 +201,7 @@
<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="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>

View file

@ -201,4 +201,6 @@
<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="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>

View file

@ -11,9 +11,9 @@
<string name="login_success">Sikeres bejelentkezés</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="authentication_failed">Sikertelen azonosítás.</string>
<string name="uploading_started">Feltöltés indul.</string>
<string name="upload_completed_notification_title">%1$s feltöltve!</string>
<string name="authentication_failed">Sikertelen hitelesítés.</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_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_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_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_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_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>
@ -190,9 +190,15 @@
<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_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="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="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="nearby_location_has_not_changed">A hely nem változott.</string>
<string name="nearby_location_not_available">A hely nem érhető el.</string>
</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_wikidata_article">Elemento Wikidata</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>

View file

@ -65,7 +65,7 @@
<string name="title_activity_settings">設定</string>
<string name="title_activity_signup">利用者登録</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_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>
@ -168,4 +168,5 @@
<string name="no_description_found">説明がありません</string>
<string name="nearby_info_menu_wikidata_article">ウィキデータ項目</string>
<string name="use_external_storage">外部ストレージを使用</string>
<string name="nearby_location_not_available">位置が無効です。</string>
</resources>

View file

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

View file

@ -191,4 +191,6 @@
<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="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>

View file

@ -17,7 +17,7 @@
<string name="upload_completed_notification_text">Допрете за да го погледате подигањето</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_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_subtitle">Допрете за да погледате</string>
<plurals name="uploads_pending_notification_indicator">
@ -73,7 +73,7 @@
<string name="title_activity_settings">Нагодувања</string>
<string name="title_activity_signup">Регистрација</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_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>
@ -201,4 +201,7 @@
<string name="send_log_file">Испрати дневничка податотека</string>
<string name="send_log_file_description">Испрати дневничка податотека на разработувачите по е-пошта</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>

View file

@ -201,4 +201,7 @@
<string name="send_log_file">Send loggfil</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="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>

View file

@ -201,4 +201,7 @@
<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="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>

View file

@ -201,4 +201,6 @@
<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="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>

View file

@ -10,6 +10,7 @@
<string name="logging_in_message">Aguarde, por favor…</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="upload_failed">Ficheiro não encontrado. Por favor, tente outro ficheiro.</string>
<string name="authentication_failed">Falha de autenticação!</string>
<string name="uploading_started">Iniciado o carregamento!</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_failed_notification_title">O carregamento de %1$s falhou</string>
<string name="upload_failed_notification_subtitle">Toque para ver</string>
<plurals name="uploads_pending_notification_indicator" fuzzy="true">
<item quantity="one">A carregar um ficheiro</item>
<item quantity="other">A carregar %d ficheiros</item>
<plurals name="uploads_pending_notification_indicator">
<item quantity="one">%d a carregar um ficheiro</item>
<item quantity="other">a carregar %d ficheiros</item>
</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_failed">Falhou</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_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_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_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="share_upload_button">Carregar</string>
<string name="multiple_share_base_title">Nomeie este conjunto</string>
@ -48,42 +50,54 @@
<string name="menu_upload_single">Enviar</string>
<string name="categories_search_text_hint">Pesquisar categorias</string>
<string name="menu_save_categories">Gravar</string>
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">Sem carregamentos ainda</item>
<item quantity="one">1 carregamento</item>
<string name="refresh_button">Atualizar</string>
<string name="gps_disabled">O GPS está desativado no seu dispositivo. Gostarias de ativá-lo?</string>
<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>
</plurals>
<plurals name="starting_multiple_uploads" fuzzy="true">
<item quantity="one">A iniciar um carregamento</item>
<plurals name="starting_multiple_uploads">
<item quantity="one">A iniciar %d carregamento</item>
<item quantity="other">A iniciar %d carregamentos</item>
</plurals>
<plurals name="multiple_uploads_title" fuzzy="true">
<item quantity="one">1 carregamento</item>
<plurals name="multiple_uploads_title">
<item quantity="one">%d carregamento</item>
<item quantity="other">%d carregamentos</item>
</plurals>
<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_activity_title">Categorias</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="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_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_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_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">&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">&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="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="waiting_first_sync">A aguardar pela primeira sincronização…</string>
<string name="no_uploads_yet">Não carregou ainda nenhuma foto.</string>
<string name="menu_retry_upload">Tente novamente</string>
<string name="menu_cancel_upload">Cancelar</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="preference_license">Licença</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_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" 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_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>
@ -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_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_4_0">CC BY 4.0</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_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_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_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>
@ -109,13 +129,78 @@
<string name="welcome_final_text">Acha que conseguiu?</string>
<string name="welcome_final_button_text">Sim!</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_description_empty">Sem descrição</string>
<string name="detail_license_empty">Licença desconhecida</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="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="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>

View file

@ -205,4 +205,7 @@
<string name="send_log_file">Выслать лог-файл</string>
<string name="send_log_file_description">Выслать лог-файл разработчикам по е-мейлу</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>

View file

@ -201,4 +201,7 @@
<string name="send_log_file">Skicka loggfil</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="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>

View file

@ -201,4 +201,7 @@
<string name="send_log_file">寄送日誌檔案</string>
<string name="send_log_file_description">經由電子郵件寄送日誌檔案給開發人員</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>

View file

@ -201,4 +201,7 @@
<string name="send_log_file">发送日志文件</string>
<string name="send_log_file_description">通过电子邮件将日志文件发送给开发人员</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>

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_not_available">Location not available.</string>
<string name="location_permission_rationale_nearby">Permission required to display a list of nearby places</string>
</resources>

View file

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

View file

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

View file

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

View file

@ -1,12 +1,182 @@
package fr.free.nrw.commons;
import android.content.SharedPreferences;
import android.support.v4.util.LruCache;
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 {
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
protected RefWatcher setupLeakCanary() {
// No leakcanary in unit tests.
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 fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.TestCommonsApplication;
import io.reactivex.observers.TestObserver;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse;
@ -28,7 +29,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
@Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class)
public class ApacheHttpClientMediaWikiApiTest {
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.R;
import fr.free.nrw.commons.TestCommonsApplication;
import fr.free.nrw.commons.location.LatLng;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
@Config(constants = BuildConfig.class, sdk = 21, application = TestCommonsApplication.class)
public class NearbyAdapterFactoryTest {
private static final Place PLACE = new Place("name", Place.Description.AIRPORT,

View file

@ -1,9 +1,8 @@
package fr.free.nrw.commons
import org.hamcrest.CoreMatchers.`is` as _is
import org.junit.Assert
import org.junit.Test
import org.hamcrest.CoreMatchers.`is` as _is
class UtilsTest {
@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.dicedmelon.gradle:jacoco-android:0.1.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"
}
}

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