diff --git a/app/build.gradle b/app/build.gradle index 1de265ccf..2bb4341f5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,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 { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 872d02b9e..9a232d1b9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,18 +2,18 @@ package="fr.free.nrw.commons"> - - - - - + + + + + - - - - - - + + + + + + @@ -32,22 +32,25 @@ android:finishOnTaskLaunch="true" /> + android:name="org.acra.CrashReportDialog" + android:excludeFromRecents="true" + android:finishOnTaskLaunch="true" + android:launchMode="singleInstance" + android:theme="@android:style/Theme.Dialog" /> + + - - + + - - + + + + android:label="@string/app_name"> @@ -55,11 +58,11 @@ + + android:name=".upload.MultipleShareActivity" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name"> @@ -69,33 +72,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"> @@ -106,27 +110,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 30cad5432..4ef4376af 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,24 +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.AndroidInjector; -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.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 @@ -54,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}; @@ -65,68 +57,12 @@ 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(); @@ -137,12 +73,6 @@ public class CommonsApplication extends Application implements HasActivityInject Timber.plant(new Timber.DebugTree()); - DaggerAppComponent - .builder() - .application(this) - .build() - .inject(this); - if (!BuildConfig.DEBUG) { ACRA.init(this); } else { @@ -153,9 +83,6 @@ public class CommonsApplication extends Application implements HasActivityInject System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0"); Fresco.initialize(this); - - //For caching area -> categories - cacheData = new CacheController(); } protected RefWatcher setupLeakCanary() { @@ -170,43 +97,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) { @@ -221,67 +123,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 AndroidInjector 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/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java index d4a19c9fc..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 @@ -19,7 +19,6 @@ import javax.inject.Singleton; import timber.log.Timber; -@Singleton public class LocationServiceManager implements LocationListener { public static final int LOCATION_REQUEST = 1; @@ -32,7 +31,6 @@ public class LocationServiceManager implements LocationListener { 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); 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 f018d1190..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 @@ -28,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; @@ -48,12 +46,17 @@ import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUE public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener { + private static final int LOCATION_REQUEST = 1; + private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed"; + @BindView(R.id.progressBar) ProgressBar progressBar; - private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed"; @Inject LocationServiceManager locationManager; + @Inject + NearbyController nearbyController; + private LatLng curLatLang; private Bundle bundle; private SharedPreferences sharedPreferences; @@ -64,7 +67,6 @@ 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); @@ -279,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(); @@ -301,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(); } 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 58c8bb8a7..624a0de0f 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 = prefs.getBoolean("useWikidata", true) ? nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage()) : nearbyPlaces.getFromWikiNeedsPictures(); 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/test/java/fr/free/nrw/commons/TestCommonsApplication.java b/app/src/test/java/fr/free/nrw/commons/TestCommonsApplication.java index f9bad405a..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,25 +1,60 @@ package fr.free.nrw.commons; -import android.app.Activity; +import android.content.SharedPreferences; +import android.support.v4.util.LruCache; import com.squareup.leakcanary.RefWatcher; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import dagger.android.AndroidInjector; +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 - private AndroidInjector mockInjector; + CommonsApplicationModule commonsApplicationModule; @Mock - private NearbyPlaces mockNearbyPlaces; + 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() { - super.onCreate(); MockitoAnnotations.initMocks(this); + super.onCreate(); } @Override @@ -29,12 +64,119 @@ public class TestCommonsApplication extends CommonsApplication { } @Override - public AndroidInjector activityInjector() { - return mockInjector; + 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; } - @Override - public synchronized NearbyPlaces getNearbyPlaces() { - return mockNearbyPlaces; + 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/nearby/NearbyActivityTest.java b/app/src/test/java/fr/free/nrw/commons/nearby/NearbyActivityTest.java index 7a0f50119..eb1cff52b 100644 --- a/app/src/test/java/fr/free/nrw/commons/nearby/NearbyActivityTest.java +++ b/app/src/test/java/fr/free/nrw/commons/nearby/NearbyActivityTest.java @@ -1,12 +1,9 @@ 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.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; @@ -14,14 +11,11 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.android.controller.ActivityController; import org.robolectric.annotation.Config; -import dagger.android.AndroidInjector; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.TestCommonsApplication; import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.location.LocationServiceManager; import static junit.framework.Assert.assertNotNull; -import static org.mockito.Matchers.isA; import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) @@ -30,9 +24,6 @@ public class NearbyActivityTest { private static final LatLng ST_LOUIS_MO_LAT_LNG = new LatLng(38.627003, -90.199402, 0); - @Mock - LocationServiceManager mockLocationManager; - private ActivityController activityController; private NearbyActivity nearbyActivity; @@ -40,13 +31,11 @@ public class NearbyActivityTest { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - AndroidInjector injector = ((TestCommonsApplication) RuntimeEnvironment.application).activityInjector(); - Mockito.doNothing().when(injector).inject(isA(NearbyActivity.class)); - when(mockLocationManager.getLastLocation()).thenReturn(ST_LOUIS_MO_LAT_LNG); + TestCommonsApplication application = (TestCommonsApplication) RuntimeEnvironment.application; + when(application.getLocationServiceManager().getLastLocation()).thenReturn(ST_LOUIS_MO_LAT_LNG); activityController = Robolectric.buildActivity(NearbyActivity.class); nearbyActivity = activityController.get(); - nearbyActivity.locationManager = mockLocationManager; } @Test 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/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