diff --git a/.gitignore b/.gitignore index 692e4a404..1ab05305e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ local.properties # OS files *.DS_Store Thumbs.db +app/gradle/wrapper/gradle-wrapper.jar +app/gradlew +app/gradlew.bat +app/gradle/wrapper/gradle-wrapper.properties diff --git a/.travis.yml b/.travis.yml index 8c0af5731..267625fdd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,12 @@ language: android +addons: + apt: + packages: + - w3m +env: + global: + - ANDROID_TARGET=android-22 + - ANDROID_ABI=armeabi-v7a android: components: - platform-tools @@ -6,8 +14,17 @@ android: - build-tools-25.0.1 - extra-google-m2repository - extra-android-m2repository + - ${ANDROID_TARGET} - android-25 - - sys-img-x86-android-18 + - sys-img-${ANDROID_ABI}-${ANDROID_TARGET} +before_script: + - echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI + - emulator -avd test -no-audio -no-window & + - android-wait-for-emulator +script: + - ./gradlew test connectedAndroidTest -stacktrace +after_failure: + - w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/androidTests/connected/*Test.html jdk: # - openjdk8 # not yet available - oraclejdk8 diff --git a/CHANGELOG.md b/CHANGELOG.md index 915f885ff..9c39134e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,25 @@ # Wikimedia Commons for Android -## v2.2.2 beta +## v2.4 beta +- Fixed memory issue with loading contributions on main screen +- Deleted images don't show up on contributions list +- Added Fresco library for image loading and LeakCanary for memory profiling +- Added navigation drawer and overhauled action bar +- Added logout functionality +- Fixed various issues with map of Nearby places + +## v2.3 beta +- Add map of Nearby places +- Add overlay dialog when a Nearby place is tapped +- Set default number of uploads to display in Main activity as 100, and add option in Settings to change it +- Detect when 2FA is used for login and display message +- Display date uploaded and image coordinates in image details page +- Display message when GPS is turned off, and when no Nearby items are found + +## v2.2.2 - Hotfix for Nearby localization issue -## v2.2.1 beta +## v2.2.1 - Hotfix for Settings crash ## v2.2 beta (will not be released to Production due to bugs with Settings) diff --git a/CREDITS b/CREDITS index 812b5329f..20a64ad9f 100644 --- a/CREDITS +++ b/CREDITS @@ -19,3 +19,9 @@ their contribution to the product. * Veyndan Stuart * Vivek Maskara * Neslihan Turan + +3rd party open source libraries used: +* Butterknife +* GSON +* Timber +* MapBox diff --git a/app/build.gradle b/app/build.gradle index bf9af869d..811639e02 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,6 @@ dependencies { compile 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar' compile 'in.yuvi:http.fluent:1.3' compile 'com.android.volley:volley:1.0.0' - compile 'com.nostra13.universalimageloader:universal-image-loader:1.8.4' compile 'ch.acra:acra:4.7.0' compile 'org.mediawiki:api:1.3' compile 'commons-codec:commons-codec:1.10' @@ -16,8 +15,21 @@ dependencies { compile 'com.google.code.gson:gson:2.7' compile "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" annotationProcessor "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" + compile 'com.jakewharton.timber:timber:4.5.1' + compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.0.2@aar'){ + transitive=true + } + compile 'com.facebook.fresco:fresco:1.3.0' + compile 'com.facebook.stetho:stetho:1.5.0' + compile "com.google.guava:guava:${GUAVA_VERSION}" testCompile 'junit:junit:4.12' + androidTestCompile "com.android.support:support-annotations:${project.supportLibVersion}" + androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' + + debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1' + releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' + testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' } android { @@ -28,10 +40,12 @@ android { defaultConfig { applicationId 'fr.free.nrw.commons' - versionCode 69 - versionName '2.2.2' + versionCode 71 + versionName '2.4' minSdkVersion project.minSdkVersion targetSdkVersion project.targetSdkVersion + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true } buildTypes { @@ -46,4 +60,4 @@ android { disable 'ExtraTranslation' abortOnError false } -} \ No newline at end of file +} diff --git a/app/quality.gradle b/app/quality.gradle index a63a02720..7ea20916a 100644 --- a/app/quality.gradle +++ b/app/quality.gradle @@ -25,7 +25,7 @@ task checkstyle(type: Checkstyle) { task pmd(type: Pmd) { ignoreFailures = true - ruleSetFiles = files("${project.rootDir}/ruleset.xml") + ruleSetFiles = files("${project.rootDir}/script/style/ruleset.xml") ruleSets = [] source 'src' diff --git a/app/src/androidTest/java/fr/free/nrw/commons/NearbyActivityTest.java b/app/src/androidTest/java/fr/free/nrw/commons/NearbyActivityTest.java new file mode 100644 index 000000000..2ac5284e9 --- /dev/null +++ b/app/src/androidTest/java/fr/free/nrw/commons/NearbyActivityTest.java @@ -0,0 +1,29 @@ +package fr.free.nrw.commons; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withText; + +import android.support.test.espresso.assertion.ViewAssertions; +import android.support.test.filters.LargeTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import fr.free.nrw.commons.nearby.NearbyActivity; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class NearbyActivityTest { + @Rule + public final ActivityTestRule nearby = + new ActivityTestRule<>(NearbyActivity.class); + + @Test + public void testActivityLaunch() { + onView(withText("Nearby Places")).check(ViewAssertions.matches(isDisplayed())); + } +} diff --git a/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityTest.java b/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityTest.java new file mode 100644 index 000000000..533498a47 --- /dev/null +++ b/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityTest.java @@ -0,0 +1,111 @@ +package fr.free.nrw.commons; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.test.espresso.Espresso; +import android.support.test.espresso.action.ViewActions; +import android.support.test.espresso.assertion.ViewAssertions; +import android.support.test.espresso.matcher.ViewMatchers; +import android.support.test.filters.LargeTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.view.View; + +import org.hamcrest.Matcher; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Map; + +import fr.free.nrw.commons.settings.SettingsActivity; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.anything; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class SettingsActivityTest { + private SharedPreferences prefs; + private Map prefValues; + + @Rule + public ActivityTestRule activityRule = + new ActivityTestRule(SettingsActivity.class, + false /* Initial touch mode */, true /* launch activity */) { + + @Override + protected void afterActivityLaunched() { + // save preferences + prefs = PreferenceManager.getDefaultSharedPreferences(this.getActivity()); + prefValues = prefs.getAll(); + } + + @Override + protected void afterActivityFinished() { + // restore preferences + SharedPreferences.Editor editor = prefs.edit(); + for (Map.Entry entry: prefValues.entrySet()) { + String key = entry.getKey(); + Object val = entry.getValue(); + if (val instanceof String) { + editor.putString(key, (String)val); + } else if (val instanceof Boolean) { + editor.putBoolean(key, (Boolean)val); + } else if (val instanceof Integer) { + editor.putInt(key, (Integer)val); + } else { + throw new RuntimeException("type not implemented: " + entry); + } + } + editor.apply(); + } + }; + + @Test + public void oneLicenseIsChecked() { + // click "License" (the first item) + Espresso.onData(anything()) + .inAdapterView(findPreferenceList()) + .atPosition(0) + .perform(ViewActions.click()); + + // test the selected item + Espresso.onView(ViewMatchers.isChecked()) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())); + } + + @Test + public void afterClickingCcby4ItWillStay() { + // click "License" (the first item) + Espresso.onData(anything()) + .inAdapterView(findPreferenceList()) + .atPosition(0) + .perform(ViewActions.click()); + + // click "Attribution 4.0" + Espresso.onView( + ViewMatchers.withText(R.string.license_name_cc_by_four) + ).perform(ViewActions.click()); + + // click "License" (the first item) + Espresso.onData(anything()) + .inAdapterView(findPreferenceList()) + .atPosition(0) + .perform(ViewActions.click()); + + // test the value remains "Attribution 4.0" + Espresso.onView(ViewMatchers.isChecked()) + .check(ViewAssertions.matches( + ViewMatchers.withText(R.string.license_name_cc_by_four) + )); + } + + private static Matcher findPreferenceList() { + return allOf( + ViewMatchers.isDescendantOfA(ViewMatchers.withId(R.id.settingsFragment)), + ViewMatchers.withResourceName("list"), + ViewMatchers.hasFocus() + ); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ff3dbf5c1..bce7c0642 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ @@ -31,6 +30,10 @@ + + + + - - - - . + SERVICE wikibase:label { bd:serviceParam wikibase:language "en" } + } + + OPTIONAL { + ?commonsArticle schema:about ?item ; + schema:isPartOf . + SERVICE wikibase:label { bd:serviceParam wikibase:language "en" } + } + } + } + GROUP BY ?item ?wikipediaArticle ?commonsArticle \ No newline at end of file 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 c5156069b..1f11be2dc 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java @@ -1,24 +1,16 @@ package fr.free.nrw.commons; +import android.content.Context; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; -import android.text.Html; -import android.text.method.LinkMovementMethod; import android.widget.TextView; -import fr.free.nrw.commons.theme.BaseActivity; - import butterknife.BindView; import butterknife.ButterKnife; +import fr.free.nrw.commons.theme.NavigationBaseActivity; -public class AboutActivity extends BaseActivity { +public class AboutActivity extends NavigationBaseActivity { @BindView(R.id.about_version) TextView versionText; - @BindView(R.id.about_license) TextView licenseText; - @BindView(R.id.about_improve) TextView improveText; - @BindView(R.id.about_privacy_policy) TextView privacyPolicyText; - @BindView(R.id.about_uploads_to) TextView uploadsToText; - @BindView(R.id.about_credits) TextView creditsText; @Override public void onCreate(Bundle savedInstanceState) { @@ -27,24 +19,12 @@ public class AboutActivity extends BaseActivity { ButterKnife.bind(this); - uploadsToText.setText(CommonsApplication.EVENTLOG_WIKI); versionText.setText(BuildConfig.VERSION_NAME); - - // We can't use formatted strings directly because it breaks with - // our localization tools. Grab an HTML string and turn it into - // a formatted string. - fixFormatting(licenseText, R.string.about_license); - fixFormatting(improveText, R.string.about_improve); - fixFormatting(privacyPolicyText, R.string.about_privacy_policy); - fixFormatting(creditsText, R.string.about_credits); - - licenseText.setMovementMethod(LinkMovementMethod.getInstance()); - improveText.setMovementMethod(LinkMovementMethod.getInstance()); - privacyPolicyText.setMovementMethod(LinkMovementMethod.getInstance()); - creditsText.setMovementMethod(LinkMovementMethod.getInstance()); + initDrawer(); } - private void fixFormatting(TextView textView, int resource) { - textView.setText(Html.fromHtml(getResources().getString(resource))); + public static void startYourself(Context context) { + Intent settingsIntent = new Intent(context, AboutActivity.class); + context.startActivity(settingsIntent); } } \ No newline at end of file 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 ebfc29b90..46f74810e 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -5,20 +5,26 @@ import android.accounts.AccountManager; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.graphics.Bitmap; import android.os.Build; +import android.database.sqlite.SQLiteDatabase; +import android.preference.PreferenceManager; import android.support.v4.util.LruCache; -import android.util.Log; -import com.android.volley.RequestQueue; -import com.android.volley.toolbox.BasicNetwork; -import com.android.volley.toolbox.DiskBasedCache; -import com.android.volley.toolbox.HurlStack; -import com.nostra13.universalimageloader.cache.disc.impl.TotalSizeLimitedDiscCache; -import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; -import com.nostra13.universalimageloader.utils.StorageUtils; +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.stetho.Stetho; + +import fr.free.nrw.commons.caching.CacheController; +import fr.free.nrw.commons.category.Category; +import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.data.DBOpenHelper; +import fr.free.nrw.commons.modifications.ModifierSequence; +import fr.free.nrw.commons.auth.AccountUtil; +import fr.free.nrw.commons.nearby.NearbyPlaces; + +import com.squareup.leakcanary.LeakCanary; import org.acra.ACRA; import org.acra.ReportingInteractionMode; @@ -33,12 +39,12 @@ import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.CoreProtocolPNames; -import org.mediawiki.api.MWApi; +import java.io.File; import java.io.IOException; -import fr.free.nrw.commons.auth.WikiAccountAuthenticator; -import fr.free.nrw.commons.caching.CacheController; +import fr.free.nrw.commons.utils.FileUtils; +import timber.log.Timber; // TODO: Use ProGuard to rip out reporting when publishing @ReportsCrashes( @@ -51,7 +57,6 @@ import fr.free.nrw.commons.caching.CacheController; ) public class CommonsApplication extends Application { - private MWApi api; private Account currentAccount = null; // Unlike a savings account... public static final String API_URL = "https://commons.wikimedia.org/w/api.php"; public static final String IMAGE_URL_BASE = "https://upload.wikimedia.org/wikipedia/commons"; @@ -70,11 +75,37 @@ public class CommonsApplication extends Application { public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com"; public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback"; - public RequestQueue volleyQueue; + private static CommonsApplication instance = null; + private AbstractHttpClient httpClient = null; + private MWApi api = null; + LruCache thumbnailUrlCache = new LruCache<>(1024); + private CacheController cacheData = null; + private DBOpenHelper dbOpenHelper = null; + private NearbyPlaces nearbyPlaces = null; - public CacheController cacheData; + /** + * 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 AbstractHttpClient createHttpClient() { + public static CommonsApplication getInstance() { + if (instance == null) { + instance = new CommonsApplication(); + } + return instance; + } + + public AbstractHttpClient getHttpClient() { + if (httpClient == null) { + httpClient = newHttpClient(); + } + return httpClient; + } + + private AbstractHttpClient newHttpClient() { BasicHttpParams params = new BasicHttpParams(); SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); @@ -85,87 +116,78 @@ public class CommonsApplication extends Application { return new DefaultHttpClient(cm, params); } - public static MWApi createMWApi() { - return new MWApi(API_URL, createHttpClient()); + public MWApi getMWApi() { + if (api == null) { + api = newMWApi(); + } + return api; + } + + private MWApi newMWApi() { + return new MWApi(API_URL, getHttpClient()); + } + + 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(); + if (LeakCanary.isInAnalyzerProcess(this)) { + // This process is dedicated to LeakCanary for heap analysis. + // You should not init your app in this process. + return; + } + LeakCanary.install(this); + + Timber.plant(new Timber.DebugTree()); + + Stetho.initializeWithDefaults(this); + if (!BuildConfig.DEBUG) { ACRA.init(this); } // Fire progress callbacks for every 3% of uploaded content System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0"); - api = createMWApi(); - ImageLoaderConfiguration imageLoaderConfiguration = new ImageLoaderConfiguration.Builder(getApplicationContext()) - .discCache(new TotalSizeLimitedDiscCache(StorageUtils.getCacheDirectory(this), 128 * 1024 * 1024)) - .build(); - ImageLoader.getInstance().init(imageLoaderConfiguration); + Fresco.initialize(this); // Initialize EventLogging EventLog.setApp(this); - // based off https://developer.android.com/training/displaying-bitmaps/cache-bitmap.html - // Cache for 1/8th of available VM memory - long maxMem = Runtime.getRuntime().maxMemory(); - if (maxMem < 48L * 1024L * 1024L) { - // Cache only one bitmap if VM memory is too small (such as Nexus One); - Log.d("Commons", "Skipping bitmap cache; max mem is: " + maxMem); - imageCache = new LruCache<>(1); - } else { - int cacheSize = (int) (maxMem / (1024 * 8)); - Log.d("Commons", "Bitmap cache size " + cacheSize + " from max mem " + maxMem); - imageCache = new LruCache(cacheSize) { - @Override - protected int sizeOf(String key, Bitmap bitmap) { - int bitmapSize; - bitmapSize = bitmap.getByteCount(); - - // The cache size will be measured in kilobytes rather than number of items. - return bitmapSize / 1024; - } - }; - } - //For caching area -> categories cacheData = new CacheController(); - - DiskBasedCache cache = new DiskBasedCache(getCacheDir(), 16 * 1024 * 1024); - volleyQueue = new RequestQueue(cache, new BasicNetwork(new HurlStack())); - volleyQueue.start(); } - private com.android.volley.toolbox.ImageLoader imageLoader; - private LruCache imageCache; - - public com.android.volley.toolbox.ImageLoader getImageLoader() { - if(imageLoader == null) { - imageLoader = new com.android.volley.toolbox.ImageLoader(volleyQueue, new com.android.volley.toolbox.ImageLoader.ImageCache() { - @Override - public Bitmap getBitmap(String key) { - return imageCache.get(key); - } - - @Override - public void putBitmap(String key, Bitmap bitmap) { - imageCache.put(key, bitmap); - } - }); - imageLoader.setBatchedResponseDelay(0); - } - return imageLoader; - } - - public MWApi getApi() { - return api; - } - + /** + * @return Account|null + */ public Account getCurrentAccount() { if(currentAccount == null) { AccountManager accountManager = AccountManager.get(this); - Account[] allAccounts = accountManager.getAccountsByType(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE); + Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType()); if(allAccounts.length != 0) { currentAccount = allAccounts[0]; } @@ -181,21 +203,12 @@ public class CommonsApplication extends Application { return false; // This should never happen } - accountManager.invalidateAuthToken(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE, api.getAuthCookie()); + accountManager.invalidateAuthToken(AccountUtil.accountType(), getMWApi().getAuthCookie()); try { String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false); - api.setAuthCookie(authCookie); + getMWApi().setAuthCookie(authCookie); return true; - } catch (OperationCanceledException e) { - e.printStackTrace(); - return false; - } catch (AuthenticatorException e) { - e.printStackTrace(); - return false; - } catch (IOException e) { - e.printStackTrace(); - return false; - } catch (NullPointerException e) { + } catch (OperationCanceledException | NullPointerException | IOException | AuthenticatorException e) { e.printStackTrace(); return false; } @@ -206,4 +219,46 @@ public class CommonsApplication extends Application { return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) || pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); } + + public void clearApplicationData(Context context) { + File cacheDirectory = context.getCacheDir(); + File applicationDirectory = new File(cacheDirectory.getParent()); + if (applicationDirectory.exists()) { + String[] fileNames = applicationDirectory.list(); + for (String fileName : fileNames) { + if (!fileName.equals("lib")) { + FileUtils.deleteFile(new File(applicationDirectory, fileName)); + } + } + } + + AccountManager accountManager = AccountManager.get(this); + Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType()); + for (int index = 0; index < allAccounts.length; index++) { + accountManager.removeAccount(allAccounts[index], null, null); + } + + //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(); + updateAllDatabases(); + currentAccount = null; + } + + /** + * Deletes all tables and re-creates them. + */ + public void updateAllDatabases() { + DBOpenHelper dbOpenHelper = CommonsApplication.getInstance().getDBOpenHelper(); + dbOpenHelper.getReadableDatabase().close(); + SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); + + ModifierSequence.Table.onDelete(db); + Category.Table.onDelete(db); + Contribution.Table.onDelete(db); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/EventLog.java b/app/src/main/java/fr/free/nrw/commons/EventLog.java index 373a47ac9..5cb36450b 100644 --- a/app/src/main/java/fr/free/nrw/commons/EventLog.java +++ b/app/src/main/java/fr/free/nrw/commons/EventLog.java @@ -4,19 +4,19 @@ import android.content.SharedPreferences; import android.os.AsyncTask; import android.os.Build; import android.preference.PreferenceManager; -import android.util.Log; import org.apache.http.HttpResponse; +import org.apache.http.impl.client.AbstractHttpClient; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; -import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import fr.free.nrw.commons.settings.Prefs; import in.yuvi.http.fluent.Http; +import timber.log.Timber; public class EventLog { @@ -30,19 +30,19 @@ public class EventLog { boolean allSuccess = true; // Not using the default URL connection, since that seems to have different behavior than the rest of the code for(LogBuilder logBuilder: logBuilders) { - HttpURLConnection conn; try { URL url = logBuilder.toUrl(); - HttpResponse response = Http.get(url.toString()).use(CommonsApplication.createHttpClient()).asResponse(); + AbstractHttpClient httpClient = CommonsApplication.getInstance().getHttpClient(); + HttpResponse response = Http.get(url.toString()).use(httpClient).asResponse(); if(response.getStatusLine().getStatusCode() != 204) { allSuccess = false; } - Log.d("Commons", "EventLog hit " + url.toString()); + Timber.d("EventLog hit %s", url); } catch (IOException e) { // Probably just ignore for now. Can be much more robust with a service, etc later on. - Log.d("Commons", "IO Error, EventLog hit skipped"); + Timber.d("IO Error, EventLog hit skipped"); } } @@ -94,9 +94,7 @@ public class EventLog { data.put("appversion", "Android/" + BuildConfig.VERSION_NAME); fullData.put("event", data); return new URL(CommonsApplication.EVENTLOG_URL + "?" + Utils.urlEncode(fullData.toString()) + ";"); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } catch (JSONException e) { + } catch (MalformedURLException | JSONException e) { throw new RuntimeException(e); } } 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 0ef067496..f3edf3be0 100644 --- a/app/src/main/java/fr/free/nrw/commons/LicenseList.java +++ b/app/src/main/java/fr/free/nrw/commons/LicenseList.java @@ -69,9 +69,8 @@ public class LicenseList { int nameId = stringIdByName(stringId); //Log.d("Commons", "LicenseList.nameForTemplate: nameId: " + nameId); if(nameId != 0) { - String name = res.getString(nameId); //Log.d("Commons", "LicenseList.nameForTemplate: name: " + name); - return name; + return res.getString(nameId); } return template; } diff --git a/app/src/main/java/fr/free/nrw/commons/MWApi.java b/app/src/main/java/fr/free/nrw/commons/MWApi.java new file mode 100644 index 000000000..785ea7886 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/MWApi.java @@ -0,0 +1,94 @@ +package fr.free.nrw.commons; + +import java.io.IOException; + +import org.apache.http.impl.client.AbstractHttpClient; +import org.mediawiki.api.ApiResult; + +/** + * @author Addshore + */ +public class MWApi extends org.mediawiki.api.MWApi { + + /** We don't actually use this but need to pass it in requests */ + private static String LOGIN_RETURN_TO_URL = "https://commons.wikimedia.org"; + + public MWApi(String apiURL, AbstractHttpClient client) { + super(apiURL, client); + } + + /** + * @param username String + * @param password String + * @return String as returned by this.getErrorCodeToReturn() + * @throws IOException On api request IO issue + */ + public String login(String username, String password) throws IOException { + String token = this.getLoginToken(); + ApiResult loginApiResult = this.action("clientlogin") + .param("rememberMe", "1") + .param("username", username) + .param("password", password) + .param("logintoken", token) + .param("loginreturnurl", LOGIN_RETURN_TO_URL) + .post(); + return this.getErrorCodeToReturn( loginApiResult ); + } + + /** + * @param username String + * @param password String + * @param twoFactorCode String + * @return String as returned by this.getErrorCodeToReturn() + * @throws IOException On api request IO issue + */ + public String login(String username, String password, String twoFactorCode) throws IOException { + String token = this.getLoginToken();//TODO cache this instead of calling again when 2FAing + ApiResult loginApiResult = this.action("clientlogin") + .param("rememberMe", "1") + .param("username", username) + .param("password", password) + .param("logintoken", token) + .param("logincontinue", "1") + .param("OATHToken", twoFactorCode) + .post(); + + return this.getErrorCodeToReturn( loginApiResult ); + } + + private String getLoginToken() throws IOException { + ApiResult tokenResult = this.action("query") + .param("action", "query") + .param("meta", "tokens") + .param("type", "login") + .post(); + return tokenResult.getString("/api/query/tokens/@logintoken"); + } + + /** + * @param loginApiResult ApiResult Any clientlogin api result + * @return String On success: "PASS" + * continue: "2FA" (More information required for 2FA) + * failure: A failure message code (defined by mediawiki) + * misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART + */ + private String getErrorCodeToReturn( ApiResult loginApiResult ) { + String status = loginApiResult.getString("/api/clientlogin/@status"); + if (status.equals("PASS")) { + this.isLoggedIn = true; + return status; + } else if (status.equals("FAIL")) { + return loginApiResult.getString("/api/clientlogin/@messagecode"); + } else if ( + status.equals("UI") + && loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest") + && loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).") + ) { + return "2FA"; + } + + // UI, REDIRECT, RESTART + return "genericerror-" + status; + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 924427cef..e3fb2baaf 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -116,10 +116,6 @@ public class Media implements Parcelable { this.creator = creator; } - public String getThumbnailUrl(int width) { - return Utils.makeThumbUrl(getImageUrl(), getFilename(), width); - } - public int getWidth() { return width; } @@ -144,6 +140,14 @@ public class Media implements Parcelable { this.license = license; } + public String getCoordinates() { + return coordinates; + } + + public void setCoordinates(String coordinates) { + this.coordinates = coordinates; + } + // Primary metadata fields protected Uri localUri; protected String imageUrl; @@ -155,6 +159,7 @@ public class Media implements Parcelable { protected int width; protected int height; protected String license; + private String coordinates; protected String creator; protected ArrayList categories; // as loaded at runtime? protected Map descriptions; // multilingual descriptions as loaded @@ -164,7 +169,7 @@ public class Media implements Parcelable { } public void setCategories(List categories) { - this.categories.removeAll(this.categories); + this.categories.clear(); this.categories.addAll(categories); } 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 f5d153e5a..280309d3d 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -1,9 +1,6 @@ package fr.free.nrw.commons; -import android.util.Log; - import org.mediawiki.api.ApiResult; -import org.mediawiki.api.MWApi; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -23,6 +20,9 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import fr.free.nrw.commons.location.LatLng; +import timber.log.Timber; + /** * Fetch additional media data from the network that we don't store locally. * @@ -31,14 +31,13 @@ import javax.xml.parsers.ParserConfigurationException; */ public class MediaDataExtractor { private boolean fetched; - private boolean processed; private String filename; private ArrayList categories; private Map descriptions; - private String author; private Date date; private String license; + private String coordinates; private LicenseList licenseList; /** @@ -49,7 +48,6 @@ public class MediaDataExtractor { categories = new ArrayList<>(); descriptions = new HashMap<>(); fetched = false; - processed = false; this.licenseList = licenseList; } @@ -64,7 +62,7 @@ public class MediaDataExtractor { throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again."); } - MWApi api = CommonsApplication.createMWApi(); + MWApi api = CommonsApplication.getInstance().getMWApi(); ApiResult result = api.action("query") .param("prop", "revisions") .param("titles", filename) @@ -111,9 +109,7 @@ public class MediaDataExtractor { doc = docBuilder.parse(new ByteArrayInputStream(source.getBytes("UTF-8"))); } catch (ParserConfigurationException e) { throw new RuntimeException(e); - } catch (IllegalStateException e) { - throw new IOException(e); - } catch (SAXException e) { + } catch (IllegalStateException | SAXException e) { throw new IOException(e); } Node templateNode = findTemplate(doc.getDocumentElement(), "information"); @@ -122,7 +118,14 @@ public class MediaDataExtractor { descriptions = getMultilingualText(descriptionNode); Node authorNode = findTemplateParameter(templateNode, "author"); - author = getFlatText(authorNode); + } + + Node coordinateTemplateNode = findTemplate(doc.getDocumentElement(), "location"); + + if (coordinateTemplateNode != null) { + coordinates = getCoordinates(coordinateTemplateNode); + } else { + coordinates = "No coordinates found"; } /* @@ -131,20 +134,20 @@ public class MediaDataExtractor { * look for 'self' template and check its first parameter * if none, look for any of the known templates */ - Log.d("Commons", "MediaDataExtractor searching for license"); + Timber.d("MediaDataExtractor searching for license"); Node selfLicenseNode = findTemplate(doc.getDocumentElement(), "self"); if (selfLicenseNode != null) { Node firstNode = findTemplateParameter(selfLicenseNode, 1); String licenseTemplate = getFlatText(firstNode); License license = licenseList.licenseForTemplate(licenseTemplate); if (license == null) { - Log.d("Commons", "MediaDataExtractor found no matching license for self parameter: " + licenseTemplate + "; faking it"); + Timber.d("MediaDataExtractor found no matching license for self parameter: %s; faking it", licenseTemplate); this.license = licenseTemplate; // hack hack! For non-selectable licenses that are still in the system. } else { // fixme: record the self-ness in here too... sigh // all this needs better server-side metadata this.license = license.getKey(); - Log.d("Commons", "MediaDataExtractor found self-license " + this.license); + Timber.d("MediaDataExtractor found self-license %s", this.license); } } else { for (License license : licenseList.values()) { @@ -153,7 +156,7 @@ public class MediaDataExtractor { if (template != null) { // Found! this.license = license.getKey(); - Log.d("Commons", "MediaDataExtractor found non-self license " + this.license); + Timber.d("MediaDataExtractor found non-self license %s", this.license); break; } } @@ -245,6 +248,25 @@ public class MediaDataExtractor { return parentNode.getTextContent(); } + /** + * Extracts the coordinates from the template and returns them as pretty formatted string. + * Loops over the children of the coordinate template: + * {{Location|47.50111007666667|19.055700301944444}} + * and extracts the latitude and longitude as a pretty string. + * + * @param parentNode The node of the coordinates template. + * @return Pretty formatted coordinates. + * @throws IOException Parsing failed. + */ + private String getCoordinates(Node parentNode) throws IOException { + NodeList childNodes = parentNode.getChildNodes(); + double latitudeText = Double.parseDouble(childNodes.item(1).getTextContent()); + double longitudeText = Double.parseDouble(childNodes.item(2).getTextContent()); + LatLng coordinates = new LatLng(latitudeText, longitudeText); + + return coordinates.getPrettyCoordinateString(); + } + // Extract a dictionary of multilingual texts from a subset of the parse tree. // Texts are wrapped in things like {{en|foo} or {{en|1=foo bar}}. // Text outside those wrappers is stuffed into a 'default' faux language key if present. @@ -290,6 +312,7 @@ public class MediaDataExtractor { media.setCategories(categories); media.setDescriptions(descriptions); + media.setCoordinates(coordinates); if (license != null) { media.setLicense(license); } diff --git a/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java b/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java new file mode 100644 index 000000000..fb73b5a2e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java @@ -0,0 +1,33 @@ +package fr.free.nrw.commons; + +import android.os.AsyncTask; +import android.support.annotation.NonNull; + +import org.mediawiki.api.ApiResult; + +class MediaThumbnailFetchTask extends AsyncTask { + private static final String THUMB_SIZE = "640"; + protected final Media media; + + public MediaThumbnailFetchTask(@NonNull Media media) { + this.media = media; + } + + @Override + protected String doInBackground(String... params) { + try { + MWApi api = CommonsApplication.getInstance().getMWApi(); + ApiResult result =api.action("query") + .param("format", "xml") + .param("prop", "imageinfo") + .param("iiprop", "url") + .param("iiurlwidth", THUMB_SIZE) + .param("titles", params[0]) + .get(); + return result.getString("/api/query/pages/page/imageinfo/ii/@thumburl"); + } catch (Exception e) { + // Do something better! + } + return null; + } +} 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 02b69b260..ccb75a3dd 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java @@ -1,47 +1,15 @@ -/** - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package fr.free.nrw.commons; import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.drawable.BitmapDrawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.AttributeSet; -import android.view.View; -import android.widget.ImageView; -import com.android.volley.VolleyError; -import com.android.volley.toolbox.ImageLoader; -import com.android.volley.toolbox.ImageLoader.ImageContainer; -import com.android.volley.toolbox.ImageLoader.ImageListener; +import com.facebook.drawee.view.SimpleDraweeView; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.contributions.ContributionsContentProvider; - -public class MediaWikiImageView extends ImageView { - - private Media mMedia; - - private ImageLoader mImageLoader; - - private ImageContainer mImageContainer; - - private View loadingView; - - private boolean isThumbnail; +public class MediaWikiImageView extends SimpleDraweeView { + private ThumbnailFetchTask currentThumbnailTask; public MediaWikiImageView(Context context) { this(context, null); @@ -49,179 +17,58 @@ public class MediaWikiImageView extends ImageView { public MediaWikiImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); - TypedArray actualAttrs = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MediaWikiImageView, 0, 0); - isThumbnail = actualAttrs.getBoolean(0, false); - actualAttrs.recycle(); } public MediaWikiImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } - public void setMedia(Media media, ImageLoader imageLoader) { - this.mMedia = media; - mImageLoader = imageLoader; - loadImageIfNecessary(false); - } - - public void setLoadingView(View loadingView) { - this.loadingView = loadingView; - } - - public View getLoadingView() { - return loadingView; - } - - private void loadImageIfNecessary(final boolean isInLayoutPass) { - loadImageIfNecessary(isInLayoutPass, false); - } - - private void loadImageIfNecessary(final boolean isInLayoutPass, final boolean tryOriginal) { - int width = getWidth(); - int height = getHeight(); - - // if the view's bounds aren't known yet, hold off on loading the image. - if (width == 0 && height == 0) { + public void setMedia(Media media) { + if (currentThumbnailTask != null) { + currentThumbnailTask.cancel(true); + } + if(media == null) { return; } - if(mMedia == null) { - return; - } - - // Do not count for density when loading thumbnails. - // FIXME: Use another 'algorithm' that doesn't punish low res devices - if(isThumbnail) { - float dpFactor = Math.max(getResources().getDisplayMetrics().density, 1.0f); - width = (int) (width / dpFactor); - height = (int) (height / dpFactor); - } - - final String mUrl; - if(tryOriginal) { - mUrl = mMedia.getImageUrl(); + if (CommonsApplication.getInstance().getThumbnailUrlCache().get(media.getFilename()) != null) { + setImageUrl(CommonsApplication.getInstance().getThumbnailUrlCache().get(media.getFilename())); } else { - // Round it to the nearest 320 - // Possible a similar size image has already been generated. - // Reduces Server cache fragmentation, also increases chance of cache hit - // If width is less than 320, we round up to 320 - int bucketedWidth = width <= 320 ? 320 : Math.round((float)width / 320.0f) * 320; - if(mMedia.getWidth() != 0 && mMedia.getWidth() < bucketedWidth) { - // If we know that the width of the image is lesser than the required width - // We don't even try to load the thumbnai, go directly to the source - loadImageIfNecessary(isInLayoutPass, true); - return; - } else { - mUrl = mMedia.getThumbnailUrl(bucketedWidth); - } + setImageUrl(null); + currentThumbnailTask = new ThumbnailFetchTask(media); + currentThumbnailTask.execute(media.getFilename()); } - - // if the URL to be loaded in this view is empty, cancel any old requests and clear the - // currently loaded image. - if (TextUtils.isEmpty(mUrl)) { - if (mImageContainer != null) { - mImageContainer.cancelRequest(); - mImageContainer = null; - } - setImageBitmap(null); - return; - } - - // Don't repeat work. Prevents onLayout cascades - // We ignore it if the image request was for either the current URL of for the full URL - // Since the full URL is always the second, and - if (mImageContainer != null && mImageContainer.getRequestUrl() != null) { - if (mImageContainer.getRequestUrl().equals(mMedia.getImageUrl()) || mImageContainer.getRequestUrl().equals(mUrl)) { - return; - } else { - // if there is a pre-existing request, cancel it if it's fetching a different URL. - mImageContainer.cancelRequest(); - BitmapDrawable actualDrawable = (BitmapDrawable)getDrawable(); - if(actualDrawable != null && actualDrawable.getBitmap() != null) { - setImageBitmap(null); - if(loadingView != null) { - loadingView.setVisibility(View.VISIBLE); - } - } - } - } - - // The pre-existing content of this view didn't match the current URL. Load the new image - // from the network. - ImageContainer newContainer = mImageLoader.get(mUrl, - new ImageListener() { - @Override - public void onErrorResponse(final VolleyError error) { - if(!tryOriginal) { - post(new Runnable() { - @Override - public void run() { - loadImageIfNecessary(false, true); - } - }); - } - } - - @Override - public void onResponse(final ImageContainer response, boolean isImmediate) { - // If this was an immediate response that was delivered inside of a layout - // pass do not set the image immediately as it will trigger a requestLayout - // inside of a layout. Instead, defer setting the image by posting back to - // the main thread. - if (isImmediate && isInLayoutPass) { - post(new Runnable() { - @Override - public void run() { - onResponse(response, false); - } - }); - return; - } - - if (response.getBitmap() != null) { - setImageBitmap(response.getBitmap()); - if(tryOriginal && mMedia instanceof Contribution && (response.getBitmap().getWidth() > mMedia.getWidth() || response.getBitmap().getHeight() > mMedia.getHeight())) { - // If there is no width information for this image, save it. This speeds up image loading massively for smaller images - mMedia.setHeight(response.getBitmap().getHeight()); - mMedia.setWidth(response.getBitmap().getWidth()); - ((Contribution)mMedia).setContentProviderClient(MediaWikiImageView.this.getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY)); - ((Contribution)mMedia).save(); - } - if(loadingView != null) { - loadingView.setVisibility(View.GONE); - } - } else { - // I'm not really sure where this would hit but not onError - } - } - }); - - // update the ImageContainer to be the new bitmap container. - mImageContainer = newContainer; - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - loadImageIfNecessary(true); } @Override protected void onDetachedFromWindow() { - if (mImageContainer != null) { - // If the view was bound to an image request, cancel it and clear - // out the image from the view. - mImageContainer.cancelRequest(); - setImageBitmap(null); - // also clear out the container so we can reload the image if necessary. - mImageContainer = null; + if (currentThumbnailTask != null) { + currentThumbnailTask.cancel(true); } super.onDetachedFromWindow(); } - @Override - protected void drawableStateChanged() { - super.drawableStateChanged(); - invalidate(); + private void setImageUrl(@Nullable String url) { + setImageURI(url); + } + + private class ThumbnailFetchTask extends MediaThumbnailFetchTask { + ThumbnailFetchTask(@NonNull Media media) { + super(media); + } + + @Override + protected void onPostExecute(String result) { + if (isCancelled()) { + return; + } + if (TextUtils.isEmpty(result) && media.getLocalUri() != null) { + result = media.getLocalUri().toString(); + } else { + // only cache meaningful thumbnails received from network. + CommonsApplication.getInstance().getThumbnailUrlCache().put(media.getFilename(), result); + } + setImageUrl(result); + } } } 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 493a654e6..9d38cd92a 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -1,14 +1,11 @@ package fr.free.nrw.commons; +import android.content.Context; import android.net.Uri; -import android.os.Build; -import android.util.Log; - -import com.nostra13.universalimageloader.core.DisplayImageOptions; -import com.nostra13.universalimageloader.core.assist.ImageScaleType; -import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; +import android.preference.PreferenceManager; import fr.free.nrw.commons.settings.Prefs; +import timber.log.Timber; import java.io.BufferedInputStream; import java.io.IOException; @@ -44,8 +41,6 @@ import org.xmlpull.v1.XmlPullParserException; public class Utils { - private static final String TAG = Utils.class.getName(); - // Get SHA1 of file from input stream public static String getSHA1(InputStream is) { @@ -53,7 +48,7 @@ public class Utils { try { digest = MessageDigest.getInstance("SHA1"); } catch (NoSuchAlgorithmException e) { - Log.e(TAG, "Exception while getting Digest", e); + Timber.e(e, "Exception while getting Digest"); return ""; } @@ -68,17 +63,17 @@ public class Utils { String output = bigInt.toString(16); // Fill to 40 chars output = String.format("%40s", output).replace(' ', '0'); - Log.i(TAG, "File SHA1: " + output); + Timber.i("File SHA1: %s", output); return output; } catch (IOException e) { - Log.e(TAG, "IO Exception", e); + Timber.e(e, "IO Exception"); return ""; } finally { try { is.close(); } catch (IOException e) { - Log.e(TAG, "Exception on closing MD5 input stream", e); + Timber.e(e, "Exception on closing MD5 input stream"); } } } @@ -124,10 +119,7 @@ public class Utils { Transformer transformer = null; try { transformer = TransformerFactory.newInstance().newTransformer(); - } catch (TransformerConfigurationException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (TransformerFactoryConfigurationError e) { + } catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) { // TODO Auto-generated catch block e.printStackTrace(); } @@ -145,26 +137,6 @@ public class Utils { return outputStream.toString(); } - private static DisplayImageOptions.Builder defaultImageOptionsBuilder; - - public static DisplayImageOptions.Builder getGenericDisplayOptions() { - if (defaultImageOptionsBuilder == null) { - defaultImageOptionsBuilder = new DisplayImageOptions.Builder().cacheInMemory() - .imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - // List views flicker badly during data updates on Android 2.3; we - // haven't quite figured out why but cells seem to be rearranged oddly. - // Disable the fade-in on 2.3 to reduce the effect. - defaultImageOptionsBuilder = defaultImageOptionsBuilder - .displayer(new FadeInBitmapDisplayer(300)); - } - defaultImageOptionsBuilder = defaultImageOptionsBuilder - .cacheInMemory() - .resetViewBeforeLoading(); - } - return defaultImageOptionsBuilder; - } - private static final URLCodec urlCodec = new URLCodec(); public static String urlEncode(String url) { @@ -184,76 +156,64 @@ public class Utils { return count; } - public static String makeThumbUrl(String imageUrl, String filename, int width) { - // Ugly Hack! - // Update: OH DEAR GOD WHAT A HORRIBLE HACK I AM SO SORRY - if (imageUrl.endsWith("webm")) { - return imageUrl.replaceFirst("test/", "test/thumb/").replace("commons/", "commons/thumb/") + "/" + width + "px--" + filename.replaceAll("File:", "").replaceAll(" ", "_") + ".jpg"; - } else { - String thumbUrl = imageUrl.replaceFirst("test/", "test/thumb/").replace("commons/", "commons/thumb/") + "/" + width + "px-" + filename.replaceAll("File:", "").replaceAll(" ", "_"); - if (thumbUrl.endsWith("jpg") || thumbUrl.endsWith("png") || thumbUrl.endsWith("jpeg")) { - return thumbUrl; - } else { - return thumbUrl + ".png"; - } - } - } - public static String capitalize(String string) { return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1); } public static String licenseTemplateFor(String license) { - if (license.equals(Prefs.Licenses.CC_BY_3)) { - return "{{self|cc-by-3.0}}"; - } else if (license.equals(Prefs.Licenses.CC_BY_4)) { - return "{{self|cc-by-4.0}}"; - } else if (license.equals(Prefs.Licenses.CC_BY_SA_3)) { - return "{{self|cc-by-sa-3.0}}"; - } else if (license.equals(Prefs.Licenses.CC_BY_SA_4)) { - return "{{self|cc-by-sa-4.0}}"; - } else if (license.equals(Prefs.Licenses.CC0)) { - return "{{self|cc-zero}}"; - } else if (license.equals(Prefs.Licenses.CC_BY)) { - return "{{self|cc-by-3.0}}"; - } else if (license.equals(Prefs.Licenses.CC_BY_SA)) { - return "{{self|cc-by-sa-3.0}}"; + 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"); + throw new RuntimeException("Unrecognized license value: " + license); } public static int licenseNameFor(String license) { - if (license.equals(Prefs.Licenses.CC_BY_3)) { - return R.string.license_name_cc_by; - } else if (license.equals(Prefs.Licenses.CC_BY_4)) { - return R.string.license_name_cc_by_four; - } else if (license.equals(Prefs.Licenses.CC_BY_SA_3)) { - return R.string.license_name_cc_by_sa; - } else if (license.equals(Prefs.Licenses.CC_BY_SA_4)) { - return R.string.license_name_cc_by_sa_four; - } else if (license.equals(Prefs.Licenses.CC0)) { - return R.string.license_name_cc0; - } else if (license.equals(Prefs.Licenses.CC_BY)) { // for backward compatibility to v2.1 - return R.string.license_name_cc_by_3_0; - } else if (license.equals(Prefs.Licenses.CC_BY_SA)) { // for backward compatibility to v2.1 - return R.string.license_name_cc_by_sa_3_0; + switch (license) { + case Prefs.Licenses.CC_BY_3: + return R.string.license_name_cc_by; + case Prefs.Licenses.CC_BY_4: + return R.string.license_name_cc_by_four; + case Prefs.Licenses.CC_BY_SA_3: + return R.string.license_name_cc_by_sa; + case Prefs.Licenses.CC_BY_SA_4: + return R.string.license_name_cc_by_sa_four; + case Prefs.Licenses.CC0: + return R.string.license_name_cc0; + case Prefs.Licenses.CC_BY: // for backward compatibility to v2.1 + return R.string.license_name_cc_by_3_0; + case Prefs.Licenses.CC_BY_SA: // for backward compatibility to v2.1 + return R.string.license_name_cc_by_sa_3_0; } - throw new RuntimeException("Unrecognized license value"); + throw new RuntimeException("Unrecognized license value: " + license); } public static String licenseUrlFor(String license) { - if (license.equals(Prefs.Licenses.CC_BY_3)) { - return "https://creativecommons.org/licenses/by/3.0/"; - } else if (license.equals(Prefs.Licenses.CC_BY_4)) { - return "https://creativecommons.org/licenses/by/4.0/"; - } else if (license.equals(Prefs.Licenses.CC_BY_SA_3)) { - return "https://creativecommons.org/licenses/by-sa/3.0/"; - } else if (license.equals(Prefs.Licenses.CC_BY_SA_4)) { - return "https://creativecommons.org/licenses/by-sa/4.0/"; - } else if (license.equals(Prefs.Licenses.CC0)) { - return "https://creativecommons.org/publicdomain/zero/1.0/"; + 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"); + throw new RuntimeException("Unrecognized license value: " + license); } public static Uri uriForWikiPage(String name) { @@ -308,4 +268,12 @@ public class Utils { public static boolean isNullOrWhiteSpace(String value) { return value == null || value.trim().isEmpty(); } + + public static boolean isDarkTheme(Context context) { + if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("theme",true)) { + return true; + }else { + return false; + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java b/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java index 30c8baf84..dab21a6e6 100644 --- a/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java @@ -10,7 +10,6 @@ import butterknife.ButterKnife; import fr.free.nrw.commons.theme.BaseActivity; public class WelcomeActivity extends BaseActivity { - private WelcomePagerAdapter adapter; @BindView(R.id.welcomePager) ViewPager pager; @BindView(R.id.welcomePagerIndicator) CirclePageIndicator indicator; @@ -20,14 +19,16 @@ public class WelcomeActivity extends BaseActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_welcome); - getSupportActionBar().hide(); + if (getSupportActionBar() != null) { + getSupportActionBar().hide(); + } ButterKnife.bind(this); setUpAdapter(); } private void setUpAdapter() { - adapter = new WelcomePagerAdapter(this); + WelcomePagerAdapter adapter = new WelcomePagerAdapter(this); pager.setAdapter(adapter); indicator.setViewPager(pager); } 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 new file mode 100644 index 000000000..773eb5d63 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java @@ -0,0 +1,61 @@ +package fr.free.nrw.commons.auth; + +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; +import android.content.ContentResolver; +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; + +public class AccountUtil { + + public static void createAccount(@Nullable AccountAuthenticatorResponse response, + String username, String password) { + + Account account = new Account(username, accountType()); + boolean created = accountManager().addAccountExplicitly(account, password, null); + + Timber.d("account creation " + (created ? "successful" : "failure")); + + if (created) { + if (response != null) { + Bundle bundle = new Bundle(); + bundle.putString(AccountManager.KEY_ACCOUNT_NAME, username); + bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType()); + + + response.onResult(bundle); + } + + } else { + if (response != null) { + response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, ""); + } + Timber.d("account creation failure"); + } + + // FIXME: If the user turns it off, it shouldn't be auto turned back on + ContentResolver.setSyncAutomatically(account, ContributionsContentProvider.AUTHORITY, true); // Enable sync by default! + ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default! + } + + @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(); + } + +} 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 6925699a1..a80023918 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 @@ -10,18 +10,18 @@ import android.os.Bundle; import java.io.IOException; -import fr.free.nrw.commons.theme.BaseActivity; import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.theme.NavigationBaseActivity; -public abstract class AuthenticatedActivity extends BaseActivity { +public abstract class AuthenticatedActivity extends NavigationBaseActivity { String accountType; CommonsApplication app; private String authCookie; - public AuthenticatedActivity(String accountType) { - this.accountType = accountType; + public AuthenticatedActivity() { + this.accountType = AccountUtil.accountType(); } private class GetAuthCookieTask extends AsyncTask { @@ -131,7 +131,7 @@ public abstract class AuthenticatedActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - app = (CommonsApplication)this.getApplicationContext(); + 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 c66c82910..0bec4bac6 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 @@ -1,171 +1,78 @@ package fr.free.nrw.commons.auth; -import android.accounts.Account; import android.accounts.AccountAuthenticatorActivity; -import android.accounts.AccountAuthenticatorResponse; -import android.accounts.AccountManager; -import android.app.Activity; import android.app.ProgressDialog; -import android.content.ContentResolver; import android.content.Intent; import android.content.SharedPreferences; -import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.NavUtils; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.view.KeyEvent; -import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; + import android.widget.Toast; - -import java.io.IOException; -import java.util.Locale; - -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.EventLog; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.WelcomeActivity; +import fr.free.nrw.commons.*; import fr.free.nrw.commons.contributions.ContributionsActivity; -import fr.free.nrw.commons.contributions.ContributionsContentProvider; -import fr.free.nrw.commons.modifications.ModificationsContentProvider; +import timber.log.Timber; public class LoginActivity extends AccountAuthenticatorActivity { public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username"; - private CommonsApplication app; - private SharedPreferences prefs = null; - Button loginButton; - Button signupButton; - EditText usernameEdit; + private Button loginButton; + private EditText usernameEdit; EditText passwordEdit; - ProgressDialog dialog; + EditText twoFactorEdit; + ProgressDialog progressDialog; - private class LoginTask extends AsyncTask { - - Activity context; - String username; - String password; - - @Override - protected void onPostExecute(String result) { - super.onPostExecute(result); - Log.d("Commons", "Login done!"); - - EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT) - .param("username", username) - .param("result", result) - .log(); - - if (result.equals("Success")) { - if (dialog != null && dialog.isShowing()) { - dialog.dismiss(); - } - Toast successToast = Toast.makeText(context, R.string.login_success, Toast.LENGTH_SHORT); - successToast.show(); - Account account = new Account(username, WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE); - boolean accountCreated = AccountManager.get(context).addAccountExplicitly(account, password, null); - - Bundle extras = context.getIntent().getExtras(); - - if (extras != null) { - Log.d("LoginActivity", "Bundle of extras: " + extras.toString()); - if (accountCreated) { // Pass the new account back to the account manager - AccountAuthenticatorResponse response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE); - Bundle authResult = new Bundle(); - authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username); - authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE); - - if (response != null) { - response.onResult(authResult); - } - } - } - // FIXME: If the user turns it off, it shouldn't be auto turned back on - ContentResolver.setSyncAutomatically(account, ContributionsContentProvider.AUTHORITY, true); // Enable sync by default! - ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default! - - Intent intent = new Intent(context, ContributionsActivity.class); - startActivity(intent); - finish(); - - } else { - int response; - if(result.equals("NetworkFailure")) { - response = R.string.login_failed_network; - } else if(result.equals("NotExists") || result.equals("Illegal") || result.equals("NotExists")) { - response = R.string.login_failed_username; - passwordEdit.setText(""); - } else if(result.equals("EmptyPass") || result.equals("WrongPass") || result.equals("WrongPluginPass")) { - response = R.string.login_failed_password; - passwordEdit.setText(""); - } else if(result.equals("Throttled")) { - response = R.string.login_failed_throttled; - } else if(result.equals("Blocked")) { - response = R.string.login_failed_blocked; - } else { - // Should never really happen - Log.d("Commons", "Login failed with reason: " + result); - response = R.string.login_failed_generic; - } - Toast.makeText(getApplicationContext(), response, Toast.LENGTH_LONG).show(); - dialog.cancel(); - } - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - dialog = new ProgressDialog(context); - dialog.setIndeterminate(true); - dialog.setTitle(getString(R.string.logging_in_title)); - dialog.setMessage(getString(R.string.logging_in_message)); - dialog.setCanceledOnTouchOutside(false); - dialog.show(); - } - - LoginTask(Activity context) { - this.context = context; - } - - @Override - protected String doInBackground(String... params) { - username = params[0]; - password = params[1]; - try { - return app.getApi().login(username, password); - } catch (IOException e) { - // Do something better! - return "NetworkFailure"; - } - } - } + private CommonsApplication app; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - app = (CommonsApplication) this.getApplicationContext(); + + app = CommonsApplication.getInstance(); + setContentView(R.layout.activity_login); + final LoginActivity that = this; + loginButton = (Button) findViewById(R.id.loginButton); - signupButton = (Button) findViewById(R.id.signupButton); + Button signupButton = (Button) findViewById(R.id.signupButton); usernameEdit = (EditText) findViewById(R.id.loginUsername); passwordEdit = (EditText) findViewById(R.id.loginPassword); - final LoginActivity that = this; + twoFactorEdit = (EditText) findViewById(R.id.loginTwoFactor); prefs = getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE); - TextWatcher loginEnabler = new TextWatcher() { + TextWatcher loginEnabler = newLoginTextWatcher(); + usernameEdit.addTextChangedListener(loginEnabler); + passwordEdit.addTextChangedListener(loginEnabler); + twoFactorEdit.addTextChangedListener(loginEnabler); + passwordEdit.setOnEditorActionListener( newLoginInputActionListener() ); + + loginButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + that.performLogin(); + } + }); + signupButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { that.signUp(v); } + }); + } + + private TextWatcher newLoginTextWatcher() { + return new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { } @@ -174,17 +81,21 @@ public class LoginActivity extends AccountAuthenticatorActivity { @Override public void afterTextChanged(Editable editable) { - if(usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0) { + if( + usernameEdit.getText().length() != 0 && + passwordEdit.getText().length() != 0 && + ( BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE ) + ) { loginButton.setEnabled(true); } else { loginButton.setEnabled(false); } } }; + } - usernameEdit.addTextChangedListener(loginEnabler); - passwordEdit.addTextChangedListener(loginEnabler); - passwordEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() { + private TextView.OnEditorActionListener newLoginInputActionListener() { + return new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { if (loginButton.isEnabled()) { @@ -198,36 +109,31 @@ public class LoginActivity extends AccountAuthenticatorActivity { } return false; } - }); - - loginButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - that.performLogin(); - } - }); - + }; } - @Override protected void onResume() { super.onResume(); - if (prefs.getBoolean("firstrun", true)) { - // Do first run stuff here then set 'firstrun' as false - Intent welcomeIntent = new Intent(this, WelcomeActivity.class); - startActivity(welcomeIntent); + this.startWelcomeIntent(); prefs.edit().putBoolean("firstrun", false).apply(); } + if (app.getCurrentAccount() != null) { + startMainActivity(); + } } + private void startWelcomeIntent() { + Intent welcomeIntent = new Intent(this, WelcomeActivity.class); + startActivity(welcomeIntent); + } @Override protected void onDestroy() { try { // To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method - if (dialog != null && dialog.isShowing()) { - dialog.dismiss(); + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); } } catch (Exception e) { e.printStackTrace(); @@ -236,15 +142,27 @@ public class LoginActivity extends AccountAuthenticatorActivity { } private void performLogin() { - String username = usernameEdit.getText().toString(); - // Because Mediawiki is upercase-first-char-then-case-sensitive :) - String canonicalUsername = Utils.capitalize(username.substring(0,1)) + username.substring(1); + Timber.d("Login to start!"); + LoginTask task = getLoginTask(); + task.execute(); + } - String password = passwordEdit.getText().toString(); + private LoginTask getLoginTask() { + return new LoginTask( + this, + canonicializeUsername( usernameEdit.getText().toString() ), + passwordEdit.getText().toString(), + twoFactorEdit.getText().toString() + ); + } - Log.d("Commons", "Login to start!"); - LoginTask task = new LoginTask(this); - task.execute(canonicalUsername, password); + /** + * Because Mediawiki is upercase-first-char-then-case-sensitive :) + * @param username String + * @return String canonicial username + */ + private String canonicializeUsername( String username ) { + return Utils.capitalize(username.substring(0,1)) + username.substring(1); } @Override @@ -257,9 +175,48 @@ public class LoginActivity extends AccountAuthenticatorActivity { return super.onOptionsItemSelected(item); } - //Called when Sign Up button is clicked + /** + * Called when Sign Up button is clicked. + * @param view View + */ public void signUp(View view) { Intent intent = new Intent(this, SignupActivity.class); startActivity(intent); } + + public void askUserForTwoFactorAuth() { + if(BuildConfig.DEBUG) { + twoFactorEdit.setVisibility(View.VISIBLE); + showUserToastAndCancelDialog( R.string.login_failed_2fa_needed ); + }else{ + showUserToastAndCancelDialog( R.string.login_failed_2fa_not_supported ); + } + } + + public void showUserToastAndCancelDialog( int resId ) { + showUserToast( resId ); + progressDialog.cancel(); + } + + private void showUserToast( int resId ) { + Toast.makeText(this, resId, Toast.LENGTH_LONG).show(); + } + + public void showSuccessToastAndDismissDialog() { + Toast successToast = Toast.makeText(this, R.string.login_success, Toast.LENGTH_SHORT); + successToast.show(); + progressDialog.dismiss(); + } + + public void emptySensitiveEditFields() { + passwordEdit.setText(""); + twoFactorEdit.setText(""); + } + + public void startMainActivity() { + Intent intent = new Intent(this, ContributionsActivity.class); + startActivity(intent); + finish(); + } + } 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 new file mode 100644 index 000000000..6db5f8654 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java @@ -0,0 +1,125 @@ +package fr.free.nrw.commons.auth; + +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; +import android.app.ProgressDialog; +import android.os.AsyncTask; +import android.os.Bundle; + +import java.io.IOException; + +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.EventLog; +import fr.free.nrw.commons.R; +import timber.log.Timber; + +class LoginTask extends AsyncTask { + + private LoginActivity loginActivity; + private String username; + private String password; + private String twoFactorCode = ""; + private CommonsApplication app; + + public LoginTask(LoginActivity loginActivity, String username, String password, String twoFactorCode) { + this.loginActivity = loginActivity; + this.username = username; + this.password = password; + this.twoFactorCode = twoFactorCode; + app = CommonsApplication.getInstance(); + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + loginActivity.progressDialog = new ProgressDialog(loginActivity); + loginActivity.progressDialog.setIndeterminate(true); + loginActivity.progressDialog.setTitle(loginActivity.getString(R.string.logging_in_title)); + loginActivity.progressDialog.setMessage(loginActivity.getString(R.string.logging_in_message)); + loginActivity.progressDialog.setCanceledOnTouchOutside(false); + loginActivity.progressDialog.show(); + } + + @Override + protected String doInBackground(String... params) { + try { + if (twoFactorCode.isEmpty()) { + return app.getMWApi().login(username, password); + } else { + return app.getMWApi().login(username, password, twoFactorCode); + } + } catch (IOException e) { + // Do something better! + return "NetworkFailure"; + } + } + + @Override + protected void onPostExecute(String result) { + super.onPostExecute(result); + Timber.d("Login done!"); + + EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT) + .param("username", username) + .param("result", result) + .log(); + + if (result.equals("PASS")) { + handlePassResult(); + } else { + handleOtherResults( result ); + } + } + + private void handlePassResult() { + loginActivity.showSuccessToastAndDismissDialog(); + + AccountAuthenticatorResponse response = null; + + Bundle extras = loginActivity.getIntent().getExtras(); + if (extras != null) { + Timber.d("Bundle of extras: %s", extras); + response = extras.getParcelable(AccountManager.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()); + response.onResult(authResult); + } + } + + AccountUtil.createAccount( response, username, password ); + loginActivity.startMainActivity(); + } + + /** + * Match known failure message codes and provide messages + * @param result String + */ + private void handleOtherResults( String result ) { + if (result.equals("NetworkFailure")) { + // Matches NetworkFailure which is created by the doInBackground method + loginActivity.showUserToastAndCancelDialog( R.string.login_failed_network ); + } else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { + // Matches nosuchuser, nosuchusershort, noname + loginActivity.showUserToastAndCancelDialog( R.string.login_failed_username ); + loginActivity.emptySensitiveEditFields(); + } else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) { + // Matches wrongpassword, wrongpasswordempty + loginActivity.showUserToastAndCancelDialog( R.string.login_failed_password ); + loginActivity.emptySensitiveEditFields(); + } else if (result.toLowerCase().contains("throttle".toLowerCase())) { + // Matches unknown throttle error codes + loginActivity.showUserToastAndCancelDialog( R.string.login_failed_throttled ); + } else if (result.toLowerCase().contains("userblocked".toLowerCase())) { + // Matches login-userblocked + loginActivity.showUserToastAndCancelDialog( R.string.login_failed_blocked ); + } else if (result.equals("2FA")) { + loginActivity.askUserForTwoFactorAuth(); + } else { + // Occurs with unhandled login failure codes + Timber.d("Login failed with reason: %s", result); + loginActivity.showUserToastAndCancelDialog( R.string.login_failed_generic ); + } + } +} 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 cd65826b0..10c095a0f 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 @@ -2,13 +2,14 @@ package fr.free.nrw.commons.auth; import android.content.Intent; import android.os.Bundle; -import android.util.Log; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Toast; +import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.theme.BaseActivity; +import timber.log.Timber; public class SignupActivity extends BaseActivity { @@ -17,9 +18,7 @@ public class SignupActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Log.d("SignupActivity", "Signup Activity started"); - - getSupportActionBar().hide(); + Timber.d("Signup Activity started"); webView = new WebView(this); setContentView(webView); @@ -37,17 +36,21 @@ public class SignupActivity extends BaseActivity { public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.equals("https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes")) { //Signup success, so clear cookies, notify user, and load LoginActivity again - Log.d("SignupActivity", "Overriding URL" + url); + Timber.d("Overriding URL %s", url); - Toast toast = Toast.makeText(getApplicationContext(), "Account created!", Toast.LENGTH_LONG); + Toast toast = Toast.makeText( + CommonsApplication.getInstance(), + "Account created!", + Toast.LENGTH_LONG + ); toast.show(); - Intent intent = new Intent(getApplicationContext(), LoginActivity.class); + Intent intent = new Intent(CommonsApplication.getInstance(), LoginActivity.class); startActivity(intent); return true; } else { //If user clicks any other links in the webview - Log.d("SignupActivity", "Not overriding URL, URL is: " + url); + Timber.d("Not overriding URL, URL is: %s", url); return false; } } 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 9194fa316..ebbd6c285 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 @@ -9,44 +9,76 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; -import org.mediawiki.api.MWApi; - import java.io.IOException; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.MWApi; public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { - public static final String COMMONS_ACCOUNT_TYPE = "fr.free.nrw.commons"; private Context context; + public WikiAccountAuthenticator(Context context) { super(context); this.context = context; } + private Bundle unsupportedOperation() { + Bundle bundle = new Bundle(); + bundle.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION); + + // HACK: the docs indicate that this is a required key bit it's not displayed to the user. + bundle.putString(AccountManager.KEY_ERROR_MESSAGE, ""); + + return bundle; + } + + private boolean supportedAccountType(@Nullable String type) { + return AccountUtil.accountType().equals(type); + } + @Override - public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException { - final Intent intent = new Intent(context, LoginActivity.class); - intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); - final Bundle bundle = new Bundle(); - bundle.putParcelable(AccountManager.KEY_INTENT, intent); + public Bundle addAccount(@NonNull AccountAuthenticatorResponse response, + @NonNull String accountType, @Nullable String authTokenType, + @Nullable String[] requiredFeatures, @Nullable Bundle options) + throws NetworkErrorException { + + if (!supportedAccountType(accountType)) { + return unsupportedOperation(); + } + + return addAccount(response); + } + + private Bundle addAccount(AccountAuthenticatorResponse response) { + Intent Intent = new Intent(context, LoginActivity.class); + Intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); + + Bundle bundle = new Bundle(); + bundle.putParcelable(AccountManager.KEY_INTENT, Intent); + return bundle; } @Override - public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException { - return null; + public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response, + @NonNull Account account, @Nullable Bundle options) + throws NetworkErrorException { + return unsupportedOperation(); } @Override public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { - return null; + return unsupportedOperation(); } private String getAuthCookie(String username, String password) throws IOException { - MWApi api = CommonsApplication.createMWApi(); + MWApi api = CommonsApplication.getInstance().getMWApi(); + //TODO add 2fa support here String result = api.login(username, password); - if(result.equals("Success")) { + if(result.equals("PASS")) { return api.getAuthCookie(); } else { return null; @@ -70,7 +102,7 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { if (authCookie != null) { final Bundle result = new Bundle(); result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); - result.putString(AccountManager.KEY_ACCOUNT_TYPE, COMMONS_ACCOUNT_TYPE); + result.putString(AccountManager.KEY_ACCOUNT_TYPE, AccountUtil.accountType()); result.putString(AccountManager.KEY_AUTHTOKEN, authCookie); return result; } @@ -87,21 +119,31 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { return bundle; } + @Nullable @Override - public String getAuthTokenLabel(String authTokenType) { + public String getAuthTokenLabel(@NonNull String authTokenType) { + //Note: the wikipedia app actually returns a string here.... + //return supportedAccountType(authTokenType) ? context.getString(R.string.wikimedia) : null; return null; } + @Nullable @Override - public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException { - final Bundle result = new Bundle(); - result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); - return result; + public Bundle hasFeatures(@NonNull AccountAuthenticatorResponse response, + @NonNull Account account, @NonNull String[] features) + throws NetworkErrorException { + Bundle bundle = new Bundle(); + bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); + return bundle; } + @Nullable @Override - public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { - return null; + public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response, + @NonNull Account account, @Nullable String authTokenType, + @Nullable Bundle options) + throws NetworkErrorException { + return unsupportedOperation(); } } diff --git a/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java b/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java index 28215b8b2..49fec4c2e 100644 --- a/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java +++ b/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java @@ -1,7 +1,5 @@ package fr.free.nrw.commons.caching; -import android.util.Log; - import com.github.varunpant.quadtree.Point; import com.github.varunpant.quadtree.QuadTree; @@ -10,15 +8,14 @@ import java.util.Arrays; import java.util.List; import fr.free.nrw.commons.upload.MwVolleyApi; +import timber.log.Timber; public class CacheController { private double x, y; private QuadTree> quadTree; - private Point>[] pointsFound; private double xMinus, xPlus, yMinus, yPlus; - private static final String TAG = CacheController.class.getName(); private static final int EARTH_RADIUS = 6378137; public CacheController() { @@ -28,40 +25,41 @@ public class CacheController { public void setQtPoint(double decLongitude, double decLatitude) { x = decLongitude; y = decLatitude; - Log.d(TAG, "New QuadTree created"); - Log.d(TAG, "X (longitude) value: " + x + ", Y (latitude) value: " + y); + Timber.d("New QuadTree created"); + Timber.d("X (longitude) value: %f, Y (latitude) value: %f", x, y); } public void cacheCategory() { List pointCatList = new ArrayList<>(); - if (MwVolleyApi.GpsCatExists.getGpsCatExists() == true) { + if (MwVolleyApi.GpsCatExists.getGpsCatExists()) { pointCatList.addAll(MwVolleyApi.getGpsCat()); - Log.d(TAG, "Categories being cached: " + pointCatList); + Timber.d("Categories being cached: %s", pointCatList); } else { - Log.d(TAG, "No categories found, so no categories cached"); + Timber.d("No categories found, so no categories cached"); } quadTree.set(x, y, pointCatList); } public List findCategory() { + Point>[] pointsFound; //Convert decLatitude and decLongitude to a coordinate offset range convertCoordRange(); pointsFound = quadTree.searchWithin(xMinus, yMinus, xPlus, yPlus); List displayCatList = new ArrayList<>(); - Log.d(TAG, "Points found in quadtree: " + Arrays.asList(pointsFound)); + Timber.d("Points found in quadtree: %s", Arrays.toString(pointsFound)); if (pointsFound.length != 0) { - Log.d(TAG, "Entering for loop"); + Timber.d("Entering for loop"); for (Point> point : pointsFound) { - Log.d(TAG, "Nearby point: " + point.toString()); + Timber.d("Nearby point: %s", point); displayCatList = point.getValue(); - Log.d(TAG, "Nearby cat: " + point.getValue()); + Timber.d("Nearby cat: %s", point.getValue()); } - Log.d(TAG, "Categories found in cache: " + displayCatList.toString()); + Timber.d("Categories found in cache: %s", displayCatList); } else { - Log.d(TAG, "No categories found in cache"); + Timber.d("No categories found in cache"); } return displayCatList; } @@ -84,6 +82,7 @@ public class CacheController { yMinus = lat - dLat * 180/Math.PI; xPlus = lon + dLon * 180/Math.PI; xMinus = lon - dLon * 180/Math.PI; - Log.d(TAG, "Search within: xMinus=" + xMinus + ", yMinus=" + yMinus + ", xPlus=" + xPlus + ", yPlus=" + yPlus); + Timber.d("Search within: xMinus=%s, yMinus=%s, xPlus=%s, yPlus=%s", + xMinus, yMinus, xPlus, yPlus); } } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapter.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapter.java index 3359150c5..2c0055c73 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapter.java @@ -14,13 +14,11 @@ import fr.free.nrw.commons.R; public class CategoriesAdapter extends BaseAdapter { - private Context context; private LayoutInflater mInflater; private ArrayList items; public CategoriesAdapter(Context context, ArrayList items) { - this.context = context; this.items = items; mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } 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 b4f9d7078..ab63ef632 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 @@ -16,7 +16,6 @@ import android.support.v7.app.AlertDialog; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; -import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; @@ -44,6 +43,7 @@ import java.util.concurrent.TimeUnit; import fr.free.nrw.commons.R; import fr.free.nrw.commons.upload.MwVolleyApi; +import timber.log.Timber; /** * Displays the category suggestion and selection screen. Category search is initiated here. @@ -79,7 +79,6 @@ public class CategorizationFragment extends Fragment { private ContentProviderClient client; protected final static int SEARCH_CATS_LIMIT = 25; - private static final String TAG = CategorizationFragment.class.getName(); public static class CategoryItem implements Parcelable { public String name; @@ -130,28 +129,28 @@ public class CategorizationFragment extends Fragment { //Retrieve the title that was saved when user tapped submit icon SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity()); String title = titleDesc.getString("Title", ""); - Log.d(TAG, "Title: " + title); + Timber.d("Title: %s", title); //Override onPostExecute to access the results of async API call titleCategoriesSub = new TitleCategories(title) { @Override protected void onPostExecute(ArrayList result) { super.onPostExecute(result); - Log.d(TAG, "Results in onPostExecute: " + result); + Timber.d("Results in onPostExecute: %s", result); titleCatItems.addAll(result); - Log.d(TAG, "TitleCatItems in onPostExecute: " + titleCatItems); + Timber.d("TitleCatItems in onPostExecute: %s", titleCatItems); mergeLatch.countDown(); } }; titleCategoriesSub.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - Log.d(TAG, "TitleCatItems in titleCatQuery: " + titleCatItems); + Timber.d("TitleCatItems in titleCatQuery: %s", titleCatItems); //Only return titleCatItems after API call has finished try { mergeLatch.await(5L, TimeUnit.SECONDS); } catch (InterruptedException e) { - Log.e(TAG, "Interrupted exception: ", e); + Timber.e(e, "Interrupted exception: "); } return titleCatItems; } @@ -191,7 +190,7 @@ public class CategorizationFragment extends Fragment { Set mergedItems = new LinkedHashSet<>(); - Log.d(TAG, "Calling APIs for GPS cats, title cats and recent cats..."); + Timber.d("Calling APIs for GPS cats, title cats and recent cats..."); List gpsItems = new ArrayList<>(); if (MwVolleyApi.GpsCatExists.getGpsCatExists()) { @@ -203,22 +202,22 @@ public class CategorizationFragment extends Fragment { //Await results of titleItems, which is likely to come in last try { mergeLatch.await(5L, TimeUnit.SECONDS); - Log.d(TAG, "Waited for merge"); + Timber.d("Waited for merge"); } catch (InterruptedException e) { - Log.e(TAG, "Interrupted Exception: ", e); + Timber.e(e, "Interrupted Exception: "); } mergedItems.addAll(gpsItems); - Log.d(TAG, "Adding GPS items: " + gpsItems); + Timber.d("Adding GPS items: %s", gpsItems); mergedItems.addAll(titleItems); - Log.d(TAG, "Adding title items: " + titleItems); + Timber.d("Adding title items: %s", titleItems); mergedItems.addAll(recentItems); - Log.d(TAG, "Adding recent items: " + recentItems); + Timber.d("Adding recent items: %s", recentItems); //Needs to be an ArrayList and not a List unless we want to modify a big portion of preexisting code ArrayList mergedItemsList = new ArrayList<>(mergedItems); - Log.d(TAG, "Merged item list: " + mergedItemsList); + Timber.d("Merged item list: %s", mergedItemsList); return mergedItemsList; } @@ -261,7 +260,7 @@ public class CategorizationFragment extends Fragment { } } else { - Log.e(TAG, "Error: Fragment is null"); + Timber.e("Error: Fragment is null"); } } @@ -285,7 +284,7 @@ public class CategorizationFragment extends Fragment { latch.await(); } catch (InterruptedException e) { - Log.w(TAG, e); + Timber.w(e); //Thread.currentThread().interrupt(); } return result; @@ -296,12 +295,12 @@ public class CategorizationFragment extends Fragment { super.onPostExecute(result); results.addAll(result); - Log.d(TAG, "Prefix result: " + result); + Timber.d("Prefix result: %s", result); String filter = categoriesFilter.getText().toString(); ArrayList resultsList = new ArrayList<>(results); categoriesCache.put(filter, resultsList); - Log.d(TAG, "Final results List: " + resultsList); + Timber.d("Final results List: %s", resultsList); categoriesAdapter.notifyDataSetChanged(); setCatsAfterAsync(resultsList, filter); @@ -315,7 +314,7 @@ public class CategorizationFragment extends Fragment { super.onPostExecute(result); results.addAll(result); - Log.d(TAG, "Method A result: " + result); + Timber.d("Method A result: %s", result); categoriesAdapter.notifyDataSetChanged(); latch.countDown(); @@ -358,8 +357,7 @@ public class CategorizationFragment extends Fragment { new String[] {name}, null); if (cursor.moveToFirst()) { - Category cat = Category.fromCursor(cursor); - return cat; + return Category.fromCursor(cursor); } } catch (RemoteException e) { // This feels lazy, but to hell with checked exceptions. :) @@ -500,7 +498,7 @@ public class CategorizationFragment extends Fragment { } } - private void backButtonDialog() { + public void backButtonDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setMessage("Are you sure you want to go back? The image will not have any categories saved.") diff --git a/app/src/main/java/fr/free/nrw/commons/category/Category.java b/app/src/main/java/fr/free/nrw/commons/category/Category.java index 645b10afc..f290dd741 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/Category.java +++ b/app/src/main/java/fr/free/nrw/commons/category/Category.java @@ -115,6 +115,11 @@ public class Category { db.execSQL(CREATE_TABLE_STATEMENT); } + public static void onDelete(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(db); + } + public static void onUpdate(SQLiteDatabase db, int from, int to) { if(from == to) { return; 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 27ab42159..de157265b 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 @@ -8,9 +8,10 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.text.TextUtils; -import android.util.Log; +import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.data.DBOpenHelper; +import timber.log.Timber; public class CategoryContentProvider extends ContentProvider { @@ -36,7 +37,7 @@ public class CategoryContentProvider extends ContentProvider { private DBOpenHelper dbOpenHelper; @Override public boolean onCreate() { - dbOpenHelper = DBOpenHelper.getInstance(getContext()); + dbOpenHelper = CommonsApplication.getInstance().getDBOpenHelper(); return false; } @@ -101,14 +102,14 @@ public class CategoryContentProvider extends ContentProvider { @Override public int bulkInsert(Uri uri, ContentValues[] values) { - Log.d("Commons", "Hello, bulk insert! (CategoryContentProvider)"); + Timber.d("Hello, bulk insert! (CategoryContentProvider)"); int uriType = uriMatcher.match(uri); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); sqlDB.beginTransaction(); switch (uriType) { case CATEGORIES: for(ContentValues value: values) { - Log.d("Commons", "Inserting! " + value.toString()); + Timber.d("Inserting! %s", value); sqlDB.insert(Category.Table.TABLE_NAME, null, value); } break; diff --git a/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java b/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java index cbc31814b..9300f640d 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java +++ b/app/src/main/java/fr/free/nrw/commons/category/MethodAUpdater.java @@ -1,11 +1,10 @@ package fr.free.nrw.commons.category; import android.os.AsyncTask; -import android.util.Log; import android.view.View; +import fr.free.nrw.commons.MWApi; import org.mediawiki.api.ApiResult; -import org.mediawiki.api.MWApi; import java.io.IOException; import java.util.ArrayList; @@ -13,6 +12,7 @@ import java.util.Calendar; import java.util.Iterator; import fr.free.nrw.commons.CommonsApplication; +import timber.log.Timber; /** * Sends asynchronous queries to the Commons MediaWiki API to retrieve categories that are close to @@ -22,7 +22,6 @@ import fr.free.nrw.commons.CommonsApplication; public class MethodAUpdater extends AsyncTask> { private String filter; - private static final String TAG = MethodAUpdater.class.getName(); CategorizationFragment catFragment; public MethodAUpdater(CategorizationFragment catFragment) { @@ -54,11 +53,11 @@ public class MethodAUpdater extends AsyncTask> { Calendar now = Calendar.getInstance(); int year = now.get(Calendar.YEAR); String yearInString = String.valueOf(year); - Log.d(TAG, "Year: " + yearInString); + Timber.d("Year: %s", yearInString); int prevYear = year - 1; String prevYearInString = String.valueOf(prevYear); - Log.d(TAG, "Previous year: " + prevYearInString); + Timber.d("Previous year: %s", prevYearInString); //Copy to Iterator to prevent ConcurrentModificationException when removing item for(iterator = items.iterator(); iterator.hasNext();) { @@ -67,12 +66,12 @@ public class MethodAUpdater extends AsyncTask> { //Check if s contains a 4-digit word anywhere within the string (.* is wildcard) //And that s does not equal the current year or previous year if(s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString)) { - Log.d(TAG, "Filtering out year " + s); + Timber.d("Filtering out year %s", s); iterator.remove(); } } - Log.d(TAG, "Items: " + items.toString()); + Timber.d("Items: %s", items); return items; } @@ -80,7 +79,7 @@ public class MethodAUpdater extends AsyncTask> { protected ArrayList doInBackground(Void... voids) { //otherwise if user has typed something in that isn't in cache, search API for matching categories - MWApi api = CommonsApplication.createMWApi(); + MWApi api = CommonsApplication.getInstance().getMWApi(); ApiResult result; ArrayList categories = new ArrayList<>(); @@ -94,9 +93,9 @@ public class MethodAUpdater extends AsyncTask> { .param("srlimit", catFragment.SEARCH_CATS_LIMIT) .param("srsearch", filter) .get(); - Log.d(TAG, "Method A URL filter" + result.toString()); + Timber.d("Method A URL filter %s", result); } catch (IOException e) { - Log.e(TAG, "IO Exception: ", e); + Timber.e(e, "IO Exception: "); //Return empty arraylist return categories; } @@ -108,8 +107,7 @@ public class MethodAUpdater extends AsyncTask> { categories.add(catString); } - Log.d(TAG, "Found categories from Method A search, waiting for filter"); - ArrayList filteredItems = new ArrayList<>(filterYears(categories)); - return filteredItems; + Timber.d("Found categories from Method A search, waiting for filter"); + return new ArrayList<>(filterYears(categories)); } } diff --git a/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java b/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java index bacd1a520..05770081d 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java +++ b/app/src/main/java/fr/free/nrw/commons/category/PrefixUpdater.java @@ -2,11 +2,10 @@ package fr.free.nrw.commons.category; import android.os.AsyncTask; import android.text.TextUtils; -import android.util.Log; import android.view.View; +import fr.free.nrw.commons.MWApi; import org.mediawiki.api.ApiResult; -import org.mediawiki.api.MWApi; import java.io.IOException; import java.util.ArrayList; @@ -14,6 +13,7 @@ import java.util.Calendar; import java.util.Iterator; import fr.free.nrw.commons.CommonsApplication; +import timber.log.Timber; /** * Sends asynchronous queries to the Commons MediaWiki API to retrieve categories that share the @@ -24,7 +24,6 @@ import fr.free.nrw.commons.CommonsApplication; public class PrefixUpdater extends AsyncTask> { private String filter; - private static final String TAG = PrefixUpdater.class.getName(); private CategorizationFragment catFragment; public PrefixUpdater(CategorizationFragment catFragment) { @@ -56,11 +55,11 @@ public class PrefixUpdater extends AsyncTask> { Calendar now = Calendar.getInstance(); int year = now.get(Calendar.YEAR); String yearInString = String.valueOf(year); - Log.d(TAG, "Year: " + yearInString); + Timber.d("Year: %s", yearInString); int prevYear = year - 1; String prevYearInString = String.valueOf(prevYear); - Log.d(TAG, "Previous year: " + prevYearInString); + Timber.d("Previous year: %s", prevYearInString); //Copy to Iterator to prevent ConcurrentModificationException when removing item for(iterator = items.iterator(); iterator.hasNext();) { @@ -69,12 +68,12 @@ public class PrefixUpdater extends AsyncTask> { //Check if s contains a 4-digit word anywhere within the string (.* is wildcard) //And that s does not equal the current year or previous year if(s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString)) { - Log.d(TAG, "Filtering out year " + s); + Timber.d("Filtering out year %s", s); iterator.remove(); } } - Log.d(TAG, "Items: " + items.toString()); + Timber.d("Items: %s", items); return items; } @@ -83,22 +82,20 @@ public class PrefixUpdater extends AsyncTask> { //If user hasn't typed anything in yet, get GPS and recent items if(TextUtils.isEmpty(filter)) { ArrayList mergedItems = new ArrayList<>(catFragment.mergeItems()); - Log.d(TAG, "Merged items, waiting for filter"); - ArrayList filteredItems = new ArrayList<>(filterYears(mergedItems)); - return filteredItems; + Timber.d("Merged items, waiting for filter"); + return new ArrayList<>(filterYears(mergedItems)); } //if user types in something that is in cache, return cached category if(catFragment.categoriesCache.containsKey(filter)) { ArrayList cachedItems = new ArrayList<>(catFragment.categoriesCache.get(filter)); - Log.d(TAG, "Found cache items, waiting for filter"); - ArrayList filteredItems = new ArrayList<>(filterYears(cachedItems)); - return filteredItems; + Timber.d("Found cache items, waiting for filter"); + return new ArrayList<>(filterYears(cachedItems)); } //otherwise if user has typed something in that isn't in cache, search API for matching categories //URL: https://commons.wikimedia.org/w/api.php?action=query&list=allcategories&acprefix=filter&aclimit=25 - MWApi api = CommonsApplication.createMWApi(); + MWApi api = CommonsApplication.getInstance().getMWApi(); ApiResult result; ArrayList categories = new ArrayList<>(); try { @@ -107,9 +104,9 @@ public class PrefixUpdater extends AsyncTask> { .param("acprefix", filter) .param("aclimit", catFragment.SEARCH_CATS_LIMIT) .get(); - Log.d(TAG, "Prefix URL filter" + result.toString()); + Timber.d("Prefix URL filter %s", result); } catch (IOException e) { - Log.e(TAG, "IO Exception: ", e); + Timber.e(e, "IO Exception: "); //Return empty arraylist return categories; } @@ -119,8 +116,7 @@ public class PrefixUpdater extends AsyncTask> { categories.add(categoryNode.getDocument().getTextContent()); } - Log.d(TAG, "Found categories from Prefix search, waiting for filter"); - ArrayList filteredItems = new ArrayList<>(filterYears(categories)); - return filteredItems; + Timber.d("Found categories from Prefix search, waiting for filter"); + return new ArrayList<>(filterYears(categories)); } } diff --git a/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java b/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java index 5434c3f1b..6ca46be0f 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java +++ b/app/src/main/java/fr/free/nrw/commons/category/TitleCategories.java @@ -1,15 +1,15 @@ package fr.free.nrw.commons.category; import android.os.AsyncTask; -import android.util.Log; +import fr.free.nrw.commons.MWApi; import org.mediawiki.api.ApiResult; -import org.mediawiki.api.MWApi; import java.io.IOException; import java.util.ArrayList; import fr.free.nrw.commons.CommonsApplication; +import timber.log.Timber; /** * Sends asynchronous queries to the Commons MediaWiki API to retrieve categories that are related to @@ -19,7 +19,7 @@ import fr.free.nrw.commons.CommonsApplication; public class TitleCategories extends AsyncTask> { private final static int SEARCH_CATS_LIMIT = 25; - private static final String TAG = TitleCategories.class.getName(); + private String title; public TitleCategories(String title) { @@ -34,7 +34,7 @@ public class TitleCategories extends AsyncTask> { @Override protected ArrayList doInBackground(Void... voids) { - MWApi api = CommonsApplication.createMWApi(); + MWApi api = CommonsApplication.getInstance().getMWApi(); ApiResult result; ArrayList items = new ArrayList<>(); @@ -48,9 +48,9 @@ public class TitleCategories extends AsyncTask> { .param("srlimit", SEARCH_CATS_LIMIT) .param("srsearch", title) .get(); - Log.d(TAG, "Searching for cats for title: " + result.toString()); + Timber.d("Searching for cats for title: %s", result); } catch (IOException e) { - Log.e(TAG, "IO Exception: ", e); + Timber.e(e, "IO Exception: "); //Return empty arraylist return items; } @@ -62,7 +62,7 @@ public class TitleCategories extends AsyncTask> { items.add(catString); } - Log.d(TAG, "Title cat query results: " + items); + Timber.d("Title cat query results: %s", items); return items; } 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 4a70151df..e58be04e1 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 @@ -44,8 +44,6 @@ public class Contribution extends Media { public static final String SOURCE_GALLERY = "gallery"; public static final String SOURCE_EXTERNAL = "external"; - private static final String TAG = "Contribution"; - private ContentProviderClient client; private Uri contentUri; private String source; @@ -197,7 +195,7 @@ public class Contribution extends Media { cv.put(Table.COLUMN_LOCAL_URI, getLocalUri().toString()); } if(getImageUrl() != null) { - cv.put(Table.COLUMN_IMAGE_URL, getImageUrl().toString()); + cv.put(Table.COLUMN_IMAGE_URL, getImageUrl()); } if(getDateUploaded() != null) { cv.put(Table.COLUMN_UPLOADED, getDateUploaded().getTime()); @@ -333,6 +331,11 @@ public class Contribution extends Media { db.execSQL(CREATE_TABLE_STATEMENT); } + public static void onDelete(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(db); + } + public static void onUpdate(SQLiteDatabase db, int from, int to) { if(from == to) { return; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index 6581e7381..5ef34335a 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -7,7 +7,6 @@ import android.os.Bundle; import android.os.Environment; import android.provider.MediaStore; import android.support.v4.app.Fragment; -import android.util.Log; import java.io.File; import java.io.IOException; @@ -15,6 +14,7 @@ import java.util.Date; import fr.free.nrw.commons.upload.ShareActivity; import fr.free.nrw.commons.upload.UploadService; +import timber.log.Timber; public class ContributionController { private Fragment fragment; @@ -38,13 +38,13 @@ public class ContributionController { String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Commons/images/" + new Date().getTime() + ".jpg"; File _photoFile = new File(path); try { - if(_photoFile.exists() == false) { + if(!_photoFile.exists()) { _photoFile.getParentFile().mkdirs(); _photoFile.createNewFile(); } } catch (IOException e) { - Log.e("Commons", "Could not create file: " + path, e); + Timber.e(e, "Could not create file: %s", path); } return Uri.fromFile(_photoFile); @@ -84,11 +84,11 @@ public class ContributionController { shareIntent.putExtra(UploadService.EXTRA_SOURCE, Contribution.SOURCE_CAMERA); break; } - Log.i("Image", "Image selected"); + Timber.i("Image selected"); try { activity.startActivity(shareIntent); } catch (SecurityException e) { - Log.e("ContributionController", "Security Exception", e); + Timber.e(e, "Security Exception"); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java index 77450cbf8..a944cf866 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java @@ -14,8 +14,6 @@ class ContributionViewHolder { final TextView seqNumView; final ProgressBar progressView; - String url; - ContributionViewHolder(View parent) { imageView = (MediaWikiImageView) parent.findViewById(R.id.contributionImage); titleView = (TextView)parent.findViewById(R.id.contributionTitle); 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 a098a4783..b23bf7fbb 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 @@ -5,16 +5,17 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.content.SharedPreferences; 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; import android.support.v4.content.Loader; import android.support.v4.widget.CursorAdapter; -import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -22,16 +23,19 @@ import android.widget.Adapter; import android.widget.AdapterView; import java.util.ArrayList; -import java.util.Objects; +import java.util.Locale; +import butterknife.ButterKnife; 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.WikiAccountAuthenticator; +import fr.free.nrw.commons.hamburger.HamburgerMenuContainer; import fr.free.nrw.commons.media.MediaDetailPagerFragment; +import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.upload.UploadService; +import timber.log.Timber; public class ContributionsActivity extends AuthenticatedActivity @@ -39,7 +43,8 @@ public class ContributionsActivity AdapterView.OnItemClickListener, MediaDetailPagerFragment.MediaDetailProvider, FragmentManager.OnBackStackChangedListener, - ContributionsListFragment.SourceRefresher { + ContributionsListFragment.SourceRefresher, + HamburgerMenuContainer { private Cursor allContributions; private ContributionsListFragment contributionsList; @@ -48,6 +53,7 @@ public class ContributionsActivity private boolean isUploadServiceConnected; private ArrayList observersWaitingForLoad = new ArrayList<>(); private String CONTRIBUTION_SELECTION = ""; + /* This sorts in the following order: Currently Uploading @@ -59,10 +65,6 @@ public class ContributionsActivity */ private String CONTRIBUTION_SORT = Contribution.Table.COLUMN_STATE + " DESC, " + Contribution.Table.COLUMN_UPLOADED + " DESC , (" + Contribution.Table.COLUMN_TIMESTAMP + " * " + Contribution.Table.COLUMN_STATE + ")"; - public ContributionsActivity() { - super(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE); - } - private ServiceConnection uploadServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder binder) { @@ -88,6 +90,15 @@ public class ContributionsActivity @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(); + if (isSettingsChanged) { + refreshSource(); + } } @Override @@ -98,7 +109,7 @@ public class ContributionsActivity @Override protected void onAuthCookieAcquired(String authCookie) { // Do a sync everytime we get here! - ContentResolver.requestSync(((CommonsApplication) getApplicationContext()).getCurrentAccount(), ContributionsContentProvider.AUTHORITY, new Bundle()); + ContentResolver.requestSync(CommonsApplication.getInstance().getCurrentAccount(), ContributionsContentProvider.AUTHORITY, new Bundle()); Intent uploadServiceIntent = new Intent(this, UploadService.class); uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); startService(uploadServiceIntent); @@ -112,23 +123,22 @@ public class ContributionsActivity @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setTitle(R.string.title_activity_contributions); 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() - contributionsList = (ContributionsListFragment)getSupportFragmentManager().findFragmentById(R.id.contributionsListFragment); + // Activity can call methods in the fragment by acquiring a + // reference to the Fragment from FragmentManager, using findFragmentById() + contributionsList = (ContributionsListFragment)getSupportFragmentManager() + .findFragmentById(R.id.contributionsListFragment); getSupportFragmentManager().addOnBackStackChangedListener(this); if (savedInstanceState != null) { - mediaDetails = (MediaDetailPagerFragment)getSupportFragmentManager().findFragmentById(R.id.contributionsFragmentContainer); - // onBackStackChanged uses mediaDetails.isVisible() but this returns false now. - // Use the saved value from before pause or orientation change. - if (mediaDetails != null && savedInstanceState.getBoolean("mediaDetailsVisible")) { - // Feels awful that we have to reset this manually! - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } + mediaDetails = (MediaDetailPagerFragment)getSupportFragmentManager() + .findFragmentById(R.id.contributionsFragmentContainer); } requestAuthToken(); + initDrawer(); + setTitle(getString(R.string.title_activity_contributions)); } @Override @@ -158,9 +168,9 @@ public class ContributionsActivity Contribution c = Contribution.fromCursor(allContributions); if(c.getState() == Contribution.STATE_FAILED) { uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c); - Log.d("Commons", "Restarting for" + c.toContentValues().toString()); + Timber.d("Restarting for %s", c.toContentValues()); } else { - Log.d("Commons", "Skipping re-upload for non-failed " + c.toContentValues().toString()); + Timber.d("Skipping re-upload for non-failed %s", c.toContentValues()); } } @@ -168,11 +178,11 @@ public class ContributionsActivity allContributions.moveToPosition(i); Contribution c = Contribution.fromCursor(allContributions); if(c.getState() == Contribution.STATE_FAILED) { - Log.d("Commons", "Deleting failed contrib " + c.toContentValues().toString()); + Timber.d("Deleting failed contrib %s", c.toContentValues()); c.setContentProviderClient(getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY)); c.delete(); } else { - Log.d("Commons", "Skipping deletion for non-failed contrib " + c.toContentValues().toString()); + Timber.d("Skipping deletion for non-failed contrib %s", c.toContentValues()); } } @@ -206,7 +216,12 @@ public class ContributionsActivity @Override public Loader onCreateLoader(int i, Bundle bundle) { - return new CursorLoader(this, ContributionsContentProvider.BASE_URI, Contribution.Table.ALL_FIELDS, CONTRIBUTION_SELECTION, null, CONTRIBUTION_SORT); + SharedPreferences sharedPref = + PreferenceManager.getDefaultSharedPreferences(this); + int uploads = sharedPref.getInt(Prefs.UPLOADS_SHOWING, 100); + return new CursorLoader(this, ContributionsContentProvider.BASE_URI, + Contribution.Table.ALL_FIELDS, CONTRIBUTION_SELECTION, null, + CONTRIBUTION_SORT + "LIMIT " + uploads); } @Override @@ -217,7 +232,18 @@ public class ContributionsActivity ((CursorAdapter)contributionsList.getAdapter()).swapCursor(cursor); } - getSupportActionBar().setSubtitle(getResources().getQuantityString(R.plurals.contributions_subtitle, cursor.getCount(), cursor.getCount())); + if (cursor.getCount() == 0 + && Locale.getDefault().getISO3Language().equals(Locale.ENGLISH.getISO3Language())) { + //cursor count is zero and language is english - + // we need to set the message for 0 case explicitly. + getSupportActionBar().setSubtitle(getResources() + .getString(R.string.contributions_subtitle_zero)); + } else { + getSupportActionBar().setSubtitle(getResources() + .getQuantityString(R.plurals.contributions_subtitle, + cursor.getCount(), + cursor.getCount())); + } contributionsList.clearSyncMessage(); notifyAndMigrateDataSetObservers(); @@ -289,15 +315,16 @@ public class ContributionsActivity @Override public void onBackStackChanged() { - if(mediaDetails != null && mediaDetails.isVisible()) { - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } else { - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - } + initBackButton(); } @Override public void refreshSource() { getSupportLoaderManager().restartLoader(0, null, this); } + + public static void startYourself(Context context) { + Intent settingsIntent = new Intent(context, ContributionsActivity.class); + context.startActivity(settingsIntent); + } } 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 07a4f586e..838b9c922 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 @@ -8,9 +8,10 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.text.TextUtils; -import android.util.Log; +import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.data.DBOpenHelper; +import timber.log.Timber; public class ContributionsContentProvider extends ContentProvider{ @@ -35,7 +36,7 @@ public class ContributionsContentProvider extends ContentProvider{ private DBOpenHelper dbOpenHelper; @Override public boolean onCreate() { - dbOpenHelper = DBOpenHelper.getInstance(getContext()); + dbOpenHelper = CommonsApplication.getInstance().getDBOpenHelper(); return false; } @@ -102,7 +103,7 @@ public class ContributionsContentProvider extends ContentProvider{ switch(uriType) { case CONTRIBUTIONS_ID: - Log.d("Commons", "Deleting contribution id " + uri.getLastPathSegment()); + Timber.d("Deleting contribution id %s", uri.getLastPathSegment()); rows = db.delete(Contribution.Table.TABLE_NAME, "_id = ?", new String[] { uri.getLastPathSegment() } @@ -117,14 +118,14 @@ public class ContributionsContentProvider extends ContentProvider{ @Override public int bulkInsert(Uri uri, ContentValues[] values) { - Log.d("Commons", "Hello, bulk insert! (ContributionsContentProvider)"); + Timber.d("Hello, bulk insert! (ContributionsContentProvider)"); int uriType = uriMatcher.match(uri); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); sqlDB.beginTransaction(); switch (uriType) { case CONTRIBUTIONS: for(ContentValues value: values) { - Log.d("Commons", "Inserting! " + value.toString()); + Timber.d("Inserting! %s", value); sqlDB.insert(Contribution.Table.TABLE_NAME, null, value); } break; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java index 3a6aa72c3..08ed3198c 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java @@ -3,27 +3,13 @@ package fr.free.nrw.commons.contributions; import android.app.Activity; import android.content.Context; import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; import android.support.v4.widget.CursorAdapter; -import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; -import com.nostra13.universalimageloader.core.DisplayImageOptions; -import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.assist.FailReason; -import com.nostra13.universalimageloader.core.assist.SimpleImageLoadingListener; - -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.MediaWikiImageView; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; class ContributionsListAdapter extends CursorAdapter { - - private DisplayImageOptions contributionDisplayOptions = Utils.getGenericDisplayOptions().build(); - private Activity activity; public ContributionsListAdapter(Activity activity, Cursor c, int flags) { @@ -38,48 +24,12 @@ class ContributionsListAdapter extends CursorAdapter { return parent; } - //FIXME: Potential cause of wrong image display bug @Override public void bindView(View view, Context context, Cursor cursor) { final ContributionViewHolder views = (ContributionViewHolder)view.getTag(); final Contribution contribution = Contribution.fromCursor(cursor); - String actualUrl = (contribution.getLocalUri() != null && !TextUtils.isEmpty(contribution.getLocalUri().toString())) ? contribution.getLocalUri().toString() : contribution.getThumbnailUrl(640); - - if(views.url == null || !views.url.equals(actualUrl)) { - if(actualUrl.startsWith("http")) { - MediaWikiImageView mwImageView = views.imageView; - mwImageView.setMedia(contribution, ((CommonsApplication) activity.getApplicationContext()).getImageLoader()); - // FIXME: For transparent images - } else { - ImageLoader.getInstance().displayImage(actualUrl, views.imageView, contributionDisplayOptions, new SimpleImageLoadingListener() { - - @Override - public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { - if(loadedImage.hasAlpha()) { - views.imageView.setBackgroundResource(android.R.color.white); - } - views.seqNumView.setVisibility(View.GONE); - } - - @Override - public void onLoadingFailed(String imageUri, View view, FailReason failReason) { - super.onLoadingFailed(imageUri, view, failReason); - MediaWikiImageView mwImageView = views.imageView; - mwImageView.setMedia(contribution, ((CommonsApplication) activity.getApplicationContext()).getImageLoader()); - } - }); - } - views.url = actualUrl; - } - - BitmapDrawable actualImageDrawable = (BitmapDrawable)views.imageView.getDrawable(); - if(actualImageDrawable != null && actualImageDrawable.getBitmap() != null && actualImageDrawable.getBitmap().hasAlpha()) { - views.imageView.setBackgroundResource(android.R.color.white); - } else { - views.imageView.setBackgroundDrawable(null); - } - + views.imageView.setMedia(contribution); views.titleView.setText(contribution.getDisplayTitle()); views.seqNumView.setText(String.valueOf(cursor.getPosition() + 1)); 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 4841ffab5..b181ba2aa 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 @@ -10,7 +10,6 @@ import android.os.Build; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; -import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -21,16 +20,13 @@ import android.widget.AdapterView; import android.widget.GridView; import android.widget.ListAdapter; import android.widget.TextView; -import android.widget.Toast; import butterknife.BindView; import butterknife.ButterKnife; -import fr.free.nrw.commons.AboutActivity; -import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.settings.SettingsActivity; import fr.free.nrw.commons.nearby.NearbyActivity; +import timber.log.Timber; import static android.app.Activity.RESULT_OK; @@ -45,7 +41,6 @@ public class ContributionsListFragment extends Fragment { @BindView(R.id.emptyMessage) TextView emptyMessage; private ContributionController controller; - private static final String TAG = "ContributionsList"; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -54,14 +49,14 @@ public class ContributionsListFragment extends Fragment { contributionsList.setOnItemClickListener((AdapterView.OnItemClickListener)getActivity()); if(savedInstanceState != null) { - Log.d(TAG, "Scrolling to " + savedInstanceState.getInt("grid-position")); + Timber.d("Scrolling to %d", savedInstanceState.getInt("grid-position")); contributionsList.setSelection(savedInstanceState.getInt("grid-position")); } //TODO: Should this be in onResume? SharedPreferences prefs = this.getActivity().getSharedPreferences("prefs", Context.MODE_PRIVATE); String lastModified = prefs.getString("lastSyncTimestamp", ""); - Log.d(TAG, "Last Sync Timestamp: " + lastModified); + Timber.d("Last Sync Timestamp: %s", lastModified); if (lastModified.equals("")) { waitingMessage.setVisibility(View.VISIBLE); @@ -96,10 +91,12 @@ public class ContributionsListFragment extends Fragment { super.onActivityResult(requestCode, resultCode, data); if ( resultCode == RESULT_OK ) { - Log.d("Contributions", "OnActivityResult() parameters: Req code: " + requestCode + " Result code: " + resultCode + " Data: " + data); + Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", + requestCode, resultCode, data); controller.handleImagePicked(requestCode, data); } else { - Log.e("Contributions", "OnActivityResult() parameters: Req code: " + requestCode + " Result code: " + resultCode + " Data: " + data); + Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", + requestCode, resultCode, data); } } @@ -125,46 +122,6 @@ public class ContributionsListFragment extends Fragment { case R.id.menu_from_camera: controller.startCameraCapture(); return true; - case R.id.menu_settings: - Intent settingsIntent = new Intent(getActivity(), SettingsActivity.class); - startActivity(settingsIntent); - return true; - case R.id.menu_about: - Intent aboutIntent = new Intent(getActivity(), AboutActivity.class); - startActivity(aboutIntent); - return true; - case R.id.menu_feedback: - Intent feedbackIntent = new Intent(Intent.ACTION_SEND); - feedbackIntent.setType("message/rfc822"); - feedbackIntent.putExtra(Intent.EXTRA_EMAIL, new String[] { CommonsApplication.FEEDBACK_EMAIL }); - feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT, BuildConfig.VERSION_NAME)); - try { - startActivity(feedbackIntent); - } - catch (ActivityNotFoundException e) { - Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show(); - } - return true; - case R.id.menu_nearby: - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (ContextCompat.checkSelfPermission(this.getActivity(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - //See http://stackoverflow.com/questions/33169455/onrequestpermissionsresult-not-being-called-in-dialog-fragment - requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 2); - return true; - } else { - Intent nearbyIntent = new Intent(getActivity(), NearbyActivity.class); - startActivity(nearbyIntent); - return true; - } - } - else { - Intent nearbyIntent = new Intent(getActivity(), NearbyActivity.class); - startActivity(nearbyIntent); - return true; - } - case R.id.menu_refresh: - ((SourceRefresher)getActivity()).refreshSource(); - return true; default: return super.onOptionsItemSelected(item); } @@ -176,7 +133,7 @@ public class ContributionsListFragment extends Fragment { // 1 = Storage allowed when gallery selected case 1: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Log.d("ContributionsList", "Call controller.startGalleryPick()"); + Timber.d("Call controller.startGalleryPick()"); controller.startGalleryPick(); } } @@ -184,7 +141,7 @@ public class ContributionsListFragment extends Fragment { // 2 = Location allowed when 'nearby places' selected case 2: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Log.d("ContributionsList", "Location permission granted"); + Timber.d("Location permission granted"); Intent nearbyIntent = new Intent(getActivity(), NearbyActivity.class); startActivity(nearbyIntent); } @@ -198,12 +155,9 @@ 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)getActivity().getApplicationContext(); - if (!app.deviceHasCamera()) { + if (!CommonsApplication.getInstance().deviceHasCamera()) { menu.findItem(R.id.menu_from_camera).setEnabled(false); } - - menu.findItem(R.id.menu_refresh).setVisible(false); } @Override 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 9cb01e02c..d7ce9df9d 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 @@ -11,17 +11,17 @@ import android.database.Cursor; import android.os.Bundle; import android.os.RemoteException; import android.text.TextUtils; -import android.util.Log; import org.mediawiki.api.ApiResult; -import org.mediawiki.api.MWApi; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.MWApi; import fr.free.nrw.commons.Utils; +import timber.log.Timber; public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { private static int COMMIT_THRESHOLD = 10; @@ -30,7 +30,10 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { } private int getLimit() { - return 500; // FIXME: Parameterize! + + int limit = 500; + Timber.d("Max number of uploads set to %d", limit); + return limit; // FIXME: Parameterize! } private static final String[] existsQuery = { Contribution.Table.COLUMN_FILENAME }; @@ -58,7 +61,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) { // This code is fraught with possibilities of race conditions, but lalalalala I can't hear you! String user = account.name; - MWApi api = CommonsApplication.createMWApi(); + MWApi api = CommonsApplication.getInstance().getMWApi(); SharedPreferences prefs = this.getContext().getSharedPreferences("prefs", Context.MODE_PRIVATE); String lastModified = prefs.getString("lastSyncTimestamp", ""); Date curTime = new Date(); @@ -71,7 +74,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { MWApi.RequestBuilder builder = api.action("query") .param("list", "logevents") .param("letype", "upload") - .param("leprop", "title|timestamp") + .param("leprop", "title|timestamp|ids") .param("leuser", user) .param("lelimit", getLimit()); if(!TextUtils.isEmpty(lastModified)) { @@ -85,18 +88,23 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { // There isn't really much we can do, eh? // FIXME: Perhaps add EventLogging? syncResult.stats.numIoExceptions += 1; // Not sure if this does anything. Shitty docs - Log.d("Commons", "Syncing failed due to " + e.toString()); + Timber.d("Syncing failed due to %s", e); return; } - Log.d("Commons", "Last modified at " + lastModified); + Timber.d("Last modified at %s", lastModified); ArrayList uploads = result.getNodes("/api/query/logevents/item"); - Log.d("Commons", uploads.size() + " results!"); + Timber.d("%d results!", uploads.size()); ArrayList imageValues = new ArrayList<>(); for(ApiResult image: uploads) { + String pageId = image.getString("@pageid"); + if (pageId.equals("0")) { + // means that this upload was deleted. + continue; + } String filename = image.getString("@title"); if(fileExists(contentProviderClient, filename)) { - Log.d("Commons", "Skipping " + filename); + Timber.d("Skipping %s", filename); continue; } String thumbUrl = Utils.makeThumbBaseUrl(filename); @@ -128,6 +136,6 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { } } prefs.edit().putString("lastSyncTimestamp", Utils.toMWDate(curTime)).apply(); - Log.d("Commons", "Oh hai, everyone! Look, a kitty!"); + Timber.d("Oh hai, everyone! Look, a kitty!"); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncService.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncService.java index 3af92935c..946da6915 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncService.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncService.java @@ -15,7 +15,7 @@ public class ContributionsSyncService extends Service { super.onCreate(); synchronized (sSyncAdapterLock) { if (sSyncAdapter == null) { - sSyncAdapter = new ContributionsSyncAdapter(getApplicationContext(), true); + sSyncAdapter = new ContributionsSyncAdapter(this, true); } } } diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java index 58ab54a4d..22171857a 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java @@ -12,16 +12,11 @@ public class DBOpenHelper extends SQLiteOpenHelper{ private static final String DATABASE_NAME = "commons.db"; private static final int DATABASE_VERSION = 6; - private static DBOpenHelper singleton = null; - public static synchronized DBOpenHelper getInstance(Context context) { - if ( singleton == null ) { - singleton = new DBOpenHelper(context); - } - return singleton; - } - - private DBOpenHelper(Context context) { + /** + * Do not use, please call CommonsApplication.getDBOpenHelper() + */ + public DBOpenHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } diff --git a/app/src/main/java/fr/free/nrw/commons/hamburger/HamburgerMenuContainer.java b/app/src/main/java/fr/free/nrw/commons/hamburger/HamburgerMenuContainer.java new file mode 100644 index 000000000..677200b54 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/hamburger/HamburgerMenuContainer.java @@ -0,0 +1,9 @@ +package fr.free.nrw.commons.hamburger; + +import android.support.v7.app.ActionBarDrawerToggle; + +public interface HamburgerMenuContainer { + void setDrawerListener(ActionBarDrawerToggle listener); + void toggleDrawer(); + boolean isDrawerVisible(); +} diff --git a/app/src/main/java/fr/free/nrw/commons/hamburger/NavigationBaseFragment.java b/app/src/main/java/fr/free/nrw/commons/hamburger/NavigationBaseFragment.java new file mode 100644 index 000000000..16c318410 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/hamburger/NavigationBaseFragment.java @@ -0,0 +1,164 @@ +package fr.free.nrw.commons.hamburger; + +import android.content.ActivityNotFoundException; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.Toast; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import fr.free.nrw.commons.AboutActivity; +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.auth.LoginActivity; +import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.nearby.NearbyActivity; +import fr.free.nrw.commons.settings.SettingsActivity; + + +public class NavigationBaseFragment extends Fragment { + @BindView(R.id.pictureOfTheDay) + ImageView pictureOfTheDay; + + @BindView(R.id.upload_item) + LinearLayout uploadItem; + + @BindView(R.id.nearby_item) + LinearLayout nearbyItem; + + @BindView(R.id.about_item) + LinearLayout aboutItem; + + @BindView(R.id.settings_item) + LinearLayout settingsItem; + + @BindView(R.id.feedback_item) + LinearLayout feedbackItem; + + @BindView(R.id.logout_item) + LinearLayout logoutItem; + + private DrawerLayout drawerLayout; + private RelativeLayout drawerPane; + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) { + View hamburgerView = inflater.inflate(R.layout.navigation_drawer_menu, container, false); + ButterKnife.bind(this, hamburgerView); + showPictureOfTheDay(); + setupHamburgerMenu(); + return hamburgerView; + } + + private void showPictureOfTheDay() { + pictureOfTheDay.setImageDrawable(getResources().getDrawable(R.drawable.commons_logo_large)); + } + + @Override + public void onResume() { + super.onResume(); + } + + private void setupHamburgerMenu() { + ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle(getActivity(), + drawerLayout, R.string.ok, R.string.cancel); + if (getActivity() instanceof HamburgerMenuContainer) { + ((HamburgerMenuContainer) getActivity()).setDrawerListener(drawerToggle); + } + } + + @OnClick(R.id.upload_item) + protected void onUploadItemClicked() { + closeDrawer(); + ContributionsActivity.startYourself(getActivity()); + } + + @OnClick(R.id.settings_item) + protected void onSettingsItemClicked() { + closeDrawer(); + SettingsActivity.startYourself(getActivity()); + } + + @OnClick(R.id.about_item) + protected void onAboutItemClicked() { + closeDrawer(); + AboutActivity.startYourself(getActivity()); + } + + @OnClick(R.id.nearby_item) + protected void onNearbyItemClicked() { + closeDrawer(); + NearbyActivity.startYourself(getActivity()); + } + + @OnClick(R.id.feedback_item) + protected void onFeedbackItemClicked() { + closeDrawer(); + Intent feedbackIntent = new Intent(Intent.ACTION_SEND); + feedbackIntent.setType("message/rfc822"); + feedbackIntent.putExtra(Intent.EXTRA_EMAIL, + new String[]{CommonsApplication.FEEDBACK_EMAIL}); + feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, + String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT, + BuildConfig.VERSION_NAME)); + try { + startActivity(feedbackIntent); + } catch (ActivityNotFoundException e) { + Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show(); + } + } + + @OnClick(R.id.logout_item) + protected void onLogoutItemClicked() { + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity()); + alertDialogBuilder.setMessage(R.string.logout_verification) + .setCancelable(false) + .setPositiveButton(R.string.yes, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + ((CommonsApplication)getActivity().getApplicationContext()) + .clearApplicationData(getContext()); + Intent nearbyIntent = new Intent + (getActivity(), LoginActivity.class); + nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(nearbyIntent); + getActivity().finish(); + } + }); + alertDialogBuilder.setNegativeButton(R.string.no, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + AlertDialog alert = alertDialogBuilder.create(); + alert.show(); + } + + private void closeDrawer() { + if (drawerLayout != null) { + drawerLayout.closeDrawer(drawerPane); + } + } + + public void setDrawerLayout(DrawerLayout drawerLayout, RelativeLayout drawerPane) { + this.drawerLayout = drawerLayout; + this.drawerPane = drawerPane; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LatLng.java b/app/src/main/java/fr/free/nrw/commons/location/LatLng.java index 839cba14e..acd67ebf7 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LatLng.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LatLng.java @@ -6,6 +6,8 @@ public class LatLng { public final double longitude; /** Accepts latitude and longitude. + * North and South values are cut off at 90° + * * @param latitude double value * @param longitude double value */ @@ -42,4 +44,53 @@ public class LatLng { public String toString() { return "lat/lng: (" + this.latitude + "," + this.longitude + ")"; } + + /** + * Rounds the float to 4 digits and returns absolute value. + * + * @param coordinate A coordinate value as string. + * @return String of the rounded number. + */ + private String formatCoordinate(double coordinate) { + double roundedNumber = Math.round(coordinate * 10000d) / 10000d; + double absoluteNumber = Math.abs(roundedNumber); + return String.valueOf(absoluteNumber); + } + + /** + * Returns "N" or "S" depending on the latitude. + * + * @return "N" or "S". + */ + private String getNorthSouth() { + if (this.latitude < 0) { + return "S"; + } + + return "N"; + } + + /** + * Returns "E" or "W" depending on the longitude. + * + * @return "E" or "W". + */ + private String getEastWest() { + if (this.longitude >= 0 && this.longitude < 180) { + return "E"; + } + + return "W"; + } + + /** + * Returns a nicely formatted coordinate string. Used e.g. in + * the detail view. + * + * @return The formatted string. + */ + public String getPrettyCoordinateString() { + return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", " + + formatCoordinate(this.longitude) + " " + this.getEastWest(); + } } 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 cd428e9a3..3c0d2b23b 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 @@ -6,10 +6,11 @@ import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; -import android.util.Log; + +import timber.log.Timber; public class LocationServiceManager implements LocationListener { - public static final String TAG = "LocationServiceManager"; + private String provider; private LocationManager locationManager; private LatLng latestLocation; @@ -31,14 +32,14 @@ public class LocationServiceManager implements LocationListener { Location location = locationManager.getLastKnownLocation(provider); //Location works, just need to 'send' GPS coords // via emulator extended controls if testing on emulator - Log.d(TAG, "Checking for location..."); + Timber.d("Checking for location..."); if (location != null) { this.onLocationChanged(location); } } catch (IllegalArgumentException e) { - Log.e(TAG, "Illegal argument exception", e); + Timber.e(e, "Illegal argument exception"); } catch (SecurityException e) { - Log.e(TAG, "Security exception", e); + Timber.e(e, "Security exception"); } } @@ -48,7 +49,7 @@ public class LocationServiceManager implements LocationListener { try { locationManager.removeUpdates(this); } catch (SecurityException e) { - Log.e(TAG, "Security exception", e); + Timber.e(e, "Security exception"); } } @@ -56,24 +57,23 @@ public class LocationServiceManager implements LocationListener { public void onLocationChanged(Location location) { double currentLatitude = location.getLatitude(); double currentLongitude = location.getLongitude(); - Log.d(TAG, "Latitude: " + String.valueOf(currentLatitude) - + " Longitude: " + String.valueOf(currentLongitude)); + Timber.d("Latitude: %f Longitude: %f", currentLatitude, currentLongitude); latestLocation = new LatLng(currentLatitude, currentLongitude); } @Override public void onStatusChanged(String provider, int status, Bundle extras) { - Log.d(TAG, provider + "'s status changed to " + status); + Timber.d("%s's status changed to %d", provider, status); } @Override public void onProviderEnabled(String provider) { - Log.d(TAG, "Provider " + provider + " enabled"); + Timber.d("Provider %s enabled", provider); } @Override public void onProviderDisabled(String provider) { - Log.d(TAG, "Provider " + provider + " disabled"); + Timber.d("Provider %s disabled", provider); } } 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 be5bf65db..59ea6b2b4 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 @@ -2,31 +2,23 @@ package fr.free.nrw.commons.media; import android.content.Intent; import android.database.DataSetObserver; -import android.graphics.Bitmap; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; -import android.util.Log; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; -import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.ProgressBar; import android.widget.ScrollView; import android.widget.TextView; -import com.android.volley.toolbox.ImageLoader; -import com.nostra13.universalimageloader.core.DisplayImageOptions; -import com.nostra13.universalimageloader.core.assist.FailReason; -import com.nostra13.universalimageloader.core.assist.ImageLoadingListener; - import java.io.IOException; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.License; import fr.free.nrw.commons.LicenseList; import fr.free.nrw.commons.Media; @@ -34,18 +26,14 @@ import fr.free.nrw.commons.MediaDataExtractor; import fr.free.nrw.commons.MediaWikiImageView; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; +import timber.log.Timber; public class MediaDetailFragment extends Fragment { private boolean editable; - private DisplayImageOptions displayOptions; private MediaDetailPagerFragment.MediaDetailProvider detailProvider; private int index; - public static MediaDetailFragment forMedia(int index) { - return forMedia(index, false); - } - public static MediaDetailFragment forMedia(int index, boolean editable) { MediaDetailFragment mf = new MediaDetailFragment(); @@ -60,16 +48,15 @@ public class MediaDetailFragment extends Fragment { return mf; } - private ImageView image; - //private EditText title; - private ProgressBar loadingProgress; - private ImageView loadingFailed; + private MediaWikiImageView image; private MediaDetailSpacer spacer; private int initialListTop = 0; private TextView title; private TextView desc; private TextView license; + private TextView coordinates; + private TextView uploadedDate; private LinearLayout categoryContainer; private ScrollView scrollView; private ArrayList categoryNames; @@ -113,9 +100,7 @@ public class MediaDetailFragment extends Fragment { final View view = inflater.inflate(R.layout.fragment_media_detail, container, false); - image = (ImageView) view.findViewById(R.id.mediaDetailImage); - loadingProgress = (ProgressBar) view.findViewById(R.id.mediaDetailImageLoading); - loadingFailed = (ImageView) view.findViewById(R.id.mediaDetailImageFailed); + image = (MediaWikiImageView) view.findViewById(R.id.mediaDetailImage); scrollView = (ScrollView) view.findViewById(R.id.mediaDetailScrollView); // Detail consists of a list view with main pane in header view, plus category list. @@ -123,29 +108,12 @@ public class MediaDetailFragment extends Fragment { title = (TextView) view.findViewById(R.id.mediaDetailTitle); desc = (TextView) view.findViewById(R.id.mediaDetailDesc); license = (TextView) view.findViewById(R.id.mediaDetailLicense); + coordinates = (TextView) view.findViewById(R.id.mediaDetailCoordinates); + uploadedDate = (TextView) view.findViewById(R.id.mediaDetailuploadeddate); categoryContainer = (LinearLayout) view.findViewById(R.id.mediaDetailCategoryContainer); licenseList = new LicenseList(getActivity()); - Media media = detailProvider.getMediaAtPosition(index); - if (media == null) { - // Ask the detail provider to ping us when we're ready - Log.d("Commons", "MediaDetailFragment not yet ready to display details; registering observer"); - dataObserver = new DataSetObserver() { - @Override - public void onChanged() { - Log.d("Commons", "MediaDetailFragment ready to display delayed details!"); - detailProvider.unregisterDataSetObserver(dataObserver); - dataObserver = null; - displayMediaDetails(detailProvider.getMediaAtPosition(index)); - } - }; - detailProvider.registerDataSetObserver(dataObserver); - } else { - Log.d("Commons", "MediaDetailFragment ready to display details"); - displayMediaDetails(media); - } - // Progressively darken the image in the background when we scroll detail pane up scrollListener = new ViewTreeObserver.OnScrollChangedListener() { @Override @@ -182,97 +150,75 @@ public class MediaDetailFragment extends Fragment { return view; } - private void displayMediaDetails(final Media media) { - //Always load image from Internet to allow viewing the desc, license, and cats - String actualUrl = media.getThumbnailUrl(640); - if(actualUrl.startsWith("http")) { - Log.d("Volley", "Actual URL starts with http and is: " + actualUrl); - - ImageLoader loader = ((CommonsApplication)getActivity().getApplicationContext()).getImageLoader(); - MediaWikiImageView mwImage = (MediaWikiImageView)image; - mwImage.setLoadingView(loadingProgress); //FIXME: Set this as an attribute - mwImage.setMedia(media, loader); - - // FIXME: For transparent images - // FIXME: keep the spinner going while we load data - // FIXME: cache this data - // Load image metadata: desc, license, categories - detailFetchTask = new AsyncTask() { - private MediaDataExtractor extractor; - + @Override public void onResume() { + super.onResume(); + Media media = detailProvider.getMediaAtPosition(index); + if (media == null) { + // Ask the detail provider to ping us when we're ready + Timber.d("MediaDetailFragment not yet ready to display details; registering observer"); + dataObserver = new DataSetObserver() { @Override - protected void onPreExecute() { - extractor = new MediaDataExtractor(media.getFilename(), licenseList); - } - - @Override - protected Boolean doInBackground(Void... voids) { - try { - extractor.fetch(); - return Boolean.TRUE; - } catch (IOException e) { - e.printStackTrace(); - } - return Boolean.FALSE; - } - - @Override - protected void onPostExecute(Boolean success) { - detailFetchTask = null; - - if (success.booleanValue()) { - extractor.fill(media); - - // Set text of desc, license, and categories - desc.setText(prettyDescription(media)); - license.setText(prettyLicense(media)); - - categoryNames.removeAll(categoryNames); - categoryNames.addAll(media.getCategories()); - - categoriesLoaded = true; - categoriesPresent = (categoryNames.size() > 0); - if (!categoriesPresent) { - // Stick in a filler element. - categoryNames.add(getString(R.string.detail_panel_cats_none)); - } - rebuildCatList(); - } else { - Log.d("Commons", "Failed to load photo details."); + public void onChanged() { + if (!isAdded()) { + return; } + Timber.d("MediaDetailFragment ready to display delayed details!"); + detailProvider.unregisterDataSetObserver(dataObserver); + dataObserver = null; + displayMediaDetails(detailProvider.getMediaAtPosition(index)); } }; - detailFetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + detailProvider.registerDataSetObserver(dataObserver); } else { - //This should not usually happen, image along with associated details should always be loaded from Internet, but keeping this for now for backup. - //Even if image is loaded from device storage, it will display, albeit with empty desc and cat. - Log.d("Volley", "Actual URL does not start with http and is: " + actualUrl); - com.nostra13.universalimageloader.core.ImageLoader.getInstance().displayImage(actualUrl, image, displayOptions, new ImageLoadingListener() { - @Override - public void onLoadingStarted(String s, View view) { - loadingProgress.setVisibility(View.VISIBLE); + Timber.d("MediaDetailFragment ready to display details"); + displayMediaDetails(media); + } + } + + private void displayMediaDetails(final Media media) { + //Always load image from Internet to allow viewing the desc, license, and cats + image.setMedia(media); + + // FIXME: For transparent images + // FIXME: keep the spinner going while we load data + // FIXME: cache this data + // Load image metadata: desc, license, categories + detailFetchTask = new AsyncTask() { + private MediaDataExtractor extractor; + + @Override + protected void onPreExecute() { + extractor = new MediaDataExtractor(media.getFilename(), licenseList); + } + + @Override + protected Boolean doInBackground(Void... voids) { + try { + extractor.fetch(); + return Boolean.TRUE; + } catch (IOException e) { + e.printStackTrace(); + } + return Boolean.FALSE; + } + + @Override + protected void onPostExecute(Boolean success) { + detailFetchTask = null; + if (!isAdded()) { + return; } - @Override - public void onLoadingFailed(String s, View view, FailReason failReason) { - loadingProgress.setVisibility(View.GONE); - loadingFailed.setVisibility(View.VISIBLE); - } - - @Override - public void onLoadingComplete(String s, View view, Bitmap bitmap) { - loadingProgress.setVisibility(View.GONE); - loadingFailed.setVisibility(View.GONE); - image.setVisibility(View.VISIBLE); - if(bitmap.hasAlpha()) { - image.setBackgroundResource(android.R.color.white); - } + if (success) { + extractor.fill(media); // Set text of desc, license, and categories desc.setText(prettyDescription(media)); license.setText(prettyLicense(media)); + coordinates.setText(prettyCoordinates(media)); + uploadedDate.setText(prettyUploadedDate(media)); - categoryNames.removeAll(categoryNames); + categoryNames.clear(); categoryNames.addAll(media.getCategories()); categoriesLoaded = true; @@ -282,27 +228,18 @@ public class MediaDetailFragment extends Fragment { categoryNames.add(getString(R.string.detail_panel_cats_none)); } rebuildCatList(); + } else { + Timber.d("Failed to load photo details."); } - - @Override - public void onLoadingCancelled(String s, View view) { - Log.e("Volley", "Image loading cancelled. But why?"); - } - }); - } + } + }; + detailFetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); title.setText(media.getDisplayTitle()); desc.setText(""); // fill in from network... license.setText(""); // fill in from network... } - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - displayOptions = Utils.getGenericDisplayOptions().build(); - } - @Override public void onDestroyView() { if (detailFetchTask != null) { @@ -377,7 +314,7 @@ public class MediaDetailFragment extends Fragment { private String prettyLicense(Media media) { String licenseKey = media.getLicense(); - Log.d("Commons", "Media license is: " + licenseKey); + Timber.d("Media license is: %s", licenseKey); if (licenseKey == null || licenseKey.equals("")) { return getString(R.string.detail_license_empty); } @@ -388,4 +325,22 @@ public class MediaDetailFragment extends Fragment { return licenseObj.getName(); } } + + private String prettyUploadedDate(Media media) { + Date date = media.getDateUploaded(); + if (date.toString() == null || date.toString().isEmpty()) { + return "Uploaded date not available"; + } + SimpleDateFormat formatter = new SimpleDateFormat("dd MMM yyyy"); + return formatter.format(date); + } + + /** + * Returns the coordinates nicely formatted. + * + * @return Coordinates as text. + */ + private String prettyCoordinates(Media media) { + return media.getCoordinates(); + } } 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 9adbdb882..c7e06cefa 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 @@ -123,7 +123,7 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa if(savedInstanceState != null) { editable = savedInstanceState.getBoolean("editable"); } - app = (CommonsApplication)getActivity().getApplicationContext(); + app = CommonsApplication.getInstance(); setHasOptionsMenu(true); } 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 a422e5f33..097651aaf 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 @@ -8,9 +8,10 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.text.TextUtils; -import android.util.Log; +import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.data.DBOpenHelper; +import timber.log.Timber; public class ModificationsContentProvider extends ContentProvider{ @@ -35,7 +36,7 @@ public class ModificationsContentProvider extends ContentProvider{ private DBOpenHelper dbOpenHelper; @Override public boolean onCreate() { - dbOpenHelper = DBOpenHelper.getInstance(getContext()); + dbOpenHelper = CommonsApplication.getInstance().getDBOpenHelper(); return false; } @@ -101,14 +102,14 @@ public class ModificationsContentProvider extends ContentProvider{ @Override public int bulkInsert(Uri uri, ContentValues[] values) { - Log.d("Commons", "Hello, bulk insert! (ModificationsContentProvider)"); + Timber.d("Hello, bulk insert! (ModificationsContentProvider)"); int uriType = uriMatcher.match(uri); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); sqlDB.beginTransaction(); switch (uriType) { case MODIFICATIONS: for(ContentValues value: values) { - Log.d("Commons", "Inserting! " + value.toString()); + Timber.d("Inserting! %s", value); sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, value); } break; 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 b17162705..898c41f7d 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 @@ -11,10 +11,9 @@ import android.content.SyncResult; import android.database.Cursor; import android.os.Bundle; import android.os.RemoteException; -import android.util.Log; +import fr.free.nrw.commons.MWApi; import org.mediawiki.api.ApiResult; -import org.mediawiki.api.MWApi; import java.io.IOException; @@ -22,6 +21,7 @@ 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 timber.log.Timber; public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { @@ -42,7 +42,7 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { // Exit early if nothing to do if(allModifications == null || allModifications.getCount() == 0) { - Log.d("Commons", "No modifications to perform"); + Timber.d("No modifications to perform"); return; } @@ -52,16 +52,16 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { } catch (OperationCanceledException | AuthenticatorException e) { throw new RuntimeException(e); } catch (IOException e) { - Log.d("Commons", "Could not authenticate :("); + Timber.d("Could not authenticate :("); return; } if(Utils.isNullOrWhiteSpace(authCookie)) { - Log.d("Commons", "Could not authenticate :("); + Timber.d("Could not authenticate :("); return; } - MWApi api = CommonsApplication.createMWApi(); + MWApi api = CommonsApplication.getInstance().getMWApi(); api.setAuthCookie(authCookie); String editToken; @@ -69,13 +69,13 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { try { editToken = api.getEditToken(); } catch (IOException e) { - Log.d("Commons", "Can not retreive edit token!"); + Timber.d("Can not retreive edit token!"); return; } allModifications.moveToFirst(); - Log.d("Commons", "Found " + allModifications.getCount() + " modifications to execute"); + Timber.d("Found %d modifications to execute", allModifications.getCount()); ContentProviderClient contributionsClient = null; try { @@ -104,11 +104,11 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { .param("titles", contrib.getFilename()) .get(); } catch (IOException e) { - Log.d("Commons", "Network fuckup on modifications sync!"); + Timber.d("Network fuckup on modifications sync!"); continue; } - Log.d("Commons", "Page content is " + Utils.getStringFromDOM(requestResult.getDocument())); + Timber.d("Page content is %s", Utils.getStringFromDOM(requestResult.getDocument())); String pageContent = requestResult.getString("/api/query/pages/page/revisions/rev"); String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent); @@ -120,16 +120,16 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { .param("summary", sequence.getEditSummary()) .post(); } catch (IOException e) { - Log.d("Commons", "Network fuckup on modifications sync!"); + Timber.d("Network fuckup on modifications sync!"); continue; } - Log.d("Commons", "Response is" + Utils.getStringFromDOM(responseResult.getDocument())); + Timber.d("Response is %s", Utils.getStringFromDOM(responseResult.getDocument())); String result = responseResult.getString("/api/edit/@result"); if(!result.equals("Success")) { // FIXME: Log this somewhere else - Log.d("Commons", "Non success result!" + result); + Timber.d("Non success result! %s", result); } else { sequence.delete(); } diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncService.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncService.java index b664aaaa4..bf6878622 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncService.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncService.java @@ -15,7 +15,7 @@ public class ModificationsSyncService extends Service { super.onCreate(); synchronized (sSyncAdapterLock) { if (sSyncAdapter == null) { - sSyncAdapter = new ModificationsSyncAdapter(getApplicationContext(), true); + sSyncAdapter = new ModificationsSyncAdapter(this, true); } } } diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java index b660c0a72..67152f85b 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java @@ -142,5 +142,10 @@ public class ModifierSequence { db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); onCreate(db); } + + public static void onDelete(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(db); + } } } 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 380d256fd..1060cf25e 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java @@ -1,36 +1,65 @@ package fr.free.nrw.commons.nearby; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.location.LocationManager; +import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.AlertDialog; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.Toast; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.theme.BaseActivity; +import fr.free.nrw.commons.theme.NavigationBaseActivity; +import fr.free.nrw.commons.utils.UriSerializer; +import timber.log.Timber; -public class NearbyActivity extends BaseActivity { + +public class NearbyActivity extends NavigationBaseActivity { + + @BindView(R.id.progressBar) + ProgressBar progressBar; + private boolean isMapViewActive = false; private LocationServiceManager locationManager; - - private static final String TAG = NearbyActivity.class.getName(); + private LatLng curLatLang; + private Bundle bundle; + private NearbyAsyncTask nearbyAsyncTask; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_nearby); + ButterKnife.bind(this); if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); } + + bundle = new Bundle(); locationManager = new LocationServiceManager(this); locationManager.registerLocationManager(); - - // Begin the transaction - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - NearbyListFragment fragment = new NearbyListFragment(); - ft.add(R.id.container, fragment); - ft.commit(); + curLatLang = locationManager.getLatestLocation(); + nearbyAsyncTask = new NearbyAsyncTask(this); + nearbyAsyncTask.execute(); + initDrawer(); } @Override @@ -47,19 +76,87 @@ public class NearbyActivity extends BaseActivity { case R.id.action_refresh: refreshView(); return true; + case R.id.action_map: + showMapView(); + if (isMapViewActive) { + item.setIcon(R.drawable.ic_list_white_24dp); + } else { + item.setIcon(R.drawable.ic_map_white_24dp); + } + return true; default: return super.onOptionsItemSelected(item); } } + protected void checkGps() { + LocationManager manager = (LocationManager) getSystemService(LOCATION_SERVICE); + if (!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + Timber.d("GPS is not enabled"); + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this); + alertDialogBuilder.setMessage(R.string.gps_disabled) + .setCancelable(false) + .setPositiveButton(R.string.enable_gps, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Intent callGPSSettingIntent = new Intent( + android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS); + Timber.d("Loaded settings page"); + startActivityForResult(callGPSSettingIntent, 1); + } + }); + alertDialogBuilder.setNegativeButton(R.string.menu_cancel_upload, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + AlertDialog alert = alertDialogBuilder.create(); + alert.show(); + } else { + Timber.d("GPS is enabled"); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == 1) { + Timber.d("User is back from Settings page"); + refreshView(); + } + } + + private void showMapView() { + if (!isMapViewActive) { + isMapViewActive = true; + if (nearbyAsyncTask.getStatus() == AsyncTask.Status.FINISHED) { + setMapFragment(); + } + + } else { + isMapViewActive = false; + if (nearbyAsyncTask.getStatus() == AsyncTask.Status.FINISHED) { + setListFragment(); + } + } + } + @Override protected void onResume() { super.onResume(); + checkGps(); + } + + @Override + protected void onPause() { + super.onPause(); + nearbyAsyncTask.cancel(true); } protected void refreshView() { - getSupportFragmentManager().beginTransaction() - .replace(R.id.container, new NearbyListFragment()).commit(); + nearbyAsyncTask = new NearbyAsyncTask(this); + nearbyAsyncTask.execute(); } public LocationServiceManager getLocationManager() { @@ -71,4 +168,93 @@ public class NearbyActivity extends BaseActivity { super.onDestroy(); locationManager.unregisterLocationManager(); } + + private class NearbyAsyncTask extends AsyncTask> { + + private Context mContext; + + private NearbyAsyncTask (Context context) { + mContext = context; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + } + + @Override + protected void onProgressUpdate(Integer... values) { + super.onProgressUpdate(values); + } + + @Override + protected List doInBackground(Void... params) { + return NearbyController + .loadAttractionsFromLocation(curLatLang, CommonsApplication.getInstance() + ); + } + + @Override + protected void onPostExecute(List placeList) { + super.onPostExecute(placeList); + + if (isCancelled()) { + return; + } + + Gson gson = new GsonBuilder() + .registerTypeAdapter(Uri.class, new UriSerializer()) + .create(); + String gsonPlaceList = gson.toJson(placeList); + String gsonCurLatLng = gson.toJson(curLatLang); + + if (placeList.size() == 0) { + int duration = Toast.LENGTH_SHORT; + Toast toast = Toast.makeText(mContext, R.string.no_nearby, duration); + toast.show(); + } + + bundle.clear(); + bundle.putString("PlaceList", gsonPlaceList); + bundle.putString("CurLatLng", gsonCurLatLng); + + // Begin the transaction + if (isMapViewActive) { + setMapFragment(); + } else { + setListFragment(); + } + + if (progressBar != null) { + progressBar.setVisibility(View.GONE); + } + } + } + + /** + * Calls fragment for map view. + */ + public void setMapFragment() { + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + Fragment fragment = new NearbyMapFragment(); + fragment.setArguments(bundle); + fragmentTransaction.replace(R.id.container, fragment); + fragmentTransaction.commit(); + } + + /** + * Calls fragment for list view. + */ + public void setListFragment() { + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + Fragment fragment = new NearbyListFragment(); + fragment.setArguments(bundle); + fragmentTransaction.replace(R.id.container, fragment); + fragmentTransaction.commit(); + } + + public static void startYourself(Context context) { + Intent settingsIntent = new Intent(context, NearbyActivity.class); + context.startActivity(settingsIntent); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapter.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapter.java index c642e9602..d2ad0b978 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapter.java @@ -1,39 +1,28 @@ package fr.free.nrw.commons.nearby; import android.content.Context; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import fr.free.nrw.commons.R; - -import java.util.List; +import timber.log.Timber; public class NearbyAdapter extends ArrayAdapter { - private List placesList; - private Context context; - - public List getPlacesList() { - return placesList; - } /** Accepts activity context and list of places. * @param context activity context - * @param places list of places */ - public NearbyAdapter(Context context, List places) { - super(context, R.layout.item_place, places); - this.context = context; - placesList = places; + public NearbyAdapter(Context context) { + super(context, R.layout.item_place); } @Override public View getView(int position, View convertView, ViewGroup parent) { // Get the data item for this position Place place = getItem(position); - Log.v("NearbyAdapter", "" + place); + Timber.v(String.valueOf(place)); // Check if an existing view is being reused, otherwise inflate the view if (convertView == null) { @@ -42,13 +31,14 @@ public class NearbyAdapter extends ArrayAdapter { } NearbyViewHolder viewHolder = new NearbyViewHolder(convertView); - viewHolder.bindModel(context, place); + viewHolder.bindModel(getContext(), place); // Return the completed view to render on screen return convertView; } @Override public long getItemId(int position) { + // TODO: use Wikidata Q-ID instead? return position; } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyBaseMarker.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyBaseMarker.java new file mode 100644 index 000000000..686b3e6ce --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyBaseMarker.java @@ -0,0 +1,88 @@ +package fr.free.nrw.commons.nearby; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.mapbox.mapboxsdk.annotations.BaseMarkerOptions; +import com.mapbox.mapboxsdk.annotations.Icon; +import com.mapbox.mapboxsdk.annotations.IconFactory; +import com.mapbox.mapboxsdk.geometry.LatLng; + +import fr.free.nrw.commons.utils.UriDeserializer; +import fr.free.nrw.commons.utils.UriSerializer; + +public class NearbyBaseMarker extends BaseMarkerOptions { + private Place place; + public NearbyBaseMarker() { + + } + + public NearbyBaseMarker place(Place place) { + this.place = place; + return getThis(); + } + + public NearbyBaseMarker(Parcel in) { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Uri.class, new UriDeserializer()) + .create(); + + position((LatLng) in.readParcelable(LatLng.class.getClassLoader())); + snippet(in.readString()); + String iconId = in.readString(); + Bitmap iconBitmap = in.readParcelable(Bitmap.class.getClassLoader()); + Icon icon = IconFactory.recreate(iconId, iconBitmap); + icon(icon); + title(in.readString()); + String gsonString = in.readString(); + place(gson.fromJson(gsonString, Place.class)); + } + + @Override + public NearbyBaseMarker getThis() { + return this; + } + + @Override + public NearbyMarker getMarker() { + return new NearbyMarker(this, place); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Uri.class, new UriSerializer()) + .create(); + + dest.writeParcelable(position, flags); + dest.writeString(snippet); + dest.writeString(icon.getId()); + dest.writeParcelable(icon.getBitmap(), flags); + dest.writeString(title); + dest.writeString(gson.toJson(place)); + } + + public Place getPlace() { + return place; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public NearbyBaseMarker createFromParcel(Parcel in) { + return new NearbyBaseMarker(in); + } + + public NearbyBaseMarker[] newArray(int size) { + return new NearbyBaseMarker[size]; + } + }; +} 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 new file mode 100644 index 000000000..1c0699e17 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -0,0 +1,115 @@ +package fr.free.nrw.commons.nearby; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import com.mapbox.mapboxsdk.annotations.Icon; +import com.mapbox.mapboxsdk.annotations.IconFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.location.LatLng; +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; + + /** + * Prepares Place list to make their distance information update later. + * @param curLatLng current location for user + * @param context context + * @return Place list without distance information + */ + public static 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(); + if (curLatLng != null) { + Timber.d("Sorting places by distance..."); + final Map distances = new HashMap<>(); + for (Place place: places) { + distances.put(place, computeDistanceBetween(place.location, curLatLng)); + } + Collections.sort(places, + new Comparator() { + @Override + public int compare(Place lhs, Place rhs) { + double lhsDistance = distances.get(lhs); + double rhsDistance = distances.get(rhs); + return (int) (lhsDistance - rhsDistance); + } + } + ); + } + return places; + } + + /** + * Loads attractions from location for list view, we need to return Place data type. + * @param curLatLng users current location + * @param placeList list of nearby places in Place data type + * @return Place list that holds nearby places + */ + public static List loadAttractionsFromLocationToPlaces( + LatLng curLatLng, + List placeList) { + placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS)); + for (Place place: placeList) { + String distance = formatDistanceBetween(curLatLng, place.location); + place.setDistance(distance); + } + return placeList; + } + + /** + *Loads attractions from location for map view, we need to return BaseMarkerOption data type. + * @param curLatLng users current location + * @param placeList list of nearby places in Place data type + * @return BaseMarkerOprions list that holds nearby places + */ + public static List loadAttractionsFromLocationToBaseMarkerOptions( + LatLng curLatLng, + List placeList, + Context context) { + List baseMarkerOptionses = new ArrayList<>(); + placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS)); + for (Place place: placeList) { + String distance = formatDistanceBetween(curLatLng, place.location); + place.setDistance(distance); + + Icon icon = IconFactory.getInstance(context) + .fromResource(R.drawable.custom_map_marker); + + NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker(); + nearbyBaseMarker.title(place.name); + nearbyBaseMarker.position( + new com.mapbox.mapboxsdk.geometry.LatLng( + place.location.latitude, + place.location.longitude)); + nearbyBaseMarker.place(place); + nearbyBaseMarker.icon(icon); + + baseMarkerOptionses.add(nearbyBaseMarker); + } + return baseMarkerOptionses; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyInfoDialog.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyInfoDialog.java new file mode 100644 index 000000000..9308413da --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyInfoDialog.java @@ -0,0 +1,163 @@ +package fr.free.nrw.commons.nearby; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; +import android.support.v7.widget.PopupMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Unbinder; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.ui.widget.OverlayDialog; +import fr.free.nrw.commons.utils.DialogUtil; + +public class NearbyInfoDialog extends OverlayDialog { + + private final static String ARG_TITLE = "placeTitle"; + private final static String ARG_DESC = "placeDesc"; + private final static String ARG_LATITUDE = "latitude"; + private final static String ARG_LONGITUDE = "longitude"; + private final static String ARG_SITE_LINK = "sitelink"; + + @BindView(R.id.link_preview_title) + TextView placeTitle; + @BindView(R.id.link_preview_extract) + TextView placeDescription; + + @BindView(R.id.link_preview_go_button) + TextView goToButton; + + @BindView(R.id.link_preview_overflow_button) + ImageView overflowButton; + + private Unbinder unbinder; + + private LatLng location; + private Sitelinks sitelinks; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.dialog_nearby_info, container, false); + unbinder = ButterKnife.bind(this, view); + initUi(); + return view; + } + + private void initUi() { + Bundle bundle = getArguments(); + placeTitle.setText(bundle.getString(ARG_TITLE)); + placeDescription.setText(bundle.getString(ARG_DESC)); + location = new LatLng(bundle.getDouble(ARG_LATITUDE), bundle.getDouble(ARG_LONGITUDE)); + getArticleLink(bundle); + } + + private void getArticleLink(Bundle bundle) { + this.sitelinks = bundle.getParcelable(ARG_SITE_LINK); + + if (sitelinks.getWikipediaLink() == null) { + goToButton.setVisibility(View.GONE); + } + + overflowButton.setVisibility(showMenu() ? View.VISIBLE : View.GONE); + + overflowButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + popupMenuListener(); + } + }); + } + + private void popupMenuListener() { + PopupMenu popupMenu = new PopupMenu(getActivity(), overflowButton); + popupMenu.inflate(R.menu.nearby_info_dialog_options); + + MenuItem commonsArticle = popupMenu.getMenu() + .findItem(R.id.nearby_info_menu_commons_article); + MenuItem wikiDataArticle = popupMenu.getMenu() + .findItem(R.id.nearby_info_menu_wikidata_article); + + commonsArticle.setEnabled(sitelinks.getCommonsLink() != null); + wikiDataArticle.setEnabled(sitelinks.getWikidataLink() != null); + + popupMenu.setOnMenuItemClickListener(menuListener); + popupMenu.show(); + } + + private boolean showMenu() { + return sitelinks.getCommonsLink() != null + || sitelinks.getWikidataLink() != null; + } + + private PopupMenu.OnMenuItemClickListener menuListener = new PopupMenu + .OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.nearby_info_menu_commons_article: + openWebView(sitelinks.getCommonsLink()); + return true; + case R.id.nearby_info_menu_wikidata_article: + openWebView(sitelinks.getWikidataLink()); + return true; + default: + break; + } + return false; + } + }; + + public static void showYourself(FragmentActivity fragmentActivity, Place place) { + NearbyInfoDialog mDialog = new NearbyInfoDialog(); + Bundle bundle = new Bundle(); + bundle.putString(ARG_TITLE, place.name); + bundle.putString(ARG_DESC, place.description); + bundle.putDouble(ARG_LATITUDE, place.location.latitude); + bundle.putDouble(ARG_LONGITUDE, place.location.longitude); + bundle.putParcelable(ARG_SITE_LINK, place.siteLinks); + mDialog.setArguments(bundle); + DialogUtil.showSafely(fragmentActivity, mDialog); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + + @OnClick(R.id.link_preview_directions_button) + void onDirectionsClick() { + //Open map app at given position + Uri gmmIntentUri = Uri.parse("geo:0,0?q=" + location.latitude + "," + location.longitude); + Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); + + if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) { + startActivity(mapIntent); + } + } + + @OnClick(R.id.link_preview_go_button) + void onReadArticleClick() { + openWebView(sitelinks.getWikipediaLink()); + } + + private void openWebView(Uri link) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, link); + startActivity(browserIntent); + } + + @OnClick(R.id.emptyLayout) + void onCloseClicked() { + dismissAllowingStateLoss(); + } +} 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 e7aedd249..bc9e22a3f 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 @@ -1,46 +1,35 @@ package fr.free.nrw.commons.nearby; -import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween; -import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; - -import android.content.Intent; -import android.content.SharedPreferences; import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; -import android.preference.PreferenceManager; import android.support.v4.app.ListFragment; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ListView; -import android.widget.ProgressBar; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnItemClick; import fr.free.nrw.commons.R; import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.utils.UriDeserializer; +import timber.log.Timber; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; +public class NearbyListFragment extends ListFragment { + private List placeList; -public class NearbyListFragment extends ListFragment implements TaskListener { + @BindView(R.id.listView) ListView listview; - private static final int MAX_RESULTS = 1000; - private NearbyAsyncTask nearbyAsyncTask; - @BindView(R.id.listview) ListView listview; - @BindView(R.id.progressBar) ProgressBar progressBar; - - private boolean isTaskRunning = false; - - private static final String TAG = NearbyListFragment.class.getName(); + private NearbyAdapter adapter; public NearbyListFragment() { } @@ -55,118 +44,43 @@ public class NearbyListFragment extends ListFragment implements TaskListener { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - Log.d(TAG, "NearbyListFragment created"); + Timber.d("NearbyListFragment created"); View view = inflater.inflate(R.layout.fragment_nearby, container, false); ButterKnife.bind(this, view); + adapter = new NearbyAdapter(getActivity()); + listview.setAdapter(adapter); return view; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { - //Check that this is the first time view is created, to avoid double list when screen orientation changed + // Check that this is the first time view is created, + // to avoid double list when screen orientation changed + Bundle bundle = this.getArguments(); + Gson gson = new GsonBuilder() + .registerTypeAdapter(Uri.class, new UriDeserializer()) + .create(); + if (bundle != null) { + String gsonPlaceList = bundle.getString("PlaceList"); + String gsonLatLng = bundle.getString("CurLatLng"); + Type listType = new TypeToken>() {}.getType(); + placeList = gson.fromJson(gsonPlaceList, listType); + Type curLatLngType = new TypeToken() {}.getType(); + LatLng curLatLng = gson.fromJson(gsonLatLng, curLatLngType); + placeList = NearbyController.loadAttractionsFromLocationToPlaces(curLatLng, placeList); + } if (savedInstanceState == null) { - nearbyAsyncTask = new NearbyAsyncTask(this); - nearbyAsyncTask.execute(); - progressBar.setVisibility(View.VISIBLE); - Log.d(TAG, "Saved instance state is null, populating ListView"); - } else { - progressBar.setVisibility(View.GONE); + adapter.clear(); + Timber.d("Saved instance state is null, populating ListView"); } - // If we are returning here from a screen orientation and the AsyncTask is still working, - // re-create and display the progress dialog. - if (isTaskRunning) { - progressBar.setVisibility(View.VISIBLE); - } + adapter.clear(); + adapter.addAll(placeList); + adapter.notifyDataSetChanged(); } - @Override - public void onSaveInstanceState(Bundle outInstanceState) { - // See http://stackoverflow.com/questions/8942135/listview-added-dublicate-item-in-list-when-screen-orientation-changes - outInstanceState.putInt("value", 1); - } - - @Override - public void onTaskStarted() { - isTaskRunning = true; - progressBar.setVisibility(View.VISIBLE); - } - - @Override - public void onTaskFinished(List result) { - if (progressBar != null) { - progressBar.setVisibility(View.GONE); - } - isTaskRunning = false; - } - - @Override - public void onDetach() { - // All dialogs should be closed before leaving the activity in order to avoid - // the: Activity has leaked window com.android.internal.policy... exception - if (progressBar != null && progressBar.isShown()) { - progressBar.setVisibility(View.GONE); - } - super.onDetach(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - - // See http://stackoverflow.com/questions/18264408/incomplete-asynctask-crashes-my-app - if (nearbyAsyncTask != null && nearbyAsyncTask.getStatus() != AsyncTask.Status.FINISHED) { - nearbyAsyncTask.cancel(true); - } - } - - private class NearbyAsyncTask extends AsyncTask> { - - private final TaskListener listener; - - public NearbyAsyncTask(TaskListener listener) { - this.listener = listener; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - listener.onTaskStarted(); - } - - @Override - protected void onProgressUpdate(Integer... values) { - super.onProgressUpdate(values); - progressBar.setProgress(values[0]); - } - - @Override - protected List doInBackground(Void... params) { - return loadAttractionsFromLocation( - ((NearbyActivity)getActivity()).getLocationManager().getLatestLocation() - ); - } - - @Override - protected void onPostExecute(List places) { - super.onPostExecute(places); - - if (isCancelled()) { - return; - } - - progressBar.setVisibility(View.GONE); - NearbyAdapter adapter = new NearbyAdapter(getActivity(), places); - - listview.setAdapter(adapter); - - listener.onTaskFinished(places); - adapter.notifyDataSetChanged(); - } - } - - @OnItemClick(R.id.listview) + @OnItemClick(R.id.listView) void onItemClicked(int position) { Place place = (Place) listview.getItemAtPosition(position); LatLng placeLatLng = place.location; @@ -174,53 +88,8 @@ public class NearbyListFragment extends ListFragment implements TaskListener { double latitude = placeLatLng.latitude; double longitude = placeLatLng.longitude; - Log.d(TAG, "Item at position " - + position + " has coords: Lat: " - + latitude + " Long: " - + longitude); + Timber.d("Item at position %d has coords: Lat: %f Long: %f", position, latitude, longitude); - //Open map app at given position - Uri gmmIntentUri = Uri.parse("geo:0,0?q=" + latitude + "," + longitude); - Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); - - if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) { - startActivity(mapIntent); - } - } - - private List loadAttractionsFromLocation(LatLng curLatLng) { - Log.d(TAG, "Loading attractions near " + curLatLng); - if (curLatLng == null) { - return Collections.emptyList(); - } - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); - List places = prefs.getBoolean("useWikidata", true) - ? NearbyPlaces.getInstance().getFromWikidataQuery( - curLatLng, Locale.getDefault().getLanguage()) - : NearbyPlaces.getInstance().getFromWikiNeedsPictures(); - if (curLatLng != null) { - Log.d(TAG, "Sorting places by distance..."); - final Map distances = new HashMap<>(); - for (Place place: places) { - distances.put(place, computeDistanceBetween(place.location, curLatLng)); - } - Collections.sort(places, - new Comparator() { - @Override - public int compare(Place lhs, Place rhs) { - double lhsDistance = distances.get(lhs); - double rhsDistance = distances.get(rhs); - return (int) (lhsDistance - rhsDistance); - } - } - ); - } - - places = places.subList(0, Math.min(places.size(), MAX_RESULTS)); - for (Place place: places) { - String distance = formatDistanceBetween(curLatLng, place.location); - place.setDistance(distance); - } - return places; + NearbyInfoDialog.showYourself(getActivity(), place); } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java new file mode 100644 index 000000000..b80d16313 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java @@ -0,0 +1,158 @@ +package fr.free.nrw.commons.nearby; + +import android.net.Uri; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.mapbox.mapboxsdk.Mapbox; +import com.mapbox.mapboxsdk.annotations.Marker; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.constants.Style; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.MapboxMapOptions; +import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; +import com.mapbox.services.android.telemetry.MapboxTelemetry; + +import java.lang.reflect.Type; +import java.util.List; + +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.utils.UriDeserializer; + +public class NearbyMapFragment extends android.support.v4.app.Fragment { + private MapView mapView; + private List baseMarkerOptionses; + private fr.free.nrw.commons.location.LatLng curLatLng; + + public NearbyMapFragment() { + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Bundle bundle = this.getArguments(); + Gson gson = new GsonBuilder() + .registerTypeAdapter(Uri.class, new UriDeserializer()) + .create(); + if (bundle != null) { + String gsonPlaceList = bundle.getString("PlaceList"); + String gsonLatLng = bundle.getString("CurLatLng"); + Type listType = new TypeToken>() {}.getType(); + List placeList = gson.fromJson(gsonPlaceList, listType); + Type curLatLngType = new TypeToken() {}.getType(); + curLatLng = gson.fromJson(gsonLatLng, curLatLngType); + baseMarkerOptionses = NearbyController + .loadAttractionsFromLocationToBaseMarkerOptions(curLatLng, + placeList, + getActivity()); + } + Mapbox.getInstance(getActivity(), + getString(R.string.mapbox_commons_app_token)); + MapboxTelemetry.getInstance().setTelemetryEnabled(false); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + if (curLatLng != null) { + setupMapView(savedInstanceState); + } + + setHasOptionsMenu(false); + + return mapView; + } + + private void setupMapView(Bundle savedInstanceState) { + MapboxMapOptions options = new MapboxMapOptions() + .styleUrl(Style.OUTDOORS) + .camera(new CameraPosition.Builder() + .target(new LatLng(curLatLng.latitude, curLatLng.longitude)) + .zoom(11) + .build()); + + // create map + mapView = new MapView(getActivity(), options); + mapView.onCreate(savedInstanceState); + mapView.getMapAsync(new OnMapReadyCallback() { + @Override + public void onMapReady(MapboxMap mapboxMap) { + mapboxMap.addMarkers(baseMarkerOptionses); + + mapboxMap.setOnMarkerClickListener(new MapboxMap.OnMarkerClickListener() { + @Override + public boolean onMarkerClick(@NonNull Marker marker) { + if (marker instanceof NearbyMarker) { + NearbyMarker nearbyMarker = (NearbyMarker) marker; + Place place = nearbyMarker.getNearbyBaseMarker().getPlace(); + NearbyInfoDialog.showYourself(getActivity(), place); + } + return false; + } + }); + } + }); + if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme",true)) { + mapView.setStyleUrl(getResources().getString(R.string.map_theme_dark)); + } else { + mapView.setStyleUrl(getResources().getString(R.string.map_theme_light)); + } + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + } + + @Override + public void onStart() { + if (mapView != null) { + mapView.onStart(); + } + super.onStart(); + } + + @Override + public void onPause() { + if (mapView != null) { + mapView.onPause(); + } + super.onPause(); + } + + @Override + public void onResume() { + if (mapView != null) { + mapView.onResume(); + } + super.onResume(); + } + + @Override + public void onStop() { + if (mapView != null) { + mapView.onStop(); + } + super.onStop(); + } + + @Override + public void onDestroyView() { + if (mapView != null) { + mapView.onDestroy(); + } + super.onDestroyView(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMarker.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMarker.java new file mode 100644 index 000000000..c65ede203 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMarker.java @@ -0,0 +1,31 @@ +package fr.free.nrw.commons.nearby; + +import com.mapbox.mapboxsdk.annotations.Marker; + +public class NearbyMarker extends Marker { + private Place place; + private NearbyBaseMarker nearbyBaseMarker; + + /** + * Creates a instance of {@link Marker} using the builder of Marker. + * + * @param baseMarkerOptions The builder used to construct the Marker. + */ + public NearbyMarker(NearbyBaseMarker baseMarkerOptions, Place place) { + super(baseMarkerOptions); + this.place = place; + this.nearbyBaseMarker = baseMarkerOptions; + } + + public NearbyBaseMarker getNearbyBaseMarker() { + return nearbyBaseMarker; + } + + public Place getPlace() { + return place; + } + + public void setNearbyBaseMarker(NearbyBaseMarker nearbyBaseMarker) { + this.nearbyBaseMarker = nearbyBaseMarker; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java index ecb0064d5..365d3e06f 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java @@ -2,17 +2,12 @@ package fr.free.nrw.commons.nearby; import android.net.Uri; import android.os.StrictMode; -import android.util.Log; - -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.location.LatLng; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; -import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -20,58 +15,31 @@ import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.utils.FileUtils; +import timber.log.Timber; public class NearbyPlaces { - private static final String TAG = NearbyPlaces.class.getName(); private static final int MIN_RESULTS = 40; - private static final double INITIAL_RADIUS = 1.0; - private static final double MAX_RADIUS = 300.0; + private static final double INITIAL_RADIUS = 1.0; // in kilometers + private static final double MAX_RADIUS = 300.0; // in kilometers private static final double RADIUS_MULTIPLIER = 1.618; - private static final String WIKIDATA_QUERY_URL = "https://query.wikidata.org/sparql?query=${QUERY}"; - private static final String WIKIDATA_QUERY_TEMPLATE = "SELECT\n" + - " (SAMPLE(?location) as ?location)\n" + - " ?item\n" + - " (SAMPLE(COALESCE(?item_label_preferred_language, ?item_label_any_language)) as ?label)\n" + - " (SAMPLE(?classId) as ?class)\n" + - " (SAMPLE(COALESCE(?class_label_preferred_language, ?class_label_any_language, \"?\")) as ?class_label)\n" + - " (SAMPLE(COALESCE(?icon0, ?icon1)) as ?icon)\n" + - " (SAMPLE(COALESCE(?emoji0, ?emoji1)) as ?emoji)\n" + - "WHERE {\n" + - " # Around given location...\n" + - " SERVICE wikibase:around {\n" + - " ?item wdt:P625 ?location.\n" + - " bd:serviceParam wikibase:center \"Point(${LONG} ${LAT})\"^^geo:wktLiteral. \n" + - " bd:serviceParam wikibase:radius \"${RADIUS}\" . # Radius in kilometers.\n" + - " }\n" + - " \n" + - " # ... and without an image.\n" + - " MINUS {?item wdt:P18 []}\n" + - " \n" + - " # Get the label in the preferred language of the user, or any other language if no label is available in that language.\n" + - " OPTIONAL {?item rdfs:label ?item_label_preferred_language. FILTER (lang(?item_label_preferred_language) = \"${LANG}\")}\n" + - " OPTIONAL {?item rdfs:label ?item_label_any_language}\n" + - " \n" + - " # Get the class label in the preferred language of the user, or any other language if no label is available in that language.\n" + - " OPTIONAL {\n" + - " ?item p:P31/ps:P31 ?classId.\n" + - " OPTIONAL {?classId rdfs:label ?class_label_preferred_language. FILTER (lang(?class_label_preferred_language) = \"${LANG}\")}\n" + - " OPTIONAL {?classId rdfs:label ?class_label_any_language}\n" + - "\n" + - " # Get icon\n" + - " OPTIONAL { ?classId wdt:P2910 ?icon0. }\n" + - " OPTIONAL { ?classId wdt:P279*/wdt:P2910 ?icon1. }\n" + - " # Get emoji\n" + - " OPTIONAL { ?classId wdt:P487 ?emoji0. }\n" + - " OPTIONAL { ?classId wdt:P279*/wdt:P487 ?emoji1. }\n" + - " }\n" + - "}\n" + - "GROUP BY ?item\n"; - private static NearbyPlaces singleton; + private static final Uri WIKIDATA_QUERY_URL = Uri.parse("https://query.wikidata.org/sparql"); + private static final Uri WIKIDATA_QUERY_UI_URL = Uri.parse("https://query.wikidata.org/"); + private final String wikidataQuery; private double radius = INITIAL_RADIUS; private List places; - private NearbyPlaces(){ + public NearbyPlaces() { + try { + String query = FileUtils.readFromResource("/assets/queries/nearby_query.rq"); + wikidataQuery = query; + Timber.v(wikidataQuery); + } catch (IOException e) { + throw new RuntimeException(e); + } } List getFromWikidataQuery(LatLng curLatLng, String lang) { @@ -81,7 +49,7 @@ public class NearbyPlaces { // increase the radius gradually to find a satisfactory number of nearby places while (radius < MAX_RADIUS) { places = getFromWikidataQuery(curLatLng, lang, radius); - Log.d(TAG, places.size() + " results at radius: " + radius); + Timber.d("%d results at radius: %f", places.size(), radius); if (places.size() >= MIN_RESULTS) { break; } else { @@ -89,33 +57,41 @@ public class NearbyPlaces { } } } catch (IOException e) { - Log.d(TAG, "" + e.toString()); + Timber.d(e.toString()); // errors tend to be caused by too many results (and time out) // try a small radius next time - Log.d(TAG, "back to initial radius: " + radius); + Timber.d("back to initial radius: %f", radius); radius = INITIAL_RADIUS; } return places; } - private List getFromWikidataQuery(LatLng cur, String lang, double radius) + private List getFromWikidataQuery(LatLng cur, + String lang, + double radius) throws IOException { List places = new ArrayList<>(); - String query = WIKIDATA_QUERY_TEMPLATE.replace("${RADIUS}", "" + radius) - .replace("${LAT}", "" + String.format(Locale.ROOT, "%.3f", cur.latitude)) - .replace("${LONG}", "" + String.format(Locale.ROOT, "%.3f", cur.longitude)) - .replace("${LANG}", "" + lang); - query = URLEncoder.encode(query, "utf-8").replace("+", "%20"); - String url = WIKIDATA_QUERY_URL.replace("${QUERY}", query); - Log.d(TAG, url); + + String query = wikidataQuery + .replace("${RAD}", String.format(Locale.ROOT, "%.2f", radius)) + .replace("${LAT}", String.format(Locale.ROOT, "%.4f", cur.latitude)) + .replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.longitude)) + .replace("${LANG}", lang); + + Timber.v("# Wikidata query: \n" + query); + + // format as a URL + Timber.d(WIKIDATA_QUERY_UI_URL.buildUpon().fragment(query).build().toString()); + String url = WIKIDATA_QUERY_URL.buildUpon() + .appendQueryParameter("query", query).build().toString(); URLConnection conn = new URL(url).openConnection(); conn.setRequestProperty("Accept", "text/tab-separated-values"); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String line; - Log.d(TAG, "Reading from query result..."); + Timber.d("Reading from query result..."); while ((line = in.readLine()) != null) { - Log.v(TAG, line); + Timber.v(line); line = line + "\n"; // to pad columns and make fields a fixed size if (!line.startsWith("\"Point")) { continue; @@ -125,6 +101,9 @@ public class NearbyPlaces { String point = fields[0]; String name = Utils.stripLocalizedString(fields[2]); String type = Utils.stripLocalizedString(fields[4]); + String wikipediaSitelink = Utils.stripLocalizedString(fields[7]); + String commonsSitelink = Utils.stripLocalizedString(fields[8]); + String wikiDataLink = Utils.stripLocalizedString(fields[1]); String icon = fields[5]; double latitude = 0; @@ -146,7 +125,12 @@ public class NearbyPlaces { type, // list type, // details Uri.parse(icon), - new LatLng(latitude, longitude) + new LatLng(latitude, longitude), + new Sitelinks.Builder() + .setWikipediaLink(wikipediaSitelink) + .setCommonsLink(commonsSitelink) + .setWikidataLink(wikiDataLink) + .build() )); } in.close(); @@ -170,7 +154,7 @@ public class NearbyPlaces { boolean firstLine = true; String line; - Log.d(TAG, "Reading from CSV file..."); + Timber.d("Reading from CSV file..."); while ((line = in.readLine()) != null) { @@ -203,28 +187,16 @@ public class NearbyPlaces { type, // list type, // details null, - new LatLng(latitude, longitude) + new LatLng(latitude, longitude), + new Sitelinks.Builder().build() )); } in.close(); } catch (IOException e) { - Log.d(TAG, e.toString()); + Timber.d(e.toString()); } } return places; } - - /** - * Get the singleton instance of this class. - * The instance is created upon the first invocation of this method, and then reused. - * - * @return The singleton instance - */ - public static synchronized NearbyPlaces getInstance() { - if (singleton == null) { - singleton = new NearbyPlaces(); - } - return singleton; - } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyViewHolder.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyViewHolder.java index 19e008068..c853dba78 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyViewHolder.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyViewHolder.java @@ -25,9 +25,12 @@ public class NearbyViewHolder implements ViewHolder { public void bindModel(Context context, Place place) { // Populate the data into the template view using the data object tvName.setText(place.name); - tvDesc.setText(place.description); + String description = place.description; + if ( description == null || description.isEmpty() || description.equals("?")) { + description = "No Description Found"; + } + tvDesc.setText(description); distance.setText(place.distance); - icon.setImageResource(ResourceUtils.getDescriptionIcon(place.description)); } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java index 222a992cc..dcc7fd74f 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java @@ -16,14 +16,17 @@ public class Place { public Bitmap image; public Bitmap secondaryImage; public String distance; + public Sitelinks siteLinks; + public Place(String name, String description, String longDescription, - Uri secondaryImageUrl, LatLng location) { + Uri secondaryImageUrl, LatLng location, Sitelinks siteLinks) { this.name = name; this.description = description; this.longDescription = longDescription; this.secondaryImageUrl = secondaryImageUrl; this.location = location; + this.siteLinks = siteLinks; } public void setDistance(String distance) { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java b/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java new file mode 100644 index 000000000..2e044221f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java @@ -0,0 +1,106 @@ +package fr.free.nrw.commons.nearby; + +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.Nullable; + +import fr.free.nrw.commons.Utils; + +public class Sitelinks implements Parcelable { + private final String wikipediaLink; + private final String commonsLink; + private final String wikidataLink; + + + protected Sitelinks(Parcel in) { + wikipediaLink = in.readString(); + commonsLink = in.readString(); + wikidataLink = in.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(wikipediaLink); + dest.writeString(commonsLink); + dest.writeString(wikidataLink); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public Sitelinks createFromParcel(Parcel in) { + return new Sitelinks(in); + } + + @Override + public Sitelinks[] newArray(int size) { + return new Sitelinks[size]; + } + }; + + @Nullable + public Uri getWikipediaLink() { + return sanitiseString(wikipediaLink); + } + + @Nullable + public Uri getCommonsLink() { + return sanitiseString(commonsLink); + } + + @Nullable + public Uri getWikidataLink() { + return sanitiseString(wikidataLink); + } + + @Nullable + private Uri sanitiseString(String stringUrl) { + stringUrl = stringUrl + .replaceAll("<", "") + .replaceAll(">", "") + .replaceAll("[\n\r]", ""); + if (!Utils.isNullOrWhiteSpace(stringUrl) && stringUrl != null) { + return Uri.parse(stringUrl); + } + return null; + } + + public Sitelinks(Sitelinks.Builder builder) { + this.wikidataLink = builder.wikidataLink; + this.wikipediaLink = builder.wikipediaLink; + this.commonsLink = builder.commonsLink; + } + + public static class Builder { + private String wikidataLink; + private String commonsLink; + private String wikipediaLink; + + public Builder() { + } + + public Sitelinks.Builder setWikipediaLink(String link) { + this.wikipediaLink = link; + return this; + } + + public Sitelinks.Builder setWikidataLink(String link) { + this.wikidataLink = link; + return this; + } + + public Sitelinks.Builder setCommonsLink(@Nullable String link) { + this.commonsLink = link; + return this; + } + + public Sitelinks build() { + return new Sitelinks(this); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/TaskListener.java b/app/src/main/java/fr/free/nrw/commons/nearby/TaskListener.java deleted file mode 100644 index 947575d31..000000000 --- a/app/src/main/java/fr/free/nrw/commons/nearby/TaskListener.java +++ /dev/null @@ -1,10 +0,0 @@ -package fr.free.nrw.commons.nearby; - -import java.util.List; - -// As per https://androidresearch.wordpress.com/2013/05/10/dealing-with-asynctask-and-screen-orientation/ -public interface TaskListener { - void onTaskStarted(); - - void onTaskFinished(List result); -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java b/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java index f4ddd54ae..722733393 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java @@ -5,6 +5,8 @@ public class Prefs { public static String TRACKING_ENABLED = "eventLogging"; public static final String DEFAULT_LICENSE = "defaultLicense"; + public static final String UPLOADS_SHOWING = "uploadsshowing"; + public static final String IS_CONTRIBUTION_COUNT_CHANGED = "ccontributionCountChanged"; public static class Licenses { public static final String CC_BY_SA_3 = "CC BY-SA 3.0"; 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 551d7752c..73c97528b 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,13 +1,19 @@ package fr.free.nrw.commons.settings; +import android.content.Context; +import android.content.Intent; import android.os.Bundle; -import android.preference.PreferenceActivity; import android.preference.PreferenceManager; import android.support.v7.app.AppCompatDelegate; +import android.view.MenuItem; +import butterknife.ButterKnife; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.theme.NavigationBaseActivity; + +public class SettingsActivity extends NavigationBaseActivity { + private SettingsFragment settingsFragment; -public class SettingsActivity extends PreferenceActivity { private AppCompatDelegate settingsDelegate; @Override @@ -19,11 +25,13 @@ public class SettingsActivity extends PreferenceActivity { setTheme(R.style.LightAppTheme); } - // Display the fragment as the main content. - getFragmentManager().beginTransaction() - .replace(android.R.id.content, new SettingsFragment()).commit(); + settingsFragment = (SettingsFragment) getFragmentManager().findFragmentById(R.id.settingsFragment); super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + + ButterKnife.bind(this); + initDrawer(); } // Get an action bar @@ -34,5 +42,25 @@ public class SettingsActivity extends PreferenceActivity { settingsDelegate = AppCompatDelegate.create(this, null); } settingsDelegate.onPostCreate(savedInstanceState); + + //Get an up button + //settingsDelegate.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + //Handle action-bar clicks + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + public static void startYourself(Context context) { + Intent settingsIntent = new Intent(context, SettingsActivity.class); + context.startActivity(settingsIntent); } } \ No newline at end of file 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 68d174013..bdac4a0f5 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 @@ -1,11 +1,17 @@ package fr.free.nrw.commons.settings; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.SharedPreferences; import android.os.Bundle; import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; @@ -19,23 +25,9 @@ public class SettingsFragment extends PreferenceFragment { // Update spinner to show selected value as summary ListPreference licensePreference = (ListPreference) findPreference(Prefs.DEFAULT_LICENSE); - // WARNING: ORDERING NEEDS TO MATCH FOR THE LICENSE NAMES AND DISPLAY VALUES - licensePreference.setEntries(new String[]{ - getString(R.string.license_name_cc0), - getString(R.string.license_name_cc_by_3_0), - getString(R.string.license_name_cc_by_4_0), - getString(R.string.license_name_cc_by_sa_3_0), - getString(R.string.license_name_cc_by_sa_4_0) - }); - licensePreference.setEntryValues(new String[]{ - Prefs.Licenses.CC0, - Prefs.Licenses.CC_BY_3, - Prefs.Licenses.CC_BY_4, - Prefs.Licenses.CC_BY_SA_3, - Prefs.Licenses.CC_BY_SA_4 - }); - licensePreference.setSummary(getString(Utils.licenseNameFor(licensePreference.getValue()))); + + // Keep summary updated when changing value licensePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { @@ -52,5 +44,45 @@ public class SettingsFragment extends PreferenceFragment { return true; } }); + + final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads"); + final SharedPreferences sharedPref = PreferenceManager + .getDefaultSharedPreferences(CommonsApplication.getInstance()); + int uploads = sharedPref.getInt(Prefs.UPLOADS_SHOWING, 100); + uploadLimit.setText(uploads + ""); + uploadLimit.setSummary(uploads + ""); + uploadLimit.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + int value = Integer.parseInt(newValue.toString()); + final SharedPreferences.Editor editor = sharedPref.edit(); + if (value > 500) { + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.maximum_limit) + .setMessage(R.string.maximum_limit_alert) + .setPositiveButton(android.R.string.yes, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + } + }) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + editor.putInt(Prefs.UPLOADS_SHOWING, 500); + editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED,true); + uploadLimit.setSummary(500 + ""); + uploadLimit.setText(500 + ""); + } else { + editor.putInt(Prefs.UPLOADS_SHOWING, Integer.parseInt(newValue.toString())); + editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED,true); + uploadLimit.setSummary(newValue.toString()); + } + editor.apply(); + return true; + } + + }); + + } -} \ No newline at end of file +} 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 f52ea84dd..d674542e0 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 @@ -6,16 +6,18 @@ import android.preference.PreferenceManager; import android.support.v7.app.AppCompatActivity; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; public class BaseActivity extends AppCompatActivity { boolean currentTheme; + @Override protected void onCreate(Bundle savedInstanceState) { - if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",true)) { + if(Utils.isDarkTheme(this)){ currentTheme = true; setTheme(R.style.DarkAppTheme); - }else { + } else { currentTheme = false; setTheme(R.style.LightAppTheme); // default } 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 new file mode 100644 index 000000000..e618cce5d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -0,0 +1,101 @@ +package fr.free.nrw.commons.theme; + +import android.os.Bundle; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RelativeLayout; + +import butterknife.BindView; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.hamburger.HamburgerMenuContainer; +import fr.free.nrw.commons.hamburger.NavigationBaseFragment; +import fr.free.nrw.commons.utils.FragmentUtils; + +import static android.support.v4.view.GravityCompat.START; + +public class NavigationBaseActivity extends BaseActivity implements HamburgerMenuContainer { + @BindView(R.id.toolbar) + Toolbar toolbar; + + @BindView(R.id.drawer_layout) + DrawerLayout drawerLayout; + + @BindView(R.id.drawer_pane) + RelativeLayout drawerPane; + + private ActionBarDrawerToggle toggle; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + public void initDrawer() { + initSubviews(); + NavigationBaseFragment baseFragment = new NavigationBaseFragment(); + baseFragment.setDrawerLayout(drawerLayout, drawerPane); + FragmentUtils.addAndCommitFragmentWithImmediateExecution(getSupportFragmentManager(), + R.id.drawer_fragment, + baseFragment); + } + + public void initSubviews() { + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + toggle = new ActionBarDrawerToggle(this, + drawerLayout, + toolbar, + R.string.navigation_drawer_open, + R.string.navigation_drawer_close); + drawerLayout.setDrawerListener(toggle); + toggle.setDrawerIndicatorEnabled(true); + toggle.syncState(); + setDrawerPaneWidth(); + } + + public void initBackButton() { + int backStackEntryCount = getSupportFragmentManager().getBackStackEntryCount(); + toggle.setDrawerIndicatorEnabled(backStackEntryCount == 0); + toggle.setToolbarNavigationClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onBackPressed(); + } + }); + } + + public void initBack() { + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + private void setDrawerPaneWidth() { + ViewGroup.LayoutParams params = drawerPane.getLayoutParams(); + // set width to lowerBound of 80% of the screen size + params.width = (getResources().getDisplayMetrics().widthPixels * 70) / 100; + drawerPane.setLayoutParams(params); + } + + @Override + public void setDrawerListener(ActionBarDrawerToggle listener) { + drawerLayout.setDrawerListener(listener); + } + + @Override + public void toggleDrawer() { + if (drawerLayout.isDrawerVisible(START)) { + drawerLayout.closeDrawer(START); + } else { + drawerLayout.openDrawer(START); + } + } + + @Override + public boolean isDrawerVisible() { + return drawerLayout.isDrawerVisible(START); + } +} 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 new file mode 100644 index 000000000..bc5fd71cd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java @@ -0,0 +1,21 @@ +package fr.free.nrw.commons.ui.widget; + +import android.content.Context; +import android.support.v7.widget.AppCompatTextView; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.util.AttributeSet; + +/** + * An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any + * links clickable. + */ +public class HtmlTextView extends AppCompatTextView { + + public HtmlTextView(Context context, AttributeSet attrs) { + super(context, attrs); + + setMovementMethod(LinkMovementMethod.getInstance()); + setText(Html.fromHtml(getText().toString())); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java b/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java new file mode 100644 index 000000000..6b2913d6d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java @@ -0,0 +1,46 @@ +package fr.free.nrw.commons.ui.widget; + +import android.app.Dialog; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.view.Gravity; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +public abstract class OverlayDialog extends DialogFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(STYLE_NO_FRAME, android.R.style.Theme_Holo_Light); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + setDialogLayoutToFullScreen(); + super.onViewCreated(view, savedInstanceState); + } + + private void setDialogLayoutToFullScreen() { + Window window = getDialog().getWindow(); + WindowManager.LayoutParams wlp = window.getAttributes(); + window.requestFeature(Window.FEATURE_NO_TITLE); + wlp.gravity = Gravity.BOTTOM; + wlp.width = WindowManager.LayoutParams.MATCH_PARENT; + wlp.height = WindowManager.LayoutParams.MATCH_PARENT; + window.setAttributes(wlp); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + Window window = dialog.getWindow(); + window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + return dialog; + } +} \ No newline at end of file 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 6d7854c26..27b9ed8d8 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 @@ -4,19 +4,18 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.AsyncTask; -import android.util.Log; +import android.support.v7.app.AlertDialog; +import fr.free.nrw.commons.MWApi; import org.mediawiki.api.ApiResult; -import org.mediawiki.api.MWApi; import java.io.IOException; import java.util.ArrayList; -import android.support.v7.app.AlertDialog; - import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.contributions.ContributionsActivity; +import timber.log.Timber; /** * Sends asynchronous queries to the Commons MediaWiki API to check that file doesn't already exist @@ -24,8 +23,6 @@ import fr.free.nrw.commons.contributions.ContributionsActivity; */ public class ExistingFileAsync extends AsyncTask { - private static final String TAG = ExistingFileAsync.class.getName(); - private String fileSHA1; private Context context; @@ -41,7 +38,7 @@ public class ExistingFileAsync extends AsyncTask { @Override protected Boolean doInBackground(Void... voids) { - MWApi api = CommonsApplication.createMWApi(); + MWApi api = CommonsApplication.getInstance().getMWApi(); ApiResult result; // https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba @@ -51,14 +48,14 @@ public class ExistingFileAsync extends AsyncTask { .param("list", "allimages") .param("aisha1", fileSHA1) .get(); - Log.d(TAG, "Searching Commons API for existing file: " + result.toString()); + Timber.d("Searching Commons API for existing file: %s", result); } catch (IOException e) { - Log.e(TAG, "IO Exception: ", e); + Timber.e(e, "IO Exception: "); return false; } ArrayList resultNodes = result.getNodes("/api/query/allimages/img"); - Log.d(TAG, "Result nodes: " + resultNodes); + Timber.d("Result nodes: %s", resultNodes); boolean fileExists; if (!resultNodes.isEmpty()) { @@ -67,7 +64,7 @@ public class ExistingFileAsync extends AsyncTask { fileExists = false; } - Log.d(TAG, "File already exists in Commons:" + fileExists); + Timber.d("File already exists in Commons: %s", fileExists); return fileExists; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index 8edd68789..0c587d566 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.upload; +import android.annotation.SuppressLint; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; @@ -20,6 +21,8 @@ public class FileUtils { * @param uri The Uri to query. * @author paulburke */ + // Can be safely suppressed, checks for isKitKat before running isDocumentUri + @SuppressLint("NewApi") public static String getPath(final Context context, final Uri uri) { final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 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 ea26cfa2c..6d1a2ac95 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 @@ -10,10 +10,11 @@ import android.media.ExifInterface; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.Nullable; -import android.util.Log; import java.io.IOException; +import timber.log.Timber; + /** * Extracts geolocation to be passed to API for category suggestions. If a picture with geolocation * is uploaded, extract latitude and longitude from EXIF data of image. If a picture without @@ -21,8 +22,6 @@ import java.io.IOException; */ public class GPSExtractor { - private static final String TAG = GPSExtractor.class.getName(); - private String filePath; private double decLatitude, decLongitude; private Double currentLatitude = null; @@ -31,8 +30,7 @@ public class GPSExtractor { public boolean imageCoordsExists; private MyLocationListener myLocationListener; private LocationManager locationManager; - private String provider; - private Criteria criteria; + public GPSExtractor(String filePath, Context context){ this.filePath = filePath; @@ -46,7 +44,7 @@ public class GPSExtractor { private boolean gpsPreferenceEnabled() { SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context); boolean gpsPref = sharedPref.getBoolean("allowGps", false); - Log.d(TAG, "Gps pref set to: " + gpsPref); + Timber.d("Gps pref set to: %b", gpsPref); return gpsPref; } @@ -55,8 +53,8 @@ public class GPSExtractor { */ protected void registerLocationManager() { locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - criteria = new Criteria(); - provider = locationManager.getBestProvider(criteria, true); + Criteria criteria = new Criteria(); + String provider = locationManager.getBestProvider(criteria, true); myLocationListener = new MyLocationListener(); try { @@ -66,9 +64,9 @@ public class GPSExtractor { myLocationListener.onLocationChanged(location); } } catch (IllegalArgumentException e) { - Log.e(TAG, "Illegal argument exception", e); + Timber.e(e, "Illegal argument exception"); } catch (SecurityException e) { - Log.e(TAG, "Security exception", e); + Timber.e(e, "Security exception"); } } @@ -76,7 +74,7 @@ public class GPSExtractor { try { locationManager.removeUpdates(myLocationListener); } catch (SecurityException e) { - Log.e(TAG, "Security exception", e); + Timber.e(e, "Security exception"); } } @@ -98,10 +96,10 @@ public class GPSExtractor { try { exif = new ExifInterface(filePath); } catch (IOException e) { - Log.w(TAG, e); + Timber.w(e); return null; } catch (IllegalArgumentException e) { - Log.w(TAG, e); + Timber.w(e); return null; } @@ -110,14 +108,15 @@ public class GPSExtractor { registerLocationManager(); imageCoordsExists = false; - Log.d(TAG, "EXIF data has no location info"); + Timber.d("EXIF data has no location info"); //Check what user's preference is for automatic location detection boolean gpsPrefEnabled = gpsPreferenceEnabled(); //Check that currentLatitude and currentLongitude have been explicitly set by MyLocationListener and do not default to (0.0,0.0) if (gpsPrefEnabled && currentLatitude != null && currentLongitude != null) { - Log.d(TAG, "Current location values: Lat = " + currentLatitude + " Long = " + currentLongitude); + Timber.d("Current location values: Lat = %f Long = %f", + currentLatitude, currentLongitude); return String.valueOf(currentLatitude) + "|" + String.valueOf(currentLongitude); } else { // No coords found @@ -128,7 +127,7 @@ public class GPSExtractor { } else { //If image has EXIF data, extract image coords imageCoordsExists = true; - Log.d(TAG, "EXIF data has location info"); + Timber.d("EXIF data has location info"); latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE); latitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF); @@ -136,8 +135,8 @@ public class GPSExtractor { longitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF); if (latitude!=null && latitude_ref!=null && longitude!=null && longitude_ref!=null) { - Log.d(TAG, "Latitude: " + latitude + " " + latitude_ref); - Log.d(TAG, "Longitude: " + longitude + " " + longitude_ref); + Timber.d("Latitude: %s %s", latitude, latitude_ref); + Timber.d("Longitude: %s %s", longitude, longitude_ref); decimalCoords = getDecimalCoords(latitude, latitude_ref, longitude, longitude_ref); return decimalCoords; @@ -160,17 +159,17 @@ public class GPSExtractor { @Override public void onStatusChanged(String provider, int status, Bundle extras) { - Log.d(TAG, provider + "'s status changed to " + status); + Timber.d("%s's status changed to %d", provider, status); } @Override public void onProviderEnabled(String provider) { - Log.d(TAG, "Provider " + provider + " enabled"); + Timber.d("Provider %s enabled", provider); } @Override public void onProviderDisabled(String provider) { - Log.d(TAG, "Provider " + provider + " disabled"); + Timber.d("Provider %s disabled", provider); } } @@ -201,7 +200,7 @@ public class GPSExtractor { } String decimalCoords = String.valueOf(decLatitude) + "|" + String.valueOf(decLongitude); - Log.d(TAG, "Latitude and Longitude are " + decimalCoords); + Timber.d("Latitude and Longitude are %s", decimalCoords); return decimalCoords; } 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 a39ca0d6c..12b38a6ab 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 @@ -14,13 +14,14 @@ import android.os.Bundle; import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentManager; import android.support.v4.content.ContextCompat; -import android.util.Log; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.Toast; +import butterknife.ButterKnife; + import java.util.ArrayList; import fr.free.nrw.commons.CommonsApplication; @@ -28,7 +29,6 @@ import fr.free.nrw.commons.EventLog; 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.WikiAccountAuthenticator; import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.media.MediaDetailPagerFragment; @@ -36,6 +36,7 @@ import fr.free.nrw.commons.modifications.CategoryModifier; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.TemplateRemoveModifier; +import timber.log.Timber; public class MultipleShareActivity extends AuthenticatedActivity @@ -53,10 +54,6 @@ public class MultipleShareActivity private UploadController uploadController; - public MultipleShareActivity() { - super(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE); - } - @Override public Media getMediaAtPosition(int i) { return photosList.get(i); @@ -116,7 +113,7 @@ public class MultipleShareActivity private void multipleUploadBegins() { - Log.d("MultipleShareActivity", "Multiple upload begins"); + Timber.d("Multiple upload begins"); final ProgressDialog dialog = new ProgressDialog(MultipleShareActivity.this); dialog.setIndeterminate(false); @@ -135,7 +132,11 @@ public class MultipleShareActivity dialog.setProgress(uploadCount); if(uploadCount == photosList.size()) { dialog.dismiss(); - Toast startingToast = Toast.makeText(getApplicationContext(), R.string.uploading_started, Toast.LENGTH_LONG); + Toast startingToast = Toast.makeText( + CommonsApplication.getInstance(), + R.string.uploading_started, + Toast.LENGTH_LONG + ); startingToast.show(); } } @@ -205,7 +206,9 @@ public class MultipleShareActivity uploadController = new UploadController(this); setContentView(R.layout.activity_multiple_uploads); - app = (CommonsApplication)this.getApplicationContext(); + app = CommonsApplication.getInstance(); + ButterKnife.bind(this); + initDrawer(); if(savedInstanceState != null) { photosList = savedInstanceState.getParcelableArrayList("uploadsList"); @@ -242,7 +245,7 @@ public class MultipleShareActivity @Override protected void onAuthCookieAcquired(String authCookie) { - app.getApi().setAuthCookie(authCookie); + app.getMWApi().setAuthCookie(authCookie); Intent intent = getIntent(); if(intent.getAction().equals(Intent.ACTION_SEND_MULTIPLE)) { 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 01526d202..52da98f54 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 @@ -21,19 +21,15 @@ import android.widget.BaseAdapter; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.GridView; -import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; -import com.nostra13.universalimageloader.core.ImageLoader; +import com.facebook.drawee.view.SimpleDraweeView; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.media.MediaDetailPagerFragment; - public class MultipleUploadListFragment extends Fragment { public interface OnMultipleUploadInitiatedHandler { @@ -48,17 +44,13 @@ public class MultipleUploadListFragment extends Fragment { private MediaDetailPagerFragment.MediaDetailProvider detailProvider; private OnMultipleUploadInitiatedHandler multipleUploadInitiatedHandler; - private DisplayImageOptions uploadDisplayOptions; - private boolean imageOnlyMode; private static class UploadHolderView { - Uri imageUri; - - ImageView image; - TextView title; - - RelativeLayout overlay; + private Uri imageUri; + private SimpleDraweeView image; + private TextView title; + private RelativeLayout overlay; } private class PhotoDisplayAdapter extends BaseAdapter { @@ -85,7 +77,7 @@ public class MultipleUploadListFragment extends Fragment { if(view == null) { view = getLayoutInflater(null).inflate(R.layout.layout_upload_item, null); holder = new UploadHolderView(); - holder.image = (ImageView) view.findViewById(R.id.uploadImage); + holder.image = (SimpleDraweeView) view.findViewById(R.id.uploadImage); holder.title = (TextView) view.findViewById(R.id.uploadTitle); holder.overlay = (RelativeLayout) view.findViewById(R.id.uploadOverlay); @@ -100,7 +92,7 @@ public class MultipleUploadListFragment extends Fragment { Contribution up = (Contribution)this.getItem(i); if(holder.imageUri == null || !holder.imageUri.equals(up.getLocalUri())) { - ImageLoader.getInstance().displayImage(up.getLocalUri().toString(), holder.image, uploadDisplayOptions); + holder.image.setImageURI(up.getLocalUri().toString()); holder.imageUri = up.getLocalUri(); } @@ -221,7 +213,6 @@ public class MultipleUploadListFragment extends Fragment { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - uploadDisplayOptions = Utils.getGenericDisplayOptions().build(); detailProvider = (MediaDetailPagerFragment.MediaDetailProvider)getActivity(); multipleUploadInitiatedHandler = (OnMultipleUploadInitiatedHandler) getActivity(); 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 f47e76dbd..1f538d201 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 @@ -2,7 +2,6 @@ package fr.free.nrw.commons.upload; import android.content.Context; import android.net.Uri; -import android.util.Log; import com.android.volley.Cache; import com.android.volley.NetworkResponse; @@ -22,6 +21,8 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import timber.log.Timber; + /** * Uses the Volley library to implement asynchronous calls to the Commons MediaWiki API to match * GPS coordinates with nearby Commons categories. Parses the results using GSON to obtain a list @@ -32,13 +33,11 @@ public class MwVolleyApi { private static RequestQueue REQUEST_QUEUE; private static final Gson GSON = new GsonBuilder().create(); private Context context; - private String coordsLog; protected static Set categorySet; private static List categoryList; private static final String MWURL = "https://commons.wikimedia.org/"; - private static final String TAG = MwVolleyApi.class.getName(); public MwVolleyApi(Context context) { this.context = context; @@ -52,13 +51,12 @@ public class MwVolleyApi { public static void setGpsCat(List cachedList) { categoryList = new ArrayList<>(); categoryList.addAll(cachedList); - Log.d(TAG, "Setting GPS cats from cache: " + categoryList.toString()); + Timber.d("Setting GPS cats from cache: %s", categoryList); } public void request(String coords) { - coordsLog = coords; String apiUrl = buildUrl(coords); - Log.d(TAG, "URL: " + apiUrl); + Timber.d("URL: %s", apiUrl); JsonRequest request = new QueryRequest(apiUrl, new LogResponseListener(), new LogResponseErrorListener()); @@ -101,7 +99,7 @@ public class MwVolleyApi { private static RequestQueue getQueue(Context context) { if (REQUEST_QUEUE == null) { - REQUEST_QUEUE = Volley.newRequestQueue(context.getApplicationContext()); + REQUEST_QUEUE = Volley.newRequestQueue(context); } return REQUEST_QUEUE; } @@ -110,7 +108,7 @@ public class MwVolleyApi { @Override public void onResponse(T response) { - Log.d(TAG, response.toString()); + Timber.d(response.toString()); } } @@ -118,12 +116,11 @@ public class MwVolleyApi { @Override public void onErrorResponse(VolleyError error) { - Log.e(TAG, error.toString()); + Timber.e(error.toString()); } } private static class QueryRequest extends JsonRequest { - private static final String TAG = QueryRequest.class.getName(); public QueryRequest(String url, Response.Listener listener, @@ -169,11 +166,11 @@ public class MwVolleyApi { private String printSet() { if (categorySet == null || categorySet.isEmpty()) { GpsCatExists.setGpsCatExists(false); - Log.d(TAG, "gpsCatExists=" + GpsCatExists.getGpsCatExists()); + Timber.d("gpsCatExists=%b", GpsCatExists.getGpsCatExists()); return "No collection of categories"; } else { GpsCatExists.setGpsCatExists(true); - Log.d(TAG, "gpsCatExists=" + GpsCatExists.getGpsCatExists()); + Timber.d("gpsCatExists=%b", GpsCatExists.getGpsCatExists()); return "CATEGORIES FOUND" + categorySet.toString(); } 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 0805ea462..5aca73a04 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 @@ -10,34 +10,32 @@ import android.os.Bundle; import android.os.Environment; import android.support.design.widget.Snackbar; import android.support.v4.app.ActivityCompat; -import android.support.v4.app.NavUtils; import android.support.v4.content.ContextCompat; -import android.util.Log; import android.view.MenuItem; import android.view.View; -import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; -import com.nostra13.universalimageloader.core.ImageLoader; +import com.facebook.drawee.view.SimpleDraweeView; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import butterknife.ButterKnife; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.EventLog; 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.WikiAccountAuthenticator; import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.modifications.CategoryModifier; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.TemplateRemoveModifier; +import timber.log.Timber; /** * Activity for the title/desc screen after image is selected. Also starts processing image @@ -48,9 +46,6 @@ public class ShareActivity implements SingleUploadFragment.OnUploadActionInitiated, CategorizationFragment.OnCategoriesSaveHandler { - private static final String TAG = ShareActivity.class.getName(); - - private SingleUploadFragment shareView; private CategorizationFragment categorizationFragment; private CommonsApplication app; @@ -61,14 +56,14 @@ public class ShareActivity private Uri mediaUri; private Contribution contribution; - private ImageView backgroundImageView; + private SimpleDraweeView backgroundImageView; + private UploadController uploadController; private CommonsApplication cacheObj; private boolean cacheFound; private GPSExtractor imageObj; - private String filePath; private String decimalCoords; private boolean useNewPermissions = false; @@ -79,10 +74,6 @@ public class ShareActivity private String description; private Snackbar snackbar; - public ShareActivity() { - super(WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE); - } - /** * Called when user taps the submit button */ @@ -112,13 +103,17 @@ public class ShareActivity getFileMetadata(false); } - Toast startingToast = Toast.makeText(getApplicationContext(), R.string.uploading_started, Toast.LENGTH_LONG); + Toast startingToast = Toast.makeText( + CommonsApplication.getInstance(), + R.string.uploading_started, + Toast.LENGTH_LONG + ); startingToast.show(); - if (cacheFound == false) { + if (!cacheFound) { //Has to be called after apiCall.request() - app.cacheData.cacheCategory(); - Log.d(TAG, "Cache the categories found"); + app.getCacheData().cacheCategory(); + Timber.d("Cache the categories found"); } uploadController.startUpload(title, mediaUri, description, mimeType, source, decimalCoords, new UploadController.ContributionUploadProgress() { @@ -195,9 +190,9 @@ public class ShareActivity @Override protected void onAuthCookieAcquired(String authCookie) { - app.getApi().setAuthCookie(authCookie); + app.getMWApi().setAuthCookie(authCookie); - shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView"); + SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView"); categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization"); if(shareView == null && categorizationFragment == null) { shareView = new SingleUploadFragment(); @@ -221,9 +216,10 @@ public class ShareActivity super.onCreate(savedInstanceState); uploadController = new UploadController(this); setContentView(R.layout.activity_share); - - app = (CommonsApplication)this.getApplicationContext(); - backgroundImageView = (ImageView)findViewById(R.id.backgroundImage); + ButterKnife.bind(this); + initBack(); + app = CommonsApplication.getInstance(); + backgroundImageView = (SimpleDraweeView)findViewById(R.id.backgroundImage); //Receive intent from ContributionController.java when user selects picture to upload Intent intent = getIntent(); @@ -240,20 +236,20 @@ public class ShareActivity if (mediaUri != null) { mediaUriString = mediaUri.toString(); - ImageLoader.getInstance().displayImage(mediaUriString, backgroundImageView); + backgroundImageView.setImageURI(mediaUriString); //Test SHA1 of image to see if it matches SHA1 of a file on Commons try { InputStream inputStream = getContentResolver().openInputStream(mediaUri); - Log.d(TAG, "Input stream created from " + mediaUriString); + Timber.d("Input stream created from %s", mediaUriString); String fileSHA1 = Utils.getSHA1(inputStream); - Log.d(TAG, "File SHA1 is: " + fileSHA1); + Timber.d("File SHA1 is: %s", fileSHA1); ExistingFileAsync fileAsyncTask = new ExistingFileAsync(fileSHA1, this); fileAsyncTask.execute(); } catch (IOException e) { - Log.d(TAG, "IO Exception: ", e); + Timber.d(e, "IO Exception: "); } } @@ -263,8 +259,8 @@ public class ShareActivity requestAuthToken(); - Log.d(TAG, "Uri: " + mediaUriString); - Log.d(TAG, "Ext storage dir: " + Environment.getExternalStorageDirectory()); + Timber.d("Uri: %s", mediaUriString); + Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { useNewPermissions = true; @@ -377,9 +373,9 @@ public class ShareActivity * @param gpsEnabled */ public void getFileMetadata(boolean gpsEnabled) { - filePath = FileUtils.getPath(this, mediaUri); - Log.d(TAG, "Filepath: " + filePath); - Log.d(TAG, "Calling GPSExtractor"); + String filePath = FileUtils.getPath(this, mediaUri); + Timber.d("Filepath: %s", filePath); + Timber.d("Calling GPSExtractor"); if(imageObj == null) { imageObj = new GPSExtractor(filePath, this); } @@ -397,28 +393,28 @@ public class ShareActivity */ public void useImageCoords() { if(decimalCoords != null) { - Log.d(TAG, "Decimal coords of image: " + decimalCoords); + Timber.d("Decimal coords of image: %s", decimalCoords); // Only set cache for this point if image has coords if (imageObj.imageCoordsExists) { double decLongitude = imageObj.getDecLongitude(); double decLatitude = imageObj.getDecLatitude(); - app.cacheData.setQtPoint(decLongitude, decLatitude); + app.getCacheData().setQtPoint(decLongitude, decLatitude); } MwVolleyApi apiCall = new MwVolleyApi(this); - List displayCatList = app.cacheData.findCategory(); + List displayCatList = app.getCacheData().findCategory(); boolean catListEmpty = displayCatList.isEmpty(); // If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories if (catListEmpty) { cacheFound = false; apiCall.request(decimalCoords); - Log.d(TAG, "displayCatList size 0, calling MWAPI" + displayCatList.toString()); + Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList); } else { cacheFound = true; - Log.d(TAG, "Cache found, setting categoryList in MwVolleyApi to " + displayCatList.toString()); + Timber.d("Cache found, setting categoryList in MwVolleyApi to %s", displayCatList); MwVolleyApi.setGpsCat(displayCatList); } } @@ -429,10 +425,10 @@ public class ShareActivity super.onPause(); try { imageObj.unregisterLocationManager(); - Log.d(TAG, "Unregistered locationManager"); + Timber.d("Unregistered locationManager"); } catch (NullPointerException e) { - Log.d(TAG, "locationManager does not exist, not unregistered"); + Timber.d("locationManager does not exist, not unregistered"); } } @@ -446,7 +442,11 @@ public class ShareActivity public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); + if(categorizationFragment!=null && categorizationFragment.isVisible()) { + categorizationFragment.backButtonDialog(); + } else { + onBackPressed(); + } return true; } return super.onOptionsItemSelected(item); 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 687bcabb0..2cf23318c 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 @@ -11,7 +11,6 @@ import android.preference.PreferenceManager; import android.support.v4.app.Fragment; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -37,6 +36,7 @@ import butterknife.OnTouch; import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; +import timber.log.Timber; public class SingleUploadFragment extends Fragment { private SharedPreferences prefs; @@ -54,8 +54,6 @@ public class SingleUploadFragment extends Fragment { private OnUploadActionInitiated uploadActionInitiatedHandler; - private static final String TAG = SingleUploadFragment.class.getName(); - @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.activity_share, menu); @@ -103,7 +101,7 @@ public class SingleUploadFragment extends Fragment { prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); - Log.d("Single Upload fragment", license); + Timber.d(license); ArrayAdapter adapter; if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme",true)) { @@ -117,7 +115,14 @@ public class SingleUploadFragment extends Fragment { licenseSpinner.setAdapter(adapter); int position = licenseItems.indexOf(getString(Utils.licenseNameFor(license))); - Log.d("Single Upload fragment", "Position:"+position+" "+getString(Utils.licenseNameFor(license))); + + // Check position is valid + if (position < 0) { + Timber.d("Invalid position: %d. Using default license", position); + position = 4; + } + + Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(license))); licenseSpinner.setSelection(position); TextWatcher uploadEnabler = new TextWatcher() { @@ -190,7 +195,7 @@ public class SingleUploadFragment extends Fragment { SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity()); String title = titleDesc.getString("Title", ""); String desc = titleDesc.getString("Desc", ""); - Log.d(TAG, "Title: " + title + ", Desc: " + desc); + Timber.d("Title: %s, Desc: %s", title, desc); titleEdit.setText(title); descEdit.setText(desc); 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 f2c147249..4266384c1 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 @@ -13,7 +13,6 @@ import android.os.IBinder; import android.preference.PreferenceManager; import android.provider.MediaStore; import android.text.TextUtils; -import android.util.Log; import java.io.IOException; import java.util.Date; @@ -23,6 +22,7 @@ import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.contributions.Contribution; +import timber.log.Timber; public class UploadController { private UploadService uploadService; @@ -36,7 +36,7 @@ public class UploadController { public UploadController(Activity activity) { this.activity = activity; - app = (CommonsApplication)activity.getApplicationContext(); + app = CommonsApplication.getInstance(); } private boolean isUploadServiceConnected; @@ -55,7 +55,7 @@ public class UploadController { }; public void prepareService() { - Intent uploadServiceIntent = new Intent(activity.getApplicationContext(), UploadService.class); + Intent uploadServiceIntent = new Intent(activity, UploadService.class); uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); activity.startService(uploadServiceIntent); activity.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE); @@ -115,11 +115,11 @@ public class UploadController { contribution.setDataLength(length); } } catch(IOException e) { - Log.e("UploadController", "IO Exception: ", e); + Timber.e(e, "IO Exception: "); } catch(NullPointerException e) { - Log.e("UploadController", "Null Pointer Exception: ", e); + Timber.e(e, "Null Pointer Exception: "); } catch(SecurityException e) { - Log.e("UploadController", "Security Exception: ", e); + Timber.e(e, "Security Exception: "); } String mimeType = (String)contribution.getTag("mimeType"); @@ -132,7 +132,7 @@ public class UploadController { if(mimeType != null) { contribution.setTag("mimeType", mimeType); imagePrefix = mimeType.startsWith("image/"); - Log.d("UploadController", "MimeType is: " + mimeType); + Timber.d("MimeType is: %s", mimeType); } if(imagePrefix && contribution.getDateCreated() == null) { 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 567eb809c..853952fae 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 @@ -10,12 +10,11 @@ import android.content.Intent; import android.graphics.BitmapFactory; import android.os.Bundle; import android.support.v4.app.NotificationCompat; -import android.util.Log; import android.webkit.MimeTypeMap; import android.widget.Toast; +import fr.free.nrw.commons.*; import org.mediawiki.api.ApiResult; -import org.mediawiki.api.MWApi; import java.io.FileNotFoundException; import java.io.IOException; @@ -26,16 +25,12 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.EventLog; -import fr.free.nrw.commons.HandlerService; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import in.yuvi.http.fluent.ProgressListener; +import timber.log.Timber; public class UploadService extends HandlerService { @@ -87,7 +82,7 @@ public class UploadService extends HandlerService { @Override public void onProgress(long transferred, long total) { - Log.d("Commons", String.format("Uploaded %d of %d", transferred, total)); + Timber.d("Uploaded %d of %d", transferred, total); if(!notificationTitleChanged) { curProgressNotification.setContentTitle(notificationProgressTitle); notificationTitleChanged = true; @@ -112,7 +107,7 @@ public class UploadService extends HandlerService { public void onDestroy() { super.onDestroy(); contributionsProviderClient.release(); - Log.d("Commons", "UploadService.onDestroy; " + unfinishedUploads + " are yet to be uploaded"); + Timber.d("UploadService.onDestroy; %s are yet to be uploaded", unfinishedUploads); } @Override @@ -120,7 +115,7 @@ public class UploadService extends HandlerService { super.onCreate(); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - app = (CommonsApplication) this.getApplicationContext(); + app = CommonsApplication.getInstance(); contributionsProviderClient = this.getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY); } @@ -149,7 +144,7 @@ public class UploadService extends HandlerService { toUpload++; if (curProgressNotification != null && toUpload != 1) { curProgressNotification.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload)); - Log.d("Commons", String.format("%d uploads left", toUpload)); + Timber.d("%d uploads left", toUpload); this.startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build()); } @@ -173,15 +168,15 @@ public class UploadService extends HandlerService { Contribution.Table.COLUMN_STATE + " = ? OR " + Contribution.Table.COLUMN_STATE + " = ?", new String[]{ String.valueOf(Contribution.STATE_QUEUED), String.valueOf(Contribution.STATE_IN_PROGRESS) } ); - Log.d("Commons", "Set " + updated + " uploads to failed"); - Log.d("Commons", "Flags is" + flags + " id is" + startId); + Timber.d("Set %d uploads to failed", updated); + Timber.d("Flags is %d id is %d", flags, startId); freshStart = false; } return START_REDELIVER_INTENT; } private void uploadContribution(Contribution contribution) { - MWApi api = app.getApi(); + MWApi api = app.getMWApi(); ApiResult result; InputStream file = null; @@ -192,12 +187,12 @@ public class UploadService extends HandlerService { //FIXME: Google Photos bug file = this.getContentResolver().openInputStream(contribution.getLocalUri()); } catch(FileNotFoundException e) { - Log.d("Exception", "File not found"); + Timber.d("File not found"); Toast fileNotFound = Toast.makeText(this, R.string.upload_failed, Toast.LENGTH_LONG); fileNotFound.show(); } - Log.d("Commons", "Before execution!"); + Timber.d("Before execution!"); curProgressNotification = new NotificationCompat.Builder(this).setAutoCancel(true) .setSmallIcon(R.drawable.ic_launcher) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher)) @@ -206,7 +201,7 @@ public class UploadService extends HandlerService { .setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload)) .setOngoing(true) .setProgress(100, 0, true) - .setContentIntent(PendingIntent.getActivity(getApplicationContext(), 0, new Intent(this, ContributionsActivity.class), 0)) + .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0)) .setTicker(getString(R.string.upload_progress_notification_title_in_progress, contribution.getDisplayTitle())); this.startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build()); @@ -218,16 +213,16 @@ public class UploadService extends HandlerService { MimeTypeMap.getSingleton().getExtensionFromMimeType((String)contribution.getTag("mimeType"))); synchronized (unfinishedUploads) { - Log.d("Commons", "making sure of uniqueness of name: " + filename); + Timber.d("making sure of uniqueness of name: %s", filename); filename = findUniqueFilename(filename); unfinishedUploads.add(filename); } if(!api.validateLogin()) { // Need to revalidate! if(app.revalidateAuthToken()) { - Log.d("Commons", "Successfully revalidated token!"); + Timber.d("Successfully revalidated token!"); } else { - Log.d("Commons", "Unable to revalidate :("); + Timber.d("Unable to revalidate :("); // TODO: Put up a new notification, ask them to re-login stopForeground(true); Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG); @@ -242,7 +237,7 @@ public class UploadService extends HandlerService { ); result = api.upload(filename, file, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), notificationUpdater); - Log.d("Commons", "Response is" + Utils.getStringFromDOM(result.getDocument())); + Timber.d("Response is %s", Utils.getStringFromDOM(result.getDocument())); curProgressNotification = null; @@ -277,7 +272,7 @@ public class UploadService extends HandlerService { .log(); } } catch(IOException e) { - Log.d("Commons", "I have a network fuckup"); + Timber.d("I have a network fuckup"); showFailedNotification(contribution); return; } finally { @@ -287,7 +282,7 @@ public class UploadService extends HandlerService { toUpload--; if(toUpload == 0) { // Sync modifications right after all uplaods are processed - ContentResolver.requestSync(((CommonsApplication) getApplicationContext()).getCurrentAccount(), ModificationsContentProvider.AUTHORITY, new Bundle()); + ContentResolver.requestSync((CommonsApplication.getInstance()).getCurrentAccount(), ModificationsContentProvider.AUTHORITY, new Bundle()); stopForeground(true); } } @@ -309,7 +304,7 @@ public class UploadService extends HandlerService { } private String findUniqueFilename(String fileName) throws IOException { - MWApi api = app.getApi(); + MWApi api = app.getMWApi(); String sequenceFileName; for ( int sequenceNumber = 1; true; sequenceNumber++ ) { if (sequenceNumber == 1) { diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java new file mode 100644 index 000000000..3992324b5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java @@ -0,0 +1,80 @@ +package fr.free.nrw.commons.utils; + +import android.app.Activity; +import android.app.Dialog; +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; + +import timber.log.Timber; + +public class DialogUtil { + + public static void dismissSafely(@Nullable Activity activity, @Nullable DialogFragment dialog) { + boolean isActivityDestroyed = false; + + if (activity == null || dialog == null) { + Timber.d("dismiss called with null activity / dialog. Ignoring."); + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + isActivityDestroyed = activity.isDestroyed(); + } + if (activity.isFinishing() || isActivityDestroyed) { + return; + } + try { + dialog.dismiss(); + + } catch (IllegalStateException e) { + Timber.e(e, "Could not dismiss dialog."); + } + } + + public static void showSafely(Activity activity, Dialog dialog) { + if (activity == null || dialog == null) { + Timber.d("Show called with null activity / dialog. Ignoring."); + return; + } + + boolean isActivityDestroyed = false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + isActivityDestroyed = activity.isDestroyed(); + } + if (activity.isFinishing() || isActivityDestroyed) { + Timber.e("Activity is not running. Could not show dialog. "); + return; + } + try { + dialog.show(); + } catch (IllegalStateException e) { + Timber.e(e, "Could not show dialog."); + } + } + + public static void showSafely(FragmentActivity activity, DialogFragment dialog) { + boolean isActivityDestroyed = false; + + if (activity == null || dialog == null) { + Timber.d("show called with null activity / dialog. Ignoring."); + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + isActivityDestroyed = activity.isDestroyed(); + } + if (activity.isFinishing() || isActivityDestroyed) { + return; + } + + try { + if (dialog.getDialog() == null || !dialog.getDialog().isShowing()) { + dialog.show(activity.getSupportFragmentManager(), dialog.getClass().getSimpleName()); + } + } catch (IllegalStateException e) { + Timber.e(e, "Could not show dialog."); + } + } +} 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 new file mode 100644 index 000000000..0596327e3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java @@ -0,0 +1,57 @@ +package fr.free.nrw.commons.utils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; + +import fr.free.nrw.commons.CommonsApplication; + +public class FileUtils { + /** + * Read and return the content of a resource file as string. + * + * @param fileName asset file's path (e.g. "/assets/queries/nearby_query.rq") + * @return the content of the file + */ + public static String readFromResource(String fileName) throws IOException { + StringBuffer buffer = new StringBuffer(); + BufferedReader reader = null; + try { + reader = new BufferedReader( + new InputStreamReader( + CommonsApplication.class.getResourceAsStream(fileName), "UTF-8")); + String line; + while ((line = reader.readLine()) != null) { + buffer.append(line + "\n"); + } + } finally { + if (reader != null) { + reader.close(); + } + } + return buffer.toString(); + } + + /** + * Deletes files. + * @param file context + */ + public static boolean deleteFile(File file) { + boolean deletedAll = true; + if (file != null) { + if (file.isDirectory()) { + String[] children = file.list(); + for (int i = 0; i < children.length; i++) { + deletedAll = deleteFile(new File(file, children[i])) && deletedAll; + } + } else { + deletedAll = file.delete(); + } + } + + return deletedAll; + } + + +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java new file mode 100644 index 000000000..aa79513e8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java @@ -0,0 +1,31 @@ +package fr.free.nrw.commons.utils; + +import android.support.annotation.IdRes; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; + +import timber.log.Timber; + +public class FragmentUtils { + + public static boolean addAndCommitFragmentWithImmediateExecution( + @NonNull FragmentManager fragmentManager, + @IdRes int containerViewId, + @NonNull Fragment fragment) { + if (fragment.isAdded()) { + Timber.w("Could not add fragment. The fragment is already added."); + return false; + } + try { + fragmentManager.beginTransaction() + .add(containerViewId, fragment) + .commitNow(); + return true; + } catch (IllegalStateException e) { + Timber.e(e, "Could not add & commit fragment. " + + "Did you mean to call commitAllowingStateLoss?"); + } + return false; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/UriDeserializer.java b/app/src/main/java/fr/free/nrw/commons/utils/UriDeserializer.java new file mode 100644 index 000000000..ad37b27fc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/UriDeserializer.java @@ -0,0 +1,18 @@ +package fr.free.nrw.commons.utils; + +import android.net.Uri; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +import java.lang.reflect.Type; + +public class UriDeserializer implements JsonDeserializer { + @Override + public Uri deserialize(final JsonElement src, final Type srcType, + final JsonDeserializationContext context) throws JsonParseException { + return Uri.parse(src.getAsString()); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/UriSerializer.java b/app/src/main/java/fr/free/nrw/commons/utils/UriSerializer.java new file mode 100644 index 000000000..d5a4c28ec --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/UriSerializer.java @@ -0,0 +1,17 @@ +package fr.free.nrw.commons.utils; + +import android.net.Uri; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; + + +public class UriSerializer implements JsonSerializer { + public JsonElement serialize(Uri src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.toString()); + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_exit_to_app_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_exit_to_app_black_24dp.png new file mode 100644 index 000000000..ad0f63e5b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_exit_to_app_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_feedback_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_feedback_black_24dp.png new file mode 100644 index 000000000..f03168559 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_feedback_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_file_upload_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_file_upload_black_24dp.png new file mode 100644 index 000000000..5e5b9fc4a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_file_upload_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_home_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_home_black_24dp.png new file mode 100644 index 000000000..9f61d7bf2 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_home_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png new file mode 100644 index 000000000..4b5ab06e1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_info_outline_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_list_white_24dp.png new file mode 100644 index 000000000..f8f7e7dda Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_location_on_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_location_on_black_24dp.png new file mode 100644 index 000000000..df1f34062 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_location_on_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_map_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_map_white_24dp.png new file mode 100644 index 000000000..b7631d7d7 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_map_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png new file mode 100644 index 000000000..acf1ddf85 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/custom_map_marker.png b/app/src/main/res/drawable-mdpi/custom_map_marker.png new file mode 100644 index 000000000..0634167c5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/custom_map_marker.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_refresh.png b/app/src/main/res/drawable-mdpi/ic_action_refresh.png deleted file mode 100644 index 4f5d2558f..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_action_refresh.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_exit_to_app_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_exit_to_app_black_24dp.png new file mode 100644 index 000000000..dee407b59 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_exit_to_app_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_feedback_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_feedback_black_24dp.png new file mode 100644 index 000000000..8be6849cb Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_feedback_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_file_upload_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_file_upload_black_24dp.png new file mode 100644 index 000000000..c5f2954bd Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_file_upload_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_home_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_home_black_24dp.png new file mode 100644 index 000000000..9f2c3d2f7 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_home_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_info_outline_black_24dp.png new file mode 100644 index 000000000..e0c9fe0eb Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_info_outline_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_list_white_24dp.png new file mode 100644 index 000000000..15d8fc2bb Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_location_on_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_location_on_black_24dp.png new file mode 100644 index 000000000..92a073827 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_location_on_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_map_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_map_white_24dp.png new file mode 100644 index 000000000..4edb178dc Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_map_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png new file mode 100644 index 000000000..c59419c02 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_exit_to_app_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_exit_to_app_black_24dp.png new file mode 100644 index 000000000..5a536d57c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_exit_to_app_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_feedback_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_feedback_black_24dp.png new file mode 100644 index 000000000..63bec2331 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_feedback_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_file_upload_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_file_upload_black_24dp.png new file mode 100644 index 000000000..41694c101 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_file_upload_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_home_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_home_black_24dp.png new file mode 100644 index 000000000..dcdcfc0a8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_home_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_info_outline_black_24dp.png new file mode 100644 index 000000000..b706f0d06 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_info_outline_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_list_white_24dp.png new file mode 100644 index 000000000..2b7253975 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_location_on_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_location_on_black_24dp.png new file mode 100644 index 000000000..b2696b6d4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_location_on_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_map_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_map_white_24dp.png new file mode 100644 index 000000000..301162e71 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_map_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png new file mode 100644 index 000000000..e84e188a1 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_exit_to_app_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_exit_to_app_black_24dp.png new file mode 100644 index 000000000..53b79d4c7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_exit_to_app_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_feedback_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_feedback_black_24dp.png new file mode 100644 index 000000000..e68ceb906 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_feedback_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_file_upload_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_file_upload_black_24dp.png new file mode 100644 index 000000000..bb5d0923b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_file_upload_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_home_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_home_black_24dp.png new file mode 100644 index 000000000..2e86cc255 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_home_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png new file mode 100644 index 000000000..3847a9fe7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_info_outline_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png new file mode 100644 index 000000000..4d2807e4e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_location_on_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_location_on_black_24dp.png new file mode 100644 index 000000000..5a21dfae6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_location_on_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_map_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_map_white_24dp.png new file mode 100644 index 000000000..0c38abeec Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_map_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png new file mode 100644 index 000000000..3023ff8da Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_exit_to_app_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_exit_to_app_black_24dp.png new file mode 100644 index 000000000..e30632b6b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_exit_to_app_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_feedback_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_feedback_black_24dp.png new file mode 100644 index 000000000..1343fa837 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_feedback_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_file_upload_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_file_upload_black_24dp.png new file mode 100644 index 000000000..9ce5b8a7b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_file_upload_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_home_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_home_black_24dp.png new file mode 100644 index 000000000..04e2b26ff Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_home_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png new file mode 100644 index 000000000..c1e2a03a4 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_list_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_list_white_24dp.png new file mode 100644 index 000000000..2a6d3b044 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_list_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_location_on_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_location_on_black_24dp.png new file mode 100644 index 000000000..7c2217e46 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_location_on_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_map_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_map_white_24dp.png new file mode 100644 index 000000000..0cbfe89e5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_map_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png new file mode 100644 index 000000000..476d5c978 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable/hamburger_item_bg.xml b/app/src/main/res/drawable/hamburger_item_bg.xml new file mode 100644 index 000000000..cdd545602 --- /dev/null +++ b/app/src/main/res/drawable/hamburger_item_bg.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_error_outline_black_24dp.xml b/app/src/main/res/drawable/ic_error_outline_black_24dp.xml new file mode 100644 index 000000000..850263ee7 --- /dev/null +++ b/app/src/main/res/drawable/ic_error_outline_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_image_black_24dp.xml b/app/src/main/res/drawable/ic_image_black_24dp.xml new file mode 100644 index 000000000..63aa7d33b --- /dev/null +++ b/app/src/main/res/drawable/ic_image_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_more_vert_white_24dp.xml b/app/src/main/res/drawable/ic_more_vert_white_24dp.xml new file mode 100644 index 000000000..d73c8d86c --- /dev/null +++ b/app/src/main/res/drawable/ic_more_vert_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_refresh_white_24dp.xml b/app/src/main/res/drawable/ic_refresh_white_24dp.xml new file mode 100644 index 000000000..d69022192 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_white_24dp.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/round_icon_unknown.xml b/app/src/main/res/drawable/round_icon_unknown.xml index ba0f2e52b..33905d93b 100644 --- a/app/src/main/res/drawable/round_icon_unknown.xml +++ b/app/src/main/res/drawable/round_icon_unknown.xml @@ -1,7 +1,5 @@ - - - - - + + + diff --git a/app/src/main/res/layout-land/welcome_do_upload.xml b/app/src/main/res/layout-land/welcome_do_upload.xml index 17db68f8f..e5c7bb3fb 100644 --- a/app/src/main/res/layout-land/welcome_do_upload.xml +++ b/app/src/main/res/layout-land/welcome_do_upload.xml @@ -24,6 +24,7 @@ android:layout_width="150dp" android:layout_height="118dp" android:scaleType="fitXY" + android:contentDescription="@string/welcome_image_mount_zao" /> diff --git a/app/src/main/res/layout-land/welcome_dont_upload.xml b/app/src/main/res/layout-land/welcome_dont_upload.xml index df7fa1e40..7ac63e560 100644 --- a/app/src/main/res/layout-land/welcome_dont_upload.xml +++ b/app/src/main/res/layout-land/welcome_dont_upload.xml @@ -21,6 +21,7 @@ android:id="@+id/selfie_x" android:layout_width="110dp" android:layout_height="wrap_content" + android:contentDescription="@string/welcome_image_no_selfies" /> diff --git a/app/src/main/res/layout-land/welcome_final.xml b/app/src/main/res/layout-land/welcome_final.xml index 62d2a9e00..a9d8c01af 100644 --- a/app/src/main/res/layout-land/welcome_final.xml +++ b/app/src/main/res/layout-land/welcome_final.xml @@ -17,13 +17,17 @@ + android:src="@drawable/welcome_wikipedia" + android:contentDescription="@string/welcome_image_welcome_wikipedia" + /> + android:src="@drawable/welcome_copyright" + android:contentDescription="@string/welcome_image_proprietary" + /> diff --git a/app/src/main/res/layout-land/welcome_image_details.xml b/app/src/main/res/layout-land/welcome_image_details.xml index b5dbe0824..a5ae1a762 100644 --- a/app/src/main/res/layout-land/welcome_image_details.xml +++ b/app/src/main/res/layout-land/welcome_image_details.xml @@ -18,6 +18,7 @@ android:paddingBottom="24dp" android:adjustViewBounds="true" android:layout_gravity="center" + android:contentDescription="@string/welcome_image_sydney_opera_house" /> + android:adjustViewBounds="true" + android:contentDescription="@string/welcome_image_welcome_wikipedia" + /> - + android:layout_height="match_parent"> - + - + - + - + - + - + - + - + - \ No newline at end of file + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_contributions.xml b/app/src/main/res/layout/activity_contributions.xml index e980662d3..106f1ff20 100644 --- a/app/src/main/res/layout/activity_contributions.xml +++ b/app/src/main/res/layout/activity_contributions.xml @@ -1,17 +1,48 @@ - + android:layout_height="match_parent"> - + android:layout_height="match_parent"> + - \ No newline at end of file + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index aeda37b39..b426f4553 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -62,6 +62,24 @@ + + + + + +