diff --git a/CHANGELOG.md b/CHANGELOG.md index 1de06a4e3..df1c2d2af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,32 @@ # Wikimedia Commons for Android -## v2.5.0 beta +## v2.6.4 beta +- Excluded httpclient and commons-logging to fix release build errors +- Fixed crashes caused by Fresco and Dagger + +## v2.6.3 beta +- Same as 2.6.2 except with localizations added for Google Code-In + +## v2.6.2 beta +- Reverted temporarily to last stable version while working on crash fix + +## v2.6.1 beta +- Failed attempt to fix crashes in release build with the previous beta release + +## v2.6.0 beta +- Multiple bugfixes for location updates and list/map loading in Nearby +- Multiple fixes for various crashes and memory leaks +- Added several unit tests +- Modified About page to include WMF disclaimer and modified Privacy Policy link to point to our individual privacy policy +- Added option for users to send logs to developers (has to be manually activated by user) +- Converted PNGs to WebPs +- Improved login screen with new design and privacy policy link +- Improved category display, if a category has an exact name entered, it will be shown first +- New UI for Nearby list +- Added product flavors for production and the beta-cluster Wikimedia servers +- Various improvements to navigation flow and backstack + +## v2.5.0 beta - Added one-time popup for beta users to provide feedback on IEG renewal proposal - Added link to Commons policies in ShareActivity - Various string fixes diff --git a/app/build.gradle b/app/build.gradle index 0ba070c43..549ffdae4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 } diff --git a/app/src/androidTest/java/fr/free/nrw/commons/NearbyActivityTest.java b/app/src/androidTest/java/fr/free/nrw/commons/NearbyActivityTest.java deleted file mode 100644 index f9ec72569..000000000 --- a/app/src/androidTest/java/fr/free/nrw/commons/NearbyActivityTest.java +++ /dev/null @@ -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 nearby = - new ActivityTestRule<>(NearbyActivity.class); - - @Test - public void testActivityLaunch() { - onView(withText(R.string.title_activity_nearby)) - .check(ViewAssertions.matches(isDisplayed())); - } -} diff --git a/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java b/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java new file mode 100644 index 000000000..9ca8c67a9 --- /dev/null +++ b/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java @@ -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)); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b0cbdd34f..e262e9088 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,20 +2,23 @@ package="fr.free.nrw.commons"> - - - - - + + + + + - - - - - - + + + + + + + + + - + - - + + - - + + + + android:label="@string/app_name"> @@ -52,11 +51,11 @@ + + android:name=".upload.MultipleShareActivity" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name"> @@ -66,33 +65,34 @@ - + android:name=".contributions.ContributionsActivity" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name" /> + + android:label="@string/title_activity_settings" /> + + + android:label="@string/title_activity_signup" /> + - - + + + android:process=":auth"> @@ -103,27 +103,25 @@ + android:name=".contributions.ContributionsSyncService" + android:exported="true"> - + + android:name="android.content.SyncAdapter" + android:resource="@xml/contributions_sync_adapter" /> + android:name=".modifications.ModificationsSyncService" + android:exported="true"> - + + android:name="android.content.SyncAdapter" + android:resource="@xml/modifications_sync_adapter" /> + android:resource="@xml/provider_paths" /> - + android:name=".contributions.ContributionsContentProvider" + android:authorities="fr.free.nrw.commons.contributions.contentprovider" + android:exported="false" + android:label="@string/provider_contributions" + android:syncable="true" /> - + android:name=".modifications.ModificationsContentProvider" + android:authorities="fr.free.nrw.commons.modifications.contentprovider" + android:exported="false" + android:label="@string/provider_modifications" + android:syncable="true" /> + - + android:name=".category.CategoryContentProvider" + android:authorities="fr.free.nrw.commons.categories.contentprovider" + android:exported="false" + android:label="@string/provider_categories" + android:syncable="false" /> diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java index 06d02aab2..0ef5fa171 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java @@ -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; diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index fcb1fa2d2..255ba761b 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -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 dispatchingActivityInjector; - @Inject MediaWikiApi mediaWikiApi; - - private static CommonsApplication instance = null; - private MediaWikiApi api = null; - private LruCache 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 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; - } - - public Boolean revalidateAuthToken() { - AccountManager accountManager = AccountManager.get(this); - Account curAccount = getCurrentAccount(); - - if (curAccount == null) { - return false; // This should never happen - } - - accountManager.invalidateAuthToken(AccountUtil.accountType(), mediaWikiApi.getAuthCookie()); - try { - String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false); - mediaWikiApi.setAuthCookie(authCookie); - return true; - } catch (OperationCanceledException | NullPointerException | IOException | AuthenticatorException e) { - e.printStackTrace(); - return false; - } + @Override + protected AndroidInjector applicationInjector() { + return injector(); } - public boolean deviceHasCamera() { - PackageManager pm = getPackageManager(); - return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) - || pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); + public CommonsApplicationComponent injector() { + if (component == null) { + component = DaggerCommonsApplicationComponent.builder() + .appModule(new CommonsApplicationModule(this)) + .build(); + } + 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 amCallback = new AccountManagerCallback() { - - private int index = 0; - - void setIndex(int index) { - this.index = index; - } - - int getIndex() { - return index; - } - - @Override - public void run(AccountManagerFuture 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 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(); diff --git a/app/src/main/java/fr/free/nrw/commons/HandlerService.java b/app/src/main/java/fr/free/nrw/commons/HandlerService.java index 61fa1f1c5..69a1ea4b9 100644 --- a/app/src/main/java/fr/free/nrw/commons/HandlerService.java +++ b/app/src/main/java/fr/free/nrw/commons/HandlerService.java @@ -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 extends Service { +import dagger.android.DaggerService; + +public abstract class HandlerService extends DaggerService { private volatile Looper threadLooper; private volatile ServiceHandler threadHandler; private String serviceName; diff --git a/app/src/main/java/fr/free/nrw/commons/LicenseList.java b/app/src/main/java/fr/free/nrw/commons/LicenseList.java index 382ceee3f..ab32f8815 100644 --- a/app/src/main/java/fr/free/nrw/commons/LicenseList.java +++ b/app/src/main/java/fr/free/nrw/commons/LicenseList.java @@ -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; + } + } + } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index 643fd68d8..0927f0338 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -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 categories; private Map 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(); diff --git a/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java b/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java index 5ccc80c06..a542cb363 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java @@ -7,16 +7,17 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi; class MediaThumbnailFetchTask extends AsyncTask { 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! } diff --git a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java index 2b017ac8b..aa7ea8a15 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java @@ -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 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); diff --git a/app/src/main/java/fr/free/nrw/commons/Utils.java b/app/src/main/java/fr/free/nrw/commons/Utils.java index 989083138..3f5eb2105 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -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); } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java index 479b47444..4114b19a9 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java @@ -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, - String username, String password) { + public static final String ACCOUNT_TYPE = "fr.free.nrw.commons"; + private final Context context; - Account account = new Account(username, accountType()); + public AccountUtil(Context context) { + this.context = context; + } + + public void createAccount(@Nullable AccountAuthenticatorResponse response, + String username, String password) { + + Account account = new Account(username, ACCOUNT_TYPE); boolean created = accountManager().addAccountExplicitly(account, password, null); 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); } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java index 5b483c328..e793d5eb9 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java @@ -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"); } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index f581de832..d17120106 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -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)); diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java index 519ea323c..fa7a671c7 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java @@ -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 { 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 { 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 { 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 { 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(); } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java new file mode 100644 index 000000000..779b73b34 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java @@ -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); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java index 4da090531..a6b66cbf6 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java @@ -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(); diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java index ea574d432..c08e27966 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java @@ -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; } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java index 0a996b7d4..b0f3e6063 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java @@ -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; + +import dagger.android.DaggerService; +import fr.free.nrw.commons.mwapi.MediaWikiApi; + +import static android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT; + +public class WikiAccountAuthenticatorService extends DaggerService { + + @Inject MediaWikiApi mwApi; + private WikiAccountAuthenticator wikiAccountAuthenticator = null; - private static WikiAccountAuthenticator wikiAccountAuthenticator = null; - @Override 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(); } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java index 1a70bc57c..5c3992bf2 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java @@ -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 categoriesAdapter; private OnCategoriesSaveHandler onCategoriesSaveHandler; private HashMap> 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 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)); } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java index e3a2661a7..ed698ec4c 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java @@ -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; } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java index 338743f02..e673c7d9d 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java @@ -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); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java index 933cda6ae..6cda47d2e 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java @@ -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,10 +47,17 @@ import static fr.free.nrw.commons.contributions.ContributionsContentProvider.AUT import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING; -public class ContributionsActivity extends AuthenticatedActivity - implements LoaderManager.LoaderCallbacks, AdapterView.OnItemClickListener, - MediaDetailPagerFragment.MediaDetailProvider, FragmentManager.OnBackStackChangedListener, - ContributionsListFragment.SourceRefresher { +public class ContributionsActivity + extends AuthenticatedActivity + implements LoaderManager.LoaderCallbacks, + 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; @@ -62,9 +67,6 @@ public class ContributionsActivity extends AuthenticatedActivity private ArrayList 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(); } @@ -122,9 +120,8 @@ public class ContributionsActivity extends AuthenticatedActivity @Override protected void onAuthCookieAcquired(String authCookie) { - // Do a sync every time we get here! - CommonsApplication app = ((CommonsApplication) getApplication()); - requestSync(app.getCurrentAccount(), AUTHORITY, new Bundle()); + // Do a sync everytime we get here! + requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.AUTHORITY, new Bundle()); Intent uploadServiceIntent = new Intent(this, UploadService.class); uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); startService(uploadServiceIntent); @@ -139,19 +136,18 @@ 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); // Activity can call methods in the fragment by acquiring a // reference to the Fragment from FragmentManager, using findFragmentById() FragmentManager supportFragmentManager = getSupportFragmentManager(); - contributionsList = (ContributionsListFragment) supportFragmentManager + contributionsList = (ContributionsListFragment)supportFragmentManager .findFragmentById(R.id.contributionsListFragment); supportFragmentManager.addOnBackStackChangedListener(this); if (savedInstanceState != null) { - mediaDetails = (MediaDetailPagerFragment) supportFragmentManager + mediaDetails = (MediaDetailPagerFragment)supportFragmentManager .findFragmentById(R.id.contributionsFragmentContainer); getSupportLoaderManager().initLoader(0, null, this); @@ -241,8 +237,7 @@ public class ContributionsActivity extends AuthenticatedActivity @Override public Loader 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 diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java index aae9fa3bc..5ec290026 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java @@ -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); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index 9a313854c..590d4e6ad 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -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); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java index c2363ebae..e67b164a8 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java @@ -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); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilder.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilder.java deleted file mode 100644 index 4d69d46e6..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilder.java +++ /dev/null @@ -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(); -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java new file mode 100644 index 000000000..27e16ce23 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -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(); + +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/AppComponent.java b/app/src/main/java/fr/free/nrw/commons/di/AppComponent.java deleted file mode 100644 index 9cc9e33dd..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/AppComponent.java +++ /dev/null @@ -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); -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/AppModule.java b/app/src/main/java/fr/free/nrw/commons/di/AppModule.java deleted file mode 100644 index c971a265f..000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/AppModule.java +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java new file mode 100644 index 000000000..2881f33fd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -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 { + 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(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java new file mode 100644 index 000000000..99d3235e7 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -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 provideLruCache() { + return new LruCache<>(1024); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java new file mode 100644 index 000000000..f18c331c5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java @@ -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(); + +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java new file mode 100644 index 000000000..ca7340cb1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -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(); + +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java new file mode 100644 index 000000000..2d4072d15 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java @@ -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(); + +} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LatLng.java b/app/src/main/java/fr/free/nrw/commons/location/LatLng.java index 787c8c8a5..3ddaab568 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LatLng.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LatLng.java @@ -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; diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java index e830d8d06..45a1a4181 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java @@ -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 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() { + if (!isLocationManagerRegistered) + isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) + && requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); + } + + private boolean requestLocationUpdatesFromProvider(String locationProvider) { 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); - } + 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); - - for (LocationUpdateListener listener : locationListeners) { - listener.onLocationChanged(lastLocation); + if (isBetterLocation(location, lastLocation)) { + lastLocation = location; + for (LocationUpdateListener listener : locationListeners) { + listener.onLocationChanged(LatLng.from(lastLocation)); + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index fe68203ec..718ee4f58 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -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 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 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); diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index 3008632d9..85d598b9d 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -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(), diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java index 0f0a81316..6dc993c9e 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java @@ -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: diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java index f2940ec74..1e886f225 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java @@ -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(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 18e89f072..19425a0c3 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -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); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java b/app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java index d3ba7c0d5..4446da738 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java @@ -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); } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java b/app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java index eabbbf82e..7a4b294fa 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java @@ -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); } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java b/app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java index ee947afbc..3fce8aaf1 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java @@ -2,11 +2,16 @@ package fr.free.nrw.commons.mwapi; import android.os.AsyncTask; -import fr.free.nrw.commons.CommonsApplication; - class LogTask extends AsyncTask { + + 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); } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java index 3e44eab4b..57d21dd95 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java @@ -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 placeList) { + private void populatePlaces(List 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); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index 6ca626299..705d5a799 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -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 loadAttractionsFromLocation(LatLng curLatLng, Context context) { + public List 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 places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage()); if (curLatLng != null) { Timber.d("Sorting places by distance..."); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java index 00b8a2840..817571668 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java @@ -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>() { }.getType(); private static final Type CUR_LAT_LNG_TYPE = new TypeToken() { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NoPermissionsFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NoPermissionsFragment.java index 5065a7d93..ca0ae0a89 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NoPermissionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NoPermissionsFragment.java @@ -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() { } diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java index 4b46ff401..d731c70d1 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java @@ -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; diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index f4398d4e2..2809eb0ab 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -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) diff --git a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java index 086b97359..f08496914 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java @@ -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 { @@ -27,7 +26,7 @@ public class BaseActivity extends AppCompatActivity { @Override protected void onResume() { // Restart activity if theme is changed - boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false); + boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false); if (currentTheme != newTheme) { //is activity theme changed Intent intent = getIntent(); finish(); diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index c27182f67..60ea325e4 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -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); } diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java b/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java index 5afa5cac3..e7f5eaeeb 100644 --- a/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java +++ b/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java @@ -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); + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java index b76150643..fee0765a4 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java @@ -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 { + interface Callback { void onResult(Result result); } @@ -28,14 +28,16 @@ public class ExistingFileAsync extends AsyncTask { 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 { @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; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java index d9d369b65..404177032 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java @@ -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(); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java index abeae0507..184778fcc 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java @@ -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 - implements MediaDetailPagerFragment.MediaDetailProvider, - AdapterView.OnItemClickListener, - FragmentManager.OnBackStackChangedListener, - MultipleUploadListFragment.OnMultipleUploadInitiatedHandler, +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 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(); } }); @@ -163,8 +167,8 @@ public class MultipleShareActivity @Override public void onCategoriesSave(List categories) { if (categories.size() > 0) { - ContentProviderClient client = getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY); - for (Contribution contribution: photosList) { + ContentProviderClient client = getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY); + for (Contribution contribution : photosList) { ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri()); categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{}))); @@ -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)) { @@ -266,7 +268,7 @@ public class MultipleShareActivity uploadsList = (MultipleUploadListFragment) getSupportFragmentManager().findFragmentByTag("uploadsList"); if (uploadsList == null) { - uploadsList = new MultipleUploadListFragment(); + uploadsList = new MultipleUploadListFragment(); getSupportFragmentManager() .beginTransaction() .add(R.id.uploadsFragmentContainer, uploadsList, "uploadsList") @@ -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()) ; } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java index 28f88e3d6..d20d8c1eb 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java @@ -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(); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java b/app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java index cf1e1c2c3..a530e79e6 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java @@ -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 categorySet; + private static Set categorySet; private static List 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; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java index fa5f0d18b..218181154 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java @@ -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 displayCatList = app.getCacheData().findCategory(); + List 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"); + } + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java index 579352991..259f38c36 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java @@ -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 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); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index 7951be98c..1d080e78f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -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; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 89b614fd6..215819d12 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -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 { 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 { 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 { @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 { 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 { 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 { 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.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 { 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 { } 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 { sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2"); } } - if (!api.fileExistsWithName(sequenceFileName) + if (!mwApi.fileExistsWithName(sequenceFileName) && !unfinishedUploads.contains(sequenceFileName)) { break; } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java index 6c3fc9a90..4191f9d6f 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java @@ -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; diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index 9bca1524a..02b3397d0 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -201,4 +201,6 @@ Unviar ficheru de rexistru Unviar ficheru de rexistru a los desendolcadores per corréu electrónicu Anicia sesión na to cuenta + L\'allugamientu nun camudó. + L\'allugamientu nun ta disponible. diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 4aaa0bcba..170b694b3 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -201,4 +201,7 @@ লগ ফাইল পাঠান ইমেইলের মাধ্যমে উন্নয়নকারীর কাছে লগ ফাইল পাঠান আপনার অ্যাকাউন্টে প্রবেশ করুন + অবস্থান পরিবর্তন হয়নি। + অবস্থান উপলব্ধ নয়। + কাছাকাছি স্থানসমূহের একটি তালিকা প্রদর্শন করতে অনুমতি প্রয়োজন diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 49b2e9d2c..0c6f86597 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -201,4 +201,6 @@ Send logfil Send logfil til udviklerne via e-post Log ind på din konto + Sted er ikke ændret. + Sted ikke tilgængeligt. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 0147104d0..5071a0199 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -201,4 +201,7 @@ Logdatei senden Logdatei an die Entwickler per E-Mail senden Bei deinem Benutzerkonto anmelden + Der Standort hat sich nicht geändert. + Der Standort ist nicht verfügbar. + Berechtigung zur Anzeige einer Liste mit Orten in der Nähe erforderlich diff --git a/app/src/main/res/values-diq/strings.xml b/app/src/main/res/values-diq/strings.xml index fd19f8cad..769b5fbd0 100644 --- a/app/src/main/res/values-diq/strings.xml +++ b/app/src/main/res/values-diq/strings.xml @@ -24,7 +24,7 @@ 1 dosye selagnayış %d dosye Selagnayışi - Barkerdışê mı + Barkerdışê mınê peyêni Ratneya Nêbı %1$d%% temamya @@ -118,4 +118,5 @@ Ake Keye Bar ke + Veciyayış diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 6dadbe0d1..abe361f56 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -199,4 +199,6 @@ Αποθηκεύσετε εικόνες που παίρνονται στην κάμερα εφαρμογής στην συσκευή σας Αποστείλατε τον φάκελλο σύνδεσης Στείλατε τον φάκελλο σύνδεσης στους δημιουργούς μέσω email + Ο εντοπισμός δεν έχει αλλάξει. + Ο τόπος δεν είναι διαθέσιμος. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d802b28bd..2c1011182 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -12,7 +12,7 @@ Acceso fallido. No se encontró el archivo. Prueba con otro. Falló la autenticación. - ¡Empenzando a subir! + Ha comenzado la carga. ¡Se subieron %1$s! Pulsa para ver tu subida Empezando la subida de %1$s @@ -201,4 +201,7 @@ Enviar archivo de registro Enviar archivo de registro a los desarrolladores por correo electrónico Accede a tu cuenta + La ubicación no ha cambiado. + La ubicación no está disponible. + Se necesita permiso para mostrar una lista de lugares cercanos diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 209478949..f2548f6cd 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -201,4 +201,7 @@ Envoyer le journal Envoyer le journal aux développeurs par courriel Connectez-vous à votre compte + L\'emplacement n\'a pas changé. + Emplacement non disponible. + Une permission est requise pour afficher une liste de lieux relatifs diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 1c2063f2c..8dd2411a1 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -201,4 +201,6 @@ Enviar ficheiro de rexistro Enviar ficheiro de rexistro ós desenvolvedores por correo electrónico Comezar sesión na súa conta + A localización non cambiou. + A localización non está dispoñible. diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 721ed066a..a571ba193 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -11,9 +11,9 @@ Sikeres bejelentkezés A bejelentkezés nem sikerült. A fájl nem található. Próbálkozz másik fájllal. - Sikertelen azonosítás. - Feltöltés indul. - %1$s feltöltve! + Sikertelen hitelesítés. + Feltöltés elindult. + %1$s feltöltve. Feltöltés megtekintése Feltöltés indul: %1$s %1$s feltöltése @@ -105,7 +105,7 @@ CC BY-SA 3.0 (Észtország) CC BY-SA 3.0 (Spanyolország) CC BY-SA 3.0 (Horvátország) - CC BY-SA 3.0 (Luxembourg) + CC BY-SA 3.0 (Luxemburg) CC BY-SA 3.0 (Hollandia) CC BY-SA 3.0 (Norvégia) CC BY-SA 3.0 (Lengyelország) @@ -190,9 +190,15 @@ nincs leírás Commons leírólap Wikidata-elem + Hiba a képek gyorsítótárazásakor 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. 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á. Engedély adása Külső tárhely használata + Az alkalmazáson belüli kamerával készült képek mentése az eszközre + Naplófájlok küldése + Naplófájlok küldése e-mailben a fejlesztőknek Bejelentkezés a fiókodba + A hely nem változott. + A hely nem érhető el. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 464461aa3..a19e95b57 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -167,4 +167,6 @@ Pagina di Commons del file Elemento Wikidata Accedi alla tua utenza + La posizione non è cambiata. + Posizione non disponibile. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 08e243703..7e8d35c21 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -65,7 +65,7 @@ 設定 利用者登録 このアプリについて - <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache ライセンス v2</a> のもとで公開されているオープン ソース ソフトウェアです。Wikimedia Commons ならびにそのロゴはウィキメディア財団の商標であり、ウィキメディア財団の許可により使用しています。このサイトはウィキメディア財団の公認3でも提携先でもありません。 + ウィキメディア・コモンズ・アプリはウィキメディア・コミュニティの助成金受給者とボランティアによって製作・メンテナンスされているオープンソースソフトウェアです。ウィキメディア財団はこのアプリの製作・開発・メンテナンスに関与していません。 ソースは <a href=\"https://github.com/commons-app/apps-android-commons\">GitHub</a> にあります。バグとアイディアは <a href=\"https://github.com/commons-app/apps-android-commons/issues\">Github</a> へ。 <a href=\"https://wikimediafoundation.org/wiki/プライバシー・ポリシー\">プライバシー・ポリシー</a> <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\">クレジット</a> @@ -168,4 +168,5 @@ 説明がありません ウィキデータ項目 外部ストレージを使用 + 位置が無効です。 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index cc2794d5f..023b60d72 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -192,10 +192,14 @@ 공용 파일 문서 위키데이터 항목 그림 캐시 처리 오류 + 이 파일을 설명할 수 있는 제목으로, 파일 이름으로 사용됩니다. 띄어쓰기를 포함한 일반적인 단어를 사용할 수 있습니다. 파일 확장자는 포함하지 마세요 권한 부여 외부 저장소 사용하기 장치의 인앱 카메라로 찍은 사진 저장하기 로그 파일 보내기 이메일로 개발자에게 로그 파일 보내기 자신의 계정으로 로그인 + 위치가 변경되지 않았습니다. + 위치를 사용할 수 없습니다. + 주변 장소의 목록을 표시하기 위한 권한이 필요합니다. diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index 1937a880e..b4895cf95 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -191,4 +191,6 @@ Log-Fichier schécken Log-Fichier per E-Mail un d\'Entwéckler schécken An Äre Benotzerkont aloggen + De Plaz huet net geännert. + Plaz ass net disponibel. diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 000d94051..8d53642b0 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -17,7 +17,7 @@ Допрете за да го погледате подигањето Почнувам со подигањето на „%1$s“ Подигање на „%1$s“ - Заврпувам со подигање на „%1$s“ + Завршувам со подигање на „%1$s“ Подигањето на „%1$s“ не успеа Допрете за да погледате @@ -73,7 +73,7 @@ Нагодувања Регистрација За извршникот - Прилогот на Ризницата има отворен код. Негови творци и оддржувачи се примателите на наменските средства од Викимедиината заедница како и членовите на заедницата. Фондацијата Викимедија нема учество во нејзиното создавање, разработка и одржување. + Прилогот на Ризницата има отворен код. Негови творци и одржувачи се примателите на наменските средства од Викимедиината заедница како и членовите на заедницата. Фондацијата Викимедија нема учество во нејзиното создавање, разработка и одржување. <a href=\"https://github.com/commons-app/apps-android-commons\">Извор</a> и <a href=\"https://commons-app.github.io/\">мреж. место</a> на GitHub</a>. Создајте нов <a href=\"https://github.com/commons-app/apps-android-commons/issues\">случај на GitHub</a> за пријавување грешки и давање предлози. <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">Заштита на личните податоци</a> <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\">Заслуги</a> @@ -201,4 +201,7 @@ Испрати дневничка податотека Испрати дневничка податотека на разработувачите по е-пошта Најавете се со вашата сметка + Местоположбата не е сменета. + Местоположбата е недостапна. + Се бара дозвола за приказ на список на околни места diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 1c7bcc54a..d0551d6d7 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -201,4 +201,7 @@ Send loggfil Send loggfil til utviklerne via epost Logg inn med kontoen din + Stedet har ikke blitt endret. + Sted ikke tilgjengelig. + Tillatelse kreves for å vise listen over steder i nærheten diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index 890f688e8..d81357127 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -201,4 +201,7 @@ Mandé l\'archivi d\'argistr Mandé l\'archivi d\'argistr ai dësvlupator për pòsta eletrònica Ch\'as colega a sò cont + Ël leu a l\'é nen cangià. + Leu nen disponìbil. + A-i é da manca dël përmess pr\'ësmon-e na lista dij pòst davzin diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 02d794372..07a9a758b 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -201,4 +201,6 @@ Enviar arquivo de registro Enviar arquivo de log para desenvolvedores por e-mail Faça login na sua conta + O local não mudou. + Localização não disponível. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index d5e906937..ff2750f57 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -10,6 +10,7 @@ Aguarde, por favor… Inicio de sessão bem sucedido O início de sessão falhou! + Ficheiro não encontrado. Por favor, tente outro ficheiro. Falha de autenticação! Iniciado o carregamento! %1$s enviado! @@ -19,11 +20,11 @@ A terminar o carregamento de %1$s O carregamento de %1$s falhou Toque para ver - - A carregar um ficheiro - A carregar %d ficheiros + + %d a carregar um ficheiro + a carregar %d ficheiros - Meus carregamentos + Meus carregamentos recentes Em espera Falhou %1$d%% concluído @@ -39,8 +40,9 @@ Não foi possível iniciar sessão - falha de rede Não foi possível iniciar sessão - verifique o seu nome de utilizador(a) Não foi possível iniciar sessão - verifique a sua palavra-passe - Demasiadas tentativas mal sucedidas. Por favor, tente de novo dentro de alguns minutos + Demasiadas tentativas malsucedidas. Por favor, tente de novo dentro de alguns minutos. Desculpe, este utilizador foi bloqueado no Commons + Precisa fornecer o seu código de ativação de dois fatores. Falha ao iniciar sessão Carregar Nomeie este conjunto @@ -48,42 +50,54 @@ Enviar Pesquisar categorias Gravar - - Sem carregamentos ainda - 1 carregamento + Atualizar + O GPS está desativado no seu dispositivo. Gostarias de ativá-lo? + Ativar GPS + Ainda não foram enviados ficheiros + + \@string/contributions_subtitle_zero + %d carregamento %d carregamentos - - A iniciar um carregamento + + A iniciar %d carregamento A iniciar %d carregamentos - - 1 carregamento + + %d carregamento %d carregamentos Nenhuma categoria correspondente %1$s encontrada 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 Categorias Configurações + Registar-se Sobre - Software em código aberto distribuído sob <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache License v2</a>. 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. - Código no <a href=\"https://github.com/commons-app/android-commons\">GitHub</a>. Erros no <a href=\" https://github.com/commons-app/apps-android-commons/issues\">Github</a>. - <a href=\"https://wikimediafoundation.org/wiki/Privacy_policy\">Política de privacidade</a> + 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. + <a href=\"https://github.com/commons-app/apps-android-commons\">Fonte</a> e <a href=\"https://commons-app.github.io/\">sítio</a> no GitHub. Criar uma nova <a href=\"https://github.com/commons-app/apps-android-commons/issues\">publicação no GitHub</a> para informar erros e sugestões. + <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">Política de privacidade</a> + <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\">Créditos</a> Sobre Enviar comentários (por e-mail) + Não foi instalado nenhum cliente de correio eletrónico Categorias usadas recentemente A aguardar pela primeira sincronização… Não carregou ainda nenhuma foto. Tente novamente Cancelar Essa imagem será licenciada sob %1$s + 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 <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines/pt\">políticas do Wikimedia Commons</a>. Descarregar Licença Usar título/descrição anteriores Obter automaticamente a localização atual Recuperar localização atual para oferecer sugestões da categoria se a imagem não é georreferenciada + Modo noturno + Utilizar tema escuro + Atribuição-CompartilhaIgual 4.0 + Atribuição 4.0 Atribuição – Compartilhamento pela mesma Licença - CC Atribuição 3.0 + Atribuição 3.0 CC0 CC BY-SA 3.0 CC BY-SA 3.0 (Áustria) @@ -98,10 +112,16 @@ CC BY-SA 3.0 (Roménia) CC BY 3.0 CC-BY-SA 4.0 + CC BY 4.0 CC Zero Wikimedia Commons armazena a maioria das imagens que são usadas na Wikipédia. As suas imagens ajudam a educar pessoas em todo o mundo! + Por favor, carregue apenas as imagens tiradas ou criadas por ti: + - 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) + Por favor, NÃO carregue: + - Autorretratos ou imagens dos seus amigos\n- Imagens descarregadas da internet\n- Capturas de ecrã de aplicações com direitos de autor Exemplo de carregamento: + - 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 Contribua com as suas imagens. Ajude os artigos da Wikipédia a ganhar vida! As imagens na Wikipédia provêm do Wikimedia Commons. As suas imagens ajudam a educar as pessoas em todo o mundo. @@ -109,13 +129,78 @@ Acha que conseguiu? Sim! Categorias - A carregar… + A carregar… Nenhuma selecionada Sem descrição Licença desconhecida Actualizar + Permissão necessária: Ler a armazenagem externa. A aplicação não pode funcionar sem isso. + Permissão necessária: Escrever na armazenagem externa. A aplicação não pode funcionar sem isso. + Permissão opcional: Obter a localização atual para sugerir categorias OK Locais Próximos + Não foram encontrados locais próximos. + Aviso + Este ficheiro já existe no Commons. Tem certeza de que deseja continuar? Sim Não + Título + Título do ficheiro multimédia + Descrição + 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. + Data de carregamento + Licença + Coordenadas + Não fornecido + Torne-se um Testador Beta + Entre no nosso canal beta na Google Play e obtenha acesso rápido a novas funcionalidades e correções de erros + Utilizar o Wikidata + (Aviso: desabilitar isso pode causar um grande consumo de dados móveis) + Código de autenticação de dois fatores + Meu limite de carregamentos recentes + Limite máximo + Não é possível visualizar mais de 500. + Definir o limite de carregamentos recentes + Atualmente, a autenticação de dois fatores não é suportada. + Deseja realmente sair? + Logótipo do Commons + Imagem de fundo + Falha na imagem de média + Nenhuma imagem encontrada + Carregar imagem + Monte Zao + Lamas + Ponte de Arco-Íris + Túlipa + Nada de autorretratos + Imagem proprietária + Bem-vindo(a) à Wikipédia + Direitos de autor de boas-vindas + Ópera de Sydney + Cancelar + Abrir + Fechar + Início + Carregar + Próximo + Acerca + Configurações + Comentários + Sair + Tutorial + Os sítios próximos não podem ser visualizados sem permissões de localização + descrição não encontrada + Página do ficheiro no Commons + Item do Wikidata + Erro durante a cache de imagens + 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 + 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. + Permitir + Utilizar a armazenagem externa + Gravar as fotografias tiradas com a câmara na aplicação do seu dispositivo + Carregar ficheiro de registo + Carregar ficheiro de registo para programadores por correio eletrónico + Inicie sessão na sua conta + A localização não foi alterada. + A localização não está disponível. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 918c760b7..5841d62cb 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -205,4 +205,7 @@ Выслать лог-файл Выслать лог-файл разработчикам по е-мейлу Войдите в свою учётную запись + Местоположение не изменено. + Местоположение недоступно. + Необходимо разрешение для отображения списка ближайших мест diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index a77c2ade6..e783ba377 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -201,4 +201,7 @@ Skicka loggfil Skicka loggfilen till utvecklarna via e-post Logga in på ditt konto + Platsen har inte ändrats. + Platsen är inte tillgänglig. + Behörighet krävs för att visa en lista över platser i närheten diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 0d2d5a958..29df3cd4d 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -201,4 +201,7 @@ 寄送日誌檔案 經由電子郵件寄送日誌檔案給開發人員 登入您的帳號 + 位置無法更改。 + 位置無效。 + 需權限來顯示附近地點清單 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 01b8c4511..83de6c706 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -201,4 +201,7 @@ 发送日志文件 通过电子邮件将日志文件发送给开发人员 登录您的账户 + 位置没有更新。 + 位置不可用。 + 需要权限以显示附近地点列表 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 81da9000f..04d49faee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -213,4 +213,5 @@ Tap this message (or hit back) to skip this step. Location has not changed. Location not available. + Permission required to display a list of nearby places diff --git a/app/src/test/java/fr/free/nrw/commons/MediaTest.java b/app/src/test/java/fr/free/nrw/commons/MediaTest.java index 21cfc2c34..4f53351ad 100644 --- a/app/src/test/java/fr/free/nrw/commons/MediaTest.java +++ b/app/src/test/java/fr/free/nrw/commons/MediaTest.java @@ -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() { diff --git a/app/src/test/java/fr/free/nrw/commons/NearbyControllerTest.java b/app/src/test/java/fr/free/nrw/commons/NearbyControllerTest.java index 6ae2063da..752b7404d 100644 --- a/app/src/test/java/fr/free/nrw/commons/NearbyControllerTest.java +++ b/app/src/test/java/fr/free/nrw/commons/NearbyControllerTest.java @@ -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 diff --git a/app/src/test/java/fr/free/nrw/commons/PageTitleTest.java b/app/src/test/java/fr/free/nrw/commons/PageTitleTest.java index 455b3fbd0..16336b1b0 100644 --- a/app/src/test/java/fr/free/nrw/commons/PageTitleTest.java +++ b/app/src/test/java/fr/free/nrw/commons/PageTitleTest.java @@ -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() { diff --git a/app/src/test/java/fr/free/nrw/commons/TestCommonsApplication.java b/app/src/test/java/fr/free/nrw/commons/TestCommonsApplication.java index af4d50a91..d6c35ab02 100644 --- a/app/src/test/java/fr/free/nrw/commons/TestCommonsApplication.java +++ b/app/src/test/java/fr/free/nrw/commons/TestCommonsApplication.java @@ -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 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 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 getLruCache() { + return lruCache; + } } diff --git a/app/src/test/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java b/app/src/test/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java index e27ca19ea..8e72b3252 100644 --- a/app/src/test/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java +++ b/app/src/test/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.java @@ -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; diff --git a/app/src/test/java/fr/free/nrw/commons/nearby/NearbyActivityTest.java b/app/src/test/java/fr/free/nrw/commons/nearby/NearbyActivityTest.java new file mode 100644 index 000000000..eb1cff52b --- /dev/null +++ b/app/src/test/java/fr/free/nrw/commons/nearby/NearbyActivityTest.java @@ -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 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")); + } + +} \ No newline at end of file diff --git a/app/src/test/java/fr/free/nrw/commons/nearby/NearbyAdapterFactoryTest.java b/app/src/test/java/fr/free/nrw/commons/nearby/NearbyAdapterFactoryTest.java index 76125aa43..c86067ce6 100644 --- a/app/src/test/java/fr/free/nrw/commons/nearby/NearbyAdapterFactoryTest.java +++ b/app/src/test/java/fr/free/nrw/commons/nearby/NearbyAdapterFactoryTest.java @@ -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, diff --git a/app/src/test/kotlin/fr/free/nrw/commons/UtilsTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/UtilsTest.kt index 5b57b6253..7efb48c24 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/UtilsTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/UtilsTest.kt @@ -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`() { diff --git a/build.gradle b/build.gradle index 1738c6dee..6b7e5dc00 100644 --- a/build.gradle +++ b/build.gradle @@ -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" } } diff --git a/dependency-injection.md b/dependency-injection.md new file mode 100644 index 000000000..5d3599e54 --- /dev/null +++ b/dependency-injection.md @@ -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); +} +``` diff --git a/design/screenshots/Chinese (Simplified) zh-CN/car-categories.png b/design/screenshots/Chinese (Simplified) zh-CN/car-categories.png new file mode 100644 index 000000000..7119b2ce9 Binary files /dev/null and b/design/screenshots/Chinese (Simplified) zh-CN/car-categories.png differ diff --git a/design/screenshots/Chinese (Simplified) zh-CN/car-description.png b/design/screenshots/Chinese (Simplified) zh-CN/car-description.png new file mode 100644 index 000000000..bd9bc20bb Binary files /dev/null and b/design/screenshots/Chinese (Simplified) zh-CN/car-description.png differ diff --git a/design/screenshots/Chinese (Simplified) zh-CN/car-details.png b/design/screenshots/Chinese (Simplified) zh-CN/car-details.png new file mode 100644 index 000000000..0f83717f3 Binary files /dev/null and b/design/screenshots/Chinese (Simplified) zh-CN/car-details.png differ diff --git a/design/screenshots/Chinese (Simplified) zh-CN/drawer.png b/design/screenshots/Chinese (Simplified) zh-CN/drawer.png new file mode 100644 index 000000000..a9de28ab0 Binary files /dev/null and b/design/screenshots/Chinese (Simplified) zh-CN/drawer.png differ diff --git a/design/screenshots/Chinese (Simplified) zh-CN/gallery.png b/design/screenshots/Chinese (Simplified) zh-CN/gallery.png new file mode 100644 index 000000000..76ab0f6d6 Binary files /dev/null and b/design/screenshots/Chinese (Simplified) zh-CN/gallery.png differ diff --git a/design/screenshots/Chinese (Simplified) zh-CN/nearby-list.png b/design/screenshots/Chinese (Simplified) zh-CN/nearby-list.png new file mode 100644 index 000000000..352a45239 Binary files /dev/null and b/design/screenshots/Chinese (Simplified) zh-CN/nearby-list.png differ diff --git a/design/screenshots/Chinese (Simplified) zh-CN/nearby-map.png b/design/screenshots/Chinese (Simplified) zh-CN/nearby-map.png new file mode 100644 index 000000000..77c5c562b Binary files /dev/null and b/design/screenshots/Chinese (Simplified) zh-CN/nearby-map.png differ diff --git a/design/screenshots/Chinese (Simplified) zh-CN/restaurant-categories.png b/design/screenshots/Chinese (Simplified) zh-CN/restaurant-categories.png new file mode 100644 index 000000000..18e4f5891 Binary files /dev/null and b/design/screenshots/Chinese (Simplified) zh-CN/restaurant-categories.png differ diff --git a/design/screenshots/Chinese (Simplified) zh-CN/restaurant-description.png b/design/screenshots/Chinese (Simplified) zh-CN/restaurant-description.png new file mode 100644 index 000000000..288af316a Binary files /dev/null and b/design/screenshots/Chinese (Simplified) zh-CN/restaurant-description.png differ diff --git a/design/screenshots/Chinese (Simplified) zh-CN/restaurant-details.png b/design/screenshots/Chinese (Simplified) zh-CN/restaurant-details.png new file mode 100644 index 000000000..7451dd9bf Binary files /dev/null and b/design/screenshots/Chinese (Simplified) zh-CN/restaurant-details.png differ diff --git a/design/screenshots/Chinese (Simplified) zh-CN/school-categories.png b/design/screenshots/Chinese (Simplified) zh-CN/school-categories.png new file mode 100644 index 000000000..747d23ea7 Binary files /dev/null and b/design/screenshots/Chinese (Simplified) zh-CN/school-categories.png differ diff --git a/design/screenshots/Chinese (Simplified) zh-CN/school-description.png b/design/screenshots/Chinese (Simplified) zh-CN/school-description.png new file mode 100644 index 000000000..3df6470eb Binary files /dev/null and b/design/screenshots/Chinese (Simplified) zh-CN/school-description.png differ diff --git a/design/screenshots/Chinese (Simplified) zh-CN/school-details.png b/design/screenshots/Chinese (Simplified) zh-CN/school-details.png new file mode 100644 index 000000000..97e031f2d Binary files /dev/null and b/design/screenshots/Chinese (Simplified) zh-CN/school-details.png differ diff --git a/design/screenshots/Chinese (Simplified) zh-CN/taking-picture.png b/design/screenshots/Chinese (Simplified) zh-CN/taking-picture.png new file mode 100644 index 000000000..0c4445f5f Binary files /dev/null and b/design/screenshots/Chinese (Simplified) zh-CN/taking-picture.png differ diff --git a/design/screenshots/Chinese (Traditional) zh-TW/car-categories.png b/design/screenshots/Chinese (Traditional) zh-TW/car-categories.png new file mode 100644 index 000000000..ac7e15836 Binary files /dev/null and b/design/screenshots/Chinese (Traditional) zh-TW/car-categories.png differ diff --git a/design/screenshots/Chinese (Traditional) zh-TW/car-description.png b/design/screenshots/Chinese (Traditional) zh-TW/car-description.png new file mode 100644 index 000000000..b87788e7e Binary files /dev/null and b/design/screenshots/Chinese (Traditional) zh-TW/car-description.png differ diff --git a/design/screenshots/Chinese (Traditional) zh-TW/car-details.png b/design/screenshots/Chinese (Traditional) zh-TW/car-details.png new file mode 100644 index 000000000..d0522d129 Binary files /dev/null and b/design/screenshots/Chinese (Traditional) zh-TW/car-details.png differ diff --git a/design/screenshots/Chinese (Traditional) zh-TW/drawer.png b/design/screenshots/Chinese (Traditional) zh-TW/drawer.png new file mode 100644 index 000000000..df757a5f8 Binary files /dev/null and b/design/screenshots/Chinese (Traditional) zh-TW/drawer.png differ diff --git a/design/screenshots/Chinese (Traditional) zh-TW/gallery.png b/design/screenshots/Chinese (Traditional) zh-TW/gallery.png new file mode 100644 index 000000000..1052557c2 Binary files /dev/null and b/design/screenshots/Chinese (Traditional) zh-TW/gallery.png differ diff --git a/design/screenshots/Chinese (Traditional) zh-TW/nearby-list.png b/design/screenshots/Chinese (Traditional) zh-TW/nearby-list.png new file mode 100644 index 000000000..9dbf7697d Binary files /dev/null and b/design/screenshots/Chinese (Traditional) zh-TW/nearby-list.png differ diff --git a/design/screenshots/Chinese (Traditional) zh-TW/nearby-map.png b/design/screenshots/Chinese (Traditional) zh-TW/nearby-map.png new file mode 100644 index 000000000..0bce625bf Binary files /dev/null and b/design/screenshots/Chinese (Traditional) zh-TW/nearby-map.png differ diff --git a/design/screenshots/Chinese (Traditional) zh-TW/restaurant-categories.png b/design/screenshots/Chinese (Traditional) zh-TW/restaurant-categories.png new file mode 100644 index 000000000..bc1edfdbb Binary files /dev/null and b/design/screenshots/Chinese (Traditional) zh-TW/restaurant-categories.png differ diff --git a/design/screenshots/Chinese (Traditional) zh-TW/restaurant-description.png b/design/screenshots/Chinese (Traditional) zh-TW/restaurant-description.png new file mode 100644 index 000000000..aab0ee1a2 Binary files /dev/null and b/design/screenshots/Chinese (Traditional) zh-TW/restaurant-description.png differ diff --git a/design/screenshots/Chinese (Traditional) zh-TW/restaurant-details.png b/design/screenshots/Chinese (Traditional) zh-TW/restaurant-details.png new file mode 100644 index 000000000..ca0f1811e Binary files /dev/null and b/design/screenshots/Chinese (Traditional) zh-TW/restaurant-details.png differ diff --git a/design/screenshots/Chinese (Traditional) zh-TW/school-categories.png b/design/screenshots/Chinese (Traditional) zh-TW/school-categories.png new file mode 100644 index 000000000..0546ff5f4 Binary files /dev/null and b/design/screenshots/Chinese (Traditional) zh-TW/school-categories.png differ diff --git a/design/screenshots/Chinese (Traditional) zh-TW/school-description.png b/design/screenshots/Chinese (Traditional) zh-TW/school-description.png new file mode 100644 index 000000000..945f00e19 Binary files /dev/null and b/design/screenshots/Chinese (Traditional) zh-TW/school-description.png differ diff --git a/design/screenshots/Chinese (Traditional) zh-TW/school-details.png b/design/screenshots/Chinese (Traditional) zh-TW/school-details.png new file mode 100644 index 000000000..84430f21d Binary files /dev/null and b/design/screenshots/Chinese (Traditional) zh-TW/school-details.png differ diff --git a/design/screenshots/Chinese (Traditional) zh-TW/taking-picture.png b/design/screenshots/Chinese (Traditional) zh-TW/taking-picture.png new file mode 100644 index 000000000..0c4445f5f Binary files /dev/null and b/design/screenshots/Chinese (Traditional) zh-TW/taking-picture.png differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7d2af2435..7a3265ee9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e9e3a344e..f16d26666 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Thu Nov 02 02:18:35 IST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-bin.zip