diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 8feca4268..12ff064e2 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -2,24 +2,28 @@ Summarize your issue in one sentence (what goes wrong, what did you expect to happen) -_Before creating an issue, please search the existing issues to see if a similar one has already been created. You can search issues by specific labels (e.g. `label:nearby `) or just by typing keywords into the search filter._ +_Before creating an issue, please search the existing issues to see if a similar one has already been created. You can search issues by specific labels (e.g. `label:nearby`) or just by typing keywords into the search filter._ **Steps to reproduce:** How can we reproduce the issue? What did you expect the app to do, and what did you see instead? -**Add System logs:** +**System logs:** +``` Add logcat files here (if possible). +Need help? See https://github.com/commons-app/apps-android-commons/wiki/Getting-app-logs-from-Android-Studio +``` + **Device and Android version:** -What make and model device (e.g., Samsung J7) did you encounter this on? What Android -version (e.g., Android 4.0 Ice Cream Sandwich or Android 6.0 Marshmallow) are you running? Is it - the stock version from the manufacturer or a custom ROM ? +What make and model device (e.g., Samsung J7) did you encounter this on? +What Android version (e.g., Android 4.0 Ice Cream Sandwich or Android 6.0 Marshmallow) are you running? +Is it the stock version from the manufacturer or a custom ROM ? - **Commons app version:** +**Commons app version:** You can find this information by going to the navigation drawer in the app and tapping 'About'. If you are building from our codebase instead of downloading the app, please also mention the branch and build variant (e.g. master and prodDebug). diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 37e104d14..50634fa60 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,19 +1,17 @@ -## Title (required) +**Description (required)** -Fixes #{GitHub issue number and title (Please do not forget adding title) } +Fixes #{GitHub issue number} {Github issue title} -## Description (required) +What changes did you make and why? -Fixes #{GitHub issue number and title} +**Tests performed (required)** -{Describe the changes made and why they were made.} +Tested {build variant, e.g. ProdDebug} on {name of device or emulator} with API level {API level}. -## Tests performed (required) +**Screenshots showing what changed (optional - for UI changes)** -Tested on {API level & name of device/emulator}, with {build variant, e.g. ProdDebug}. +Need help? See https://support.google.com/android/answer/9075928 -## Screenshots showing what changed (optional) - -{Only for user interface changes, otherwise remove this section. See [how to take a screenshot](https://android.stackexchange.com/questions/1759/how-to-take-a-screenshot-with-an-android-device)} +--- -_Note: Please ensure that you have read CONTRIBUTING.md if this is your first pull request._ \ No newline at end of file +_Note: Please ensure that you have read CONTRIBUTING.md if this is your first pull request._ diff --git a/README.md b/README.md index 57272bdcf..54649a830 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Wikimedia Commons Android app [![Build status](https://api.travis-ci.org/commons-app/apps-android-commons.svg)](https://travis-ci.org/commons-app/apps-android-commons) +# Wikimedia Commons Android app [![Build status](https://api.travis-ci.org/commons-app/apps-android-commons.svg?branch=master)](https://travis-ci.org/commons-app/apps-android-commons) The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons. Download the app [here][1], or view our [website][2]. @@ -11,37 +11,13 @@ Initially started by the Wikimedia Foundation, this app is now maintained by gra ## Documentation -We try to have an extensive documentation at [our wiki here at Github][5]: +We try to have an extensive documentation at [our wiki here at Github][4]: -* [User Documentation][6] -* [Contributor Documentation][7] - * [Volunteers Welcome!][9] +* [User Documentation][5] +* [Contributor Documentation][6] + * [Volunteers Welcome!][7] * [Developer Documentation][8] - -## Libraries Used ## - -* [Picasso][11] -* [RSS-Parser][12] -* [ViewPagerIndicator][13] -* [PhotoView][14] -* [Acra][15] -* [Renderers][16] -* [Gson][17] -* [Timber][18] -* [Java-String-Similarity][19] -* [ReadMoreTextView][20] -* [MaterialShowcaseView][21] -* [Butterknife][22] -* [OKHttp][23] -* [Okio][24] -* [RxJava][25] -* [JSoup][26] -* [Fresco][27] -* [Stetho][28] -* [Dagger][29] -* [Java-HTTP-Fluent][30] -* [CircleProgressBar][31] -* [Leak Canary][32] + * [Libraries Used][9] ## Contributors ## @@ -60,37 +36,18 @@ Thank you all for your work! ## License ## -This software is open source, licensed under the [Apache License 2.0][4]. - +This software is open source, licensed under the [Apache License 2.0][10]. [1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons [2]: https://commons-app.github.io/ [3]: https://github.com/commons-app/apps-android-commons/issues -[4]: https://www.apache.org/licenses/LICENSE-2.0 -[5]: https://github.com/commons-app/apps-android-commons/wiki -[6]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation -[7]: https://github.com/commons-app/apps-android-commons/wiki#contributor-documentation + +[4]: https://github.com/commons-app/apps-android-commons/wiki +[5]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation +[6]: https://github.com/commons-app/apps-android-commons/wiki#contributor-documentation +[7]: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21 [8]: https://github.com/commons-app/apps-android-commons/wiki#developer-documentation -[9]: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21 -[10]: https://meta.wikimedia.org/wiki/Grants:Project/Improve_%27Upload_to_Commons%27_Android_App/Renewal -[11]: https://github.com/square/picasso -[13]: https://github.com/avianey/Android-ViewPagerIndicator -[14]: https://github.com/chrisbanes/PhotoView -[15]: https://github.com/ACRA/acra -[16]: https://github.com/pedrovgs/Renderers -[17]: https://github.com/google/gson -[18]: https://github.com/JakeWharton/timber -[19]: https://github.com/tdebatty/java-string-similarity -[20]: https://github.com/bravoborja/ReadMoreTextView -[21]: https://github.com/deano2390/MaterialShowcaseView -[22]: https://github.com/JakeWharton/butterknife -[23]: https://github.com/square/okhttp -[24]: https://github.com/square/okio -[25]: https://github.com/ReactiveX/RxJava -[27]: https://github.com/facebook/fresco -[28]: https://github.com/facebook/stetho -[29]: https://github.com/google/dagger -[30]: https://github.com/yuvipanda/java-http-fluent/blob/master/src/main/java/in/yuvi/http/fluent/Http.java -[31]: https://github.com/dinuscxj/CircleProgressBar -[32]: https://github.com/square/leakcanary +[9]: https://github.com/commons-app/apps-android-commons/wiki/Libraries-used + +[10]: https://www.apache.org/licenses/LICENSE-2.0 diff --git a/app/build.gradle b/app/build.gradle index 6d1c349d9..6118d99d5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,7 +13,6 @@ dependencies { implementation 'ch.acra:acra:4.9.2' - implementation 'org.mediawiki:api:1.3' implementation 'commons-codec:commons-codec:1.10' implementation 'com.github.pedrovgs:renderers:3.3.3' implementation 'com.google.code.gson:gson:2.8.5' @@ -32,6 +31,7 @@ dependencies { transitive = true } implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0' + //noinspection GradleCompatible implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION" implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION" @@ -44,6 +44,7 @@ dependencies { implementation 'com.squareup.okio:okio:1.14.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' // Because RxAndroid releases are few and far between, it is recommended you also + // explicitly depend on RxJava's latest version for bug fixes and new features. implementation 'io.reactivex.rxjava2:rxjava:2.2.0' implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' @@ -131,7 +132,7 @@ android { flavorDimensions 'tier' productFlavors { prod { - + applicationId 'fr.free.nrw.commons' buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"" @@ -153,8 +154,8 @@ android { buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\"" buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\"" buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\"" - buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\"" - buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\"" + buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.contentprovider\"" + buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.locations.contentprovider\"" dimension 'tier' } diff --git a/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java b/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java deleted file mode 100644 index d2db4614f..000000000 --- a/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.net.Uri; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import fr.free.nrw.commons.BuildConfig; - -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -@RunWith(AndroidJUnit4.class) -public class FileUtilsTest { - @Test - public void isSelfOwned() throws Exception { - Uri uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".provider/document/1"); - boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri); - assertThat(selfOwned, is(true)); - } - - @Test - public void isNotSelfOwned() throws Exception { - Uri uri = Uri.parse("content://com.android.providers.media.documents/document/1"); - boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri); - assertThat(selfOwned, is(false)); - } -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2d9c9a438..ad76ee14d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ - + @@ -41,56 +41,44 @@ - - + + - - - - + + + - - - + android:parentActivityName=".contributions.MainActivity" /> - - @@ -104,18 +92,18 @@ + android:parentActivityName=".contributions.MainActivity" /> + android:parentActivityName=".contributions.MainActivity" /> - + - + @@ -177,21 +165,18 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" /> - - - Map arraysToMap(K[] kArray, V[] vArray){ + if(kArray.length!=vArray.length) + throw new RuntimeException("arraysToMap array sizes don't match"); + Map map=new LinkedHashMap<>(); + for (int i=0;i shareScreen(screenshot)); @@ -387,7 +387,7 @@ public class AchievementsActivity extends NavigationBaseActivity { */ private boolean checkAccount(){ Account currentAccount = sessionManager.getCurrentAccount(); - if(currentAccount == null) { + if (currentAccount == null) { Timber.d("Current account is null"); ViewUtil.showLongToast(this, getResources().getString(R.string.user_not_logged_in)); sessionManager.forceLogin(this); diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/BitmapUtils.java b/app/src/main/java/fr/free/nrw/commons/achievements/BitmapUtils.java index b7400117d..57318a12d 100644 --- a/app/src/main/java/fr/free/nrw/commons/achievements/BitmapUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/achievements/BitmapUtils.java @@ -19,7 +19,7 @@ public class BitmapUtils { */ public static BitmapDrawable writeOnDrawable(Bitmap bm, String text, Context context){ Bitmap.Config config = bm.getConfig(); - if(config == null){ + if (config == null){ config = Bitmap.Config.ARGB_8888; } Bitmap bitmap = Bitmap.createBitmap(bm.getWidth(),bm.getHeight(),config); diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/FeaturedImages.java b/app/src/main/java/fr/free/nrw/commons/achievements/FeaturedImages.java index 8b9992e28..a943ed4a3 100644 --- a/app/src/main/java/fr/free/nrw/commons/achievements/FeaturedImages.java +++ b/app/src/main/java/fr/free/nrw/commons/achievements/FeaturedImages.java @@ -22,4 +22,4 @@ public class FeaturedImages { public int getFeaturedPicturesOnWikimediaCommons() { return featuredPicturesOnWikimediaCommons; } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/FeedbackResponse.java b/app/src/main/java/fr/free/nrw/commons/achievements/FeedbackResponse.java index 37fea3e2d..b8323f973 100644 --- a/app/src/main/java/fr/free/nrw/commons/achievements/FeedbackResponse.java +++ b/app/src/main/java/fr/free/nrw/commons/achievements/FeedbackResponse.java @@ -61,4 +61,4 @@ public class FeedbackResponse { public int getImagesEditedBySomeoneElse() { return imagesEditedBySomeoneElse; } -} \ No newline at end of file +} 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 a157035b9..b7c639f24 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 @@ -16,7 +16,8 @@ import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE; public abstract class AuthenticatedActivity extends NavigationBaseActivity { - @Inject SessionManager sessionManager; + @Inject + protected SessionManager sessionManager; @Inject MediaWikiApi mediaWikiApi; private String 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 371f561ca..f4d5dd7c9 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 @@ -43,7 +43,7 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.category.CategoryImagesActivity; -import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.theme.NavigationBaseActivity; @@ -139,7 +139,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) .show()); - if(BuildConfig.FLAVOR.equals("beta")){ + if (BuildConfig.FLAVOR.equals("beta")){ loginCredentials.setText(getString(R.string.login_credential)); } else { loginCredentials.setVisibility(View.GONE); @@ -381,10 +381,10 @@ public class LoginActivity extends AccountAuthenticatorActivity { super.onRestoreInstanceState(savedInstanceState); loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGING_IN, false); errorMessageShown = savedInstanceState.getBoolean(ERROR_MESSAGE_SHOWN, false); - if(loginCurrentlyInProgress){ + if (loginCurrentlyInProgress){ performLogin(); } - if(errorMessageShown){ + if (errorMessageShown){ resultantError = savedInstanceState.getString(RESULTANT_ERROR); handleOtherResults(resultantError); } @@ -399,7 +399,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { public void showMessageAndCancelDialog(@StringRes int resId) { showMessage(resId, R.color.secondaryDarkColor); - if(progressDialog != null){ + if (progressDialog != null){ progressDialog.cancel(); } } @@ -415,7 +415,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { } public void startMainActivity() { - NavigationBaseActivity.startActivityWithFlags(this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); + NavigationBaseActivity.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); finish(); } diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java index cad65a87a..b0f94e322 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java @@ -63,4 +63,4 @@ public class BookmarksPagerAdapter extends FragmentPagerAdapter { BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(pages.get(0).getPage()); fragment.onResume(); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java index 4232c1750..90b3ef457 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesFragment.java @@ -105,7 +105,7 @@ public class BookmarkPicturesFragment extends DaggerFragment { */ @SuppressLint("CheckResult") private void initList() { - if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { + if (!NetworkUtils.isInternetConnectionEstablished(getContext())) { handleNoInternet(); return; } @@ -179,7 +179,7 @@ public class BookmarkPicturesFragment extends DaggerFragment { * @param collection List of new Media to be displayed */ private void handleSuccess(List collection) { - if(collection == null) { + if (collection == null) { initErrorView(); return; } @@ -188,7 +188,7 @@ public class BookmarkPicturesFragment extends DaggerFragment { return; } - if(gridAdapter == null) { + if (gridAdapter == null) { setAdapter(collection); } else { if (gridAdapter.containsAll(collection)) { diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapterFactory.java index 417121c44..77b6615da 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapterFactory.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesAdapterFactory.java @@ -1,24 +1,23 @@ package fr.free.nrw.commons.category; import com.pedrogomez.renderers.ListAdapteeCollection; -import com.pedrogomez.renderers.RVRendererAdapter; import com.pedrogomez.renderers.RendererBuilder; import java.util.Collections; import java.util.List; -class CategoriesAdapterFactory { - private final CategoriesRenderer.CategoryClickedListener listener; +public class CategoriesAdapterFactory { + private final CategoryClickedListener listener; - CategoriesAdapterFactory(CategoriesRenderer.CategoryClickedListener listener) { + public CategoriesAdapterFactory(CategoryClickedListener listener) { this.listener = listener; } - public RVRendererAdapter create(List placeList) { + public CategoryRendererAdapter create(List placeList) { RendererBuilder builder = new RendererBuilder() .bind(CategoryItem.class, new CategoriesRenderer(listener)); ListAdapteeCollection collection = new ListAdapteeCollection<>( placeList != null ? placeList : Collections.emptyList()); - return new RVRendererAdapter<>(builder, collection); + return new CategoryRendererAdapter(builder, collection); } } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java new file mode 100644 index 000000000..8375f4972 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesModel.java @@ -0,0 +1,227 @@ +package fr.free.nrw.commons.category; + +import android.content.SharedPreferences; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; + +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.upload.GpsCategoryModel; +import fr.free.nrw.commons.utils.StringSortingUtils; +import io.reactivex.Observable; +import timber.log.Timber; + +public class CategoriesModel implements CategoryClickedListener { + private static final int SEARCH_CATS_LIMIT = 25; + + private final MediaWikiApi mwApi; + private final CategoryDao categoryDao; + private final SharedPreferences prefs; + private final SharedPreferences directPrefs; + + private HashMap> categoriesCache; + private List selectedCategories; + + @Inject GpsCategoryModel gpsCategoryModel; + @Inject + public CategoriesModel(MediaWikiApi mwApi, + CategoryDao categoryDao, + @Named("default_preferences") SharedPreferences prefs, + @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs) { + this.mwApi = mwApi; + this.categoryDao = categoryDao; + this.prefs = prefs; + this.directPrefs = directPrefs; + this.categoriesCache = new HashMap<>(); + this.selectedCategories = new ArrayList<>(); + } + + //region Misc. utility methods + public Comparator sortBySimilarity(final String filter) { + Comparator stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter); + return (firstItem, secondItem) -> stringSimilarityComparator + .compare(firstItem.getName(), secondItem.getName()); + } + + public boolean containsYear(String item) { + //Check for current and previous year to exclude these categories from removal + Calendar now = Calendar.getInstance(); + int year = now.get(Calendar.YEAR); + String yearInString = String.valueOf(year); + + int prevYear = year - 1; + String prevYearInString = String.valueOf(prevYear); + Timber.d("Previous year: %s", prevYearInString); + + //Check if item contains a 4-digit word anywhere within the string (.* is wildcard) + //And that item does not equal the current year or previous year + //And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750) + //Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029 + return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString)) + || item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)") + || (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*"))); + } + + public void updateCategoryCount(CategoryItem item) { + Category category = categoryDao.find(item.getName()); + + // Newly used category... + if (category == null) { + category = new Category(null, item.getName(), new Date(), 0); + } + + category.incTimesUsed(); + categoryDao.save(category); + } + //endregion + + //region Category Caching + public void cacheAll(HashMap> categories) { + categoriesCache.putAll(categories); + } + + public HashMap> getCategoriesCache() { + return categoriesCache; + } + + boolean cacheContainsKey(String term) { + return categoriesCache.containsKey(term); + } + //endregion + + //region Category searching + public Observable searchAll(String term, List imageTitleList) { + //If user hasn't typed anything in yet, get GPS and recent items + if (TextUtils.isEmpty(term)) { + return gpsCategories() + .concatWith(titleCategories(imageTitleList)) + .concatWith(recentCategories()); + } + + //if user types in something that is in cache, return cached category + if (cacheContainsKey(term)) { + return Observable.fromIterable(getCachedCategories(term)) + .map(name -> new CategoryItem(name, false)); + } + + //otherwise, search API for matching categories + return mwApi + .allCategories(term, SEARCH_CATS_LIMIT) + .map(name -> new CategoryItem(name, false)); + } + + public Observable searchCategories(String term, List imageTitleList) { + //If user hasn't typed anything in yet, get GPS and recent items + if (TextUtils.isEmpty(term)) { + return gpsCategories() + .concatWith(titleCategories(imageTitleList)) + .concatWith(recentCategories()); + } + + return mwApi + .searchCategories(term, SEARCH_CATS_LIMIT) + .map(s -> new CategoryItem(s, false)); + } + + private ArrayList getCachedCategories(String term) { + return categoriesCache.get(term); + } + + public Observable defaultCategories(List titleList) { + Observable directCat = directCategories(); + if (hasDirectCategories()) { + Timber.d("Image has direct Cat"); + return directCat + .concatWith(gpsCategories()) + .concatWith(titleCategories(titleList)) + .concatWith(recentCategories()); + } else { + Timber.d("Image has no direct Cat"); + return gpsCategories() + .concatWith(titleCategories(titleList)) + .concatWith(recentCategories()); + } + } + + private boolean hasDirectCategories() { + return !directPrefs.getString("Category", "").equals(""); + } + + private Observable directCategories() { + String directCategory = directPrefs.getString("Category", ""); + List categoryList = new ArrayList<>(); + Timber.d("Direct category found: " + directCategory); + + if (!directCategory.equals("")) { + categoryList.add(directCategory); + Timber.d("DirectCat does not equal emptyString. Direct Cat list has " + categoryList); + } + return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false)); + } + + Observable gpsCategories() { + return Observable.fromIterable(gpsCategoryModel.getCategoryList()) + .map(name -> new CategoryItem(name, false)); + } + + private Observable titleCategories(List titleList) { + return Observable.fromIterable(titleList) + .concatMap(this::getTitleCategories); + } + + private Observable getTitleCategories(String title) { + return mwApi.searchTitles(title, SEARCH_CATS_LIMIT) + .map(name -> new CategoryItem(name, false)); + } + + private Observable recentCategories() { + return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT)) + .map(s -> new CategoryItem(s, false)); + } + //endregion + + //region Category Selection + @Override + public void categoryClicked(CategoryItem item) { + if (item.isSelected()) { + selectCategory(item); + updateCategoryCount(item); + } else { + unselectCategory(item); + } + } + + public void selectCategory(CategoryItem item) { + selectedCategories.add(item); + } + + public void unselectCategory(CategoryItem item) { + selectedCategories.remove(item); + } + + public int selectedCategoriesCount() { + return selectedCategories.size(); + } + + public List getSelectedCategories() { + return selectedCategories; + } + + public List getCategoryStringList() { + List output = new ArrayList<>(); + for (CategoryItem item : selectedCategories) { + output.add(item.getName()); + } + return output; + } + //endregion + +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java b/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java index 81cccdb72..f9a349ccb 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoriesRenderer.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.category; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -11,7 +12,7 @@ import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; -class CategoriesRenderer extends Renderer { +public class CategoriesRenderer extends Renderer { @BindView(R.id.tvName) CheckedTextView checkedView; private final CategoryClickedListener listener; @@ -44,11 +45,8 @@ class CategoriesRenderer extends Renderer { @Override public void render() { CategoryItem item = getContent(); + Log.e("Commons", "Rendering: "+item); checkedView.setChecked(item.isSelected()); checkedView.setText(item.getName()); } - - interface CategoryClickedListener { - void categoryClicked(CategoryItem item); - } } 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 deleted file mode 100644 index c53c29379..000000000 --- a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java +++ /dev/null @@ -1,421 +0,0 @@ -package fr.free.nrw.commons.category; - - -import android.content.SharedPreferences; -import android.os.Bundle; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.ProgressBar; -import android.widget.TextView; - -import com.jakewharton.rxbinding2.view.RxView; -import com.jakewharton.rxbinding2.widget.RxTextView; -import com.pedrogomez.renderers.RVRendererAdapter; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; -import javax.inject.Named; - -import butterknife.BindView; -import butterknife.ButterKnife; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.mwapi.MediaWikiApi; -import fr.free.nrw.commons.upload.GpsCategoryModel; -import fr.free.nrw.commons.utils.StringSortingUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import timber.log.Timber; - -import static android.view.KeyEvent.ACTION_UP; -import static android.view.KeyEvent.KEYCODE_BACK; - -/** - * Displays the category suggestion and selection screen. Category search is initiated here. - */ -public class CategorizationFragment extends CommonsDaggerSupportFragment { - - public static final int SEARCH_CATS_LIMIT = 25; - - @BindView(R.id.categoriesListBox) - RecyclerView categoriesList; - @BindView(R.id.categoriesSearchBox) - EditText categoriesFilter; - @BindView(R.id.categoriesSearchInProgress) - ProgressBar categoriesSearchInProgress; - @BindView(R.id.categoriesNotFound) - TextView categoriesNotFoundView; - @BindView(R.id.categoriesExplanation) - TextView categoriesSkip; - - @Inject MediaWikiApi mwApi; - @Inject @Named("default_preferences") SharedPreferences prefs; - @Inject @Named("prefs") SharedPreferences prefsPrefs; - @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; - @Inject CategoryDao categoryDao; - @Inject GpsCategoryModel gpsCategoryModel; - - private RVRendererAdapter categoriesAdapter; - private OnCategoriesSaveHandler onCategoriesSaveHandler; - private HashMap> categoriesCache; - private List selectedCategories = new ArrayList<>(); - private TitleTextWatcher textWatcher = new TitleTextWatcher(); - private boolean hasDirectCategories = false; - - private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> { - if (item.isSelected()) { - selectedCategories.add(item); - updateCategoryCount(item); - } else { - selectedCategories.remove(item); - } - }); - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_categorization, container, false); - ButterKnife.bind(this, rootView); - - categoriesList.setLayoutManager(new LinearLayoutManager(getContext())); - - ArrayList items = new ArrayList<>(); - categoriesCache = new HashMap<>(); - if (savedInstanceState != null) { - items.addAll(savedInstanceState.getParcelableArrayList("currentCategories")); - //noinspection unchecked - categoriesCache.putAll((HashMap>) savedInstanceState - .getSerializable("categoriesCache")); - } - - categoriesAdapter = adapterFactory.create(items); - categoriesList.setAdapter(categoriesAdapter); - - - categoriesFilter.addTextChangedListener(textWatcher); - - categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> { - if (!hasFocus) { - ViewUtil.hideKeyboard(v); - } - }); - - RxTextView.textChanges(categoriesFilter) - .takeUntil(RxView.detaches(categoriesFilter)) - .debounce(500, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(filter -> updateCategoryList(filter.toString())); - return rootView; - } - - @Override - public void onDestroyView() { - categoriesFilter.removeTextChangedListener(textWatcher); - super.onDestroyView(); - } - - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - menu.clear(); - inflater.inflate(R.menu.fragment_categorization, menu); - } - - @Override - public void onResume() { - super.onResume(); - - View rootView = getView(); - if (rootView != null) { - rootView.setFocusableInTouchMode(true); - rootView.requestFocus(); - rootView.setOnKeyListener((v, keyCode, event) -> { - if (event.getAction() == ACTION_UP && keyCode == KEYCODE_BACK) { - showBackButtonDialog(); - return true; - } - return false; - }); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - int itemCount = categoriesAdapter.getItemCount(); - ArrayList items = new ArrayList<>(itemCount); - for (int i = 0; i < itemCount; i++) { - items.add(categoriesAdapter.getItem(i)); - } - outState.putParcelableArrayList("currentCategories", items); - outState.putSerializable("categoriesCache", categoriesCache); - } - - @Override - public boolean onOptionsItemSelected(MenuItem menuItem) { - switch (menuItem.getItemId()) { - case R.id.menu_save_categories: - if (selectedCategories.size() > 0) { - //Some categories selected, proceed to submission - onCategoriesSaveHandler.onCategoriesSave(getStringList(selectedCategories)); - } else { - //No categories selected, prompt the user to select some - showConfirmationDialog(); - } - return true; - default: - return super.onOptionsItemSelected(menuItem); - } - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(true); - onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity(); - getActivity().setTitle(R.string.categories_activity_title); - } - - private void updateCategoryList(String filter) { - Observable.fromIterable(selectedCategories) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe(disposable -> { - categoriesSearchInProgress.setVisibility(View.VISIBLE); - categoriesNotFoundView.setVisibility(View.GONE); - categoriesSkip.setVisibility(View.GONE); - categoriesAdapter.clear(); - }) - .observeOn(Schedulers.io()) - .concatWith( - searchAll(filter) - .mergeWith(searchCategories(filter)) - .concatWith(TextUtils.isEmpty(filter) - ? defaultCategories() : Observable.empty()) - ) - .filter(categoryItem -> !containsYear(categoryItem.getName())) - .distinct() - .sorted(sortBySimilarity(filter)) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - s -> categoriesAdapter.add(s), - Timber::e, - () -> { - categoriesAdapter.notifyDataSetChanged(); - categoriesSearchInProgress.setVisibility(View.GONE); - - if (categoriesAdapter.getItemCount() == selectedCategories.size()) { - // There are no suggestions - if (TextUtils.isEmpty(filter)) { - // Allow to send image with no categories - categoriesSkip.setVisibility(View.VISIBLE); - } else { - // Inform the user that the searched term matches no category - categoriesNotFoundView.setText(getString(R.string.categories_not_found, filter)); - categoriesNotFoundView.setVisibility(View.VISIBLE); - } - } - } - ); - } - - private Comparator sortBySimilarity(final String filter) { - Comparator stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter); - return (firstItem, secondItem) -> stringSimilarityComparator - .compare(firstItem.getName(), secondItem.getName()); - } - - private List getStringList(List input) { - List output = new ArrayList<>(); - for (CategoryItem item : input) { - output.add(item.getName()); - } - return output; - } - - private Observable defaultCategories() { - Observable directCat = directCategories(); - if (hasDirectCategories) { - Timber.d("Image has direct Cat"); - return directCat - .concatWith(gpsCategories()) - .concatWith(titleCategories()) - .concatWith(recentCategories()); - } - else { - Timber.d("Image has no direct Cat"); - return gpsCategories() - .concatWith(titleCategories()) - .concatWith(recentCategories()); - } - } - - private Observable directCategories() { - String directCategory = directPrefs.getString("Category", ""); - // Strip newlines to prevent blank categories, and to tidy existing categories - directCategory = directCategory.replace("\n", ""); - - List categoryList = new ArrayList<>(); - Timber.d("Direct category found: " + "'" + directCategory + "'"); - - if (!directCategory.equals("")) { - hasDirectCategories = true; - categoryList.add(directCategory); - Timber.d("DirectCat does not equal emptyString. Direct Cat list has " + categoryList); - } - return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false)); - } - - private Observable gpsCategories() { - return Observable.fromIterable(gpsCategoryModel.getCategoryList()) - .map(name -> new CategoryItem(name, false)); - } - - private Observable titleCategories() { - //Retrieve the title that was saved when user tapped submit icon - String title = prefs.getString("Title", ""); - - return mwApi - .searchTitles(title, SEARCH_CATS_LIMIT) - .map(name -> new CategoryItem(name, false)); - } - - private Observable recentCategories() { - return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT)) - .map(s -> new CategoryItem(s, false)); - } - - private Observable searchAll(String term) { - //If user hasn't typed anything in yet, get GPS and recent items - if (TextUtils.isEmpty(term)) { - return Observable.empty(); - } - - //if user types in something that is in cache, return cached category - if (categoriesCache.containsKey(term)) { - return Observable.fromIterable(categoriesCache.get(term)) - .map(name -> new CategoryItem(name, false)); - } - - //otherwise, search API for matching categories - return mwApi - .allCategories(term, SEARCH_CATS_LIMIT) - .map(name -> new CategoryItem(name, false)); - } - - private Observable searchCategories(String term) { - //If user hasn't typed anything in yet, get GPS and recent items - if (TextUtils.isEmpty(term)) { - return Observable.empty(); - } - - return mwApi - .searchCategories(term, SEARCH_CATS_LIMIT) - .map(s -> new CategoryItem(s, false)); - } - - private boolean containsYear(String item) { - //Check for current and previous year to exclude these categories from removal - Calendar now = Calendar.getInstance(); - int year = now.get(Calendar.YEAR); - String yearInString = String.valueOf(year); - - int prevYear = year - 1; - String prevYearInString = String.valueOf(prevYear); - Timber.d("Previous year: %s", prevYearInString); - - //Check if item contains a 4-digit word anywhere within the string (.* is wildcard) - //And that item does not equal the current year or previous year - //And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750) - //Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029 - return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString)) - || item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)") - || (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*"))); - } - - private void updateCategoryCount(CategoryItem item) { - Category category = categoryDao.find(item.getName()); - - // Newly used category... - if (category == null) { - category = new Category(null, item.getName(), new Date(), 0); - } - - category.incTimesUsed(); - categoryDao.save(category); - } - - public int getCurrentSelectedCount() { - return selectedCategories.size(); - } - - /** - * Show dialog asking for confirmation to leave without saving categories. - */ - public void showBackButtonDialog() { - new AlertDialog.Builder(getActivity()) - .setMessage("Are you sure you want to go back? The image will not " - + "have any categories saved.") - .setTitle("Warning") - .setPositiveButton(android.R.string.no, (dialog, id) -> { - //No need to do anything, user remains on categorization screen - }) - .setNegativeButton(android.R.string.yes, (dialog, id) -> getActivity().finish()) - .create() - .show(); - } - - private void showConfirmationDialog() { - new AlertDialog.Builder(getActivity()) - .setMessage("Images without categories are rarely usable. " - + "Are you sure you want to submit without selecting " - + "categories?") - .setTitle("No Categories Selected") - .setPositiveButton(android.R.string.no, (dialog, id) -> { - //Exit menuItem so user can select their categories - }) - .setNegativeButton(android.R.string.yes, (dialog, id) -> { - //Proceed to submission - onCategoriesSaveHandler.onCategoriesSave(getStringList(selectedCategories)); - }) - .create() - .show(); - } - - private class TitleTextWatcher implements TextWatcher { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } - - @Override - public void afterTextChanged(Editable editable) { - if (getActivity() != null) { - getActivity().invalidateOptionsMenu(); - } - } - } -} 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 f2d83d2e5..8ea3c442c 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 @@ -93,4 +93,4 @@ public class Category { this.contentUri = contentUri; } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryClickedListener.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryClickedListener.java new file mode 100644 index 000000000..df99b4060 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryClickedListener.java @@ -0,0 +1,5 @@ +package fr.free.nrw.commons.category; + +public interface CategoryClickedListener { + void categoryClicked(CategoryItem item); +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java index 3495d710c..ae45952e4 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java @@ -26,4 +26,4 @@ public class CategoryImageController { public List getCategoryImages(String categoryName) { return mediaWikiApi.getCategoryImages(categoryName); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java index f24cc0b95..1ead68701 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java @@ -101,7 +101,7 @@ public class CategoryImagesListFragment extends DaggerFragment { */ @SuppressLint("CheckResult") private void initList() { - if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { + if (!NetworkUtils.isInternetConnectionEstablished(getContext())) { handleNoInternet(); return; } @@ -208,7 +208,7 @@ public class CategoryImagesListFragment extends DaggerFragment { */ @SuppressLint("CheckResult") private void fetchMoreImages() { - if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { + if (!NetworkUtils.isInternetConnectionEstablished(getContext())) { handleNoInternet(); return; } @@ -227,13 +227,13 @@ public class CategoryImagesListFragment extends DaggerFragment { * @param collection List of new Media to be displayed */ private void handleSuccess(List collection) { - if(collection == null || collection.isEmpty()) { + if (collection == null || collection.isEmpty()) { initErrorView(); hasMoreImages = false; return; } - if(gridAdapter == null) { + if (gridAdapter == null) { setAdapter(collection); } else { if (gridAdapter.containsAll(collection)) { diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java index f6bacfb51..f3ade09d8 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryItem.java @@ -3,7 +3,7 @@ package fr.free.nrw.commons.category; import android.os.Parcel; import android.os.Parcelable; -class CategoryItem implements Parcelable { +public class CategoryItem implements Parcelable { private final String name; private boolean selected; @@ -71,4 +71,9 @@ class CategoryItem implements Parcelable { public int hashCode() { return name.hashCode(); } + + @Override + public String toString() { + return "CategoryItem: '" + name + '\''; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryRendererAdapter.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryRendererAdapter.java new file mode 100644 index 000000000..887ad595f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryRendererAdapter.java @@ -0,0 +1,22 @@ +package fr.free.nrw.commons.category; + +import com.pedrogomez.renderers.AdapteeCollection; +import com.pedrogomez.renderers.RVRendererAdapter; +import com.pedrogomez.renderers.RendererBuilder; + +import java.util.ArrayList; + +public class CategoryRendererAdapter extends RVRendererAdapter { + CategoryRendererAdapter(RendererBuilder rendererBuilder, AdapteeCollection collection) { + super(rendererBuilder, collection); + } + + protected ArrayList allItems() { + int itemCount = getItemCount(); + ArrayList items = new ArrayList<>(itemCount); + for (int i = 0; i < itemCount; i++) { + items.add(getItem(i)); + } + return items; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java index ca4059e9d..988e992fa 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java @@ -109,4 +109,4 @@ public class GridViewAdapter extends ArrayAdapter { author.setVisibility(View.GONE); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/SubCategoryListFragment.java b/app/src/main/java/fr/free/nrw/commons/category/SubCategoryListFragment.java index ed21d011b..984d87fae 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/SubCategoryListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/SubCategoryListFragment.java @@ -73,7 +73,7 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment { categoryName = getArguments().getString("categoryName"); isParentCategory = getArguments().getBoolean("isParentCategory"); initSubCategoryList(); - if(getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ + if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); } else{ @@ -91,7 +91,7 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment { */ public void initSubCategoryList() { categoriesNotFoundView.setVisibility(GONE); - if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { + if (!NetworkUtils.isInternetConnectionEstablished(getContext())) { handleNoInternet(); return; } @@ -118,7 +118,7 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment { * @param subCategoryList */ private void handleSuccess(List subCategoryList) { - if(subCategoryList == null || subCategoryList.isEmpty()) { + if (subCategoryList == null || subCategoryList.isEmpty()) { initEmptyView(); } else { diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolService.java b/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolService.java index e0fd495ef..2cda95a7d 100644 --- a/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolService.java +++ b/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolService.java @@ -121,4 +121,4 @@ public class ThreadPoolService implements Executor { return new ThreadPoolService(this); } } -} \ No newline at end of file +} 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 9f5084e6a..cdbc8c28d 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 @@ -2,8 +2,11 @@ package fr.free.nrw.commons.contributions; import android.net.Uri; import android.os.Parcel; +import android.support.annotation.IntDef; import android.support.annotation.NonNull; +import android.support.annotation.StringDef; +import java.lang.annotation.Retention; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; @@ -13,6 +16,8 @@ import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.settings.Prefs; +import static java.lang.annotation.RetentionPolicy.SOURCE; + public class Contribution extends Media { public static Creator CREATOR = new Creator() { @@ -33,6 +38,10 @@ public class Contribution extends Media { public static final int STATE_QUEUED = 2; public static final int STATE_IN_PROGRESS = 3; + @Retention(SOURCE) + @StringDef({SOURCE_CAMERA, SOURCE_GALLERY, SOURCE_EXTERNAL}) + public @interface FileSource {} + public static final String SOURCE_CAMERA = "camera"; public static final String SOURCE_GALLERY = "gallery"; public static final String SOURCE_EXTERNAL = "external"; @@ -40,7 +49,6 @@ public class Contribution extends Media { private Uri contentUri; private String source; private String editSummary; - private Date timestamp; private int state; private long transferred; private String decimalCoords; @@ -48,14 +56,13 @@ public class Contribution extends Media { private String wikiDataEntityId; private Uri contentProviderUri; - public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp, + public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date dateCreated, int state, long dataLength, Date dateUploaded, long transferred, String source, String description, String creator, boolean isMultiple, int width, int height, String license) { - super(localUri, imageUrl, filename, description, dataLength, timestamp, dateUploaded, creator); + super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator); this.contentUri = contentUri; this.state = state; - this.timestamp = timestamp; this.transferred = transferred; this.source = source; this.isMultiple = isMultiple; @@ -69,14 +76,12 @@ public class Contribution extends Media { super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator); this.decimalCoords = decimalCoords; this.editSummary = editSummary; - timestamp = new Date(System.currentTimeMillis()); } public Contribution(Parcel in) { super(in); contentUri = in.readParcelable(Uri.class.getClassLoader()); source = in.readString(); - timestamp = (Date) in.readSerializable(); state = in.readInt(); transferred = in.readLong(); isMultiple = in.readInt() == 1; @@ -87,12 +92,13 @@ public class Contribution extends Media { super.writeToParcel(parcel, flags); parcel.writeParcelable(contentUri, flags); parcel.writeString(source); - parcel.writeSerializable(timestamp); parcel.writeInt(state); parcel.writeLong(transferred); parcel.writeInt(isMultiple ? 1 : 0); } + + public boolean getMultiple() { return isMultiple; } @@ -121,14 +127,6 @@ public class Contribution extends Media { this.contentUri = contentUri; } - public Date getTimestamp() { - return timestamp; - } - - public void setTimestamp(Date timestamp) { - this.timestamp = timestamp; - } - public int getState() { return state; } @@ -141,10 +139,6 @@ public class Contribution extends Media { this.dateUploaded = date; } - public String getTrackingTemplates() { - return "{{subst:unc}}"; // Remove when we have categorization - } - public String getPageContents() { StringBuilder buffer = new StringBuilder(); SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); @@ -169,8 +163,15 @@ public class Contribution extends Media { buffer.append("== {{int:license-header}} ==\n") .append(licenseTemplateFor(getLicense())).append("\n\n") - .append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n") - .append(getTrackingTemplates()); + .append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n"); + if(categories!=null&&categories.size()!=0) { + for (int i = 0; i < categories.size(); i++) { + String category = categories.get(i); + buffer.append("\n[[Category:").append(category).append("]]"); + } + } + else + buffer.append("{{subst:unc}}"); return buffer.toString(); } @@ -184,7 +185,7 @@ public class Contribution extends Media { } public Contribution() { - timestamp = new Date(System.currentTimeMillis()); + } public String getSource() { @@ -232,7 +233,7 @@ public class Contribution extends Media { /** * When the corresponding wikidata entity is known as in case of nearby uploads, it can be set * using the setter method - * @param wikiDataEntityId + * @param wikiDataEntityId wikiDataEntityId */ public void setWikiDataEntityId(String wikiDataEntityId) { this.wikiDataEntityId = wikiDataEntityId; 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 3aacb6155..c3a0f329b 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 @@ -5,22 +5,26 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.content.FileProvider; import java.io.File; +import java.util.ArrayList; import java.util.Date; import java.util.List; -import fr.free.nrw.commons.upload.ShareActivity; +import fr.free.nrw.commons.upload.UploadActivity; import timber.log.Timber; import static android.content.Intent.ACTION_GET_CONTENT; import static android.content.Intent.ACTION_SEND; +import static android.content.Intent.ACTION_SEND_MULTIPLE; import static android.content.Intent.EXTRA_STREAM; import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA; import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY; @@ -31,6 +35,7 @@ public class ContributionController { public static final int SELECT_FROM_GALLERY = 1; public static final int SELECT_FROM_CAMERA = 2; + public static final int PICK_IMAGE_MULTIPLE = 3; private Fragment fragment; @@ -79,6 +84,14 @@ public class ContributionController { } public void startGalleryPick() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + startMultipleGalleryPick(); + } else { + startSingleGalleryPick(); + } + } + + public void startSingleGalleryPick() { //FIXME: Starts gallery (opens Google Photos) Intent pickImageIntent = new Intent(ACTION_GET_CONTENT); pickImageIntent.setType("image/*"); @@ -87,15 +100,41 @@ public class ContributionController { Timber.d("Fragment is not added, startActivityForResult cannot be called"); return; } - Timber.d("startGalleryPick() called with pickImageIntent"); + Timber.d("startSingleGalleryPick() called with pickImageIntent"); fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY); } + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) + public void startMultipleGalleryPick() { + Intent pickImageIntent = new Intent(ACTION_GET_CONTENT); + pickImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + pickImageIntent.setType("image/*"); + if (!fragment.isAdded()) { + Timber.d("Fragment is not added, startActivityForResult cannot be called"); + return; + } + Timber.d("startMultipleGalleryPick() called with pickImageIntent"); + + fragment.startActivityForResult(pickImageIntent, PICK_IMAGE_MULTIPLE); + } + + public void handleImagesPicked(int requestCode, @Nullable ArrayList uri) { + FragmentActivity activity = fragment.getActivity(); + Intent shareIntent = new Intent(activity, UploadActivity.class); + shareIntent.setAction(ACTION_SEND_MULTIPLE); + shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY); + shareIntent.putExtra(EXTRA_STREAM, uri); + shareIntent.setType("image/jpeg"); + if (activity != null) { + activity.startActivity(shareIntent); + } + } + public void handleImagePicked(int requestCode, @Nullable Uri uri, boolean isDirectUpload, String wikiDataEntityId) { FragmentActivity activity = fragment.getActivity(); Timber.d("handleImagePicked() called with onActivityResult(). Boolean isDirectUpload: " + isDirectUpload + "String wikiDataEntityId: " + wikiDataEntityId); - Intent shareIntent = new Intent(activity, ShareActivity.class); + Intent shareIntent = new Intent(activity, UploadActivity.class); shareIntent.setAction(ACTION_SEND); switch (requestCode) { case SELECT_FROM_GALLERY: diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java index 6d290b1a5..4c83af393 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java @@ -98,7 +98,8 @@ public class ContributionDao { cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime()); } cv.put(Table.COLUMN_LENGTH, contribution.getDataLength()); - cv.put(Table.COLUMN_TIMESTAMP, contribution.getTimestamp().getTime()); + //This was always meant to store the date created..If somehow date created is not fetched while actually saving the contribution, lets save today's date + cv.put(Table.COLUMN_TIMESTAMP, contribution.getDateCreated()==null?System.currentTimeMillis():contribution.getDateCreated().getTime()); cv.put(Table.COLUMN_STATE, contribution.getState()); cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred()); cv.put(Table.COLUMN_SOURCE, contribution.getSource()); 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 a944cf866..3dc79a50b 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 @@ -15,10 +15,10 @@ class ContributionViewHolder { final ProgressBar progressView; ContributionViewHolder(View parent) { - imageView = (MediaWikiImageView) parent.findViewById(R.id.contributionImage); - titleView = (TextView)parent.findViewById(R.id.contributionTitle); - stateView = (TextView)parent.findViewById(R.id.contributionState); - seqNumView = (TextView)parent.findViewById(R.id.contributionSequenceNumber); - progressView = (ProgressBar)parent.findViewById(R.id.contributionProgress); + imageView = parent.findViewById(R.id.contributionImage); + titleView = parent.findViewById(R.id.contributionTitle); + stateView = parent.findViewById(R.id.contributionState); + seqNumView = parent.findViewById(R.id.contributionSequenceNumber); + progressView = parent.findViewById(R.id.contributionProgress); } } 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 deleted file mode 100644 index a903c6339..000000000 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java +++ /dev/null @@ -1,382 +0,0 @@ -package fr.free.nrw.commons.contributions; - -import android.accounts.Account; -import android.content.ComponentName; -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.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.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.Adapter; -import android.widget.AdapterView; - -import java.util.ArrayList; - -import javax.inject.Inject; -import javax.inject.Named; - -import butterknife.ButterKnife; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.HandlerService; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.AuthenticatedActivity; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.mwapi.MediaWikiApi; -import fr.free.nrw.commons.quiz.QuizChecker; -import fr.free.nrw.commons.settings.Prefs; -import fr.free.nrw.commons.upload.UploadService; -import fr.free.nrw.commons.utils.ContributionUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import timber.log.Timber; - -import static android.content.ContentResolver.requestSync; -import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; -import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS; -import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; -import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING; - -public class ContributionsActivity - extends AuthenticatedActivity - implements LoaderManager.LoaderCallbacks, - AdapterView.OnItemClickListener, - MediaDetailPagerFragment.MediaDetailProvider, - FragmentManager.OnBackStackChangedListener, - ContributionsListFragment.SourceRefresher { - - @Inject MediaWikiApi mediaWikiApi; - @Inject SessionManager sessionManager; - @Inject @Named("default_preferences") SharedPreferences prefs; - @Inject ContributionDao contributionDao; - - private Cursor allContributions; - private ContributionsListFragment contributionsList; - private MediaDetailPagerFragment mediaDetails; - private UploadService uploadService; - private boolean isUploadServiceConnected; - private ArrayList observersWaitingForLoad = new ArrayList<>(); - - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - - private ServiceConnection uploadServiceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName componentName, IBinder binder) { - uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder) - .getService(); - isUploadServiceConnected = true; - } - - @Override - public void onServiceDisconnected(ComponentName componentName) { - // this should never happen - Timber.e(new RuntimeException("UploadService died but the rest of the process did not!")); - } - }; - - @Override - protected void onDestroy() { - compositeDisposable.clear(); - getSupportFragmentManager().removeOnBackStackChangedListener(this); - super.onDestroy(); - if (isUploadServiceConnected) { - unbindService(uploadServiceConnection); - } - } - - @Override - protected void onResume() { - super.onResume(); - boolean isSettingsChanged = prefs.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false); - prefs.edit().putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false).apply(); - if (isSettingsChanged) { - refreshSource(); - } - } - - @Override - protected void onAuthCookieAcquired(String authCookie) { - // Do a sync everytime we get here! - requestSync(sessionManager.getCurrentAccount(), BuildConfig.CONTRIBUTION_AUTHORITY, new Bundle()); - Intent uploadServiceIntent = new Intent(this, UploadService.class); - uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); - startService(uploadServiceIntent); - bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE); - - allContributions = contributionDao.loadAllContributions(); - - getSupportLoaderManager().initLoader(0, null, this); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_contributions); - ButterKnife.bind(this); - - // Activity can call methods in the fragment by acquiring a - // reference to the Fragment from FragmentManager, using findFragmentById() - FragmentManager supportFragmentManager = getSupportFragmentManager(); - contributionsList = (ContributionsListFragment)supportFragmentManager - .findFragmentById(R.id.contributionsListFragment); - - supportFragmentManager.addOnBackStackChangedListener(this); - if (savedInstanceState != null) { - mediaDetails = (MediaDetailPagerFragment)supportFragmentManager - .findFragmentById(R.id.contributionsFragmentContainer); - - getSupportLoaderManager().initLoader(0, null, this); - } - - requestAuthToken(); - initDrawer(); - setTitle(getString(R.string.title_activity_contributions)); - - - if(checkAccount()) { - new QuizChecker(this, - sessionManager.getCurrentAccount().name, - mediaWikiApi); - } - if(!BuildConfig.FLAVOR.equalsIgnoreCase("beta")){ - setUploadCount(); - } - - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - boolean mediaDetailsVisible = mediaDetails != null && mediaDetails.isVisible(); - outState.putBoolean("mediaDetailsVisible", mediaDetailsVisible); - } - - /** - * Replace whatever is in the current contributionsFragmentContainer view with - * mediaDetailPagerFragment, and preserve previous state in back stack. - * Called when user selects a contribution. - */ - private void showDetail(int i) { - if (mediaDetails == null || !mediaDetails.isVisible()) { - mediaDetails = new MediaDetailPagerFragment(); - FragmentManager supportFragmentManager = getSupportFragmentManager(); - supportFragmentManager - .beginTransaction() - .replace(R.id.contributionsFragmentContainer, mediaDetails) - .addToBackStack(null) - .commit(); - supportFragmentManager.executePendingTransactions(); - } - mediaDetails.showImage(i); - } - - public void retryUpload(int i) { - allContributions.moveToPosition(i); - Contribution c = contributionDao.fromCursor(allContributions); - if (c.getState() == STATE_FAILED) { - uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c); - Timber.d("Restarting for %s", c.toString()); - } else { - Timber.d("Skipping re-upload for non-failed %s", c.toString()); - } - } - - public void deleteUpload(int i) { - allContributions.moveToPosition(i); - Contribution c = contributionDao.fromCursor(allContributions); - if (c.getState() == STATE_FAILED) { - Timber.d("Deleting failed contrib %s", c.toString()); - // If upload fails and then user decides to cancel upload at all, which means contribution - // object will be deleted. So we have to delete temp file for that contribution. - ContributionUtils.removeTemporaryFile(c.getLocalUri()); - contributionDao.delete(c); - } else { - Timber.d("Skipping deletion for non-failed contrib %s", c.toString()); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - if (mediaDetails.isVisible()) { - getSupportFragmentManager().popBackStack(); - } - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - protected void onAuthFailure() { - finish(); // If authentication failed, we just exit - } - - @Override - public void onItemClick(AdapterView adapterView, View view, int position, long item) { - showDetail(position); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - return super.onCreateOptionsMenu(menu); - } - - @Override - public Loader onCreateLoader(int i, Bundle bundle) { - int uploads = prefs.getInt(UPLOADS_SHOWING, 100); - return new CursorLoader(this, BASE_URI, - ALL_FIELDS, "", null, - ContributionDao.CONTRIBUTION_SORT + "LIMIT " + uploads); - } - - @Override - public void onLoadFinished(Loader cursorLoader, Cursor cursor) { - contributionsList.changeProgressBarVisibility(false); - - if (contributionsList.getAdapter() == null) { - contributionsList.setAdapter(new ContributionsListAdapter(getApplicationContext(), - cursor, 0, contributionDao)); - } else { - ((CursorAdapter) contributionsList.getAdapter()).swapCursor(cursor); - } - - if(contributionsList.getAdapter().getCount()>0){ - contributionsList.changeEmptyScreen(false); - } - contributionsList.clearSyncMessage(); - notifyAndMigrateDataSetObservers(); - } - - @Override - public void onLoaderReset(Loader cursorLoader) { - ((CursorAdapter) contributionsList.getAdapter()).swapCursor(null); - } - - //FIXME: Potential cause of wrong image display bug - @Override - public Media getMediaAtPosition(int i) { - if (contributionsList.getAdapter() == null) { - // not yet ready to return data - return null; - } else { - return contributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i)); - } - } - - @Override - public int getTotalMediaCount() { - if (contributionsList.getAdapter() == null) { - return 0; - } - return contributionsList.getAdapter().getCount(); - } - - @SuppressWarnings("ConstantConditions") - private void setUploadCount() { - compositeDisposable.add(mediaWikiApi - .getUploadCount(sessionManager.getCurrentAccount().name) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::displayUploadCount, - t -> Timber.e(t, "Fetching upload count failed") - )); - } - - private void displayUploadCount(Integer uploadCount) { - if (isFinishing() - || getSupportActionBar() == null - || getResources() == null) { - return; - } - - getSupportActionBar().setSubtitle(getResources() - .getQuantityString(R.plurals.contributions_subtitle, - uploadCount, uploadCount)); - } - - public void betaSetUploadCount(int betaUploadCount) { - displayUploadCount(betaUploadCount); - } - - - @Override - public void notifyDatasetChanged() { - // Do nothing for now - } - - private void notifyAndMigrateDataSetObservers() { - Adapter adapter = contributionsList.getAdapter(); - - // First, move the observers over to the adapter now that we have it. - for (DataSetObserver observer : observersWaitingForLoad) { - adapter.registerDataSetObserver(observer); - } - observersWaitingForLoad.clear(); - - // Now fire off a first notification... - for (DataSetObserver observer : observersWaitingForLoad) { - observer.onChanged(); - } - } - - @Override - public void registerDataSetObserver(DataSetObserver observer) { - Adapter adapter = contributionsList.getAdapter(); - if (adapter == null) { - observersWaitingForLoad.add(observer); - } else { - adapter.registerDataSetObserver(observer); - } - } - - @Override - public void unregisterDataSetObserver(DataSetObserver observer) { - Adapter adapter = contributionsList.getAdapter(); - if (adapter == null) { - observersWaitingForLoad.remove(observer); - } else { - adapter.unregisterDataSetObserver(observer); - } - } - - /** - * to ensure user is logged in - * @return - */ - private boolean checkAccount() { - Account currentAccount = sessionManager.getCurrentAccount(); - if (currentAccount == null) { - Timber.d("Current account is null"); - ViewUtil.showLongToast(this, getResources().getString(R.string.user_not_logged_in)); - sessionManager.forceLogin(this); - return false; - } - return true; - } - - @Override - public void onBackStackChanged() { - initBackButton(); - } - - @Override - public void refreshSource() { - getSupportLoaderManager().restartLoader(0, null, this); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java new file mode 100644 index 000000000..eea4e25d5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -0,0 +1,625 @@ +package fr.free.nrw.commons.contributions; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; + +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.app.LoaderManager; +import android.support.v4.widget.CursorAdapter; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Adapter; +import android.widget.AdapterView; + +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; + +import javax.inject.Inject; +import javax.inject.Named; + +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.HandlerService; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.location.LocationServiceManager; +import fr.free.nrw.commons.location.LocationUpdateListener; +import fr.free.nrw.commons.media.MediaDetailPagerFragment; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.nearby.NearbyController; +import fr.free.nrw.commons.nearby.NearbyNoificationCardView; +import fr.free.nrw.commons.nearby.Place; +import fr.free.nrw.commons.notification.NotificationController; +import fr.free.nrw.commons.notification.UnreadNotificationsCheckAsync; +import fr.free.nrw.commons.settings.Prefs; +import fr.free.nrw.commons.upload.UploadService; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; +import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS; +import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; +import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUEST; +import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING; +import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; + +public class ContributionsFragment + extends CommonsDaggerSupportFragment + implements LoaderManager.LoaderCallbacks, + AdapterView.OnItemClickListener, + MediaDetailPagerFragment.MediaDetailProvider, + FragmentManager.OnBackStackChangedListener, + ContributionsListFragment.SourceRefresher, + LocationUpdateListener + { + @Inject + @Named("default_preferences") + SharedPreferences prefs; + @Inject + ContributionDao contributionDao; + @Inject + MediaWikiApi mediaWikiApi; + @Inject + NotificationController notificationController; + @Inject + NearbyController nearbyController; + + private ArrayList observersWaitingForLoad = new ArrayList<>(); + private Cursor allContributions; + private UploadService uploadService; + private boolean isUploadServiceConnected; + private CompositeDisposable compositeDisposable = new CompositeDisposable(); + CountDownLatch waitForContributionsListFragment = new CountDownLatch(1); + + private ContributionsListFragment contributionsListFragment; + private MediaDetailPagerFragment mediaDetailPagerFragment; + public static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag"; + public static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag"; + + public NearbyNoificationCardView nearbyNoificationCardView; + private Disposable placesDisposable; + private LatLng curLatLng; + + private boolean firstLocationUpdate = true; + private LocationServiceManager locationManager; + + private boolean isFragmentAttachedBefore = false; + + + /** + * Since we will need to use parent activity on onAuthCookieAcquired, we have to wait + * fragment to be attached. Latch will be responsible for this sync. + */ + private ServiceConnection uploadServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName componentName, IBinder binder) { + uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder) + .getService(); + isUploadServiceConnected = true; + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + // this should never happen + Timber.e(new RuntimeException("UploadService died but the rest of the process did not!")); + } + }; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_contributions, container, false); + nearbyNoificationCardView = view.findViewById(R.id.card_view_nearby); + + if (savedInstanceState != null) { + mediaDetailPagerFragment = (MediaDetailPagerFragment)getChildFragmentManager().findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG); + contributionsListFragment = (ContributionsListFragment) getChildFragmentManager().findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG); + + if (savedInstanceState.getBoolean("mediaDetailsVisible")) { + setMediaDetailPagerFragment(); + } else { + setContributionsListFragment(); + } + } else { + setContributionsListFragment(); + } + + if(!BuildConfig.FLAVOR.equalsIgnoreCase("beta")){ + setUploadCount(); + } + + return view; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + /* + - There are some operations we need auth, so we need to make sure isAuthCookieAcquired. + - And since we use same retained fragment doesn't want to make all network operations + all over again on same fragment attached to recreated activity, we do this network + operations on first time fragment atached to an activity. Then they will be retained + until fragment life time ends. + */ + if (((MainActivity)getActivity()).isAuthCookieAcquired && !isFragmentAttachedBefore) { + onAuthCookieAcquired(((MainActivity)getActivity()).uploadServiceIntent); + isFragmentAttachedBefore = true; + new UnreadNotificationsCheckAsync((MainActivity) getActivity(), notificationController).execute(); + + } + } + + /** + * Replace FrameLayout with ContributionsListFragment, user will see contributions list. + * Creates new one if null. + */ + public void setContributionsListFragment() { + // show tabs on contribution list is visible + ((MainActivity)getActivity()).showTabs(); + // show nearby card view on contributions list is visible + if (nearbyNoificationCardView != null) { + if (prefs.getBoolean("displayNearbyCardView", true)) { + nearbyNoificationCardView.setVisibility(View.VISIBLE); + } else { + nearbyNoificationCardView.setVisibility(View.GONE); + } + } + + // Create if null + if (getContributionsListFragment() == null) { + contributionsListFragment = new ContributionsListFragment(); + } + FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); + // When this container fragment is created, we fill it with our ContributionsListFragment + transaction.replace(R.id.root_frame, contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG); + transaction.addToBackStack(CONTRIBUTION_LIST_FRAGMENT_TAG); + transaction.commit(); + getChildFragmentManager().executePendingTransactions(); + } + + /** + * Replace FrameLayout with MediaDetailPagerFragment, user will see details of selected media. + * Creates new one if null. + */ + public void setMediaDetailPagerFragment() { + // hide tabs on media detail view is visible + ((MainActivity)getActivity()).hideTabs(); + // hide nearby card view on media detail is visible + nearbyNoificationCardView.setVisibility(View.GONE); + + // Create if null + if (getMediaDetailPagerFragment() == null) { + mediaDetailPagerFragment = new MediaDetailPagerFragment(); + } + FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); + // When this container fragment is created, we fill it with our MediaDetailPagerFragment + //transaction.addToBackStack(null); + transaction.add(R.id.root_frame, mediaDetailPagerFragment, MEDIA_DETAIL_PAGER_FRAGMENT_TAG); + transaction.addToBackStack(MEDIA_DETAIL_PAGER_FRAGMENT_TAG); + transaction.commit(); + getChildFragmentManager().executePendingTransactions(); + + } + + /** + * Just getter method of ContributionsListFragment child of ContributionsFragment + * @return contributionsListFragment, if any created + */ + public ContributionsListFragment getContributionsListFragment() { + return contributionsListFragment; + } + + /** + * Just getter method of MediaDetailPagerFragment child of ContributionsFragment + * @return mediaDetailsFragment, if any created + */ + public MediaDetailPagerFragment getMediaDetailPagerFragment() { + return mediaDetailPagerFragment; + } + + @Override + public void onBackStackChanged() { + ((MainActivity)getActivity()).initBackButton(); + } + + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + int uploads = prefs.getInt(UPLOADS_SHOWING, 100); + return new CursorLoader(getActivity(), BASE_URI, //TODO find out the reason we pass activity here + ALL_FIELDS, "", null, + ContributionDao.CONTRIBUTION_SORT + "LIMIT " + uploads); + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + if (contributionsListFragment != null) { + contributionsListFragment.changeProgressBarVisibility(false); + + if (contributionsListFragment.getAdapter() == null) { + contributionsListFragment.setAdapter(new ContributionsListAdapter(getActivity().getApplicationContext(), + cursor, 0, contributionDao)); + } else { + ((CursorAdapter) contributionsListFragment.getAdapter()).swapCursor(cursor); + } + + contributionsListFragment.clearSyncMessage(); + notifyAndMigrateDataSetObservers(); + } + } + + @Override + public void onLoaderReset(Loader cursorLoader) { + ((CursorAdapter) contributionsListFragment.getAdapter()).swapCursor(null); + } + + private void notifyAndMigrateDataSetObservers() { + Adapter adapter = contributionsListFragment.getAdapter(); + + // First, move the observers over to the adapter now that we have it. + for (DataSetObserver observer : observersWaitingForLoad) { + adapter.registerDataSetObserver(observer); + } + observersWaitingForLoad.clear(); + + // Now fire off a first notification... + for (DataSetObserver observer : observersWaitingForLoad) { + observer.onChanged(); + } + } + + /** + * Called when onAuthCookieAcquired is called on authenticated parent activity + * @param uploadServiceIntent + */ + public void onAuthCookieAcquired(Intent uploadServiceIntent) { + // Since we call onAuthCookieAcquired method from onAttach, isAdded is still false. So don't use it + + if (getActivity() != null) { // If fragment is attached to parent activity + getActivity().bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE); + isUploadServiceConnected = true; + allContributions = contributionDao.loadAllContributions(); + getActivity().getSupportLoaderManager().initLoader(0, null, ContributionsFragment.this); + } + + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case LOCATION_REQUEST: { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Timber.d("Location permission granted, refreshing view"); + // No need to display permission request button anymore + nearbyNoificationCardView.displayPermissionRequestButton(false); + locationManager.registerLocationManager(); + } else { + // Still ask for permission + nearbyNoificationCardView.displayPermissionRequestButton(true); + } + } + break; + + default: + // This is needed to allow the request codes from the Fragments to be routed appropriately + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + @Override + public void onItemClick(AdapterView adapterView, View view, int i, long l) { + // show detail at a position + showDetail(i); + } + + + /** + * Replace whatever is in the current contributionsFragmentContainer view with + * mediaDetailPagerFragment, and preserve previous state in back stack. + * Called when user selects a contribution. + */ + private void showDetail(int i) { + if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) { + mediaDetailPagerFragment = new MediaDetailPagerFragment(); + setMediaDetailPagerFragment(); + } + mediaDetailPagerFragment.showImage(i); + } + + /** + * Retry upload when it is failed + * @param i position of upload which will be retried + */ + public void retryUpload(int i) { + allContributions.moveToPosition(i); + Contribution c = contributionDao.fromCursor(allContributions); + if (c.getState() == STATE_FAILED) { + uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c); + Timber.d("Restarting for %s", c.toString()); + } else { + Timber.d("Skipping re-upload for non-failed %s", c.toString()); + } + } + + /** + * Delete a failed upload attempt + * @param i position of upload attempt which will be deteled + */ + public void deleteUpload(int i) { + allContributions.moveToPosition(i); + Contribution c = contributionDao.fromCursor(allContributions); + if (c.getState() == STATE_FAILED) { + Timber.d("Deleting failed contrib %s", c.toString()); + contributionDao.delete(c); + } else { + Timber.d("Skipping deletion for non-failed contrib %s", c.toString()); + } + } + + @Override + public void refreshSource() { + getActivity().getSupportLoaderManager().restartLoader(0, null, this); + } + + @Override + public Media getMediaAtPosition(int i) { + if (contributionsListFragment.getAdapter() == null) { + // not yet ready to return data + return null; + } else { + return contributionDao.fromCursor((Cursor) contributionsListFragment.getAdapter().getItem(i)); + } + } + + @Override + public int getTotalMediaCount() { + if (contributionsListFragment.getAdapter() == null) { + return 0; + } + return contributionsListFragment.getAdapter().getCount(); + } + + @Override + public void notifyDatasetChanged() { + + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + Adapter adapter = contributionsListFragment.getAdapter(); + if (adapter == null) { + observersWaitingForLoad.add(observer); + } else { + adapter.registerDataSetObserver(observer); + } + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + Adapter adapter = contributionsListFragment.getAdapter(); + if (adapter == null) { + observersWaitingForLoad.remove(observer); + } else { + adapter.unregisterDataSetObserver(observer); + } + } + + @SuppressWarnings("ConstantConditions") + private void setUploadCount() { + + compositeDisposable.add(mediaWikiApi + .getUploadCount(((MainActivity)getActivity()).sessionManager.getCurrentAccount().name) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::displayUploadCount, + t -> Timber.e(t, "Fetching upload count failed") + )); + } + + private void displayUploadCount(Integer uploadCount) { + if (getActivity().isFinishing() + || getResources() == null) { + return; + } + + ((MainActivity)getActivity()).setNumOfUploads(uploadCount); + + } + + public void betaSetUploadCount(int betaUploadCount) { + displayUploadCount(betaUploadCount); + } + + /** + * Updates notification indicator on toolbar to indicate there are unread notifications + * @param isThereUnreadNotifications true if user checked notifications before last notification date + */ + public void updateNotificationsNotification(boolean isThereUnreadNotifications) { + ((MainActivity)getActivity()).updateNotificationIcon(isThereUnreadNotifications); + } + + @Override + public void onStart() { + super.onStart(); + } + + @Override + public void onPause() { + super.onPause(); + locationManager.removeLocationListener(this); + locationManager.unregisterLocationManager(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + boolean mediaDetailsVisible = mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible(); + outState.putBoolean("mediaDetailsVisible", mediaDetailsVisible); + } + + @Override + public void onResume() { + super.onResume(); + locationManager = new LocationServiceManager(getActivity()); + + firstLocationUpdate = true; + locationManager.addLocationListener(this); + + boolean isSettingsChanged = prefs.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false); + prefs.edit().putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false).apply(); + if (isSettingsChanged) { + refreshSource(); + } + + + if (prefs.getBoolean("displayNearbyCardView", true)) { + nearbyNoificationCardView.cardViewVisibilityState = NearbyNoificationCardView.CardViewVisibilityState.LOADING; + nearbyNoificationCardView.setVisibility(View.VISIBLE); + checkGPS(); + + } else { + // Hide nearby notification card view if related shared preferences is false + nearbyNoificationCardView.setVisibility(View.GONE); + } + + + } + + + /** + * Check GPS to decide displaying request permission button or not. + */ + private void checkGPS() { + if (!locationManager.isProviderEnabled()) { + Timber.d("GPS is not enabled"); + nearbyNoificationCardView.permissionType = NearbyNoificationCardView.PermissionType.ENABLE_GPS; + nearbyNoificationCardView.displayPermissionRequestButton(true); + } else { + Timber.d("GPS is enabled"); + checkLocationPermission(); + } + } + + private void checkLocationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (locationManager.isLocationPermissionGranted()) { + nearbyNoificationCardView.permissionType = NearbyNoificationCardView.PermissionType.NO_PERMISSION_NEEDED; + nearbyNoificationCardView.displayPermissionRequestButton(false); + locationManager.registerLocationManager(); + } else { + nearbyNoificationCardView.permissionType = NearbyNoificationCardView.PermissionType.ENABLE_LOCATION_PERMISSON; + nearbyNoificationCardView.displayPermissionRequestButton(true); + } + } else { + // If device is under Marshmallow, we already checked for GPS + nearbyNoificationCardView.permissionType = NearbyNoificationCardView.PermissionType.NO_PERMISSION_NEEDED; + nearbyNoificationCardView.displayPermissionRequestButton(false); + locationManager.registerLocationManager(); + } + } + + + private void updateClosestNearbyCardViewInfo() { + curLatLng = locationManager.getLastLocation(); + + placesDisposable = Observable.fromCallable(() -> nearbyController + .loadAttractionsFromLocation(curLatLng, true)) // thanks to boolean, it will only return closest result + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::updateNearbyNotification, + throwable -> { + Timber.d(throwable); + updateNearbyNotification(null); + }); + } + + private void updateNearbyNotification(@Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { + + if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null && nearbyPlacesInfo.placeList.size() > 0) { + Place closestNearbyPlace = nearbyPlacesInfo.placeList.get(0); + String distance = formatDistanceBetween(curLatLng, closestNearbyPlace.location); + closestNearbyPlace.setDistance(distance); + nearbyNoificationCardView.updateContent (true, closestNearbyPlace); + } else { + // Means that no close nearby place is found + nearbyNoificationCardView.updateContent (false, null); + } + } + + @Override + public void onDestroy() { + compositeDisposable.clear(); + getChildFragmentManager().removeOnBackStackChangedListener(this); + locationManager.unregisterLocationManager(); + locationManager.removeLocationListener(this); + // Try to prevent a possible NPE + locationManager.context = null; + super.onDestroy(); + + if (isUploadServiceConnected) { + if (getActivity() != null) { + getActivity().unbindService(uploadServiceConnection); + isUploadServiceConnected = false; + } + } + + if (placesDisposable != null) { + placesDisposable.dispose(); + } + } + + @Override + public void onLocationChangedSignificantly(LatLng latLng) { + // Will be called if location changed more than 1000 meter + // Do nothing on slight changes for using network efficiently + firstLocationUpdate = false; + updateClosestNearbyCardViewInfo(); + } + + @Override + public void onLocationChangedSlightly(LatLng latLng) { + /* Update closest nearby notification card onLocationChangedSlightly + If first time to update location after onResume, then no need to wait for significant + location change. Any closest location is better than no location + */ + if (firstLocationUpdate) { + updateClosestNearbyCardViewInfo(); + // Turn it to false, since it is not first location update anymore. To change closest location + // notifiction, we need to wait for a significant location change. + firstLocationUpdate = false; + } + } + + @Override + public void onLocationChangedMedium(LatLng latLng) { + // Update closest nearby card view if location changed more than 500 meters + updateClosestNearbyCardViewInfo(); + } +} + 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 a04c1120f..b238c8b8b 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 @@ -1,25 +1,31 @@ package fr.free.nrw.commons.contributions; +import android.content.ClipData; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.widget.FloatingActionButton; import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; +import android.util.Log; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; import android.widget.AdapterView; import android.widget.GridView; import android.widget.ListAdapter; import android.widget.ProgressBar; import android.widget.TextView; +import java.util.ArrayList; import java.util.Arrays; import javax.inject.Inject; @@ -30,7 +36,7 @@ import butterknife.ButterKnife; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.R; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.nearby.NearbyActivity; +import fr.free.nrw.commons.utils.PermissionUtils; import timber.log.Timber; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; @@ -38,6 +44,11 @@ import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; import static android.app.Activity.RESULT_OK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.view.View.GONE; +import static fr.free.nrw.commons.contributions.ContributionController.SELECT_FROM_GALLERY; + +/** + * Created by root on 01.06.2018. + */ public class ContributionsListFragment extends CommonsDaggerSupportFragment { @@ -47,100 +58,121 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { TextView waitingMessage; @BindView(R.id.loadingContributionsProgressBar) ProgressBar progressBar; + @BindView(R.id.fab_plus) + FloatingActionButton fabPlus; + @BindView(R.id.fab_camera) + FloatingActionButton fabCamera; + @BindView(R.id.fab_galery) + FloatingActionButton fabGalery; @BindView(R.id.noDataYet) TextView noDataYet; - @Inject - @Named("prefs") - SharedPreferences prefs; @Inject @Named("default_preferences") SharedPreferences defaultPrefs; - private ContributionController controller; + private Animation fab_close; + private Animation fab_open; + private Animation rotate_forward; + private Animation rotate_backward; - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.fragment_contributions, container, false); - ButterKnife.bind(this, v); + private boolean isFabOpen = false; + public ContributionController controller; - contributionsList.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity()); - if (savedInstanceState != null) { - Timber.d("Scrolling to %d", savedInstanceState.getInt("grid-position")); - contributionsList.setSelection(savedInstanceState.getInt("grid-position")); - } + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_contributions_list, container, false); + ButterKnife.bind(this, view); - //TODO: Should this be in onResume? - String lastModified = prefs.getString("lastSyncTimestamp", ""); - Timber.d("Last Sync Timestamp: %s", lastModified); - - if (lastModified.equals("")) { - waitingMessage.setVisibility(View.VISIBLE); - } else { - waitingMessage.setVisibility(GONE); - } + contributionsList.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment()); changeEmptyScreen(true); changeProgressBarVisibility(true); - return v; + return view; } - public ListAdapter getAdapter() { - return contributionsList.getAdapter(); - } - - public void setAdapter(ListAdapter adapter) { - this.contributionsList.setAdapter(adapter); - - if(BuildConfig.FLAVOR.equalsIgnoreCase("beta")){ - ((ContributionsActivity) getActivity()).betaSetUploadCount(adapter.getCount()); + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (controller == null) { + controller = new ContributionController(this); } + controller.loadState(savedInstanceState); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (controller != null) { + controller.saveState(outState); + } else { + controller = new ContributionController(this); + } + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + initializeAnimations(); + setListeners(); } public void changeEmptyScreen(boolean isEmpty){ this.noDataYet.setVisibility(isEmpty ? View.VISIBLE : View.GONE); } - public void changeProgressBarVisibility(boolean isVisible) { - this.progressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE); + private void initializeAnimations() { + fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open); + fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close); + rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward); + rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward); } - @Override - public void onSaveInstanceState(Bundle outState) { - if (outState == null) { - outState = new Bundle(); - } - super.onSaveInstanceState(outState); - controller.saveState(outState); - outState.putInt("grid-position", contributionsList.getFirstVisiblePosition()); - } + private void setListeners() { - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - //FIXME: must get the file data for Google Photos when receive the intent answer, in the onActivityResult method - super.onActivityResult(requestCode, resultCode, data); - - if (resultCode == RESULT_OK) { - Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", - requestCode, resultCode, data); - if (requestCode == ContributionController.SELECT_FROM_CAMERA) { - // If coming from camera, pass null as uri. Because camera photos get saved to a - // fixed directory - controller.handleImagePicked(requestCode, null, false, null); - } else { - controller.handleImagePicked(requestCode, data.getData(), false, null); + fabPlus.setOnClickListener(view -> animateFAB(isFabOpen)); + fabCamera.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) { + // Here, thisActivity is the current activity + if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + if (shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { + // Show an explanation to the user *asynchronously* -- don't block + // this thread waiting for the user's response! After the user + // sees the explanation, try again to request the permission. + new AlertDialog.Builder(getParentFragment().getActivity()) + .setMessage(getString(R.string.write_storage_permission_rationale)) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + getActivity().requestPermissions + (new String[]{WRITE_EXTERNAL_STORAGE}, PermissionUtils.CAMERA_PERMISSION_FROM_CONTRIBUTION_LIST); + dialog.dismiss(); + }) + .setNegativeButton(android.R.string.cancel, null) + .create() + .show(); + } else { + // No explanation needed, we can request the permission. + requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, + 3); + // MY_PERMISSIONS_WRITE_EXTERNAL_STORAGE is an + // app-defined int constant. The callback method gets the + // result of the request. + } + } else { + controller.startCameraCapture(); + } + } else { + controller.startCameraCapture(); + } } - } else { - Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", - requestCode, resultCode, data); - } - } + }); - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_from_gallery: + fabGalery.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { //Gallery crashes before reach ShareActivity screen so must implement permissions check here if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { @@ -156,10 +188,11 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { // this thread waiting for the user's response! After the user // sees the explanation, try again to request the permission. - new AlertDialog.Builder(getActivity()) + new AlertDialog.Builder(getParentFragment().getActivity()) .setMessage(getString(R.string.read_storage_permission_rationale)) .setPositiveButton(android.R.string.ok, (dialog, which) -> { - requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, 1); + getActivity().requestPermissions + (new String[]{READ_EXTERNAL_STORAGE}, PermissionUtils.GALLERY_PERMISSION_FROM_CONTRIBUTION_LIST); dialog.dismiss(); }) .setNegativeButton(android.R.string.cancel, null) @@ -179,59 +212,64 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { } } else { controller.startGalleryPick(); - return true; } } else { controller.startGalleryPick(); - return true; } + } + }); + } - return true; - case R.id.menu_from_camera: - boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) { - // Here, thisActivity is the current activity - if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - if (shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { - // Show an explanation to the user *asynchronously* -- don't block - // this thread waiting for the user's response! After the user - // sees the explanation, try again to request the permission. - new AlertDialog.Builder(getActivity()) - .setMessage(getString(R.string.write_storage_permission_rationale)) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 3); - dialog.dismiss(); - }) - .setNegativeButton(android.R.string.cancel, null) - .create() - .show(); - } else { - // No explanation needed, we can request the permission. - requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, - 3); - // MY_PERMISSIONS_WRITE_EXTERNAL_STORAGE is an - // app-defined int constant. The callback method gets the - // result of the request. - } - } else { - controller.startCameraCapture(); - return true; - } - } else { - controller.startCameraCapture(); - return true; - } - return true; - default: - return super.onOptionsItemSelected(item); + private void animateFAB(boolean isFabOpen) { + this.isFabOpen = !isFabOpen; + if (fabPlus.isShown()){ + if (isFabOpen) { + fabPlus.startAnimation(rotate_backward); + fabCamera.startAnimation(fab_close); + fabGalery.startAnimation(fab_close); + fabCamera.hide(); + fabGalery.hide(); + } else { + fabPlus.startAnimation(rotate_forward); + fabCamera.startAnimation(fab_open); + fabGalery.startAnimation(fab_open); + fabCamera.show(); + fabGalery.show(); + } + this.isFabOpen=!isFabOpen; } } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { + public void onAttach(Context context) { + super.onAttach(context); + ContributionsFragment parentFragment = (ContributionsFragment)getParentFragment(); + parentFragment.waitForContributionsListFragment.countDown(); + } + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK) { + Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", + requestCode, resultCode, data); + if (requestCode == ContributionController.SELECT_FROM_CAMERA) { + // If coming from camera, pass null as uri. Because camera photos get saved to a + // fixed directory + controller.handleImagePicked(requestCode, null, false, null); + } else if (requestCode == ContributionController.PICK_IMAGE_MULTIPLE) { + handleMultipleImages(requestCode, data); + } else if (requestCode == ContributionController.SELECT_FROM_GALLERY){ + controller.handleImagePicked(requestCode, data.getData(), false, null); + } + } else { + Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", + requestCode, resultCode, data); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); Timber.d("onRequestPermissionsResult: req code = " + " perm = " + Arrays.toString(permissions) + " grant =" + Arrays.toString(grantResults)); @@ -246,11 +284,12 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { break; // 2 = Location allowed when 'nearby places' selected case 2: { - if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) { + // TODO: understand and fix + /*if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) { Timber.d("Location permission granted"); - Intent nearbyIntent = new Intent(getActivity(), NearbyActivity.class); + Intent nearbyIntent = new Intent(getActivity(), MainActivity.class); startActivity(nearbyIntent); - } + }*/ } break; case 3: { @@ -262,42 +301,60 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { } } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - menu.clear(); // See http://stackoverflow.com/a/8495697/17865 - inflater.inflate(R.menu.fragment_contributions_list, menu); + private void handleMultipleImages(int requestCode, Intent data) { + if (getContext() == null) { + return; + } - if (!deviceHasCamera()) { - menu.findItem(R.id.menu_from_camera).setEnabled(false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + && data.getClipData() != null) { + ClipData mClipData = data.getClipData(); + ArrayList mArrayUri = new ArrayList(); + for (int i = 0; i < mClipData.getItemCount(); i++) { + + ClipData.Item item = mClipData.getItemAt(i); + Uri uri = item.getUri(); + mArrayUri.add(uri); + } + Log.v("LOG_TAG", "Selected Images" + mArrayUri.size()); + controller.handleImagesPicked(requestCode, mArrayUri); + } else if(data.getData() != null) { + controller.handleImagePicked(SELECT_FROM_GALLERY, data.getData(), false, null); } } - public boolean deviceHasCamera() { - PackageManager pm = getContext().getPackageManager(); - return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) || - pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - controller = new ContributionController(this); - setHasOptionsMenu(true); - } - - @Override - public void onDestroy() { - super.onDestroy(); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - controller.loadState(savedInstanceState); + + /** + * Responsible to set progress bar invisible and visible + * @param isVisible True when contributions list should be hidden. + */ + public void changeProgressBarVisibility(boolean isVisible) { + this.progressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE); } + /** + * Clears sync message displayed with progress bar before contributions list became visible + */ protected void clearSyncMessage() { waitingMessage.setVisibility(GONE); + noDataYet.setVisibility(GONE); + } + + public ListAdapter getAdapter() { + return contributionsList.getAdapter(); + } + + /** + * Sets adapter to contributions list. If beta mode, sets upload count for beta explicitly. + * @param adapter List adapter for uploads of contributor + */ + public void setAdapter(ListAdapter adapter) { + this.contributionsList.setAdapter(adapter); + + if(BuildConfig.FLAVOR.equalsIgnoreCase("beta")){ + //TODO: add betaSetUploadCount method + ((ContributionsFragment) getParentFragment()).betaSetUploadCount(adapter.getCount()); + } } public interface SourceRefresher { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java new file mode 100644 index 000000000..e3ed7f18e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -0,0 +1,550 @@ +package fr.free.nrw.commons.contributions; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.ViewPager; + +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.ImageView; + + +import javax.inject.Inject; +import javax.inject.Named; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.auth.AuthenticatedActivity; +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.location.LocationServiceManager; +import fr.free.nrw.commons.nearby.NearbyFragment; +import fr.free.nrw.commons.nearby.NearbyMapFragment; +import fr.free.nrw.commons.notification.NotificationActivity; +import fr.free.nrw.commons.theme.NavigationBaseActivity; +import fr.free.nrw.commons.upload.UploadService; +import fr.free.nrw.commons.utils.PermissionUtils; +import fr.free.nrw.commons.utils.ViewUtil; +import timber.log.Timber; + +import static android.content.ContentResolver.requestSync; +import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUEST; + +public class MainActivity extends AuthenticatedActivity implements FragmentManager.OnBackStackChangedListener { + + @Inject + SessionManager sessionManager; + @BindView(R.id.tab_layout) + TabLayout tabLayout; + @BindView(R.id.pager) + public UnswipableViewPager viewPager; + @Inject + public LocationServiceManager locationManager; + @Inject + @Named("default_preferences") + public SharedPreferences prefs; + + + public Intent uploadServiceIntent; + public boolean isAuthCookieAcquired = false; + + public ContributionsActivityPagerAdapter contributionsActivityPagerAdapter; + public final int CONTRIBUTIONS_TAB_POSITION = 0; + public final int NEARBY_TAB_POSITION = 1; + + public boolean isContributionsFragmentVisible = true; // False means nearby fragment is visible + private Menu menu; + private boolean isThereUnreadNotifications = false; + + private boolean onOrientationChanged = false; + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_contributions); + ButterKnife.bind(this); + + requestAuthToken(); + initDrawer(); + setTitle(getString(R.string.navigation_item_home)); // Should I create a new string variable with another name instead? + + if (savedInstanceState != null ) { + onOrientationChanged = true; // Will be used in nearby fragment to determine significant update of map + + //If nearby map was visible, call on Tab Selected to call all nearby operations + if (savedInstanceState.getInt("viewPagerCurrentItem") == 1) { + ((NearbyFragment)contributionsActivityPagerAdapter.getItem(1)).onTabSelected(onOrientationChanged); + } + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt("viewPagerCurrentItem", viewPager.getCurrentItem()); + } + + @Override + protected void onAuthCookieAcquired(String authCookie) { + // Do a sync everytime we get here! + requestSync(sessionManager.getCurrentAccount(), BuildConfig.CONTRIBUTION_AUTHORITY, new Bundle()); + uploadServiceIntent = new Intent(this, UploadService.class); + uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); + startService(uploadServiceIntent); + + addTabsAndFragments(); + isAuthCookieAcquired = true; + if (contributionsActivityPagerAdapter.getItem(0) != null) { + ((ContributionsFragment)contributionsActivityPagerAdapter.getItem(0)).onAuthCookieAcquired(uploadServiceIntent); + } + } + + private void addTabsAndFragments() { + contributionsActivityPagerAdapter = new ContributionsActivityPagerAdapter(getSupportFragmentManager()); + viewPager.setAdapter(contributionsActivityPagerAdapter); + + tabLayout.addTab(tabLayout.newTab().setText(getResources().getString(R.string.contributions_fragment))); + tabLayout.addTab(tabLayout.newTab().setText(getResources().getString(R.string.nearby_fragment))); + + // Set custom view to add nearby info icon next to text + View nearbyTabLinearLayout = LayoutInflater.from(this).inflate(R.layout.custom_nearby_tab_layout, null); + View nearbyInfoPopupWindowLayout = LayoutInflater.from(this).inflate(R.layout.nearby_info_popup_layout, null); + ImageView nearbyInfo = nearbyTabLinearLayout.findViewById(R.id.nearby_info_image); + tabLayout.getTabAt(1).setCustomView(nearbyTabLinearLayout); + + nearbyInfo.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + /*new AlertDialog.Builder(MainActivity.this) + .setTitle(R.string.title_activity_nearby) + .setMessage(R.string.showcase_view_whole_nearby_activity) + .setCancelable(true) + .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel()) + .create() + .show();*/ + String popupText = getResources().getString(R.string.showcase_view_whole_nearby_activity); + ViewUtil.displayPopupWindow(nearbyInfo, MainActivity.this, nearbyInfoPopupWindowLayout, popupText); + } + }); + + if (uploadServiceIntent != null) { + // If auth cookie already acquired notify contrib fragmnet so that it san operate auth required actions + ((ContributionsFragment)contributionsActivityPagerAdapter.getItem(CONTRIBUTIONS_TAB_POSITION)).onAuthCookieAcquired(uploadServiceIntent); + } + setTabAndViewPagerSynchronisation(); + } + + /** + * Adds number of uploads next to tab text "Contributions" then it will look like + * "Contributions (NUMBER)" + * @param uploadCount + */ + public void setNumOfUploads(int uploadCount) { + tabLayout.getTabAt(0).setText(getResources().getString(R.string.contributions_fragment) +" "+ getResources() + .getQuantityString(R.plurals.contributions_subtitle, + uploadCount, uploadCount)); + } + + /** + * Normally tab layout and view pager has no relation, which means when you swipe view pager + * tab won't change and vice versa. So we have to notify each of them. + */ + private void setTabAndViewPagerSynchronisation() { + //viewPager.canScrollHorizontally(false); + viewPager.setFocusableInTouchMode(true); + + viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + + } + + @Override + public void onPageSelected(int position) { + switch (position) { + case CONTRIBUTIONS_TAB_POSITION: + Timber.d("Contributions tab selected"); + tabLayout.getTabAt(CONTRIBUTIONS_TAB_POSITION).select(); + isContributionsFragmentVisible = true; + updateMenuItem(); + + break; + case NEARBY_TAB_POSITION: + Timber.d("Nearby tab selected"); + tabLayout.getTabAt(NEARBY_TAB_POSITION).select(); + isContributionsFragmentVisible = false; + updateMenuItem(); + // Do all permission and GPS related tasks on tab selected, not on create + ((NearbyFragment)contributionsActivityPagerAdapter.getItem(1)).onTabSelected(onOrientationChanged); + + break; + default: + tabLayout.getTabAt(CONTRIBUTIONS_TAB_POSITION).select(); + break; + } + } + + @Override + public void onPageScrollStateChanged(int state) { + + } + }); + + tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + viewPager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + + } + }); + } + + public void hideTabs() { + changeDrawerIconToBakcButton(); + if (tabLayout != null) { + tabLayout.setVisibility(View.GONE); + } + } + + public void showTabs() { + changeDrawerIconToDefault(); + if (tabLayout != null) { + tabLayout.setVisibility(View.VISIBLE); + } + } + + @Override + protected void onAuthFailure() { + + } + + @Override + public void onBackPressed() { + String contributionsFragmentTag = ((ContributionsActivityPagerAdapter) viewPager.getAdapter()).makeFragmentName(R.id.pager, 0); + String nearbyFragmentTag = ((ContributionsActivityPagerAdapter) viewPager.getAdapter()).makeFragmentName(R.id.pager, 1); + if (getSupportFragmentManager().findFragmentByTag(contributionsFragmentTag) != null && isContributionsFragmentVisible) { + // Meas that contribution fragment is visible (not nearby fragment) + ContributionsFragment contributionsFragment = (ContributionsFragment) getSupportFragmentManager().findFragmentByTag(contributionsFragmentTag); + + if (contributionsFragment.getChildFragmentManager().findFragmentByTag(ContributionsFragment.MEDIA_DETAIL_PAGER_FRAGMENT_TAG) != null) { + // Means that media details fragment is visible to uer instead of contributions list fragment (As chils fragment) + // Then we want to go back to contributions list fragment on backbutton pressed from media detail fragment + contributionsFragment.getChildFragmentManager().popBackStack(); + // Tabs were invisible when Media Details Fragment is active, make them visible again on Contrib List Fragment active + showTabs(); + // Nearby Notification Card View was invisible when Media Details Fragment is active, make it visible again on Contrib List Fragment active, according to preferences + if (prefs.getBoolean("displayNearbyCardView", true)) { + contributionsFragment.nearbyNoificationCardView.setVisibility(View.VISIBLE); + } else { + contributionsFragment.nearbyNoificationCardView.setVisibility(View.GONE); + } + } else { + finish(); + } + } else if (getSupportFragmentManager().findFragmentByTag(nearbyFragmentTag) != null && !isContributionsFragmentVisible) { + // Meas that nearby fragment is visible (not contributions fragment) + // Set current item to contributions activity instead of closing the activity + viewPager.setCurrentItem(0); + } else { + super.onBackPressed(); + } + } + + @Override + public void onBackStackChanged() { + initBackButton(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.contribution_activity_notification_menu, menu); + + if (!isThereUnreadNotifications) { + // TODO: used vectors are not compatible with API 19 and below, change them + menu.findItem(R.id.notifications).setIcon(ContextCompat.getDrawable(this, R.drawable.ic_notification_white_clip_art)); + } else { + menu.findItem(R.id.notifications).setIcon(ContextCompat.getDrawable(this, R.drawable.ic_notification_white_clip_art_dot)); + } + + this.menu = menu; + + updateMenuItem(); + + return true; + } + + /** + * Responsible with displaying required menu items according to displayed fragment. + * Notifications icon when contributions list is visible, list sheet icon when nearby is visible + */ + private void updateMenuItem() { + if (menu != null) { + if (isContributionsFragmentVisible) { + // Display notifications menu item + menu.findItem(R.id.notifications).setVisible(true); + menu.findItem(R.id.list_sheet).setVisible(false); + Timber.d("Contributions activity notifications menu item is visible"); + } else { + // Display bottom list menu item + menu.findItem(R.id.notifications).setVisible(false); + menu.findItem(R.id.list_sheet).setVisible(true); + Timber.d("Contributions activity list sheet menu item is visible"); + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.notifications: + // Starts notification activity on click to notification icon + NavigationBaseActivity.startActivityWithFlags(this, NotificationActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); + finish(); + return true; + + case R.id.list_sheet: + if (contributionsActivityPagerAdapter.getItem(1) != null) { + ((NearbyFragment)contributionsActivityPagerAdapter.getItem(1)).listOptionMenuIteClicked(); + } + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private boolean deviceHasCamera() { + PackageManager pm = getPackageManager(); + return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) || + pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); + } + + /** + * Updte notification icon if there is an unread notification + * @param isThereUnreadNotifications true if user didn't visit notifications activity since + * latest notification came to account + */ + public void updateNotificationIcon(boolean isThereUnreadNotifications) { + if (!isThereUnreadNotifications) { + this.isThereUnreadNotifications = false; + menu.findItem(R.id.notifications).setIcon(ContextCompat.getDrawable(this, R.drawable.ic_notification_white_clip_art)); + } else { + this.isThereUnreadNotifications = true; + menu.findItem(R.id.notifications).setIcon(ContextCompat.getDrawable(this, R.drawable.ic_notification_white_clip_art_dot)); + } + } + + public class ContributionsActivityPagerAdapter extends FragmentPagerAdapter { + FragmentManager fragmentManager; + private boolean isContributionsListFragment = true; // to know what to put in first tab, Contributions of Media Details + + + public ContributionsActivityPagerAdapter(FragmentManager fragmentManager) { + super(fragmentManager); + this.fragmentManager = fragmentManager; + } + + @Override + public int getCount() { + return 2; + } + + /* + * Do not use getItem method to access fragments on pager adapter. User reference vairables + * instead. + * */ + @Override + public Fragment getItem(int position) { + switch (position){ + case 0: + ContributionsFragment retainedContributionsFragment = getContributionsFragment(0); + if (retainedContributionsFragment != null) { + /** + * ContributionsFragment is parent of ContributionsListFragment and + * MediaDetailsFragment. If below decides which child will be visible. + */ + if (isContributionsListFragment) { + retainedContributionsFragment.setContributionsListFragment(); + } else { + retainedContributionsFragment.setMediaDetailPagerFragment(); + } + return retainedContributionsFragment; + } else { + // If we reach here, retainedContributionsFragment is null + return new ContributionsFragment(); + + } + + case 1: + NearbyFragment retainedNearbyFragment = getNearbyFragment(1); + if (retainedNearbyFragment != null) { + return retainedNearbyFragment; + } else { + // If we reach here, retainedNearbyFragment is null + return new NearbyFragment(); + } + default: + return null; + } + } + + /** + * Generates fragment tag with makeFragmentName method to get retained contributions fragment + * @param position index of tabs, in our case 0 or 1 + * @return + */ + private ContributionsFragment getContributionsFragment(int position) { + String tag = makeFragmentName(R.id.pager, position); + return (ContributionsFragment)fragmentManager.findFragmentByTag(tag); + } + + /** + * Generates fragment tag with makeFragmentName method to get retained nearby fragment + * @param position index of tabs, in our case 0 or 1 + * @return + */ + private NearbyFragment getNearbyFragment(int position) { + String tag = makeFragmentName(R.id.pager, position); + return (NearbyFragment)fragmentManager.findFragmentByTag(tag); + } + + /** + * A simple hack to use retained fragment when getID is called explicitly, if we don't use + * this method, a new fragment will be initialized on each explicit calls of getID + * @param viewId id of view pager + * @param index index of tabs, in our case 0 or 1 + * @return + */ + public String makeFragmentName(int viewId, int index) { + return "android:switcher:" + viewId + ":" + index; + } + + /** + * In first tab we can have ContributionsFragment or Media details fragment. This method + * is responsible to update related boolean + * @param isContributionsListFragment true when contribution fragment should be visible, false + * means user clicked to MediaDetails + */ + private void updateContributionFragmentTabContent(boolean isContributionsListFragment) { + this.isContributionsListFragment = isContributionsListFragment; + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + ContributionsListFragment contributionsListFragment = + (ContributionsListFragment) contributionsActivityPagerAdapter + .getItem(0).getChildFragmentManager() + .findFragmentByTag(ContributionsFragment.CONTRIBUTION_LIST_FRAGMENT_TAG); + contributionsListFragment.onActivityResult(requestCode, resultCode, data); + } + + @Override + public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + switch (requestCode) { + case LOCATION_REQUEST: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Timber.d("Location permission given"); + } else { + // If nearby fragment is visible and location permission is not given, send user back to contrib fragment + if (!isContributionsFragmentVisible) { + viewPager.setCurrentItem(CONTRIBUTIONS_TAB_POSITION); + + // TODO: If contrib fragment is visible and location permission is not given, display permission request button + } else { + + } + } + return; + } + // Storage permission for gallery + case PermissionUtils.GALLERY_PERMISSION_FROM_CONTRIBUTION_LIST: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Storage permission given + ContributionsListFragment contributionsListFragment = + (ContributionsListFragment) contributionsActivityPagerAdapter + .getItem(0).getChildFragmentManager() + .findFragmentByTag(ContributionsFragment.CONTRIBUTION_LIST_FRAGMENT_TAG); + contributionsListFragment.controller.startGalleryPick(); + } + return; + } + + case PermissionUtils.CAMERA_PERMISSION_FROM_CONTRIBUTION_LIST: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Storage permission given + ContributionsListFragment contributionsListFragment = + (ContributionsListFragment) contributionsActivityPagerAdapter + .getItem(0).getChildFragmentManager() + .findFragmentByTag(ContributionsFragment.CONTRIBUTION_LIST_FRAGMENT_TAG); + contributionsListFragment.controller.startCameraCapture(); + } + return; + } + + case PermissionUtils.CAMERA_PERMISSION_FROM_NEARBY_MAP: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Storage permission given + NearbyMapFragment nearbyMapFragment = + ((NearbyFragment) contributionsActivityPagerAdapter + .getItem(1)).nearbyMapFragment; + nearbyMapFragment.controller.startCameraCapture(); + } + return; + } + + case PermissionUtils.GALLERY_PERMISSION_FROM_NEARBY_MAP: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Storage permission given + NearbyMapFragment nearbyMapFragment = + ((NearbyFragment) contributionsActivityPagerAdapter + .getItem(1)).nearbyMapFragment; + nearbyMapFragment.controller.startGalleryPick(); + } + return; + } + + default: + return; + } + } + + @Override + protected void onDestroy() { + locationManager.unregisterLocationManager(); + // Remove ourself from hashmap to prevent memory leaks + locationManager = null; + super.onDestroy(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/UnswipableViewPager.java b/app/src/main/java/fr/free/nrw/commons/contributions/UnswipableViewPager.java new file mode 100644 index 000000000..46c8f3502 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/contributions/UnswipableViewPager.java @@ -0,0 +1,30 @@ +package fr.free.nrw.commons.contributions; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; + +public class UnswipableViewPager extends ViewPager{ + public UnswipableViewPager(@NonNull Context context) { + super(context); + } + + public UnswipableViewPager(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + // Unswipable + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Unswipable + return false; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java index f35a6c38e..81a1e0aba 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java @@ -182,4 +182,4 @@ public class DeleteTask extends AsyncTask { .setPriority(PRIORITY_HIGH); notificationManager.notify(NOTIFICATION_DELETE, notificationBuilder.build()); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java index d26bae178..6ae133fa3 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -9,14 +9,13 @@ import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.SignupActivity; import fr.free.nrw.commons.bookmarks.BookmarksActivity; import fr.free.nrw.commons.category.CategoryDetailsActivity; +import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.category.CategoryImagesActivity; -import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.explore.SearchActivity; -import fr.free.nrw.commons.nearby.NearbyActivity; + import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.settings.SettingsActivity; -import fr.free.nrw.commons.upload.MultipleShareActivity; -import fr.free.nrw.commons.upload.ShareActivity; +import fr.free.nrw.commons.upload.UploadActivity; @Module @SuppressWarnings({"WeakerAccess", "unused"}) @@ -29,13 +28,7 @@ public abstract class ActivityBuilderModule { abstract WelcomeActivity bindWelcomeActivity(); @ContributesAndroidInjector - abstract ShareActivity bindShareActivity(); - - @ContributesAndroidInjector - abstract MultipleShareActivity bindMultipleShareActivity(); - - @ContributesAndroidInjector - abstract ContributionsActivity bindContributionsActivity(); + abstract MainActivity bindContributionsActivity(); @ContributesAndroidInjector abstract SettingsActivity bindSettingsActivity(); @@ -46,15 +39,15 @@ public abstract class ActivityBuilderModule { @ContributesAndroidInjector abstract SignupActivity bindSignupActivity(); - @ContributesAndroidInjector - abstract NearbyActivity bindNearbyActivity(); - @ContributesAndroidInjector abstract NotificationActivity bindNotificationActivity(); @ContributesAndroidInjector abstract CategoryImagesActivity bindFeaturedImagesActivity(); + @ContributesAndroidInjector + abstract UploadActivity bindUploadActivity(); + @ContributesAndroidInjector abstract SearchActivity bindSearchActivity(); diff --git a/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java b/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java index 2faf09b83..87a0d9da0 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java @@ -90,4 +90,4 @@ public class ApplicationlessInjection return instance; } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index e6a61f307..8c019da5d 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -1,10 +1,17 @@ package fr.free.nrw.commons.di; +import android.app.Activity; import android.content.ContentProviderClient; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.v4.util.LruCache; +import android.view.inputmethod.InputMethodManager; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import javax.inject.Named; import javax.inject.Singleton; @@ -12,12 +19,14 @@ import javax.inject.Singleton; import dagger.Module; import dagger.Provides; import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.nearby.NearbyPlaces; +import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.upload.UploadController; import fr.free.nrw.commons.wikidata.WikidataEditListener; import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl; @@ -38,6 +47,35 @@ public class CommonsApplicationModule { return this.applicationContext; } + @Provides + public InputMethodManager provideInputMethodManager() { + return (InputMethodManager) applicationContext.getSystemService(Activity.INPUT_METHOD_SERVICE); + } + + @Provides + @Named("licenses") + public List provideLicenses(Context context) { + List licenseItems = new ArrayList<>(); + licenseItems.add(context.getString(R.string.license_name_cc0)); + licenseItems.add(context.getString(R.string.license_name_cc_by)); + licenseItems.add(context.getString(R.string.license_name_cc_by_sa)); + licenseItems.add(context.getString(R.string.license_name_cc_by_four)); + licenseItems.add(context.getString(R.string.license_name_cc_by_sa_four)); + return licenseItems; + } + + @Provides + @Named("licenses_by_name") + public Map provideLicensesByName(Context context) { + Map byName = new HashMap<>(); + byName.put(context.getString(R.string.license_name_cc0), Prefs.Licenses.CC0); + byName.put(context.getString(R.string.license_name_cc_by), Prefs.Licenses.CC_BY_3); + byName.put(context.getString(R.string.license_name_cc_by_sa), Prefs.Licenses.CC_BY_SA_3); + byName.put(context.getString(R.string.license_name_cc_by_four), Prefs.Licenses.CC_BY_4); + byName.put(context.getString(R.string.license_name_cc_by_sa_four), Prefs.Licenses.CC_BY_SA_4); + return byName; + } + @Provides public AccountUtil providesAccountUtil(Context context) { return new AccountUtil(context); @@ -120,6 +158,17 @@ public class CommonsApplicationModule { return context.getSharedPreferences("direct_nearby_upload_prefs", MODE_PRIVATE); } + /** + * Is used to determine when user is viewed notifications activity last + * @param context + * @return date of lastReadNotificationDate + */ + @Provides + @Named("last_read_notification_date") + public SharedPreferences providesLastReadNotificationDatePreferences(Context context) { + return context.getSharedPreferences("last_read_notification_date", MODE_PRIVATE); + } + @Provides public UploadController providesUploadController(SessionManager sessionManager, @Named("default_preferences") SharedPreferences sharedPreferences, Context context) { return new UploadController(sessionManager, context, sharedPreferences); @@ -173,4 +222,4 @@ public class CommonsApplicationModule { public boolean provideIsBetaVariant() { return BuildConfig.FLAVOR.equals("beta"); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java index 9f06725de..9287e1bfc 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java @@ -40,4 +40,4 @@ public abstract class CommonsDaggerAppCompatActivity extends AppCompatActivity i activityInjector.inject(this); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java index 3c4cb9914..10fa74a9b 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java @@ -28,4 +28,4 @@ public abstract class CommonsDaggerBroadcastReceiver extends BroadcastReceiver { serviceInjector.inject(this); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java index 38506c4ca..06adee489 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java @@ -29,4 +29,4 @@ public abstract class CommonsDaggerContentProvider extends ContentProvider { serviceInjector.inject(this); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java index 995c517a1..41f661db4 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java @@ -29,4 +29,4 @@ public abstract class CommonsDaggerIntentService extends IntentService { serviceInjector.inject(this); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java index dc6c10b9f..0d045d2ce 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java @@ -28,4 +28,4 @@ public abstract class CommonsDaggerService extends Service { serviceInjector.inject(this); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java index 8c33e7a98..3f2556466 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java @@ -62,4 +62,4 @@ public abstract class CommonsDaggerSupportFragment extends Fragment implements H throw new IllegalArgumentException(String.format("No injector was found for %s", getClass().getCanonicalName())); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java index 9868f3576..b664d49e1 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -4,29 +4,25 @@ import dagger.Module; import dagger.android.ContributesAndroidInjector; import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; -import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.category.CategoryImagesListFragment; import fr.free.nrw.commons.category.SubCategoryListFragment; +import fr.free.nrw.commons.contributions.ContributionsFragment; import fr.free.nrw.commons.contributions.ContributionsListFragment; import fr.free.nrw.commons.explore.categories.SearchCategoryFragment; import fr.free.nrw.commons.explore.images.SearchImageFragment; import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment; import fr.free.nrw.commons.media.MediaDetailFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment; +import fr.free.nrw.commons.nearby.NearbyFragment; import fr.free.nrw.commons.nearby.NearbyListFragment; import fr.free.nrw.commons.nearby.NearbyMapFragment; import fr.free.nrw.commons.nearby.NoPermissionsFragment; import fr.free.nrw.commons.settings.SettingsFragment; -import fr.free.nrw.commons.upload.MultipleUploadListFragment; -import fr.free.nrw.commons.upload.SingleUploadFragment; @Module @SuppressWarnings({"WeakerAccess", "unused"}) public abstract class FragmentBuilderModule { - @ContributesAndroidInjector - abstract CategorizationFragment bindCategorizationFragment(); - @ContributesAndroidInjector abstract ContributionsListFragment bindContributionsListFragment(); @@ -48,12 +44,6 @@ public abstract class FragmentBuilderModule { @ContributesAndroidInjector abstract SettingsFragment bindSettingsFragment(); - @ContributesAndroidInjector - abstract MultipleUploadListFragment bindMultipleUploadListFragment(); - - @ContributesAndroidInjector - abstract SingleUploadFragment bindSingleUploadFragment(); - @ContributesAndroidInjector abstract CategoryImagesListFragment bindFeaturedImagesListFragment(); @@ -69,6 +59,12 @@ public abstract class FragmentBuilderModule { @ContributesAndroidInjector abstract RecentSearchesFragment bindRecentSearchesFragment(); + @ContributesAndroidInjector + abstract ContributionsFragment bindContributionsFragment(); + + @ContributesAndroidInjector + abstract NearbyFragment bindNearbyFragment(); + @ContributesAndroidInjector abstract BookmarkPicturesFragment bindBookmarkPictureListFragment(); diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.java index 7f2b3ff93..520c55a57 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.java @@ -95,7 +95,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment { public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false); ButterKnife.bind(this, rootView); - if(getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ + if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); } else{ @@ -124,7 +124,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment { public void updateCategoryList(String query) { this.query = query; categoriesNotFoundView.setVisibility(GONE); - if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { + if (!NetworkUtils.isInternetConnectionEstablished(getContext())) { handleNoInternet(); return; } @@ -173,7 +173,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment { */ private void handleSuccess(List mediaList) { queryList = mediaList; - if(mediaList == null || mediaList.isEmpty()) { + if (mediaList == null || mediaList.isEmpty()) { initErrorView(); } else { diff --git a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java index 7c7f1cdd9..8b28f50d5 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.explore.images; +import android.annotation.SuppressLint; import android.content.SharedPreferences; import android.content.res.Configuration; import android.os.Bundle; @@ -97,7 +98,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment { public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false); ButterKnife.bind(this, rootView); - if(getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ + if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ imagesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); } else{ @@ -123,12 +124,13 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment { * Checks for internet connection and then initializes the recycler view with 25 images of the searched query * Clearing imageAdapter every time new keyword is searched so that user can see only new results */ + @SuppressLint("CheckResult") public void updateImageList(String query) { this.query = query; - if(imagesNotFoundView != null) { + if (imagesNotFoundView != null) { imagesNotFoundView.setVisibility(GONE); } - if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { + if (!NetworkUtils.isInternetConnectionEstablished(getContext())) { handleNoInternet(); return; } @@ -146,6 +148,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment { /** * Adds more results to existing search results */ + @SuppressLint("CheckResult") public void addImagesToList(String query) { this.query = query; progressBar.setVisibility(View.VISIBLE); @@ -163,13 +166,11 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment { */ private void handlePaginationSuccess(List mediaList) { progressBar.setVisibility(View.GONE); - if (mediaList.size()!=0){ - if (!queryList.get(queryList.size()-1).getFilename().equals(mediaList.get(mediaList.size()-1).getFilename())) { - queryList.addAll(mediaList); - imagesAdapter.addAll(mediaList); - imagesAdapter.notifyDataSetChanged(); - ((SearchActivity)getContext()).viewPagerNotifyDataSetChanged(); - } + if (mediaList.size() != 0 || !queryList.get(queryList.size() - 1).getFilename().equals(mediaList.get(mediaList.size() - 1).getFilename())) { + queryList.addAll(mediaList); + imagesAdapter.addAll(mediaList); + imagesAdapter.notifyDataSetChanged(); + ((SearchActivity) getContext()).viewPagerNotifyDataSetChanged(); } } @@ -182,7 +183,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment { */ private void handleSuccess(List mediaList) { queryList = mediaList; - if(mediaList == null || mediaList.isEmpty()) { + if (mediaList == null || mediaList.isEmpty()) { initErrorView(); } else { diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearch.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearch.java index ed1aa1d6b..ed6fb903a 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearch.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearch.java @@ -68,4 +68,4 @@ public class RecentSearch { this.contentUri = contentUri; } -} \ No newline at end of file +} 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 4a137beed..c58aa1e10 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 @@ -26,12 +26,13 @@ public class LocationServiceManager implements LocationListener { private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 2 * 60 * 100; private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 10; - private Context context; + public Context context; private LocationManager locationManager; private Location lastLocation; + //private Location lastLocationDuplicate; // Will be used for nearby card view on contributions activity private final List locationListeners = new CopyOnWriteArrayList<>(); private boolean isLocationManagerRegistered = false; - private Set locationExplanationDisplayed = new HashSet<>(); + public Set locationExplanationDisplayed = new HashSet<>(); /** * Constructs a new instance of LocationServiceManager. @@ -253,15 +254,25 @@ public class LocationServiceManager implements LocationListener { @Override public void onLocationChanged(Location location) { + Timber.d("on location changed"); if (isBetterLocation(location, lastLocation) .equals(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) { lastLocation = location; + //lastLocationDuplicate = location; for (LocationUpdateListener listener : locationListeners) { listener.onLocationChangedSignificantly(LatLng.from(lastLocation)); } - } else if (isBetterLocation(location, lastLocation) + } else if (location.distanceTo(lastLocation) >= 500) { + // Update nearby notification card at every 500 meters. + for (LocationUpdateListener listener : locationListeners) { + listener.onLocationChangedMedium(LatLng.from(lastLocation)); + } + } + + else if (isBetterLocation(location, lastLocation) .equals(LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) { lastLocation = location; + //lastLocationDuplicate = location; for (LocationUpdateListener listener : locationListeners) { listener.onLocationChangedSlightly(LatLng.from(lastLocation)); } @@ -286,6 +297,7 @@ public class LocationServiceManager implements LocationListener { public enum LocationChangeType{ LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving + LOCATION_MEDIUM_CHANGED, //Between slight and significant changes, will be used for nearby card view updates. LOCATION_NOT_CHANGED, PERMISSION_JUST_GRANTED, MAP_UPDATED diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java index f3e920e18..61ff26b11 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.location; public interface LocationUpdateListener { - void onLocationChangedSignificantly(LatLng latLng); - void onLocationChangedSlightly(LatLng latLng); + void onLocationChangedSignificantly(LatLng latLng); // Will be used to update all nearby markers on the map + void onLocationChangedSlightly(LatLng latLng); // Will be used to track users motion + void onLocationChangedMedium(LatLng latLng); // Will be used updating nearby card view notification } diff --git a/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java b/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java index 407014db5..7f4bb2ab7 100644 --- a/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java +++ b/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java @@ -42,4 +42,4 @@ public class CommonsLogSender extends LogsSender { return builder.toString(); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java b/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java index ce8174382..8630af4da 100644 --- a/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java +++ b/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java @@ -141,4 +141,4 @@ public class FileLoggingTree extends Timber.DebugTree implements LogLevelSettabl LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); logger.addAppender(rollingFileAppender); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java b/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java index 83b8854cc..5eeca6d3e 100644 --- a/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java +++ b/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java @@ -5,4 +5,4 @@ package fr.free.nrw.commons.logging; */ public interface LogLevelSettableTree { void setLogLevel(int logLevel); -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java b/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java index 21b133aa2..bdf07d392 100644 --- a/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java @@ -16,10 +16,10 @@ public final class LogUtils { * @return */ public static String getLogDirectory(boolean isBeta) { - if(isBeta) { + if (isBeta) { return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/beta"; } else { return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/prod"; } } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java b/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java index 16c40cf44..59ddb2523 100644 --- a/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java +++ b/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java @@ -180,4 +180,4 @@ public abstract class LogsSender implements ReportSender { zos.flush(); zos.close(); } -} \ No newline at end of file +} 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 ffac70170..bf649474e 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 @@ -49,6 +49,7 @@ import fr.free.nrw.commons.MediaWikiImageView; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.category.CategoryDetailsActivity; +import fr.free.nrw.commons.contributions.ContributionsFragment; import fr.free.nrw.commons.delete.DeleteTask; import fr.free.nrw.commons.delete.ReasonBuilder; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; @@ -156,7 +157,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity(); + detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) (getParentFragment().getParentFragment()); if (savedInstanceState != null) { editable = savedInstanceState.getBoolean("editable"); @@ -222,12 +223,14 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { }; view.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener); locale = getResources().getConfiguration().locale; + return view; } @Override public void onResume() { super.onResume(); + ((ContributionsFragment)(getParentFragment().getParentFragment())).nearbyNoificationCardView.setVisibility(View.GONE); media = detailProvider.getMediaAtPosition(index); if (media == null) { // Ask the detail provider to ping us when we're ready @@ -271,7 +274,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { @Override protected Boolean doInBackground(Void... voids) { // Local files have no filename yet - if(media.getFilename() == null) { + if (media.getFilename() == null) { return Boolean.FALSE; } try { @@ -343,7 +346,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { } rebuildCatList(); - if(media.getCreator() == null || media.getCreator().equals("")) { + if (media.getCreator() == null || media.getCreator().equals("")) { authorLayout.setVisibility(GONE); } else { author.setText(media.getCreator()); @@ -357,7 +360,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { if (!TextUtils.isEmpty(licenseLink(media))) { openWebBrowser(licenseLink(media)); } else { - if(isCategoryImage) { + if (isCategoryImage) { Timber.d("Unable to fetch license URL for %s", media.getLicense()); } else { Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT); @@ -427,14 +430,14 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { @OnClick(R.id.seeMore) public void onSeeMoreClicked(){ - if(nominatedForDeletion.getVisibility()== VISIBLE) { + if (nominatedForDeletion.getVisibility()== VISIBLE) { openWebBrowser(media.getFilePageTitle().getMobileUri().toString()); } } private void enableDeleteButton(boolean visibility) { delete.setEnabled(visibility); - if(visibility) { + if (visibility) { delete.setTextColor(getResources().getColor(R.color.primaryTextColor)); } else { delete.setTextColor(getResources().getColor(R.color.deleteButtonLight)); @@ -444,15 +447,26 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { private void rebuildCatList() { categoryContainer.removeAllViews(); // @fixme add the category items - for (String cat : categoryNames) { - View catLabel = buildCatLabel(cat, categoryContainer); + + //As per issue #1826(see https://github.com/commons-app/apps-android-commons/issues/1826), some categories come suffixed with strings prefixed with |. As per the discussion + //that was meant for alphabetical sorting of the categories and can be safely removed. + for (int i = 0; i < categoryNames.size(); i++) { + String categoryName = categoryNames.get(i); + //Removed everything after '|' + int indexOfPipe = categoryName.indexOf('|'); + if (indexOfPipe != -1) { + categoryName = categoryName.substring(0, indexOfPipe); + //Set the updated category to the list as well + categoryNames.set(i, categoryName); + } + View catLabel = buildCatLabel(categoryName, categoryContainer); categoryContainer.addView(catLabel); } } private View buildCatLabel(final String catName, ViewGroup categoryContainer) { final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false); - final CompatTextView textView = (CompatTextView) item.findViewById(R.id.mediaDetailCategoryItemText); + final CompatTextView textView = item.findViewById(R.id.mediaDetailCategoryItemText); textView.setText(catName); if (categoriesLoaded && categoriesPresent) { 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 23d3f2466..7caa9454a 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 @@ -38,7 +38,6 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao; import fr.free.nrw.commons.category.CategoryDetailsActivity; import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.explore.SearchActivity; import fr.free.nrw.commons.mwapi.MediaWikiApi; @@ -99,7 +98,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple pager.setAdapter(adapter); pager.setCurrentItem(pageNumber, false); - if(getActivity() == null) { + if (getActivity() == null) { Timber.d("Returning as activity is destroyed!"); return; } @@ -133,11 +132,11 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple @Override public boolean onOptionsItemSelected(MenuItem item) { - if(getActivity() == null) { + if (getActivity() == null) { Timber.d("Returning as activity is destroyed!"); return true; } - MediaDetailProvider provider = (MediaDetailProvider) getActivity(); + MediaDetailProvider provider = (MediaDetailProvider) getParentFragment(); Media m = provider.getMediaAtPosition(pager.getCurrentItem()); switch (item.getItemId()) { case R.id.menu_bookmark_current_image: @@ -156,7 +155,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple viewIntent.setAction(ACTION_VIEW); viewIntent.setData(m.getFilePageTitle().getMobileUri()); //check if web browser available - if(viewIntent.resolveActivity(getActivity().getPackageManager()) != null){ + if (viewIntent.resolveActivity(getActivity().getPackageManager()) != null){ startActivity(viewIntent); } else { Toast toast = Toast.makeText(getContext(), getString(R.string.no_web_browser), LENGTH_SHORT); @@ -174,12 +173,12 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple return true; case R.id.menu_retry_current_image: // Retry - ((ContributionsActivity) getActivity()).retryUpload(pager.getCurrentItem()); + //((MainActivity) getActivity()).retryUpload(pager.getCurrentItem()); getActivity().getSupportFragmentManager().popBackStack(); return true; case R.id.menu_cancel_current_image: // todo: delete image - ((ContributionsActivity) getActivity()).deleteUpload(pager.getCurrentItem()); + //((MainActivity) getActivity()).deleteUpload(pager.getCurrentItem()); getActivity().getSupportFragmentManager().popBackStack(); return true; default: @@ -193,7 +192,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple * @param media */ private void setWallpaper(Media media) { - if(media.getImageUrl() == null || media.getImageUrl().isEmpty()) { + if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) { Timber.d("Media URL not present"); return; } @@ -254,7 +253,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple menu.clear(); // see http://stackoverflow.com/a/8495697/17865 inflater.inflate(R.menu.fragment_image_detail, menu); if (pager != null) { - MediaDetailProvider provider = (MediaDetailProvider) getActivity(); + MediaDetailProvider provider = (MediaDetailProvider) getParentFragment(); if(provider == null) { return; } @@ -326,7 +325,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple @Override public void onPageScrolled(int i, float v, int i2) { - if(getActivity() == null) { + if(getParentFragment().getActivity() == null) { Timber.d("Returning as activity is destroyed!"); return; } @@ -347,7 +346,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple e.printStackTrace(); } } - getActivity().supportInvalidateOptionsMenu(); + getParentFragment().getActivity().supportInvalidateOptionsMenu(); } @Override @@ -381,22 +380,22 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple public Fragment getItem(int i) { if (i == 0) { // See bug https://code.google.com/p/android/issues/detail?id=27526 - if(getActivity() == null) { + if(getParentFragment().getActivity() == null) { Timber.d("Skipping getItem. Returning as activity is destroyed!"); return null; } - pager.postDelayed(() -> getActivity().supportInvalidateOptionsMenu(), 5); + pager.postDelayed(() -> getParentFragment().getActivity().supportInvalidateOptionsMenu(), 5); } return MediaDetailFragment.forMedia(i, editable, isFeaturedImage); } @Override public int getCount() { - if(getActivity() == null) { + if (getActivity() == null) { Timber.d("Skipping getCount. Returning as activity is destroyed!"); return 0; } - return ((MediaDetailProvider) getActivity()).getTotalMediaCount(); + return ((MediaDetailProvider) getParentFragment()).getTotalMediaCount(); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index eeb343da7..2e1cbb1ac 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -244,9 +244,9 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { Timber.d("Central auth token isn't valid. Trying to fetch a fresh token"); api.removeAllCookies(); String loginResultCode = login(AccountUtil.getUserName(context), AccountUtil.getPassword(context)); - if(loginResultCode.equals("PASS")) { + if (loginResultCode.equals("PASS")) { return getCentralAuthToken(); - } else if(loginResultCode.equals("2FA")) { + } else if (loginResultCode.equals("2FA")) { Timber.e("Cannot refresh session for 2FA enabled user. Login required"); } else { Timber.e("Error occurred in refreshing session. Error code is %s", loginResultCode); diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/CustomApiResult.java b/app/src/main/java/fr/free/nrw/commons/mwapi/CustomApiResult.java index a36e8e0fd..9eb96a164 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/CustomApiResult.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/CustomApiResult.java @@ -117,4 +117,4 @@ public class CustomApiResult { return null; } } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/CustomMwApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/CustomMwApi.java index 61fc7cf71..fa7ffb8e7 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/CustomMwApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/CustomMwApi.java @@ -58,7 +58,7 @@ public class CustomMwApi { } public String getAuthCookie() { - if(authCookie == null){ + if (authCookie == null){ authCookie = ""; List cookies = client.getCookieStore().getCookies(); for(Cookie cookie: cookies) { @@ -102,14 +102,14 @@ public class CustomMwApi { } public String getUserID() throws IOException { - if(this.userID == null || this.userID.equals("0")) { + if (this.userID == null || this.userID.equals("0")) { this.validateLogin(); } return userID; } public String getUserName() throws IOException { - if(this.userID == null || this.userID.equals("0")) { + if (this.userID == null || this.userID.equals("0")) { this.validateLogin(); } return userName; @@ -122,7 +122,7 @@ public class CustomMwApi { String token = tokenData.getString("/api/login/@token"); CustomApiResult confirmData = this.action("login").param("lgname", username).param("lgpassword", password).param("lgtoken", token).post(); String finalResult = confirmData.getString("/api/login/@result"); - if(finalResult.equals("Success")) { + if (finalResult.equals("Success")) { isLoggedIn = true; } return finalResult; @@ -149,7 +149,7 @@ public class CustomMwApi { .data("comment", comment) .data("filename", filename) .sendProgressListener(uploadProgressListener); - if(length != -1) { + if (length != -1) { builder.file("file", filename, file, length); } else { builder.file("file", filename, file); @@ -177,4 +177,4 @@ public class CustomMwApi { return CustomApiResult.fromRequestBuilder(builder, client); } } -; \ No newline at end of file +; diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java index ca25fccf3..5d82b5492 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java @@ -4,9 +4,11 @@ import android.os.Build; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; +import android.util.Log; import fr.free.nrw.commons.R; import fr.free.nrw.commons.contributions.ContributionController; +import fr.free.nrw.commons.utils.PermissionUtils; import timber.log.Timber; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; @@ -24,8 +26,9 @@ class DirectUpload { } // These permission requests will be handled by the Fragments. - // Do not use requestCode 1 as it will conflict with NearbyActivity's requestCodes + // Do not use requestCode 1 as it will conflict with NearbyFragment's requestCodes void initiateGalleryUpload() { + Log.d("deneme7","initiateGalleryUpload"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (ContextCompat.checkSelfPermission(fragment.getActivity(), READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED) { if (fragment.shouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE)) { @@ -33,40 +36,42 @@ class DirectUpload { .setMessage(fragment.getActivity().getString(R.string.read_storage_permission_rationale)) .setPositiveButton(android.R.string.ok, (dialog, which) -> { Timber.d("Requesting permissions for read external storage"); - fragment.requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, 4); + fragment.getActivity().requestPermissions + (new String[]{READ_EXTERNAL_STORAGE}, PermissionUtils.GALLERY_PERMISSION_FROM_NEARBY_MAP); dialog.dismiss(); }) .setNegativeButton(android.R.string.cancel, null) .create() .show(); } else { - fragment.requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, - 4); + fragment.getActivity().requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, PermissionUtils.GALLERY_PERMISSION_FROM_NEARBY_MAP); } } else { - controller.startGalleryPick(); + controller.startSingleGalleryPick(); } } else { - controller.startGalleryPick(); + controller.startSingleGalleryPick(); } } void initiateCameraUpload() { + Log.d("deneme7","initiateCameraUpload"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (ContextCompat.checkSelfPermission(fragment.getActivity(), WRITE_EXTERNAL_STORAGE) != PERMISSION_GRANTED) { if (fragment.shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { new AlertDialog.Builder(fragment.getActivity()) .setMessage(fragment.getActivity().getString(R.string.write_storage_permission_rationale)) .setPositiveButton(android.R.string.ok, (dialog, which) -> { - fragment.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 5); + fragment.getActivity().requestPermissions + (new String[]{WRITE_EXTERNAL_STORAGE}, PermissionUtils.CAMERA_PERMISSION_FROM_NEARBY_MAP); dialog.dismiss(); }) .setNegativeButton(android.R.string.cancel, null) .create() .show(); } else { - fragment.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 5); + fragment.getActivity().requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, PermissionUtils.CAMERA_PERMISSION_FROM_NEARBY_MAP); } } else { controller.startCameraCapture(); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapterFactory.java index 3adc8e529..628ed1add 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapterFactory.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapterFactory.java @@ -45,4 +45,4 @@ public class NearbyAdapterFactory { rendererAdapter.notifyDataSetChanged(); rendererAdapter.diffUpdate(newPlaceList); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index 39deee7ad..a84e86218 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -39,6 +39,7 @@ public class NearbyController { this.prefs = prefs; } + /** * Prepares Place list to make their distance information update later. * @@ -46,15 +47,15 @@ public class NearbyController { * @return NearbyPlacesInfo a variable holds Place list without distance information * and boundary coordinates of current Place List */ - public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) throws IOException { - + public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng, boolean returnClosestResult) throws IOException { Timber.d("Loading attractions near %s", curLatLng); NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo(); if (curLatLng == null) { + Timber.d("Loading attractions neari, but curLatLng is null"); return null; } - List places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage()); + List places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage(), returnClosestResult); if (null != places && places.size() > 0) { LatLng[] boundaryCoordinates = {places.get(0).location, // south @@ -168,7 +169,7 @@ public class NearbyController { } public class NearbyPlacesInfo { - List placeList; // List of nearby places - LatLng[] boundaryCoordinates; // Corners of nearby area + public List placeList; // List of nearby places + public LatLng[] boundaryCoordinates; // Corners of nearby area } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java similarity index 65% rename from app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java rename to app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java index 2143496f0..5cafedcbc 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java @@ -5,20 +5,19 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.graphics.Typeface; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Handler; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.design.widget.BottomSheetBehavior; +import android.support.design.widget.Snackbar; 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.LayoutInflater; + import android.view.View; +import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.ProgressBar; @@ -33,11 +32,11 @@ import javax.inject.Named; import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.contributions.MainActivity; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType; import fr.free.nrw.commons.location.LocationUpdateListener; -import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.UriSerializer; import fr.free.nrw.commons.utils.ViewUtil; @@ -47,23 +46,18 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; -import uk.co.deanwild.materialshowcaseview.IShowcaseListener; -import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView; import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED; import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED; import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED; - -public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener, - WikidataEditListener.WikidataP18EditListener { - - private static final int LOCATION_REQUEST = 1; +public class NearbyFragment extends CommonsDaggerSupportFragment + implements LocationUpdateListener, + WikidataEditListener.WikidataP18EditListener { @BindView(R.id.progressBar) ProgressBar progressBar; - @BindView(R.id.bottom_sheet) LinearLayout bottomSheet; @BindView(R.id.bottom_sheet_details) @@ -77,59 +71,127 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp LocationServiceManager locationManager; @Inject NearbyController nearbyController; - @Inject WikidataEditListener wikidataEditListener; - @Inject - @Named("application_preferences") SharedPreferences applicationPrefs; - private LatLng curLatLng; - private Bundle bundle; - private Disposable placesDisposable; - private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed - private BottomSheetBehavior bottomSheetBehavior; // Behavior for list bottom sheet - private BottomSheetBehavior bottomSheetBehaviorForDetails; // Behavior for details bottom sheet + WikidataEditListener wikidataEditListener; + @Inject + @Named("application_preferences") + SharedPreferences applicationPrefs; + public NearbyMapFragment nearbyMapFragment; private NearbyListFragment nearbyListFragment; private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName(); private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName(); - private View listButton; // Reference to list button to use in tutorial + private Bundle bundle; + private BottomSheetBehavior bottomSheetBehavior; // Behavior for list bottom sheet + private BottomSheetBehavior bottomSheetBehaviorForDetails; // Behavior for details bottom sheet + + private LatLng curLatLng; + private Disposable placesDisposable; + private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed + public View view; + private Snackbar snackbar; + + private LatLng lastKnownLocation; private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; private BroadcastReceiver broadcastReceiver; - private boolean isListShowcaseAdded = false; - private boolean isMapShowCaseAdded = false; - - private LatLng lastKnownLocation; - - private MaterialShowcaseView secondSingleShowCaseView; + private boolean onOrientationChanged = false; @Override - protected void onCreate(Bundle savedInstanceState) { + public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_nearby); - ButterKnife.bind(this); - resumeFragment(); - bundle = new Bundle(); - - initBottomSheetBehaviour(); - initDrawer(); - wikidataEditListener.setAuthenticationStateListener(this); + setRetainInstance(true); } + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_nearby, container, false); + ButterKnife.bind(this, view); + + /*// Resume the fragment if exist + resumeFragment();*/ + bundle = new Bundle(); + initBottomSheetBehaviour(); + wikidataEditListener.setAuthenticationStateListener(this); + this.view = view; + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + if (savedInstanceState != null) { + onOrientationChanged = true; + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); + } + } + + /** + * Hide or expand bottom sheet according to states of all sheets + */ + public void listOptionMenuIteClicked() { + if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_COLLAPSED || bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_HIDDEN){ + bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + }else if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_EXPANDED){ + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + + } + + /** + * Resume fragments if they exists + */ private void resumeFragment() { // Find the retained fragment on activity restarts nearbyMapFragment = getMapFragment(); nearbyListFragment = getListFragment(); } + /** + * Returns the map fragment added to child fragment manager previously, if exists. + */ + private NearbyMapFragment getMapFragment() { + return (NearbyMapFragment) getChildFragmentManager().findFragmentByTag(TAG_RETAINED_MAP_FRAGMENT); + } + + private void removeMapFragment() { + if (nearbyMapFragment != null) { + android.support.v4.app.FragmentManager fm = getFragmentManager(); + fm.beginTransaction().remove(nearbyMapFragment).commit(); + nearbyMapFragment = null; + } + } + + + /** + * Returns the list fragment added to child fragment manager previously, if exists. + */ + private NearbyListFragment getListFragment() { + return (NearbyListFragment) getChildFragmentManager().findFragmentByTag(TAG_RETAINED_LIST_FRAGMENT); + } + + private void removeListFragment() { + if (nearbyListFragment != null) { + android.support.v4.app.FragmentManager fm = getFragmentManager(); + fm.beginTransaction().remove(nearbyListFragment).commit(); + nearbyListFragment = null; + } + } + + /** + * Initialize bottom sheet behaviour (sheet for map list.) Set height 9/16 of all window. + * Add callback for bottom sheet changes, so that we can sync it with bottom sheet for details + * (sheet for nearby details) + */ private void initBottomSheetBehaviour() { transparentView.setAlpha(0); - - bottomSheet.getLayoutParams().height = getWindowManager() + bottomSheet.getLayoutParams().height = getActivity().getWindowManager() .getDefaultDisplay().getHeight() / 16 * 9; bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet); - // TODO initProperBottomSheetBehavior(); bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { @Override @@ -148,257 +210,44 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_nearby, menu); - - new Handler().post(() -> { - - listButton = findViewById(R.id.action_display_list); - - secondSingleShowCaseView = new MaterialShowcaseView.Builder(this) - .setTarget(listButton) - .setDismissText(getString(R.string.showcase_view_got_it_button)) - .setContentText(getString(R.string.showcase_view_list_icon)) - .setDelay(500) // optional but starting animations immediately in onCreate can make them choppy - .singleUse(ViewUtil.SHOWCASE_VIEW_ID_1) // provide a unique ID used to ensure it is only shown once - .setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD)) - .setListener(new IShowcaseListener() { - @Override - public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) { - - } - - // If dismissed, we can inform fragment to start showcase sequence there - @Override - public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) { - nearbyMapFragment.onNearbyMaterialShowcaseDismissed(); - } - }) - .build(); - - isListShowcaseAdded = true; - - if (isMapShowCaseAdded) { // If map showcase is also ready, start ShowcaseSequence - // Probably this case is not possible. Just added to be careful - setMapViewTutorialShowCase(); - } - }); - - return super.onCreateOptionsMenu(menu); + public void prepareViewsForSheetPosition(int bottomSheetState) { + // TODO } @Override - public boolean onOptionsItemSelected(MenuItem item) { - - // Handle item selection - switch (item.getItemId()) { - case R.id.action_display_list: - if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_COLLAPSED || bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_HIDDEN){ - bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); - }else if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_EXPANDED){ - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - } - - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private void requestLocationPermissions() { - if (!isFinishing()) { - locationManager.requestPermissions(this); - } + public void onLocationChangedSignificantly(LatLng latLng) { + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - switch (requestCode) { - case LOCATION_REQUEST: { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Timber.d("Location permission granted, refreshing view"); - //Still need to check if GPS is enabled - checkGps(); - lastKnownLocation = locationManager.getLKL(); - refreshView(PERMISSION_JUST_GRANTED); - } else { - //If permission not granted, go to page that says Nearby Places cannot be displayed - hideProgressBar(); - showLocationPermissionDeniedErrorDialog(); - } - } - break; - - default: - // This is needed to allow the request codes from the Fragments to be routed appropriately - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - } + public void onLocationChangedSlightly(LatLng latLng) { + refreshView(LOCATION_SLIGHTLY_CHANGED); } - private void showLocationPermissionDeniedErrorDialog() { - new AlertDialog.Builder(this) - .setMessage(R.string.nearby_needs_permissions) - .setCancelable(false) - .setPositiveButton(R.string.give_permission, (dialog, which) -> { - //will ask for the location permission again - checkGps(); - }) - .setNegativeButton(R.string.cancel, (dialog, which) -> { - //dismiss dialog and finish activity - dialog.cancel(); - finish(); - }) - .create() - .show(); - } - private void checkGps() { - if (!locationManager.isProviderEnabled()) { - Timber.d("GPS is not enabled"); - new AlertDialog.Builder(this) - .setMessage(R.string.gps_disabled) - .setCancelable(false) - .setPositiveButton(R.string.enable_gps, - (dialog, id) -> { - Intent callGPSSettingIntent = new Intent( - android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS); - Timber.d("Loaded settings page"); - startActivityForResult(callGPSSettingIntent, 1); - }) - .setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> { - showLocationPermissionDeniedErrorDialog(); - dialog.cancel(); - }) - .create() - .show(); - } else { - Timber.d("GPS is enabled"); - checkLocationPermission(); - } - } - - private void checkLocationPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (locationManager.isLocationPermissionGranted()) { - refreshView(LOCATION_SIGNIFICANTLY_CHANGED); - } else { - // Should we show an explanation? - if (locationManager.isPermissionExplanationRequired(this)) { - // Show an explanation to the user *asynchronously* -- don't block - // this thread waiting for the user's response! After the user - // sees the explanation, try again to request the permission. - new AlertDialog.Builder(this) - .setMessage(getString(R.string.location_permission_rationale_nearby)) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - requestLocationPermissions(); - dialog.dismiss(); - }) - .setNegativeButton(android.R.string.cancel, (dialog, id) -> { - showLocationPermissionDeniedErrorDialog(); - dialog.cancel(); - }) - .create() - .show(); - - } else { - // No explanation needed, we can request the permission. - requestLocationPermissions(); - } - } - } else { - refreshView(LOCATION_SIGNIFICANTLY_CHANGED); - } + @Override + public void onLocationChangedMedium(LatLng latLng) { + // For nearby map actions, there are no differences between 500 meter location change (aka medium change) and slight change + refreshView(LOCATION_SLIGHTLY_CHANGED); } @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(LOCATION_SIGNIFICANTLY_CHANGED); - } + public void onWikidataEditSuccessful() { + refreshView(MAP_UPDATED); } - @Override - protected void onStart() { - super.onStart(); - locationManager.addLocationListener(this); - registerLocationUpdates(); - } - - @Override - protected void onStop() { - super.onStop(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (placesDisposable != null) { - placesDisposable.dispose(); - } - } - - @Override - protected void onResume() { - super.onResume(); - lockNearbyView = false; - checkGps(); - addNetworkBroadcastReceiver(); - } - - @Override - public void onPause() { - super.onPause(); - // this means that this activity will not be recreated now, user is leaving it - // or the activity is otherwise finishing - if(isFinishing()) { - // we will not need this fragment anymore, this may also be a good place to signal - // to the retained fragment object to perform its own cleanup. - removeMapFragment(); - removeListFragment(); - - } - unregisterReceiver(broadcastReceiver); - broadcastReceiver = null; - locationManager.removeLocationListener(this); - locationManager.unregisterLocationManager(); - - } - - private void addNetworkBroadcastReceiver() { - IntentFilter intentFilter = new IntentFilter(NETWORK_INTENT_ACTION); - - broadcastReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (NetworkUtils.isInternetConnectionEstablished(NearbyActivity.this)) { - refreshView(LOCATION_SIGNIFICANTLY_CHANGED); - } else { - ViewUtil.showLongToast(NearbyActivity.this, getString(R.string.no_internet)); - } - } - }; - - this.registerReceiver(broadcastReceiver, intentFilter); - } - - /** * This method should be the single point to load/refresh nearby places * * @param locationChangeType defines if location shanged significantly or slightly */ - private void refreshView(LocationChangeType locationChangeType) { + private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) { + Timber.d("Refreshing nearby places"); if (lockNearbyView) { return; } - if (!NetworkUtils.isInternetConnectionEstablished(this)) { + if (!NetworkUtils.isInternetConnectionEstablished(getActivity())) { hideProgressBar(); return; } @@ -408,7 +257,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp if (curLatLng != null && curLatLng.equals(lastLocation) && !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed - return; + if (!onOrientationChanged) { + return; + } } curLatLng = lastLocation; @@ -421,9 +272,15 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp return; } + /* + onOrientation changed is true whenever activities orientation changes. After orientation + change we want to refresh map significantly, doesn't matter if location changed significantly + or not. Thus, we included onOrientatinChanged boolean to if clause + */ if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED) || locationChangeType.equals(PERMISSION_JUST_GRANTED) - || locationChangeType.equals(MAP_UPDATED)) { + || locationChangeType.equals(MAP_UPDATED) + || onOrientationChanged) { progressBar.setVisibility(View.VISIBLE); //TODO: This hack inserts curLatLng before populatePlaces is called (see #1440). Ideally a proper fix should be found @@ -435,7 +292,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp bundle.putString("CurLatLng", gsonCurLatLng); placesDisposable = Observable.fromCallable(() -> nearbyController - .loadAttractionsFromLocation(curLatLng)) + .loadAttractionsFromLocation(curLatLng, false)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::populatePlaces, @@ -455,40 +312,8 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp } } - /** - * This method first checks if the location permissions has been granted and then register the location manager for updates. - */ - private void registerLocationUpdates() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (locationManager.isLocationPermissionGranted()) { - locationManager.registerLocationManager(); - } else { - // Should we show an explanation? - if (locationManager.isPermissionExplanationRequired(this)) { - new AlertDialog.Builder(this) - .setMessage(getString(R.string.location_permission_rationale_nearby)) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - requestLocationPermissions(); - dialog.dismiss(); - }) - .setNegativeButton(android.R.string.cancel, (dialog, id) -> { - showLocationPermissionDeniedErrorDialog(); - dialog.cancel(); - }) - .create() - .show(); - - } else { - // No explanation needed, we can request the permission. - requestLocationPermissions(); - } - } - } else { - locationManager.registerLocationManager(); - } - } - private void populatePlaces(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { + Timber.d("Populating nearby places"); List placeList = nearbyPlacesInfo.placeList; LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates; Gson gson = new GsonBuilder() @@ -499,7 +324,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates); if (placeList.size() == 0) { - ViewUtil.showSnackbar(findViewById(R.id.container), R.string.no_nearby); + ViewUtil.showSnackbar(view.findViewById(R.id.container), R.string.no_nearby); } bundle.putString("PlaceList", gsonPlaceList); @@ -520,47 +345,13 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp updateMapFragment(false); updateListFragment(); } - - isMapShowCaseAdded = true; - } - - public void setMapViewTutorialShowCase() { - /* - *This showcase view will be the first step of our nearbyMaterialShowcaseSequence. The reason we use a - * single item instead of adding another step to nearbyMaterialShowcaseSequence is that we are not able to - * call withoutShape() method on steps. For mapView we need an showcase view without - * any circle on it, it should cover the whole page. - * */ - MaterialShowcaseView firstSingleShowCaseView = new MaterialShowcaseView.Builder(this) - .setTarget(nearbyMapFragment.mapView) - .setDismissText(getString(R.string.showcase_view_got_it_button)) - .setContentText(getString(R.string.showcase_view_whole_nearby_activity)) - .setDelay(500) // optional but starting animations immediately in onCreate can make them choppy - .singleUse(ViewUtil.SHOWCASE_VIEW_ID_2) // provide a unique ID used to ensure it is only shown once - .withoutShape() // no shape on map view since there are no view to focus on - .setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD)) - .setListener(new IShowcaseListener() { - @Override - public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) { - - } - - @Override - public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) { - /* Add other nearbyMaterialShowcaseSequence here, it will make the user feel as they are a - * nearbyMaterialShowcaseSequence whole together. - * */ - secondSingleShowCaseView.show(NearbyActivity.this); - } - }) - .build(); - - if (applicationPrefs.getBoolean("firstRunNearby", true)) { - applicationPrefs.edit().putBoolean("firstRunNearby", false).apply(); - firstSingleShowCaseView.show(this); - } } + /** + * Lock nearby view updates while updating map or list. Because we don't want new update calls + * when we already updating for old location update. + * @param lock + */ private void lockNearbyView(boolean lock) { if (lock) { lockNearbyView = true; @@ -573,53 +364,22 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp } } - private void hideProgressBar() { - if (progressBar != null) { - progressBar.setVisibility(View.GONE); - } - } - - private NearbyMapFragment getMapFragment() { - return (NearbyMapFragment) getSupportFragmentManager().findFragmentByTag(TAG_RETAINED_MAP_FRAGMENT); - } - - private void removeMapFragment() { - if (nearbyMapFragment != null) { - android.support.v4.app.FragmentManager fm = getSupportFragmentManager(); - fm.beginTransaction().remove(nearbyMapFragment).commit(); - nearbyMapFragment = null; - } - } - - private NearbyListFragment getListFragment() { - return (NearbyListFragment) getSupportFragmentManager().findFragmentByTag(TAG_RETAINED_LIST_FRAGMENT); - } - - private void removeListFragment() { - if (nearbyListFragment != null) { - android.support.v4.app.FragmentManager fm = getSupportFragmentManager(); - fm.beginTransaction().remove(nearbyListFragment).commit(); - nearbyListFragment = null; - } - } - private void updateMapFragment(boolean isSlightUpdate) { /* - * Significant update means updating nearby place markers. Slightly update means only - * updating current location marker and camera target. - * We update our map Significantly on each 1000 meter change, but we can't never know - * the frequency of nearby places. Thus we check if we are close to the boundaries of - * our nearby markers, we update our map Significantly. - * */ - + Significant update means updating nearby place markers. Slightly update means only + updating current location marker and camera target. + We update our map Significantly on each 1000 meter change, but we can't never know + the frequency of nearby places. Thus we check if we are close to the boundaries of + our nearby markers, we update our map Significantly. + */ NearbyMapFragment nearbyMapFragment = getMapFragment(); if (nearbyMapFragment != null && curLatLng != null) { hideProgressBar(); // In case it is visible (this happens, not an impossible case) /* - * If we are close to nearby places boundaries, we need a significant update to - * get new nearby places. Check order is south, north, west, east - * */ + * If we are close to nearby places boundaries, we need a significant update to + * get new nearby places. Check order is south, north, west, east + * */ if (nearbyMapFragment.boundaryCoordinates != null && (curLatLng.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude() || curLatLng.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude() @@ -627,7 +387,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp || curLatLng.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) { // populate places placesDisposable = Observable.fromCallable(() -> nearbyController - .loadAttractionsFromLocation(curLatLng)) + .loadAttractionsFromLocation(curLatLng, false)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::populatePlaces, @@ -642,6 +402,15 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp return; } + /* + If this is the map update just after orientation change, then it is not a slight update + anymore. We want to significantly update map after each orientation change + */ + if (onOrientationChanged) { + isSlightUpdate = false; + onOrientationChanged = false; + } + if (isSlightUpdate) { nearbyMapFragment.setBundleForUpdtes(bundle); nearbyMapFragment.updateMapSlightly(); @@ -668,7 +437,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp * Calls fragment for map view. */ private void setMapFragment() { - FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction(); nearbyMapFragment = new NearbyMapFragment(); nearbyMapFragment.setArguments(bundle); fragmentTransaction.replace(R.id.container, nearbyMapFragment, TAG_RETAINED_MAP_FRAGMENT); @@ -679,7 +448,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp * Calls fragment for list view. */ private void setListFragment() { - FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction(); nearbyListFragment = new NearbyListFragment(); nearbyListFragment.setArguments(bundle); fragmentTransaction.replace(R.id.container_sheet, nearbyListFragment, TAG_RETAINED_LIST_FRAGMENT); @@ -688,26 +457,235 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp fragmentTransaction.commitAllowingStateLoss(); } - @Override - public void onLocationChangedSignificantly(LatLng latLng) { - refreshView(LOCATION_SIGNIFICANTLY_CHANGED); + private void hideProgressBar() { + if (progressBar != null) { + progressBar.setVisibility(View.GONE); + } } - @Override - public void onLocationChangedSlightly(LatLng latLng) { - refreshView(LOCATION_SLIGHTLY_CHANGED); + /** + * This method first checks if the location permissions has been granted and then register the location manager for updates. + */ + private void registerLocationUpdates() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (locationManager.isLocationPermissionGranted()) { + locationManager.registerLocationManager(); + } else { + // Should we show an explanation? + if (locationManager.isPermissionExplanationRequired(getActivity())) { + new AlertDialog.Builder(getActivity()) + .setMessage(getString(R.string.location_permission_rationale_nearby)) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + requestLocationPermissions(); + dialog.dismiss(); + }) + .setNegativeButton(android.R.string.cancel, (dialog, id) -> { + showLocationPermissionDeniedErrorDialog(); + dialog.cancel(); + }) + .create() + .show(); + + } else { + // No explanation needed, we can request the permission. + requestLocationPermissions(); + } + } + } else { + locationManager.registerLocationManager(); + } } - public void prepareViewsForSheetPosition(int bottomSheetState) { - // TODO + private void requestLocationPermissions() { + if (!getActivity().isFinishing()) { + locationManager.requestPermissions(getActivity()); + } + } + + private void showLocationPermissionDeniedErrorDialog() { + new AlertDialog.Builder(getActivity()) + .setMessage(R.string.nearby_needs_permissions) + .setCancelable(false) + .setPositiveButton(R.string.give_permission, (dialog, which) -> { + //will ask for the location permission again + checkGps(); + }) + .setNegativeButton(R.string.cancel, (dialog, which) -> { + //dismiss dialog and send user to contributions tab instead + dialog.cancel(); + ((MainActivity)getActivity()).viewPager.setCurrentItem(((MainActivity)getActivity()).CONTRIBUTIONS_TAB_POSITION); + }) + .create() + .show(); + } + + /** + * Checks device GPS permission first for all API levels + */ + private void checkGps() { + Timber.d("checking GPS"); + if (!locationManager.isProviderEnabled()) { + Timber.d("GPS is not enabled"); + new AlertDialog.Builder(getActivity()) + .setMessage(R.string.gps_disabled) + .setCancelable(false) + .setPositiveButton(R.string.enable_gps, + (dialog, id) -> { + Intent callGPSSettingIntent = new Intent( + android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS); + Timber.d("Loaded settings page"); + startActivityForResult(callGPSSettingIntent, 1); + }) + .setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> { + showLocationPermissionDeniedErrorDialog(); + dialog.cancel(); + }) + .create() + .show(); + } else { + Timber.d("GPS is enabled"); + checkLocationPermission(); + } + } + + /** + * This method ideally should be called from inside of CheckGPS method. If device GPS is enabled + * then we need to control app specific permissions for >=M devices. For other devices, enabled + * GPS is enough for nearby, so directly call refresh view. + */ + private void checkLocationPermission() { + Timber.d("Checking location permission"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (locationManager.isLocationPermissionGranted()) { + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); + } else { + // Should we show an explanation? + if (locationManager.isPermissionExplanationRequired(getActivity())) { + // Show an explanation to the user *asynchronously* -- don't block + // this thread waiting for the user's response! After the user + // sees the explanation, try again to request the permission. + new AlertDialog.Builder(getActivity()) + .setMessage(getString(R.string.location_permission_rationale_nearby)) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + requestLocationPermissions(); + dialog.dismiss(); + }) + .setNegativeButton(android.R.string.cancel, (dialog, id) -> { + showLocationPermissionDeniedErrorDialog(); + dialog.cancel(); + }) + .create() + .show(); + + } else { + // No explanation needed, we can request the permission. + requestLocationPermissions(); + } + } + } else { + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); + } } private void showErrorMessage(String message) { - ViewUtil.showLongToast(NearbyActivity.this, message); + ViewUtil.showLongToast(getActivity(), message); + } + + private void addNetworkBroadcastReceiver() { + IntentFilter intentFilter = new IntentFilter(NETWORK_INTENT_ACTION); + snackbar = Snackbar.make(transparentView , R.string.no_internet, Snackbar.LENGTH_INDEFINITE); + + broadcastReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (snackbar != null) { + if (NetworkUtils.isInternetConnectionEstablished(getActivity())) { + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); + snackbar.dismiss(); + } else { + snackbar.show(); + } + } + } + }; + + getActivity().registerReceiver(broadcastReceiver, intentFilter); + } @Override - public void onWikidataEditSuccessful() { - refreshView(MAP_UPDATED); + public void onResume() { + super.onResume(); + // Resume the fragment if exist + resumeFragment(); + } + + public void onTabSelected(boolean onOrientationChanged) { + Timber.d("On nearby tab selected"); + this.onOrientationChanged = onOrientationChanged; + performNearbyOperations(); + + } + + /** + * Calls nearby operations in required order. + */ + private void performNearbyOperations() { + locationManager.addLocationListener(this); + registerLocationUpdates(); + lockNearbyView = false; + checkGps(); + addNetworkBroadcastReceiver(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (placesDisposable != null) { + placesDisposable.dispose(); + } + } + + @Override + public void onDetach() { + super.onDetach(); + snackbar = null; + broadcastReceiver = null; + } + + @Override + public void onStart() { + super.onStart(); + } + + @Override + public void onPause() { + super.onPause(); + // this means that this activity will not be recreated now, user is leaving it + // or the activity is otherwise finishing + if(getActivity().isFinishing()) { + // we will not need this fragment anymore, this may also be a good place to signal + // to the retained fragment object to perform its own cleanup. + //removeMapFragment(); + removeListFragment(); + + } + if (broadcastReceiver != null) { + getActivity().unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + if (locationManager != null) { + locationManager.removeLocationListener(this); + locationManager.unregisterLocationManager(); + } } } + + 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 5f0a1ebe2..3cf60c31d 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 @@ -73,7 +73,7 @@ public class NearbyListFragment extends DaggerFragment { ViewGroup container, Bundle savedInstanceState) { Timber.d("NearbyListFragment created"); - View view = inflater.inflate(R.layout.fragment_nearby, container, false); + View view = inflater.inflate(R.layout.fragment_nearby_list, container, false); recyclerView = view.findViewById(R.id.listView); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); @@ -164,4 +164,4 @@ public class NearbyListFragment extends DaggerFragment { this.bundleForUpdates = bundleForUpdates; } -} \ No newline at end of file +} 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 index d73cdf6d5..628ab72f9 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java @@ -7,7 +7,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Color; -import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; @@ -16,6 +15,7 @@ import android.support.design.widget.BottomSheetBehavior; import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.FloatingActionButton; import android.support.v7.app.AlertDialog; +import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -109,7 +109,7 @@ public class NearbyMapFragment extends DaggerFragment { private Animation fab_close; private Animation fab_open; private Animation rotate_forward; - private ContributionController controller; + public ContributionController controller; private DirectUpload directUpload; private Place place; @@ -122,9 +122,7 @@ public class NearbyMapFragment extends DaggerFragment { private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.06; private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04; - private boolean isSecondMaterialShowcaseDismissed; private boolean isMapReady; - private MaterialShowcaseView thirdSingleShowCaseView; private Bundle bundleForUpdtes;// Carry information from activity about changed nearby places and current location @@ -177,6 +175,13 @@ public class NearbyMapFragment extends DaggerFragment { setRetainInstance(true); } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -214,7 +219,12 @@ public class NearbyMapFragment extends DaggerFragment { }); } + /** + * Updates map slightly means it doesn't updates all nearby markers around. It just updates + * location tracker marker of user. + */ public void updateMapSlightly() { + Timber.d("updateMapSlightly called, bundle is:"+bundleForUpdtes); if (mapboxMap != null) { Gson gson = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriDeserializer()) @@ -229,7 +239,13 @@ public class NearbyMapFragment extends DaggerFragment { } + /** + * Updates map significantly means it updates nearby markers and location tracker marker. It is + * called when user is out of boundaries (south, north, east or west) of markers drawn by + * previous nearby call. + */ public void updateMapSignificantly() { + Timber.d("updateMapSignificantly called, bundle is:"+bundleForUpdtes); if (mapboxMap != null) { if (bundleForUpdtes != null) { Gson gson = new GsonBuilder() @@ -281,7 +297,7 @@ public class NearbyMapFragment extends DaggerFragment { // Make camera to follow user on location change CameraPosition position ; - if(ViewUtil.isPortrait(getActivity())){ + if (ViewUtil.isPortrait(getActivity())){ position = new CameraPosition.Builder() .target(isBottomListSheetExpanded ? new LatLng(curMapBoxLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT, @@ -309,13 +325,19 @@ public class NearbyMapFragment extends DaggerFragment { } } + /** + * Updates camera position according to list sheet status. If list sheet is collapsed, camera + * focus should be in the center. If list sheet is expanded, camera focus should be visible + * on the gap between list sheet and tab layout. + * @param isBottomListSheetExpanded + */ private void updateMapCameraAccordingToBottomSheet(boolean isBottomListSheetExpanded) { CameraPosition position; this.isBottomListSheetExpanded = isBottomListSheetExpanded; if (mapboxMap != null && curLatLng != null) { if (isBottomListSheetExpanded) { // Make camera to follow user on location change - if(ViewUtil.isPortrait(getActivity())) { + if (ViewUtil.isPortrait(getActivity())) { position = new CameraPosition.Builder() .target(new LatLng(curLatLng.getLatitude() - CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT, curLatLng.getLongitude())) // Sets the new camera target above @@ -345,39 +367,40 @@ public class NearbyMapFragment extends DaggerFragment { } private void initViews() { - bottomSheetList = getActivity().findViewById(R.id.bottom_sheet); + Timber.d("initViews called"); + bottomSheetList = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.bottom_sheet); bottomSheetListBehavior = BottomSheetBehavior.from(bottomSheetList); - bottomSheetDetails = getActivity().findViewById(R.id.bottom_sheet_details); + bottomSheetDetails = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.bottom_sheet_details); bottomSheetDetailsBehavior = BottomSheetBehavior.from(bottomSheetDetails); bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); bottomSheetDetails.setVisibility(View.VISIBLE); - fabPlus = getActivity().findViewById(R.id.fab_plus); - fabCamera = getActivity().findViewById(R.id.fab_camera); - fabGallery = getActivity().findViewById(R.id.fab_galery); - fabRecenter = getActivity().findViewById(R.id.fab_recenter); + fabPlus = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_plus); + fabCamera = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_camera); + fabGallery = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_galery); + fabRecenter = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_recenter); - fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open); - fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close); - rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward); - rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward); + fab_open = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.fab_open); + fab_close = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.fab_close); + rotate_forward = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.rotate_forward); + rotate_backward = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.rotate_backward); - transparentView = getActivity().findViewById(R.id.transparentView); + transparentView = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.transparentView); - description = getActivity().findViewById(R.id.description); - title = getActivity().findViewById(R.id.title); - distance = getActivity().findViewById(R.id.category); - icon = getActivity().findViewById(R.id.icon); + description = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.description); + title = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.title); + distance = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.category); + icon = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.icon); - wikidataButton = getActivity().findViewById(R.id.wikidataButton); - wikipediaButton = getActivity().findViewById(R.id.wikipediaButton); - directionsButton = getActivity().findViewById(R.id.directionsButton); - commonsButton = getActivity().findViewById(R.id.commonsButton); + wikidataButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikidataButton); + wikipediaButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikipediaButton); + directionsButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.directionsButton); + commonsButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.commonsButton); - wikidataButtonText = getActivity().findViewById(R.id.wikidataButtonText); - wikipediaButtonText = getActivity().findViewById(R.id.wikipediaButtonText); - directionsButtonText = getActivity().findViewById(R.id.directionsButtonText); - commonsButtonText = getActivity().findViewById(R.id.commonsButtonText); + wikidataButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikidataButtonText); + wikipediaButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikipediaButtonText); + directionsButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.directionsButtonText); + commonsButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.commonsButtonText); bookmarkButton = getActivity().findViewById(R.id.bookmarkButton); bookmarkButtonImage = getActivity().findViewById(R.id.bookmarkButtonImage); @@ -416,7 +439,7 @@ public class NearbyMapFragment extends DaggerFragment { mapView.getMapAsync(mapboxMap -> { CameraPosition position; - if(ViewUtil.isPortrait(getActivity())){ + if (ViewUtil.isPortrait(getActivity())){ position = new CameraPosition.Builder() .target(isBottomListSheetExpanded ? new LatLng(curLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT, @@ -494,6 +517,7 @@ public class NearbyMapFragment extends DaggerFragment { } private void setupMapView(Bundle savedInstanceState) { + Timber.d("setupMapView called"); MapboxMapOptions options = new MapboxMapOptions() .compassGravity(Gravity.BOTTOM | Gravity.LEFT) .compassMargins(new int[]{12, 0, 0, 24}) @@ -505,15 +529,19 @@ public class NearbyMapFragment extends DaggerFragment { .zoom(11) .build()); - // create map - mapView = new MapView(getActivity(), options); - mapView.onCreate(savedInstanceState); - mapView.getMapAsync(mapboxMap -> { - ((NearbyActivity)getActivity()).setMapViewTutorialShowCase(); - NearbyMapFragment.this.mapboxMap = mapboxMap; - updateMapSignificantly(); - }); - mapView.setStyleUrl("asset://mapstyle.json"); + if (!getParentFragment().getActivity().isFinishing()) { + mapView = new MapView(getParentFragment().getActivity(), options); + // create map + mapView.onCreate(savedInstanceState); + mapView.getMapAsync(new OnMapReadyCallback() { + @Override + public void onMapReady(MapboxMap mapboxMap) { + NearbyMapFragment.this.mapboxMap = mapboxMap; + updateMapSignificantly(); + } + }); + mapView.setStyleUrl("asset://mapstyle.json"); + } } /** @@ -542,6 +570,7 @@ public class NearbyMapFragment extends DaggerFragment { * move. */ private void addCurrentLocationMarker(MapboxMap mapboxMap) { + Timber.d("addCurrentLocationMarker is called"); if (currentLocationMarker != null) { currentLocationMarker.remove(); // Remove previous marker, we are not Hansel and Gretel } @@ -564,8 +593,11 @@ public class NearbyMapFragment extends DaggerFragment { mapboxMap.addPolygon(currentLocationPolygonOptions); } + /** + * Adds markers for nearby places to mapbox map + */ private void addNearbyMarkerstoMapBoxMap() { - + Timber.d("addNearbyMarkerstoMapBoxMap is called"); mapboxMap.addMarkers(baseMarkerOptions); mapboxMap.setOnInfoWindowCloseListener(marker -> { @@ -624,6 +656,12 @@ public class NearbyMapFragment extends DaggerFragment { return circle; } + /** + * If nearby details bottom sheet state is collapsed: show fab plus + * If nearby details bottom sheet state is expanded: show fab plus + * If nearby details bottom sheet state is hidden: hide all fabs + * @param bottomSheetState + */ public void prepareViewsForSheetPosition(int bottomSheetState) { switch (bottomSheetState) { @@ -648,6 +686,9 @@ public class NearbyMapFragment extends DaggerFragment { } } + /** + * Hides all fabs + */ private void hideFAB() { removeAnchorFromFABs(fabPlus); @@ -679,25 +720,14 @@ public class NearbyMapFragment extends DaggerFragment { private void showFAB() { - addAnchorToBigFABs(fabPlus, getActivity().findViewById(R.id.bottom_sheet_details).getId()); + addAnchorToBigFABs(fabPlus, ((NearbyFragment)getParentFragment()).view.findViewById(R.id.bottom_sheet_details).getId()); fabPlus.show(); - addAnchorToSmallFABs(fabGallery, getActivity().findViewById(R.id.empty_view).getId()); + addAnchorToSmallFABs(fabGallery, ((NearbyFragment)getParentFragment()).view.findViewById(R.id.empty_view).getId()); - addAnchorToSmallFABs(fabCamera, getActivity().findViewById(R.id.empty_view1).getId()); - thirdSingleShowCaseView = new MaterialShowcaseView.Builder(this.getActivity()) - .setTarget(fabPlus) - .setDismissText(getString(R.string.showcase_view_got_it_button)) - .setContentText(getString(R.string.showcase_view_plus_fab)) - .setDelay(500) // optional but starting animations immediately in onCreate can make them choppy - .singleUse(ViewUtil.SHOWCASE_VIEW_ID_3) // provide a unique ID used to ensure it is only shown once - .setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD)) - .build(); + addAnchorToSmallFABs(fabCamera, ((NearbyFragment)getParentFragment()).view.findViewById(R.id.empty_view1).getId()); isMapReady = true; - if (isSecondMaterialShowcaseDismissed) { - thirdSingleShowCaseView.show(getActivity()); - } } @@ -724,6 +754,11 @@ public class NearbyMapFragment extends DaggerFragment { floatingActionButton.setLayoutParams(params); } + /** + * Same botom sheet carries information for all nearby places, so we need to pass information + * (title, description, distance and links) to view on nearby marker click + * @param place Place of clicked nearby marker + */ private void passInfoToSheet(Place place) { this.place = place; @@ -795,7 +830,7 @@ public class NearbyMapFragment extends DaggerFragment { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Timber.d("onRequestPermissionsResult: req code = " + " perm = " + permissions + " grant =" + grantResults); - // Do not use requestCode 1 as it will conflict with NearbyActivity's requestCodes + // Do not use requestCode 1 as it will conflict with NearbyFragment's requestCodes switch (requestCode) { // 4 = "Read external storage" allowed when gallery selected case 4: { @@ -873,18 +908,13 @@ public class NearbyMapFragment extends DaggerFragment { } } + /** + * This bundle is sent whenever and updte for nearby map comes, not for recreation, for updates + */ public void setBundleForUpdtes(Bundle bundleForUpdtes) { this.bundleForUpdtes = bundleForUpdtes; } - public void onNearbyMaterialShowcaseDismissed() { - isSecondMaterialShowcaseDismissed = true; - if (isMapReady) { - thirdSingleShowCaseView.show(getActivity()); - } - } - - @Override public void onStart() { if (mapView != null) { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNoificationCardView.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNoificationCardView.java new file mode 100644 index 000000000..c44fbf763 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNoificationCardView.java @@ -0,0 +1,283 @@ +package fr.free.nrw.commons.nearby; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.widget.CoordinatorLayout; +import android.support.design.widget.SwipeDismissBehavior; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.CardView; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import android.widget.Toast; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.contributions.MainActivity; +import fr.free.nrw.commons.utils.ViewUtil; +import timber.log.Timber; + +/** + * Custom card view for nearby notification card view on main screen, above contributions list + */ +public class NearbyNoificationCardView extends CardView{ + + private static final float MINIMUM_THRESHOLD_FOR_SWIPE = 100; + private Context context; + + private Button permissionRequestButton; + private RelativeLayout contentLayout; + private TextView notificationTitle; + private TextView notificationDistance; + private ImageView notificationIcon; + private ProgressBar progressBar; + + public CardViewVisibilityState cardViewVisibilityState; + + public PermissionType permissionType; + + float x1,x2; + + public NearbyNoificationCardView(@NonNull Context context) { + super(context); + this.context = context; + cardViewVisibilityState = CardViewVisibilityState.INVISIBLE; + init(); + } + + public NearbyNoificationCardView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + this.context = context; + cardViewVisibilityState = CardViewVisibilityState.INVISIBLE; + init(); + } + + public NearbyNoificationCardView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + this.context = context; + cardViewVisibilityState = CardViewVisibilityState.INVISIBLE; + init(); + } + + private void init() { + View rootView = inflate(context, R.layout.nearby_card_view, this); + + permissionRequestButton = rootView.findViewById(R.id.permission_request_button); + contentLayout = rootView.findViewById(R.id.content_layout); + + notificationTitle = rootView.findViewById(R.id.nearby_title); + notificationDistance = rootView.findViewById(R.id.nearby_distance); + + notificationIcon = rootView.findViewById(R.id.nearby_icon); + + progressBar = rootView.findViewById(R.id.progressBar); + + setActionListeners(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + // If you don't setVisibility after getting layout params, then you will se an empty space in place of nerabyNotificationCardView + if (((MainActivity)context).prefs.getBoolean("displayNearbyCardView", true)) { + this.setVisibility(VISIBLE); + } else { + this.setVisibility(GONE); + } + } + + + private void setActionListeners() { + this.setOnClickListener(view -> ((MainActivity)context).viewPager.setCurrentItem(1)); + + this.setOnTouchListener( + (v, event) -> { + boolean isSwipe = false; + float deltaX=0.0f; + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + x1 = event.getX(); + break; + case MotionEvent.ACTION_UP: + x2 = event.getX(); + deltaX = x2 - x1; + if (deltaX < 0) { + //Right to left swipe + isSwipe = true; + } else if (deltaX > 0) { + //Left to right swipe + isSwipe = true; + } + break; + } + if (isSwipe && (pixelToDp(Math.abs(deltaX)) > MINIMUM_THRESHOLD_FOR_SWIPE)) { + v.setVisibility(GONE); + // Save shared preference for nearby card view accordingly + ((MainActivity) context).prefs.edit() + .putBoolean("displayNearbyCardView", false).apply(); + ViewUtil.showLongToast(context, getResources().getString(R.string.nearby_notification_dismiss_message)); + return true; + } + return false; + }); + } + + private float pixelToDp(float pixels) { + return (pixels / Resources.getSystem().getDisplayMetrics().density); + } + + /** + * Sets permission request button visible and content layout invisible, then adds correct + * permission request actions to permission request button according to PermissionType enum + * @param isPermissionRequestButtonNeeded true if permissions missing + */ + public void displayPermissionRequestButton(boolean isPermissionRequestButtonNeeded) { + if (isPermissionRequestButtonNeeded) { + cardViewVisibilityState = CardViewVisibilityState.ASK_PERMISSION; + contentLayout.setVisibility(GONE); + permissionRequestButton.setVisibility(VISIBLE); + + if (permissionType == PermissionType.ENABLE_LOCATION_PERMISSON) { + + permissionRequestButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + if (!((MainActivity)context).isFinishing()) { + ((MainActivity) context).locationManager.requestPermissions((MainActivity) context); + } + } + }); + + } else if (permissionType == PermissionType.ENABLE_GPS) { + + permissionRequestButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + new AlertDialog.Builder(context) + .setMessage(R.string.gps_disabled) + .setCancelable(false) + .setPositiveButton(R.string.enable_gps, + (dialog, id) -> { + Intent callGPSSettingIntent = new Intent( + android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS); + Timber.d("Loaded settings page"); + ((MainActivity) context).startActivityForResult(callGPSSettingIntent, 1); + }) + .setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> { + dialog.cancel(); + displayPermissionRequestButton(true); + }) + .create() + .show(); + } + }); + } + + + } else { + cardViewVisibilityState = CardViewVisibilityState.LOADING; + permissionRequestButton.setVisibility(GONE); + contentLayout.setVisibility(VISIBLE); + // Set visibility of elements in content layout once it become visible + progressBar.setVisibility(VISIBLE); + notificationTitle.setVisibility(GONE); + notificationDistance.setVisibility(GONE); + notificationIcon.setVisibility(GONE); + + permissionRequestButton.setVisibility(GONE); + } + } + + /** + * Pass place information to views. + * @param isClosestNearbyPlaceFound false if there are no close place + * @param place Closes place where we will get information from + */ + public void updateContent(boolean isClosestNearbyPlaceFound, Place place) { + Timber.d("Update nearby card notification content"); + cardViewVisibilityState = CardViewVisibilityState.READY; + permissionRequestButton.setVisibility(GONE); + contentLayout.setVisibility(VISIBLE); + // Make progress bar invisible once data is ready + progressBar.setVisibility(GONE); + // And content views visible since they are ready + notificationTitle.setVisibility(VISIBLE); + notificationDistance.setVisibility(VISIBLE); + notificationIcon.setVisibility(VISIBLE); + + if (isClosestNearbyPlaceFound) { + notificationTitle.setText(place.name); + notificationDistance.setText(place.distance); + } else { + notificationDistance.setText(""); + notificationTitle.setText(R.string.no_close_nearby); + } + } + + @Override + protected void onVisibilityChanged(@NonNull View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + if (visibility == VISIBLE) { + /** + * Sometimes we need to preserve previous state of notification card view without getting + * any data from user. Ie. wen user came back from Media Details fragment to Contrib List + * fragment, we need to know what was the state of card view, and set it to exact same state. + */ + switch (cardViewVisibilityState) { + case READY: + permissionRequestButton.setVisibility(GONE); + contentLayout.setVisibility(VISIBLE); + // Make progress bar invisible once data is ready + progressBar.setVisibility(GONE); + // And content views visible since they are ready + notificationTitle.setVisibility(VISIBLE); + notificationDistance.setVisibility(VISIBLE); + notificationIcon.setVisibility(VISIBLE); + break; + case LOADING: + permissionRequestButton.setVisibility(GONE); + contentLayout.setVisibility(VISIBLE); + // Set visibility of elements in content layout once it become visible + progressBar.setVisibility(VISIBLE); + notificationTitle.setVisibility(GONE); + notificationDistance.setVisibility(GONE); + notificationIcon.setVisibility(GONE); + permissionRequestButton.setVisibility(GONE); + break; + case ASK_PERMISSION: + contentLayout.setVisibility(GONE); + permissionRequestButton.setVisibility(VISIBLE); + break; + default: + break; + } + } + } + + /** + * This states will help us to preserve progress bar and content layout states + */ + public enum CardViewVisibilityState { + LOADING, + READY, + INVISIBLE, + ASK_PERMISSION, + } + + /** + * We need to know which kind of permission we need to request, then update permission request + * button action accordingly + */ + public enum PermissionType { + ENABLE_GPS, + ENABLE_LOCATION_PERMISSON, // For only after Marsmallow + NO_PERMISSION_NEEDED + } +} 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 f9d35d63e..6ca4af318 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 @@ -23,9 +23,9 @@ import timber.log.Timber; public class NearbyPlaces { - private static final int MIN_RESULTS = 40; + private static int MIN_RESULTS = 40; private static final double INITIAL_RADIUS = 1.0; // in kilometers - private static final double MAX_RADIUS = 300.0; // in kilometers + private static double MAX_RADIUS = 300.0; // in kilometers private static final double RADIUS_MULTIPLIER = 1.618; 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/"); @@ -41,9 +41,22 @@ public class NearbyPlaces { } } - List getFromWikidataQuery(LatLng curLatLng, String lang) throws IOException { + List getFromWikidataQuery(LatLng curLatLng, String lang, boolean returnClosestResult) throws IOException { List places = Collections.emptyList(); + /** + * If returnClosestResult is true, then this means that we are trying to get closest point + * to use in cardView above contributions list + */ + if (returnClosestResult) { + MIN_RESULTS = 1; // Return closest nearby place + MAX_RADIUS = 5; // Return places only in 5 km area + radius = INITIAL_RADIUS; // refresh radius again, otherwise increased radius is grater than MAX_RADIUS, thus returns null + } else { + MIN_RESULTS = 40; + MAX_RADIUS = 300.0; // in kilometers + } + // increase the radius gradually to find a satisfactory number of nearby places while (radius <= MAX_RADIUS) { try { @@ -103,12 +116,19 @@ public class NearbyPlaces { String point = fields[0]; String wikiDataLink = Utils.stripLocalizedString(fields[1]); String name = Utils.stripLocalizedString(fields[2]); + + //getting icon link here + String identifier = Utils.stripLocalizedString(fields[3]); + //getting the ID which is at the end of link + identifier = identifier.split("/")[Utils.stripLocalizedString(fields[3]).split("/").length-1]; + //replaced the extra > char from fields + identifier = identifier.replace(">",""); + String type = Utils.stripLocalizedString(fields[4]); String icon = fields[5]; String wikipediaSitelink = Utils.stripLocalizedString(fields[7]); String commonsSitelink = Utils.stripLocalizedString(fields[8]); String category = Utils.stripLocalizedString(fields[9]); - Timber.v("Name: " + name + ", type: " + type + ", category: " + category + ", wikipediaSitelink: " + wikipediaSitelink + ", commonsSitelink: " + commonsSitelink); double latitude; @@ -127,7 +147,7 @@ public class NearbyPlaces { places.add(new Place( name, - Place.Label.fromText(type), // list + Place.Label.fromText(identifier), // list type, // details Uri.parse(icon), new LatLng(latitude, longitude, 0), @@ -143,4 +163,5 @@ public class NearbyPlaces { return places; } + } 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 b13f63bc2..7635ac06f 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 @@ -121,27 +121,30 @@ public class Place { */ public enum Label { - BUILDING("building", R.drawable.round_icon_generic_building), - HOUSE("house", R.drawable.round_icon_house), - COTTAGE("cottage", R.drawable.round_icon_house), - FARMHOUSE("farmhouse", R.drawable.round_icon_house), - CHURCH("church", R.drawable.round_icon_church), - RAILWAY_STATION("railway station", R.drawable.round_icon_railway_station), - GATEHOUSE("gatehouse", R.drawable.round_icon_gatehouse), - MILESTONE("milestone", R.drawable.round_icon_milestone), - INN("inn", R.drawable.round_icon_house), - CITY("city", R.drawable.round_icon_city), - SECONDARY_SCHOOL("secondary school", R.drawable.round_icon_school), - EDU("edu", R.drawable.round_icon_school), - ISLE("isle", R.drawable.round_icon_island), - MOUNTAIN("mountain", R.drawable.round_icon_mountain), - AIRPORT("airport", R.drawable.round_icon_airport), - BRIDGE("bridge", R.drawable.round_icon_bridge), - ROAD("road", R.drawable.round_icon_road), - FOREST("forest", R.drawable.round_icon_forest), - PARK("park", R.drawable.round_icon_park), - RIVER("river", R.drawable.round_icon_river), - WATERFALL("waterfall", R.drawable.round_icon_waterfall), + BUILDING("Q41176", R.drawable.round_icon_generic_building), + HOUSE("Q3947", R.drawable.round_icon_house), + COTTAGE("Q5783996", R.drawable.round_icon_house), + FARMHOUSE("Q489357", R.drawable.round_icon_house), + CHURCH("Q16970", R.drawable.round_icon_church), //changed from church to church building + RAILWAY_STATION("Q55488", R.drawable.round_icon_railway_station), + GATEHOUSE("Q277760", R.drawable.round_icon_gatehouse), + MILESTONE("Q10145", R.drawable.round_icon_milestone), + INN("Q256020", R.drawable.round_icon_house), //Q27686 + HOTEL("Q27686", R.drawable.round_icon_house), + CITY("Q515", R.drawable.round_icon_city), + UNIVERSITY("Q3918",R.drawable.round_icon_school), //added university + SCHOOL("Q3914", R.drawable.round_icon_school), //changed from "secondary school" to school + EDUCATION("Q8434", R.drawable.round_icon_school), //changed from edu to education, there is no id for "edu" + ISLE("Q23442", R.drawable.round_icon_island), + MOUNTAIN("Q8502", R.drawable.round_icon_mountain), + AIRPORT("Q1248784", R.drawable.round_icon_airport), + BRIDGE("Q12280", R.drawable.round_icon_bridge), + ROAD("Q34442", R.drawable.round_icon_road), + FOREST("Q4421", R.drawable.round_icon_forest), + PARK("Q22698", R.drawable.round_icon_park), + RIVER("Q4022", R.drawable.round_icon_river), + WATERFALL("Q34038", R.drawable.round_icon_waterfall), + TEMPLE("Q44539",R.drawable.round_icon_church), UNKNOWN("?", R.drawable.round_icon_unknown); private static final Map TEXT_TO_DESCRIPTION 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 index 8993db344..f4925af0e 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java @@ -100,4 +100,4 @@ public class Sitelinks implements Parcelable { return new Sitelinks(this); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/Notification.java b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java index e6d759f66..0116d024c 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/Notification.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java @@ -11,13 +11,15 @@ public class Notification { public String description; public String link; public String iconUrl; + public String dateWithYear; - public Notification(NotificationType notificationType, String notificationText, String date, String description, String link, String iconUrl) { + public Notification(NotificationType notificationType, String notificationText, String date, String description, String link, String iconUrl, String dateWithYear) { this.notificationType = notificationType; this.notificationText = notificationText; this.date = date; this.description = description; this.link = link; this.iconUrl = iconUrl; + this.dateWithYear = dateWithYear; } } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java index baaad04f5..ebeb7f718 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -17,6 +17,7 @@ import android.widget.RelativeLayout; import com.pedrogomez.renderers.RVRendererAdapter; import java.util.Collections; +import java.util.Date; import java.util.List; import javax.inject.Inject; @@ -25,6 +26,7 @@ import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.utils.NetworkUtils; @@ -85,6 +87,11 @@ public class NotificationActivity extends NavigationBaseActivity { private void addNotifications() { Timber.d("Add notifications"); + // Store when add notification is called last + long currentDate = new Date(System.currentTimeMillis()).getTime(); + getSharedPreferences("prefs", MODE_PRIVATE).edit().putLong("last_read_notification_date", currentDate).apply(); + Timber.d("Set last notification read date to current date:"+ currentDate); + if(mNotificationWorkerFragment == null){ Observable.fromCallable(() -> { progressBar.setVisibility(View.VISIBLE); @@ -115,7 +122,7 @@ public class NotificationActivity extends NavigationBaseActivity { } private void setAdapter(List notificationList) { - if(notificationList == null || notificationList.isEmpty()) { + if (notificationList == null || notificationList.isEmpty()) { ViewUtil.showSnackbar(relativeLayout, R.string.no_notifications); return; } @@ -140,4 +147,10 @@ public class NotificationActivity extends NavigationBaseActivity { .commit(); mNotificationWorkerFragment.setNotificationList(notificationList); } + + @Override + public void onBackPressed() { + startActivityWithFlags( + this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, + Intent.FLAG_ACTIVITY_SINGLE_TOP); } } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java index b4a1d36e0..f843cda66 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java @@ -1,6 +1,11 @@ package fr.free.nrw.commons.notification; +import android.graphics.Color; import android.text.Html; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextPaint; +import android.text.style.ClickableSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -60,7 +65,25 @@ public class NotificationRenderer extends Renderer { notificationText = notificationText.trim().replaceAll("(^\\s*)|(\\s*$)", ""); notificationText = Html.fromHtml(notificationText).toString(); notificationText = notificationText.concat(" "); - title.setText(notificationText); + + SpannableString ss = new SpannableString(notificationText); + ClickableSpan clickableSpan = new ClickableSpan() { + @Override + public void onClick(View view) { + listener.notificationClicked(getContent()); + } + + @Override + public void updateDrawState(TextPaint ds) { + super.updateDrawState(ds); + ds.setUnderlineText(false); + ds.setColor(Color.BLACK); + } + }; + + // Attach a ClickableSpan to the range (start:0, end:notificationText.length()) of the String + ss.setSpan(clickableSpan, 0, notificationText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + title.setText(ss, TextView.BufferType.SPANNABLE); } public interface NotificationClicked{ diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationType.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationType.java index b83b23b2a..0386ead95 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationType.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationType.java @@ -24,4 +24,4 @@ public enum NotificationType { } return UNKNOWN; } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java index e7c87d3f4..7c19c516c 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java @@ -144,7 +144,7 @@ public class NotificationUtils { notificationText = getWelcomeMessage(context, document); break; } - return new Notification(type, notificationText, getTimestamp(document), description, link, iconUrl); + return new Notification(type, notificationText, getTimestamp(document), description, link, iconUrl, getTimestampWithYear(document)); } private static String getNotificationText(Node document) { @@ -193,7 +193,7 @@ public class NotificationUtils { private static String getNotificationIconUrl(Node document) { String format = "%s%s"; Node iconUrl = getNode(getModel(document), "iconUrl"); - if(iconUrl == null) { + if (iconUrl == null) { return null; } else { String url = iconUrl.getTextContent(); @@ -247,6 +247,14 @@ public class NotificationUtils { return ""; } + private static String getTimestampWithYear(Node document) { + Element timestampElement = (Element) getNode(document, "timestamp"); + if (timestampElement != null) { + return timestampElement.getAttribute("utcunix"); + } + return ""; + } + private static String getNotificationDescription(Node document) { Element titleElement = (Element) getNode(document, "title"); if (titleElement != null) { diff --git a/app/src/main/java/fr/free/nrw/commons/notification/UnreadNotificationsCheckAsync.java b/app/src/main/java/fr/free/nrw/commons/notification/UnreadNotificationsCheckAsync.java new file mode 100644 index 000000000..cb504f9c5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/UnreadNotificationsCheckAsync.java @@ -0,0 +1,81 @@ +package fr.free.nrw.commons.notification; + +import android.os.AsyncTask; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.Date; +import java.util.List; + +import fr.free.nrw.commons.contributions.MainActivity; +import fr.free.nrw.commons.contributions.ContributionsFragment; +import timber.log.Timber; + +/** + * This asynctask will check unread notifications after a date (date user check notifications last) + */ + +public class UnreadNotificationsCheckAsync extends AsyncTask { + + WeakReference context; + NotificationController notificationController; + + + public UnreadNotificationsCheckAsync(MainActivity context, NotificationController notificationController) { + this.context = new WeakReference<>(context); + this.notificationController = notificationController; + } + + @Override + protected Notification doInBackground(Void... voids) { + Notification lastNotification = null; + + try { + lastNotification = findLastNotification(notificationController.getNotifications()); + } catch (IOException e) { + e.printStackTrace(); + } + + return lastNotification; + } + + @Override + protected void onPostExecute(Notification lastNotification) { + super.onPostExecute(lastNotification); + + if (lastNotification == null) { + return; + } + + Date lastNotificationCheckDate = new Date(context.get() + .getSharedPreferences("prefs",0) + .getLong("last_read_notification_date", 0)); + Timber.d("You may have unread notifications since"+lastNotificationCheckDate); + + boolean isThereUnreadNotifications; + + Date lastReadNotificationDate = new java.util.Date(Long.parseLong(lastNotification.dateWithYear)*1000); + + if (lastNotificationCheckDate.before(lastReadNotificationDate)) { + isThereUnreadNotifications = true; + } else { + isThereUnreadNotifications = false; + } + + // Check if activity is still running + if (context.get().getWindow().getDecorView().isShown() && !context.get().isFinishing()) { + // Check if fragment is not null and visible + if (context.get().isContributionsFragmentVisible && context.get().contributionsActivityPagerAdapter.getItem(0) != null) { + ((ContributionsFragment)(context.get().contributionsActivityPagerAdapter.getItem(0))).updateNotificationsNotification(isThereUnreadNotifications); + } + } + } + + private Notification findLastNotification(List allNotifications) { + if (allNotifications.size() > 0) { + return allNotifications.get(allNotifications.size()-1); + } else { + return null; + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java index 10b808b1f..92075864e 100644 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java @@ -51,7 +51,7 @@ public class QuizActivity extends AppCompatActivity { */ @OnClick(R.id.next_button) public void setNextQuestion(){ - if( questionIndex <= quiz.size() && (positiveAnswer.isChecked() || negativeAnswer.isChecked())) { + if ( questionIndex <= quiz.size() && (positiveAnswer.isChecked() || negativeAnswer.isChecked())) { evaluateScore(); } else if ( !positiveAnswer.isChecked() && !negativeAnswer.isChecked()){ AlertDialog.Builder alert = new AlertDialog.Builder(this); @@ -107,11 +107,11 @@ public class QuizActivity extends AppCompatActivity { * to evaluate score and check whether answer is correct or wrong */ public void evaluateScore() { - if((quiz.get(questionIndex).isAnswer() && positiveAnswer.isChecked()) || + if ((quiz.get(questionIndex).isAnswer() && positiveAnswer.isChecked()) || (!quiz.get(questionIndex).isAnswer() && negativeAnswer.isChecked()) ){ customAlert(getResources().getString(R.string.correct),quiz.get(questionIndex).getAnswerMessage() ); score++; - } else{ + } else { customAlert(getResources().getString(R.string.wrong), quiz.get(questionIndex).getAnswerMessage()); } } @@ -127,12 +127,12 @@ public class QuizActivity extends AppCompatActivity { alert.setMessage(Message); alert.setPositiveButton(R.string.continue_message, (dialog, which) -> { questionIndex++; - if(questionIndex == quiz.size()){ + if (questionIndex == quiz.size()) { Intent i = new Intent(QuizActivity.this, QuizResultActivity.class); dialog.dismiss(); i.putExtra("QuizResult",score); startActivity(i); - }else { + } else { displayQuestion(); } }); diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java index 654c08fa9..7ae2cbe79 100644 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java @@ -73,7 +73,7 @@ public class QuizChecker { */ private void setTotalUploadCount(int uploadCount) { totalUploadCount = uploadCount - countPref.getInt(UPLOAD_SHARED_PREFERENCE,0); - if( totalUploadCount < 0){ + if ( totalUploadCount < 0){ totalUploadCount = 0; countPref.edit().putInt(UPLOAD_SHARED_PREFERENCE,0).apply(); } @@ -104,7 +104,7 @@ public class QuizChecker { */ private void setRevertParameter(int revertCountFetched) { revertCount = revertCountFetched - revertPref.getInt(REVERT_SHARED_PREFERENCE,0); - if(revertCount < 0){ + if (revertCount < 0){ revertCount = 0; revertPref.edit().putInt(REVERT_SHARED_PREFERENCE, 0).apply(); } @@ -116,7 +116,7 @@ public class QuizChecker { * to check whether the criterion to call quiz is satisfied */ private void calculateRevertParameter() { - if( revertCount < 0 || totalUploadCount < 0){ + if ( revertCount < 0 || totalUploadCount < 0){ revertPref.edit().putInt(REVERT_SHARED_PREFERENCE, 0).apply(); countPref.edit().putInt(UPLOAD_SHARED_PREFERENCE,0).apply(); return; diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java index 95258d81b..6b806d248 100644 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java @@ -8,7 +8,6 @@ import android.net.Uri; import android.os.Bundle; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; -import android.os.Bundle; import com.dinuscxj.progressbar.CircleProgressBar; @@ -16,7 +15,7 @@ import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.contributions.MainActivity; import android.support.v7.widget.Toolbar; import android.view.LayoutInflater; @@ -26,16 +25,9 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; -import com.dinuscxj.progressbar.CircleProgressBar; - import java.io.File; import java.io.FileOutputStream; -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.ContributionsActivity; /** * Displays the final score of quiz and congratulates the user @@ -57,13 +49,13 @@ public class QuizResultActivity extends AppCompatActivity { ButterKnife.bind(this); setSupportActionBar(toolbar); - if( getIntent() != null) { + if ( getIntent() != null) { Bundle extras = getIntent().getExtras(); int score = extras.getInt("QuizResult"); setScore(score); }else{ startActivityWithFlags( - this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, + this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP); super.onBackPressed(); } @@ -87,14 +79,14 @@ public class QuizResultActivity extends AppCompatActivity { @OnClick(R.id.quiz_result_next) public void launchContributionActivity(){ startActivityWithFlags( - this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, + this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP); } @Override public void onBackPressed() { startActivityWithFlags( - this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, + this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP); super.onBackPressed(); } @@ -186,9 +178,9 @@ public class QuizResultActivity extends AppCompatActivity { AlertDialog.Builder alertadd = new AlertDialog.Builder(QuizResultActivity.this); LayoutInflater factory = LayoutInflater.from(QuizResultActivity.this); final View view = factory.inflate(R.layout.image_alert_layout, null); - ImageView screenShotImage = (ImageView) view.findViewById(R.id.alert_image); + ImageView screenShotImage = view.findViewById(R.id.alert_image); screenShotImage.setImageBitmap(screenshot); - TextView shareMessage = (TextView) view.findViewById(R.id.alert_text); + TextView shareMessage = view.findViewById(R.id.alert_text); shareMessage.setText(R.string.quiz_result_share_message); alertadd.setView(view); alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot)); diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.java b/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.java index 9d907fc08..79756871d 100644 --- a/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.java @@ -44,7 +44,7 @@ public class RadioGroupHelper { public RadioGroupHelper(View rootView, int... radiosIDs) { super(); for (int radioButtonID : radiosIDs) { - add((RadioButton)rootView.findViewById(radioButtonID)); + add(rootView.findViewById(radioButtonID)); } } @@ -58,7 +58,7 @@ public class RadioGroupHelper { */ View.OnClickListener onClickListener = v -> { for (CompoundButton rb : radioButtons) { - if(rb != v) rb.setChecked(false); + if (rb != v) rb.setChecked(false); } }; -} \ No newline at end of file +} 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 54b462097..ecfbe14e0 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 @@ -67,4 +67,4 @@ public class SettingsActivity extends NavigationBaseActivity { return super.onOptionsItemSelected(item); } } -} \ 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 8ef8e84a4..9668f11ee 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 @@ -8,7 +8,7 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity; public abstract class BaseActivity extends CommonsDaggerAppCompatActivity { - boolean currentTheme; + protected boolean currentTheme; @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index 77c94ca1d..763a6d09a 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -13,6 +13,7 @@ import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -32,11 +33,9 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.achievements.AchievementsActivity; import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.bookmarks.BookmarksActivity; -import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.category.CategoryImagesActivity; -import fr.free.nrw.commons.contributions.ContributionsActivity; -import fr.free.nrw.commons.nearby.NearbyActivity; +import fr.free.nrw.commons.bookmarks.BookmarksActivity; import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.settings.SettingsActivity; import timber.log.Timber; @@ -91,6 +90,23 @@ public abstract class NavigationBaseActivity extends BaseActivity } } + public void changeDrawerIconToBakcButton() { + toggle.setDrawerIndicatorEnabled(false); + toggle.setHomeAsUpIndicator(R.drawable.ic_arrow_back_white); + toggle.setToolbarNavigationClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onBackPressed(); + } + }); + } + + public void changeDrawerIconToDefault() { + if (toggle != null) { + toggle.setDrawerIndicatorEnabled(true); + } + } + /** * Set the username in navigationHeader. */ @@ -156,13 +172,9 @@ public abstract class NavigationBaseActivity extends BaseActivity case R.id.action_home: drawerLayout.closeDrawer(navigationView); startActivityWithFlags( - this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, + this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP); return true; - case R.id.action_nearby: - drawerLayout.closeDrawer(navigationView); - startActivityWithFlags(this, NearbyActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - return true; case R.id.action_about: drawerLayout.closeDrawer(navigationView); startActivityWithFlags(this, AboutActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/CompatTextView.java b/app/src/main/java/fr/free/nrw/commons/ui/widget/CompatTextView.java index abe2e2554..22ae33ec6 100644 --- a/app/src/main/java/fr/free/nrw/commons/ui/widget/CompatTextView.java +++ b/app/src/main/java/fr/free/nrw/commons/ui/widget/CompatTextView.java @@ -97,4 +97,4 @@ public class CompatTextView extends AppCompatTextView { a.recycle(); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java b/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java index 4e73776c1..2f6f08105 100644 --- a/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java +++ b/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java @@ -48,4 +48,4 @@ public class HtmlTextView extends AppCompatTextView { return Html.fromHtml(source); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Description.java b/app/src/main/java/fr/free/nrw/commons/upload/Description.java index b19992574..ae18d4adb 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/Description.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/Description.java @@ -1,56 +1,72 @@ package fr.free.nrw.commons.upload; -import android.text.TextUtils; +import java.util.List; +/** + * Holds a description of an item being uploaded by {@link UploadActivity} + */ class Description { - private String languageId; - private String languageDisplayText; + private String languageCode; private String descriptionText; - private boolean set; private int selectedLanguageIndex = -1; - public String getLanguageId() { - return languageId; + /** + * @return The language code ie. "en" or "fr" + */ + String getLanguageCode() { + return languageCode; } - public void setLanguageId(String languageId) { - this.languageId = languageId; + /** + * @param languageCode The language code ie. "en" or "fr" + */ + void setLanguageCode(String languageCode) { + this.languageCode = languageCode; } - public String getLanguageDisplayText() { - return languageDisplayText; - } - - public void setLanguageDisplayText(String languageDisplayText) { - this.languageDisplayText = languageDisplayText; - } - - public String getDescriptionText() { + String getDescriptionText() { return descriptionText; } - public void setDescriptionText(String descriptionText) { + void setDescriptionText(String descriptionText) { this.descriptionText = descriptionText; - - if (!TextUtils.isEmpty(descriptionText)) { - set = true; - } } - public boolean isSet() { - return set; - } - - public void setSet(boolean set) { - this.set = set; - } - - public int getSelectedLanguageIndex() { + /** + * @return the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter} + */ + int getSelectedLanguageIndex() { return selectedLanguageIndex; } - public void setSelectedLanguageIndex(int selectedLanguageIndex) { + /** + * @param selectedLanguageIndex the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter} + */ + void setSelectedLanguageIndex(int selectedLanguageIndex) { this.selectedLanguageIndex = selectedLanguageIndex; } + + + /** + * Formats the list of descriptions into the format Commons requires for uploads. + * + * @param descriptions the list of descriptions, description is ignored if text is null. + * @return a string with the pattern of {{en|1=descriptionText}} + */ + static String formatList(List descriptions) { + StringBuilder descListString = new StringBuilder(); + for (Description description : descriptions) { + if (!description.isEmpty()) { + String individualDescription = String.format("{{%s|1=%s}}", description.getLanguageCode(), + description.getDescriptionText()); + descListString.append(individualDescription); + } + } + return descListString.toString(); + } + + public boolean isEmpty() { + return descriptionText == null || descriptionText.isEmpty(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java index 5dfd02cae..8c2432ee1 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/DescriptionsAdapter.java @@ -2,12 +2,12 @@ package fr.free.nrw.commons.upload; import android.content.Context; import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.view.ViewCompat; import android.support.v7.widget.AppCompatSpinner; import android.support.v7.widget.RecyclerView; -import android.text.Editable; import android.text.TextUtils; -import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -17,182 +17,241 @@ import android.widget.AdapterView.OnItemSelectedListener; import android.widget.EditText; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Locale; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnTouch; +import butterknife.Optional; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.utils.AbstractTextWatcher; +import fr.free.nrw.commons.utils.BiMap; import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.subjects.BehaviorSubject; +import io.reactivex.subjects.Subject; +import timber.log.Timber; import static android.view.MotionEvent.ACTION_UP; class DescriptionsAdapter extends RecyclerView.Adapter { - List descriptions; - List languages; + private Title title; + private List descriptions; private Context context; private Callback callback; + private Subject titleChangedSubject; - public DescriptionsAdapter() { + private BiMap selectedLanguages; + private UploadView uploadView; + + DescriptionsAdapter(UploadView uploadView) { + title = new Title(); descriptions = new ArrayList<>(); - descriptions.add(new Description()); - languages = new ArrayList<>(); + titleChangedSubject = BehaviorSubject.create(); + selectedLanguages = new BiMap<>(); + this.uploadView = uploadView; } - public void setCallback(Callback callback) { + void setCallback(Callback callback) { this.callback = callback; } - public void setDescriptions(List descriptions) { + void setItems(Title title, List descriptions) { this.descriptions = descriptions; + this.title = title; + selectedLanguages = new BiMap<>(); notifyDataSetChanged(); } - public void setLanguages(List languages) { - this.languages = languages; + @Override + public int getItemViewType(int position) { + if (position == 0) return 1; + else return 2; } + @NonNull @Override - public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.row_item_description, parent, false); + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view; + if (viewType == 1) { + view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.row_item_title, parent, false); + } else { + view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.row_item_description, parent, false); + } context = parent.getContext(); - ViewHolder viewHolder = new ViewHolder(view); - return viewHolder; + return new ViewHolder(view); } @Override - public void onBindViewHolder(ViewHolder holder, int position) { + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.init(position); } @Override public int getItemCount() { - return descriptions.size(); + return descriptions.size() + 1; } - public List getDescriptions() { + List getDescriptions() { return descriptions; } - public void addDescription(Description description) { + void addDescription(Description description) { this.descriptions.add(description); - notifyItemInserted(descriptions.size() - 1); + notifyItemInserted(descriptions.size() + 1); } + public Title getTitle() { + return title; + } + + public void setTitle(Title title) { + this.title = title; + notifyItemInserted(0); + } public class ViewHolder extends RecyclerView.ViewHolder { + @Nullable @BindView(R.id.spinner_description_languages) AppCompatSpinner spinnerDescriptionLanguages; - @BindView(R.id.et_description_text) - EditText etDescriptionText; - private View view; + @BindView(R.id.description_item_edit_text) + EditText descItemEditText; + + private View view; public ViewHolder(View itemView) { super(itemView); ButterKnife.bind(this, itemView); this.view = itemView; + Timber.i("descItemEditText:" + descItemEditText); } public void init(int position) { - Description description = descriptions.get(position); - if (!TextUtils.isEmpty(description.getDescriptionText())) { - etDescriptionText.setText(description.getDescriptionText()); - } else { - etDescriptionText.setText(""); - } - Drawable drawableRight = context.getResources() - .getDrawable(R.drawable.mapbox_info_icon_default); - if (position != 0) { - etDescriptionText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); - } else { - etDescriptionText.setCompoundDrawablesWithIntrinsicBounds(null, null, drawableRight, null); - } - - etDescriptionText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { - - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { - - } - - @Override - public void afterTextChanged(Editable editable) { - description.setDescriptionText(editable.toString()); - } - }); - - etDescriptionText.setOnFocusChangeListener((v, hasFocus) -> { - if (!hasFocus) { - ViewUtil.hideKeyboard(v); - } - }); - - SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(context, - R.layout.row_item_languages_spinner); - Collections.sort(languages, (language, t1) -> language.getLocale().getDisplayLanguage() - .compareTo(t1.getLocale().getDisplayLanguage().toString())); - languagesAdapter.setLanguages(languages); - languagesAdapter.notifyDataSetChanged(); - spinnerDescriptionLanguages.setAdapter(languagesAdapter); - - if (description.getSelectedLanguageIndex() == -1) { - if (position == 0) { - int defaultLocaleIndex = getIndexOfUserDefaultLocale(); - spinnerDescriptionLanguages.setSelection(defaultLocaleIndex); + if (position == 0) { + Timber.d("Title is " + title); + if (!title.isEmpty()) { + descItemEditText.setText(title.toString()); } else { - spinnerDescriptionLanguages.setSelection(0); + descItemEditText.setText(""); } + + descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(), null); + + descItemEditText.addTextChangedListener(new AbstractTextWatcher(titleText ->{ + title.setTitleText(titleText); + titleChangedSubject.onNext(titleText); + })); + + descItemEditText.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + ViewUtil.hideKeyboard(v); + } else { + uploadView.setTopCardState(false); + } + }); + } else { - spinnerDescriptionLanguages.setSelection(description.getSelectedLanguageIndex()); + Description description = descriptions.get(position - 1); + Timber.d("Description is " + description); + if (!TextUtils.isEmpty(description.getDescriptionText())) { + descItemEditText.setText(description.getDescriptionText()); + } else { + descItemEditText.setText(""); + } + if (position == 1) { + descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(), null); + } else { + descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); + } + + descItemEditText.addTextChangedListener(new AbstractTextWatcher(descriptionText -> { + description.setDescriptionText(descriptionText); + })); + descItemEditText.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + ViewUtil.hideKeyboard(v); + } else { + uploadView.setTopCardState(false); + } + }); + + SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(context, + R.layout.row_item_languages_spinner, selectedLanguages); + languagesAdapter.notifyDataSetChanged(); + spinnerDescriptionLanguages.setAdapter(languagesAdapter); + + if (description.getSelectedLanguageIndex() == -1) { + if (position == 1) { + int defaultLocaleIndex = languagesAdapter.getIndexOfUserDefaultLocale(context); + spinnerDescriptionLanguages.setSelection(defaultLocaleIndex); + } else { + spinnerDescriptionLanguages.setSelection(0); + } + } else { + spinnerDescriptionLanguages.setSelection(description.getSelectedLanguageIndex()); + selectedLanguages.put(spinnerDescriptionLanguages, description.getLanguageCode()); + } + + //TODO do it the butterknife way + spinnerDescriptionLanguages.setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int position, + long l) { + description.setSelectedLanguageIndex(position); + String languageCode = ((SpinnerLanguagesAdapter) adapterView.getAdapter()).getLanguageCode(position); + description.setLanguageCode(languageCode); + selectedLanguages.remove(adapterView); + selectedLanguages.put(adapterView, languageCode); + ((SpinnerLanguagesAdapter) adapterView.getAdapter()).selectedLangCode = languageCode; + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + + } + }); } - languages.get(spinnerDescriptionLanguages.getSelectedItemPosition()).setSet(true); - - //TODO do it the butterknife way - spinnerDescriptionLanguages.setOnItemSelectedListener(new OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int position, - long l) { - //TODO handle case when user tries to select an already selected language - updateDescriptionBasedOnSelectedLanguageIndex(description, position); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - - } - }); - - } - @OnTouch(R.id.et_description_text) + @Optional + @OnTouch(R.id.description_item_edit_text) boolean descriptionInfo(View view, MotionEvent motionEvent) { - + //Title info is visible only for the title if (getAdapterPosition() == 0) { - //Description info is visible only for the first item + if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) { + final int value = view.getRight() - descItemEditText + .getCompoundDrawables()[2] + .getBounds().width(); + if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) { + callback.showAlert(R.string.media_detail_title, R.string.title_info); + return true; + } + } else { + final int value = descItemEditText.getLeft() + descItemEditText + .getCompoundDrawables()[0] + .getBounds().width(); + if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) { + callback.showAlert(R.string.media_detail_title, R.string.title_info); + return true; + } + } + //Description info is visible only for the first description + } else if (getAdapterPosition() == 1) { final int value; if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) { - value = etDescriptionText.getRight() - etDescriptionText - .getCompoundDrawables()[2] - .getBounds().width() - etDescriptionText.getPaddingRight(); - if (motionEvent.getAction() == ACTION_UP && motionEvent.getX() >= value) { + value = view.getRight() - descItemEditText.getCompoundDrawables()[2].getBounds().width(); + if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) { callback.showAlert(R.string.media_detail_description, R.string.description_info); return true; } } else { - value = etDescriptionText.getLeft() + etDescriptionText + value = descItemEditText.getLeft() + descItemEditText .getCompoundDrawables()[0] .getBounds().width(); if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) { @@ -206,27 +265,12 @@ class DescriptionsAdapter extends RecyclerView.AdapterResponsible for checking if the picture that the user is trying to upload is useful or not. Will attempt to filter - * away completely black,fuzzy/blurry pictures(for now). - * - *

todo: Detect selfies? - */ - -public class DetectUnwantedPicturesAsync extends AsyncTask { - - private final String imageMediaFilePath; - public final WeakReference activityWeakReference; - - DetectUnwantedPicturesAsync(WeakReference activityWeakReference, String imageMediaFilePath) { - //this.callback = callback; - this.imageMediaFilePath = imageMediaFilePath; - this.activityWeakReference = activityWeakReference; - } - - @Override - protected ImageUtils.Result doInBackground(Void... voids) { - try { - Timber.d("FilePath: " + imageMediaFilePath); - if (imageMediaFilePath == null) { - return ImageUtils.Result.IMAGE_OK; - } - - BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(imageMediaFilePath,false); - - return ImageUtils.checkIfImageIsTooDark(decoder); - } catch (IOException ioe) { - Timber.e(ioe, "IO Exception"); - return ImageUtils.Result.IMAGE_OK; - } - } - - @Override - protected void onPostExecute(ImageUtils.Result result) { - super.onPostExecute(result); - Activity activity = activityWeakReference.get(); - - if (result != ImageUtils.Result.IMAGE_OK) { - //show appropriate error message - String errorMessage = result == ImageUtils.Result.IMAGE_DARK ? activity.getString(R.string.upload_image_too_dark) : activity.getString(R.string.upload_image_blurry); - AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(activity); - errorDialogBuilder.setMessage(errorMessage); - errorDialogBuilder.setTitle(activity.getString(R.string.warning)); - errorDialogBuilder.setPositiveButton(activity.getString(R.string.no), (dialogInterface, i) -> { - //user does not wish to upload the picture, take them back to ContributionsActivity - Intent intent = new Intent(activity, ContributionsActivity.class); - dialogInterface.dismiss(); - activity.startActivity(intent); - }); - errorDialogBuilder.setNegativeButton(activity.getString(R.string.yes), (dialogInterface, i) -> { - //user wishes to go ahead with the upload of this picture, just dismiss this dialog - dialogInterface.dismiss(); - }); - - AlertDialog errorDialog = errorDialogBuilder.create(); - if (!activity.isFinishing()) { - errorDialog.show(); - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DexterPermissionObtainer.java b/app/src/main/java/fr/free/nrw/commons/upload/DexterPermissionObtainer.java new file mode 100644 index 000000000..64f483572 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/DexterPermissionObtainer.java @@ -0,0 +1,153 @@ +package fr.free.nrw.commons.upload; + +import android.app.Activity; + +import com.karumi.dexter.Dexter; +import com.karumi.dexter.DexterBuilder; +import com.karumi.dexter.listener.PermissionDeniedResponse; +import com.karumi.dexter.listener.PermissionGrantedResponse; +import com.karumi.dexter.listener.single.BasePermissionListener; + +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.utils.DialogUtil; +import fr.free.nrw.commons.utils.ExternalStorageUtils; +import fr.free.nrw.commons.utils.PermissionUtils; +import io.reactivex.Completable; +import io.reactivex.subjects.CompletableSubject; +import timber.log.Timber; + +public class DexterPermissionObtainer { + private final String requestedPermission; + private android.app.AlertDialog storagePermissionInfoDialog; + private DexterBuilder dexterStoragePermissionBuilder; + + private PermissionDeniedResponse permissionDeniedResponse; + + private boolean storagePromptInProgress; + + private final String rationaleTitle; + private final String rationaleText; + + private Activity activity; + + private CompletableSubject storagePromptObservable; + + /** + * @param activity The activity that is requesting the permission + * @param requestedPermission The permission being requested in the form of Manifest.permission.* + * @param rationaleTitle The title of the rationale dialog + * @param rationaleText The text inside the rationale dialog + */ + DexterPermissionObtainer(Activity activity, String requestedPermission, String rationaleTitle, String rationaleText) { + this.activity = activity; + this.rationaleTitle = rationaleTitle; + this.rationaleText = rationaleText; + this.requestedPermission = requestedPermission; + this.storagePromptObservable = CompletableSubject.create(); + initPermissionsRationaleDialog(); + } + + /** + * Checks if storage permissions are obtained, prompts the users to grant storage permissions if necessary. + * When storage permission is present, onPermissionObtained is called. + */ + Completable confirmStoragePermissions() { + if (ExternalStorageUtils.isStoragePermissionGranted(activity)) { + Timber.i("Storage permissions already granted."); + storagePromptObservable.onComplete(); + } else if (!storagePromptInProgress) { + if (storagePromptObservable.hasComplete()) { + storagePromptObservable = CompletableSubject.create(); + } + //If permission is not there, ask for it + storagePromptInProgress = true; + askDexterToHandleExternalStoragePermission(); + } + return storagePromptObservable; + } + + + /** + * To be called when the user returns to the original activity after manually enabling storage permissions. + */ + void onManualPermissionReturned() { + //OnActivity result, no matter what the result is, our function can handle that. + askDexterToHandleExternalStoragePermission(); + } + + /** + * This method initialised the Dexter's permission builder (if not already initialised). Also makes sure that the builder is initialised + * only once, otherwise we would'nt know on which instance of it, the user is working on. And after the builder is initialised, it checks + * for the required permission and then handles the permission status, thanks to Dexter's appropriate callbacks. + */ + private void askDexterToHandleExternalStoragePermission() { + Timber.d("External storage permission is being requested"); + if (null == dexterStoragePermissionBuilder) { + dexterStoragePermissionBuilder = Dexter.withActivity(activity) + .withPermission(requestedPermission) + .withListener(new BasePermissionListener() { + @Override + public void onPermissionGranted(PermissionGrantedResponse response) { + Timber.d("User has granted us the permission for writing the external storage"); + //If permission is granted, well and good + storagePromptInProgress = false; + storagePromptObservable.onComplete(); + //onPermissionObtained.run(); + } + + @Override + public void onPermissionDenied(PermissionDeniedResponse response) { + Timber.d("User has granted us the permission for writing the external storage"); + //If permission is not granted in whatsoever scenario, we show him a dialog stating why we need the permission + permissionDeniedResponse = response; + if (null != storagePermissionInfoDialog && !storagePermissionInfoDialog + .isShowing()) { + storagePermissionInfoDialog.show(); + } + } + }); + } + dexterStoragePermissionBuilder.check(); + } + + /** + * We have agreed to show a dialog showing why we need a particular permission. + * This method is used to initialise the dialog which is going to show the permission's rationale. + * The dialog is initialised along with a callback for positive and negative user actions. + */ + private void initPermissionsRationaleDialog() { + if (storagePermissionInfoDialog == null) { + storagePermissionInfoDialog = DialogUtil + .getAlertDialogWithPositiveAndNegativeCallbacks( + activity, + rationaleTitle, rationaleText, + R.drawable.ic_launcher, new DialogUtil.Callback() { + @Override + public void onPositiveButtonClicked() { + //If the user is willing to give us the permission + //But had somehow previously choose never ask again, we take him to app settings to manually enable permission + if (null == permissionDeniedResponse) { + //Dexter returned null, lets see if this ever happens + Timber.w("Dexter returned null as permissionDeniedResponse"); + } else if (permissionDeniedResponse.isPermanentlyDenied()) { + PermissionUtils.askUserToManuallyEnablePermissionFromSettings(activity); + Timber.i("Permission permanently denied."); + } else { + //or if we still have chance to show runtime permission dialog, we show him that. + askDexterToHandleExternalStoragePermission(); + Timber.d("Asking via Dexter for permission."); + } + } + + @Override + public void onNegativeButtonClicked() { + //This was the behaviour as of now, I was planning to maybe snack him with some message + //and then call finish after some time, or may be it could be associated with some action + // on the snack. If the user does not want us to give the permission, even after showing + // rationale dialog, lets not trouble him any more. + activity.finish(); + } + }); + } + } +} 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 deleted file mode 100644 index f74c40867..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java +++ /dev/null @@ -1,95 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.support.v7.app.AlertDialog; - -import java.io.IOException; -import java.lang.ref.WeakReference; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.ContributionsActivity; -import fr.free.nrw.commons.mwapi.MediaWikiApi; -import timber.log.Timber; - -/** - * Sends asynchronous queries to the Commons MediaWiki API to check that file doesn't already exist - * Displays a warning to the user if the file already exists on Commons - */ -public class ExistingFileAsync extends AsyncTask { - - interface Callback { - void onResult(Result result); - } - - public enum Result { - NO_DUPLICATE, - DUPLICATE_PROCEED, - DUPLICATE_CANCELLED - } - - private final WeakReference activity; - private final MediaWikiApi api; - private final String fileSha1; - private final WeakReference context; - private final Callback callback; - - public ExistingFileAsync(WeakReference activity, String fileSha1, WeakReference context, Callback callback, MediaWikiApi mwApi) { - this.activity = activity; - this.fileSha1 = fileSha1; - this.context = context; - this.callback = callback; - this.api = mwApi; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - } - - @Override - protected Boolean doInBackground(Void... voids) { - - // https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba - boolean fileExists; - try { - String fileSha1 = this.fileSha1; - fileExists = api.existingFile(fileSha1); - } catch (IOException e) { - Timber.e(e, "IO Exception: "); - return false; - } - - Timber.d("File already exists in Commons: %s", fileExists); - return fileExists; - } - - @Override - protected void onPostExecute(Boolean fileExists) { - super.onPostExecute(fileExists); - - // If file exists, display warning to user. - // Use soft warning for now (user able to choose to proceed) until have determined that implementation works without bugs - if (fileExists) { - AlertDialog.Builder builder = new AlertDialog.Builder(context.get()); - builder.setMessage(R.string.file_exists) - .setTitle(R.string.warning); - builder.setPositiveButton(R.string.no, (dialog, id) -> { - //Go back to ContributionsActivity - Intent intent = new Intent(context.get(), ContributionsActivity.class); - context.get().startActivity(intent); - callback.onResult(Result.DUPLICATE_CANCELLED); - }); - builder.setNegativeButton(R.string.yes, (dialog, id) -> callback.onResult(Result.DUPLICATE_PROCEED)); - - AlertDialog dialog = builder.create(); - if (!activity.get().isFinishing()) { - dialog.show(); - } - } else { - callback.onResult(Result.NO_DUPLICATE); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java index b29d686f5..c5b6df227 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java @@ -1,21 +1,21 @@ package fr.free.nrw.commons.upload; import android.annotation.SuppressLint; -import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; +import android.media.ExifInterface; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.ParcelFileDescriptor; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.lang.ref.WeakReference; import java.util.Date; import java.util.List; @@ -44,89 +44,44 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse { @Inject @Named("default_preferences") SharedPreferences prefs; - private Uri mediaUri; + private String filePath; private ContentResolver contentResolver; private GPSExtractor imageObj; private Context context; private String decimalCoords; - private boolean haveCheckedForOtherImages = false; - private String filePath; + private ExifInterface exifInterface; private boolean useExtStorage; - private boolean cacheFound; + private boolean haveCheckedForOtherImages = false; private GPSExtractor tempImageObj; - FileProcessor(Uri mediaUri, ContentResolver contentResolver, Context context) { - this.mediaUri = mediaUri; + FileProcessor(@NonNull String filePath, ContentResolver contentResolver, Context context) { + this.filePath = filePath; this.contentResolver = contentResolver; this.context = context; ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this); + try { + exifInterface=new ExifInterface(filePath); + } catch (IOException e) { + Timber.e(e); + } useExtStorage = prefs.getBoolean("useExternalStorage", true); } - /** - * Gets file path from media URI. - * In older devices getPath() may fail depending on the source URI, creating and using a copy of the file seems to work instead. - * - * @return file path of media - */ - @Nullable - private String getPathOfMediaOrCopy() { - filePath = FileUtils.getPath(context, mediaUri); - Timber.d("Filepath: " + filePath); - if (filePath == null) { - String copyPath = null; - try { - ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r"); - if (descriptor != null) { - if (useExtStorage) { - copyPath = FileUtils.createCopyPath(descriptor); - return copyPath; - } - copyPath = getApplicationContext().getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + ".jpg"; - FileUtils.copy(descriptor.getFileDescriptor(), copyPath); - Timber.d("Filepath (copied): %s", copyPath); - return copyPath; - } - } catch (IOException e) { - Timber.w(e, "Error in file " + copyPath); - return null; - } - } - return filePath; - } - /** * Processes file coordinates, either from EXIF data or user location - * - * @param gpsEnabled if true use GPS */ - GPSExtractor processFileCoordinates(boolean gpsEnabled) { + GPSExtractor processFileCoordinates(SimilarImageInterface similarImageInterface) { Timber.d("Calling GPSExtractor"); - try { - ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (descriptor != null) { - imageObj = new GPSExtractor(descriptor.getFileDescriptor()); - } - } else { - String filePath = getPathOfMediaOrCopy(); - if (filePath != null) { - imageObj = new GPSExtractor(filePath); - } - } - - decimalCoords = imageObj.getCoords(); - if (decimalCoords == null || !imageObj.imageCoordsExists) { - //Find other photos taken around the same time which has gps coordinates - if (!haveCheckedForOtherImages) - findOtherImages();// Do not do repeat the process - } else { - useImageCoords(); - } - - } catch (FileNotFoundException e) { - Timber.w("File not found: " + mediaUri, e); + imageObj = new GPSExtractor(exifInterface); + decimalCoords = imageObj.getCoords(); + if (decimalCoords == null || !imageObj.imageCoordsExists) { + //Find other photos taken around the same time which has gps coordinates + if (!haveCheckedForOtherImages) + findOtherImages(similarImageInterface);// Do not do repeat the process + } else { + useImageCoords(); } + return imageObj; } @@ -136,10 +91,10 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse { /** * Find other images around the same location that were taken within the last 20 sec - * + * @param similarImageInterface */ - private void findOtherImages() { - Timber.d("filePath" + getPathOfMediaOrCopy()); + private void findOtherImages(SimilarImageInterface similarImageInterface) { + Timber.d("filePath" + filePath); long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created File folder = new File(filePath.substring(0, filePath.lastIndexOf('/'))); @@ -154,7 +109,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse { tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos ParcelFileDescriptor descriptor = null; try { - descriptor = contentResolver.openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r"); + descriptor = contentResolver.openFileDescriptor(Uri.fromFile(file), "r"); } catch (FileNotFoundException e) { e.printStackTrace(); } @@ -173,12 +128,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse { if (tempImageObj.getCoords() != null && tempImageObj.imageCoordsExists) { // Current image has gps coordinates and it's not current gps locaiton Timber.d("This file has image coords:" + file.getAbsolutePath()); - SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment(); - Bundle args = new Bundle(); - args.putString("originalImagePath", filePath); - args.putString("possibleImagePath", file.getAbsolutePath()); - newFragment.setArguments(args); - newFragment.show(((AppCompatActivity) context).getSupportFragmentManager(), "dialog"); + similarImageInterface.showSimilarImageFragment(filePath, file.getAbsolutePath()); break; } } @@ -210,7 +160,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse { // If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories if (catListEmpty) { - cacheFound = false; apiCall.request(decimalCoords) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) @@ -223,7 +172,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse { ); Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList); } else { - cacheFound = true; Timber.d("Cache found, setting categoryList in model to %s", displayCatList); gpsCategoryModel.setCategoryList(displayCatList); } @@ -232,20 +180,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse { } } - boolean isCacheFound() { - return cacheFound; - } - - /** - * Calls the async task that detects if image is fuzzy, too dark, etc - */ - void detectUnwantedPictures() { - String imageMediaFilePath = FileUtils.getPath(context, mediaUri); - DetectUnwantedPicturesAsync detectUnwantedPicturesAsync - = new DetectUnwantedPicturesAsync(new WeakReference((Activity) context), imageMediaFilePath); - detectUnwantedPicturesAsync.execute(); - } - @Override public void onPositiveResponse() { imageObj = tempImageObj; @@ -259,4 +193,4 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse { Timber.d("EXIF from imageObj"); useImageCoords(); } -} +} \ No newline at end of file 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 0cd45c189..9401c941e 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,6 +1,7 @@ package fr.free.nrw.commons.upload; import android.annotation.SuppressLint; +import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.SharedPreferences; @@ -12,6 +13,7 @@ import android.os.ParcelFileDescriptor; import android.preference.PreferenceManager; import android.provider.DocumentsContract; import android.provider.MediaStore; +import android.provider.OpenableColumns; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -33,6 +35,8 @@ import java.util.Date; import timber.log.Timber; +import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext; + public class FileUtils { /** @@ -76,21 +80,32 @@ public class FileUtils { /** * In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead. + * * @return path of copy */ - @Nullable - static String createCopyPath(ParcelFileDescriptor descriptor) { - try { - String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + ".jpg"; - File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp"); - newFile.mkdir(); - FileUtils.copy(descriptor.getFileDescriptor(), copyPath); - Timber.d("Filepath (copied): %s", copyPath); - return copyPath; - } catch (IOException e) { - Timber.e(e); - return null; - } + @NonNull + static String createExternalCopyPathAndCopy(Uri uri, ContentResolver contentResolver) throws IOException { + FileDescriptor fileDescriptor = contentResolver.openFileDescriptor(uri, "r").getFileDescriptor(); + String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + "." + getFileExt(uri, contentResolver); + File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp"); + newFile.mkdir(); + FileUtils.copy(fileDescriptor, copyPath); + Timber.d("Filepath (copied): %s", copyPath); + return copyPath; + } + + /** + * In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead. + * + * @return path of copy + */ + @NonNull + static String createCopyPathAndCopy(Uri uri, Context context) throws IOException { + FileDescriptor fileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r").getFileDescriptor(); + String copyPath = context.getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + "." + getFileExt(uri, context.getContentResolver()); + FileUtils.copy(fileDescriptor, copyPath); + Timber.d("Filepath (copied): %s", copyPath); + return copyPath; } /** @@ -121,13 +136,13 @@ public class FileUtils { if ("primary".equalsIgnoreCase(type)) { returnPath = Environment.getExternalStorageDirectory() + "/" + split[1]; } - } else if (isDownloadsDocument(uri)) { // DownloadsProvider + } else if (isDownloadsDocument(uri)) { // DownloadsProvider final String id = DocumentsContract.getDocumentId(uri); final Uri contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/document"), Long.valueOf(id)); - returnPath = getDataColumn(context, contentUri, null, null); + returnPath = getDataColumn(context, contentUri, null, null); } else if (isMediaDocument(uri)) { // MediaProvider final String docId = DocumentsContract.getDocumentId(uri); @@ -166,7 +181,7 @@ public class FileUtils { returnPath = uri.getPath(); } - if(returnPath == null) { + if (returnPath == null) { //fetching path may fail depending on the source URI and all hope is lost //so we will create and use a copy of the file, which seems to work String copyPath = null; @@ -304,6 +319,7 @@ public class FileUtils { /** * Read and return the content of a resource file as string. + * * @param fileName asset file's path (e.g. "/queries/nearby_query.rq") * @return the content of the file */ @@ -330,6 +346,7 @@ public class FileUtils { /** * Deletes files. + * * @param file context */ public static boolean deleteFile(File file) { @@ -355,7 +372,7 @@ public class FileUtils { commonsAppDirectory.mkdir(); } - File logsFile = new File(commonsAppDirectory,"logs.txt"); + File logsFile = new File(commonsAppDirectory, "logs.txt"); if (logsFile.exists()) { //old logs file is useless logsFile.delete(); @@ -377,4 +394,39 @@ public class FileUtils { } } + public static String getFilename(Uri uri, ContentResolver contentResolver) { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) + return ""; + String result = null; + if (uri.getScheme().equals("content")) { + try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); + } + } + } + if (result == null) { + result = uri.getPath(); + int cut = result.lastIndexOf('/'); + if (cut != -1) { + result = result.substring(cut + 1); + } + } + return result; + } + + public static String getFileExt(String fileName){ + //Default file extension + String extension=".jpg"; + + int i = fileName.lastIndexOf('.'); + if (i > 0) { + extension = fileName.substring(i+1); + } + return extension; + } + + public static String getFileExt(Uri uri, ContentResolver contentResolver) { + return getFileExt(getFilename(uri, contentResolver)); + } } \ No newline at end of file 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 e45b31f05..a6b150c42 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 @@ -16,11 +16,22 @@ import timber.log.Timber; */ public class GPSExtractor { - private ExifInterface exif; + public static final GPSExtractor DUMMY= new GPSExtractor(); private double decLatitude; private double decLongitude; public boolean imageCoordsExists; + private String latitude; + private String longitude; + private String latitudeRef; + private String longitudeRef; + private String decimalCoords; + /** + * Dummy constructor. + */ + private GPSExtractor(){ + + } /** * Construct from the file descriptor of the image (only for API 24 or newer). * @param fileDescriptor the file descriptor of the image @@ -28,7 +39,8 @@ public class GPSExtractor { @RequiresApi(24) public GPSExtractor(@NonNull FileDescriptor fileDescriptor) { try { - exif = new ExifInterface(fileDescriptor); + ExifInterface exif = new ExifInterface(fileDescriptor); + processCoords(exif); } catch (IOException | IllegalArgumentException e) { Timber.w(e); } @@ -41,47 +53,53 @@ public class GPSExtractor { */ public GPSExtractor(@NonNull String path) { try { - exif = new ExifInterface(path); + ExifInterface exif = new ExifInterface(path); + processCoords(exif); } catch (IOException | IllegalArgumentException e) { Timber.w(e); } } + /** + * Construct from the file path of the image. + * @param exif exif interface of the image + * + */ + public GPSExtractor(@NonNull ExifInterface exif){ + processCoords(exif); + } + + private void processCoords(ExifInterface exif){ + //If image has no EXIF data and user has enabled GPS setting, get user's location + //Always return null as a temporary fix for #1599 + if (exif != null && exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) != null) { + //If image has EXIF data, extract image coords + imageCoordsExists = true; + Timber.d("EXIF data has location info"); + + latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE); + latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF); + longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE); + longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF); + } + } + /** * Extracts geolocation (either of image from EXIF data, or of user) * @return coordinates as string (needs to be passed as a String in API query) */ @Nullable public String getCoords() { - String latitude; - String longitude; - String latitudeRef; - String longitudeRef; - String decimalCoords; + if(decimalCoords!=null){ + return decimalCoords; + }else if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) { + Timber.d("Latitude: %s %s", latitude, latitudeRef); + Timber.d("Longitude: %s %s", longitude, longitudeRef); - //If image has no EXIF data and user has enabled GPS setting, get user's location - //TODO: Always return null as a temporary fix for #1599 - if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) { - return null; + decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef); + return decimalCoords; } else { - //If image has EXIF data, extract image coords - imageCoordsExists = true; - Timber.d("EXIF data has location info"); - - latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE); - latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF); - longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE); - longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF); - - if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) { - Timber.d("Latitude: %s %s", latitude, latitudeRef); - Timber.d("Longitude: %s %s", longitude, longitudeRef); - - decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef); - return decimalCoords; - } else { - return null; - } + return null; } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java b/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java new file mode 100644 index 000000000..ff100e16e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/HeightLimitedRecyclerView.java @@ -0,0 +1,52 @@ +package fr.free.nrw.commons.upload; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Point; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.Display; + +/** + * Created by Ilgaz Er on 8/7/2018. + */ +public class HeightLimitedRecyclerView extends RecyclerView { + + int height; + + + public HeightLimitedRecyclerView(Context context) { + super(context); + DisplayMetrics displayMetrics = new DisplayMetrics(); + ((Activity) getContext()).getWindowManager() + .getDefaultDisplay() + .getMetrics(displayMetrics); + height=displayMetrics.heightPixels; + } + + public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + DisplayMetrics displayMetrics = new DisplayMetrics(); + ((Activity) getContext()).getWindowManager() + .getDefaultDisplay() + .getMetrics(displayMetrics); + height=displayMetrics.heightPixels; + } + + public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + DisplayMetrics displayMetrics = new DisplayMetrics(); + ((Activity) getContext()).getWindowManager() + .getDefaultDisplay() + .getMetrics(displayMetrics); + height=displayMetrics.heightPixels; + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + heightSpec = MeasureSpec.makeMeasureSpec((int) (height*0.3), MeasureSpec.AT_MOST); + super.onMeasure(widthSpec, heightSpec); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Language.java b/app/src/main/java/fr/free/nrw/commons/upload/Language.java index 8d4b27239..ab03a4db7 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/Language.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/Language.java @@ -3,7 +3,6 @@ package fr.free.nrw.commons.upload; import java.util.Locale; class Language { - private Locale locale; private boolean isSet = false; 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 deleted file mode 100644 index 1349ed773..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java +++ /dev/null @@ -1,493 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.Manifest; -import android.Manifest.permission; -import android.app.AlertDialog; -import android.app.ProgressDialog; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.database.DataSetObserver; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.ParcelFileDescriptor; -import android.support.annotation.Nullable; -import android.support.v4.app.ActivityCompat; -import android.support.v4.app.FragmentManager; -import android.support.v4.content.ContextCompat; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.AdapterView; -import android.widget.Toast; - -import com.karumi.dexter.Dexter; -import com.karumi.dexter.DexterBuilder; -import com.karumi.dexter.listener.PermissionDeniedResponse; -import com.karumi.dexter.listener.PermissionGrantedResponse; -import com.karumi.dexter.listener.single.BasePermissionListener; - -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Named; - -import butterknife.ButterKnife; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.AuthenticatedActivity; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.category.CategorizationFragment; -import fr.free.nrw.commons.category.OnCategoriesSaveHandler; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.modifications.CategoryModifier; -import fr.free.nrw.commons.modifications.ModifierSequence; -import fr.free.nrw.commons.modifications.ModifierSequenceDao; -import fr.free.nrw.commons.modifications.TemplateRemoveModifier; -import fr.free.nrw.commons.mwapi.MediaWikiApi; -import fr.free.nrw.commons.utils.ContributionUtils; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.DialogUtil.Callback; -import fr.free.nrw.commons.utils.ExternalStorageUtils; -import fr.free.nrw.commons.utils.PermissionUtils; -import timber.log.Timber; - -//TODO: We should use this class to see how multiple uploads are handled, and then REMOVE it. - -public class MultipleShareActivity extends AuthenticatedActivity - implements MediaDetailPagerFragment.MediaDetailProvider, - AdapterView.OnItemClickListener, - FragmentManager.OnBackStackChangedListener, - MultipleUploadListFragment.OnMultipleUploadInitiatedHandler, - OnCategoriesSaveHandler, - ActivityCompat.OnRequestPermissionsResultCallback{ - - @Inject - MediaWikiApi mwApi; - @Inject - SessionManager sessionManager; - @Inject - UploadController uploadController; - @Inject - ModifierSequenceDao modifierSequenceDao; - @Inject - @Named("default_preferences") - SharedPreferences prefs; - - private ArrayList photosList = null; - - private MultipleUploadListFragment uploadsList; - private MediaDetailPagerFragment mediaDetails; - private CategorizationFragment categorizationFragment; - - private boolean locationPermitted = false; - private boolean isMultipleUploadsPrepared = false; - private boolean isMultipleUploadsFinalised = false; // Checks is user clicked to upload button or regret before this phase - private final String TAG="#MultipleShareActivity#"; - private AlertDialog storagePermissionInfoDialog; - private DexterBuilder dexterStoragePermissionBuilder; - - private PermissionDeniedResponse permissionDeniedResponse; - - @Override - public Media getMediaAtPosition(int i) { - return photosList.get(i); - } - - @Override - public int getTotalMediaCount() { - if (photosList == null) { - return 0; - } - return photosList.size(); - } - - @Override - public void notifyDatasetChanged() { - if (uploadsList != null) { - uploadsList.notifyDatasetChanged(); - } - } - - @Override - public void registerDataSetObserver(DataSetObserver observer) { - // fixme implement me if needed - } - - @Override - public void unregisterDataSetObserver(DataSetObserver observer) { - // fixme implement me if needed - } - - @Override - public void onItemClick(AdapterView adapterView, View view, int index, long item) { - showDetail(index); - } - - @Override - public void OnMultipleUploadInitiated() { - // No need to request external permission here, because if user can reach this point, then she permission granted - Timber.d("OnMultipleUploadInitiated"); - multipleUploadBegins(); - } - - private void multipleUploadBegins() { - - Timber.d("Multiple upload begins"); - final ProgressDialog dialog = new ProgressDialog(this); - dialog.setIndeterminate(false); - dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - dialog.setMax(photosList.size()); - dialog.setTitle(getResources().getQuantityString(R.plurals.starting_multiple_uploads, photosList.size(), photosList.size())); - dialog.show(); - - for (int i = 0; i < photosList.size(); i++) { - Contribution up = photosList.get(i); - final int uploadCount = i + 1; // Goddamn Java - - uploadController.startUpload(up, contribution -> { - dialog.setProgress(uploadCount); - if (uploadCount == photosList.size()) { - dialog.dismiss(); - Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG); - startingToast.show(); - } - }); - } - - uploadsList.setImageOnlyMode(true); - - categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization"); - if (categorizationFragment == null) { - categorizationFragment = new CategorizationFragment(); - } - // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next - View target = getCurrentFocus(); - if (target != null) { - InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) - imm.hideSoftInputFromWindow(target.getWindowToken(), 0); - } - getSupportFragmentManager().beginTransaction() - .add(R.id.uploadsFragmentContainer, categorizationFragment, "categorization") - .commitAllowingStateLoss(); - isMultipleUploadsFinalised = true; - //See http://stackoverflow.com/questions/7469082/getting-exception-illegalstateexception-can-not-perform-this-action-after-onsa - } - - @Override - public void onCategoriesSave(List categories) { - if (categories.size() > 0) { - for (Contribution contribution : photosList) { - ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri()); - - categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{}))); - categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized")); - - modifierSequenceDao.save(categoriesSequence); - } - } - // FIXME: Make sure that the content provider is up - // This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin - ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default! - finish(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - if (mediaDetails.isVisible()) { - getSupportFragmentManager().popBackStack(); - } - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_multiple_uploads); - ButterKnife.bind(this); - initDrawer(); - initPermissionsRationaleDialog(); - - if (savedInstanceState != null) { - photosList = savedInstanceState.getParcelableArrayList("uploadsList"); - } - - getSupportFragmentManager().addOnBackStackChangedListener(this); - requestAuthToken(); - - //TODO: 15/10/17 should location permission be explicitly requested if not provided? - //check if location permission is enabled - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { - { - locationPermitted = true; - } - } - } - - - /** - * We have agreed to show a dialog showing why we need a particular permission. - * This method is used to initialise the dialog which is going to show the permission's rationale. - * The dialog is initialised along with a callback for positive and negative user actions. - */ - private void initPermissionsRationaleDialog() { - if (storagePermissionInfoDialog == null) { - storagePermissionInfoDialog = DialogUtil - .getAlertDialogWithPositiveAndNegativeCallbacks( - MultipleShareActivity.this, - getString(R.string.storage_permission), getString( - R.string.write_storage_permission_rationale_for_image_share), - R.drawable.ic_launcher, new Callback() { - @Override - public void onPositiveButtonClicked() { - //If the user is willing to give us the permission - //But had somehow previously choose never ask again, we take him to app settings to manually enable permission - if(null== permissionDeniedResponse){ - //Dexter returned null, lets see if this ever happens - return; - } - else if (permissionDeniedResponse.isPermanentlyDenied()) { - PermissionUtils.askUserToManuallyEnablePermissionFromSettings(MultipleShareActivity.this); - } else { - //or if we still have chance to show runtime permission dialog, we show him that. - askDexterToHandleExternalStoragePermission(); - } - } - - @Override - public void onNegativeButtonClicked() { - //This was the behaviour as of now, I was planning to maybe snack him with some message - //and then call finish after some time, or may be it could be associated with some action on the snack - //If the user does not want us to give the permission, even after showing rationale dialog, lets not trouble him anymore - finish(); - } - }); - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - getSupportFragmentManager().removeOnBackStackChangedListener(this); - uploadController.cleanup(); - } - - private void showDetail(int i) { - if (mediaDetails == null || !mediaDetails.isVisible()) { - mediaDetails = new MediaDetailPagerFragment(true, false); - getSupportFragmentManager() - .beginTransaction() - .replace(R.id.uploadsFragmentContainer, mediaDetails) - .addToBackStack(null) - .commit(); - getSupportFragmentManager().executePendingTransactions(); - } - mediaDetails.showImage(i); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - /* This will be true if permission request is granted before we request. Otherwise we will - * explicitly call operations under this method again. - */ - if (isMultipleUploadsPrepared) { - super.onSaveInstanceState(outState); - Timber.d("onSaveInstanceState multiple uploads is prepared, permission granted"); - outState.putParcelableArrayList("uploadsList", photosList); - } else { - Timber.d("onSaveInstanceState multiple uploads is not prepared, permission not granted"); - return; - } - } - - @Override - protected void onAuthCookieAcquired(String authCookie) { - // Multiple uploads prepared boolean is used to decide when to call multipleUploadsBegin() - isMultipleUploadsFinalised = false; - isMultipleUploadsPrepared = false; - mwApi.setAuthCookie(authCookie); - if (!ExternalStorageUtils.isStoragePermissionGranted(this)) { - //If permission is not there, handle the negative cases - askDexterToHandleExternalStoragePermission(); - isMultipleUploadsPrepared = false; - return; // Postpone operation to do after gettion permission - } else { - isMultipleUploadsPrepared = true; - prepareMultipleUploadList(); - } - } - - /** - * This method initialised the Dexter's permission builder (if not already initialised). Also makes sure that the builder is initialised - * only once, otherwise we would'nt know on which instance of it, the user is working on. And after the builder is initialised, it checks for the required - * permission and then handles the permission status, thanks to Dexter's appropriate callbacks. - */ - private void askDexterToHandleExternalStoragePermission() { - Timber.d(TAG, "External storage permission is being requested"); - if (null == dexterStoragePermissionBuilder) { - dexterStoragePermissionBuilder = Dexter.withActivity(this) - .withPermission(permission.WRITE_EXTERNAL_STORAGE) - .withListener(new BasePermissionListener() { - @Override - public void onPermissionGranted(PermissionGrantedResponse response) { - Timber.d(TAG,"User has granted us the permission for writing the external storage"); - //If permission is granted, well and good - prepareMultipleUploadList(); - } - - @Override - public void onPermissionDenied(PermissionDeniedResponse response) { - Timber.d(TAG,"User has granted us the permission for writing the external storage"); - //If permission is not granted in whatsoever scenario, we show him a dialog stating why we need the permission - permissionDeniedResponse=response; - if (null != storagePermissionInfoDialog && !storagePermissionInfoDialog - .isShowing()) { - storagePermissionInfoDialog.show(); - } - } - }); - } - dexterStoragePermissionBuilder.check(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) { - //OnActivity result, no matter what the result is, our function can handle that. - askDexterToHandleExternalStoragePermission(); - } - } - - /** - * Prepares a list from files will be uploaded. Saves these files temporarily to external - * storage. Adds them to uploads list - */ - private void prepareMultipleUploadList() { - Intent intent = getIntent(); - - if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) { - if (photosList == null) { - photosList = new ArrayList<>(); - ArrayList urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); - for (int i = 0; i < urisList.size(); i++) { - Contribution up = new Contribution(); - Uri uri = urisList.get(i); - // Use temporarily saved file Uri instead - uri = ContributionUtils.saveFileBeingUploadedTemporarily(this, uri); - up.setLocalUri(uri); - up.setTag("mimeType", intent.getType()); - up.setTag("sequence", i); - up.setSource(Contribution.SOURCE_EXTERNAL); - up.setMultiple(true); - String imageGpsCoordinates = extractImageGpsData(uri); - if (imageGpsCoordinates != null) { - Timber.d("GPS data for image found!"); - up.setDecimalCoords(imageGpsCoordinates); - } - photosList.add(up); - } - } - - uploadsList = (MultipleUploadListFragment) getSupportFragmentManager().findFragmentByTag("uploadsList"); - if (uploadsList == null) { - uploadsList = new MultipleUploadListFragment(); - getSupportFragmentManager() - .beginTransaction() - .add(R.id.uploadsFragmentContainer, uploadsList, "uploadsList") - .commit(); - } - setTitle(getResources().getQuantityString(R.plurals.multiple_uploads_title, photosList.size(), photosList.size())); - uploadController.prepareService(); - } - } - - @Override - protected void onAuthFailure() { - Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG); - failureToast.show(); - finish(); - } - - @Override - public void onBackStackChanged() { - getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible()); - } - - /** - * Will attempt to extract the gps coordinates using exif data or by using the current - * location if available for the image who's imageUri has been provided. - * @param imageUri The uri of the image who's GPS coordinates data we wish to extract - * @return GPS coordinates as a String as is returned by {@link GPSExtractor} - */ - @Nullable - private String extractImageGpsData(Uri imageUri) { - Timber.d("Entering extractImagesGpsData"); - - if (imageUri == null) { - //now why would you do that??? - return null; - } - - GPSExtractor gpsExtractor = null; - - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(imageUri,"r"); - if (fd != null) { - gpsExtractor = new GPSExtractor(fd.getFileDescriptor()); - } - } else { - String filePath = FileUtils.getPath(this,imageUri); - if (filePath != null) { - gpsExtractor = new GPSExtractor(filePath); - } - } - - if (gpsExtractor != null) { - //get image coordinates from exif data or user location - return gpsExtractor.getCoords(); - } - - } catch (FileNotFoundException fnfe) { - Timber.w(fnfe); - return null; - } - - return null; - } - - // If on back pressed before sharing - @Override - public void onBackPressed() { - super.onBackPressed(); - } - - @Override - protected void onStop() { - // Remove saved files if activity is stopped before upload operation, ie user changed mind - if (!isMultipleUploadsFinalised) { - if (photosList != null) { - for (Contribution contribution : photosList) { - Timber.d("User changed mind, didn't click to upload button, deleted file: "+contribution.getLocalUri()); - ContributionUtils.removeTemporaryFile(contribution.getLocalUri()); - } - } - } - super.onStop(); - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java deleted file mode 100644 index 0dfdf5589..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java +++ /dev/null @@ -1,254 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.content.Context; -import android.graphics.Point; -import android.net.Uri; -import android.os.Bundle; -import android.support.graphics.drawable.VectorDrawableCompat; -import android.support.v4.app.Fragment; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.DisplayMetrics; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.GridView; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.Toast; - -import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; -import com.facebook.drawee.view.SimpleDraweeView; - -import butterknife.BindView; -import butterknife.ButterKnife; -import dagger.android.support.AndroidSupportInjection; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.utils.ViewUtil; - -public class MultipleUploadListFragment extends Fragment { - - public interface OnMultipleUploadInitiatedHandler { - void OnMultipleUploadInitiated(); - } - - @BindView(R.id.multipleShareBackground) - GridView photosGrid; - - @BindView(R.id.multipleBaseTitle) - EditText baseTitle; - - private PhotoDisplayAdapter photosAdapter; - private TitleTextWatcher textWatcher = new TitleTextWatcher(); - - private Point photoSize; - private MediaDetailPagerFragment.MediaDetailProvider detailProvider; - private OnMultipleUploadInitiatedHandler multipleUploadInitiatedHandler; - - private boolean imageOnlyMode; - - private static class UploadHolderView { - private Uri imageUri; - private SimpleDraweeView image; - private TextView title; - private RelativeLayout overlay; - } - - @Override - public void onAttach(Context context) { - AndroidSupportInjection.inject(this); - super.onAttach(context); - } - - private class PhotoDisplayAdapter extends BaseAdapter { - - @Override - public int getCount() { - return detailProvider.getTotalMediaCount(); - } - - @Override - public Object getItem(int i) { - return detailProvider.getMediaAtPosition(i); - } - - @Override - public long getItemId(int i) { - return i; - } - - @Override - public View getView(int i, View view, ViewGroup viewGroup) { - UploadHolderView holder; - - if (view == null) { - view = LayoutInflater.from(getContext()).inflate(R.layout.layout_upload_item, viewGroup, false); - holder = new UploadHolderView(); - holder.image = view.findViewById(R.id.uploadImage); - holder.title = view.findViewById(R.id.uploadTitle); - holder.overlay = view.findViewById(R.id.uploadOverlay); - - holder.image.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, photoSize.y)); - holder.image.setHierarchy(GenericDraweeHierarchyBuilder - .newInstance(getResources()) - .setPlaceholderImage(VectorDrawableCompat.create(getResources(), - R.drawable.ic_image_black_24dp, getContext().getTheme())) - .setFailureImage(VectorDrawableCompat.create(getResources(), - R.drawable.ic_error_outline_black_24dp, getContext().getTheme())) - .build()); - view.setTag(holder); - } else { - holder = (UploadHolderView) view.getTag(); - } - - Contribution up = (Contribution) this.getItem(i); - - if (holder.imageUri == null || !holder.imageUri.equals(up.getLocalUri())) { - holder.image.setImageURI(up.getLocalUri().toString()); - holder.imageUri = up.getLocalUri(); - } - - if (!imageOnlyMode) { - holder.overlay.setVisibility(View.VISIBLE); - holder.title.setText(up.getFilename()); - } else { - holder.overlay.setVisibility(View.GONE); - } - - return view; - } - } - - @Override - public void onStop() { - super.onStop(); - - // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next - View target = getActivity().getCurrentFocus(); - ViewUtil.hideKeyboard(target); - } - - // FIXME: Wrong result type - private Point calculatePicDimension(int count) { - DisplayMetrics screenMetrics = getResources().getDisplayMetrics(); - int screenWidth = screenMetrics.widthPixels; - int screenHeight = screenMetrics.heightPixels; - - int picWidth = Math.min((int) Math.sqrt(screenWidth * screenHeight / count), screenWidth); - picWidth = Math.min((int) (192 * screenMetrics.density), Math.max((int) (120 * screenMetrics.density), picWidth / 48 * 48)); - int picHeight = Math.min(picWidth, (int) (192 * screenMetrics.density)); // Max Height is same as Contributions list - - return new Point(picWidth, picHeight); - } - - public void notifyDatasetChanged() { - if (photosAdapter != null) { - photosAdapter.notifyDataSetChanged(); - } - } - - public void setImageOnlyMode(boolean mode) { - imageOnlyMode = mode; - if (imageOnlyMode) { - baseTitle.setVisibility(View.GONE); - } else { - baseTitle.setVisibility(View.VISIBLE); - } - photosAdapter.notifyDataSetChanged(); - photosGrid.setEnabled(!mode); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_multiple_uploads_list, container, false); - ButterKnife.bind(this,view); - photosAdapter = new PhotoDisplayAdapter(); - photosGrid.setAdapter(photosAdapter); - photosGrid.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity()); - photoSize = calculatePicDimension(detailProvider.getTotalMediaCount()); - photosGrid.setColumnWidth(photoSize.x); - - baseTitle.addTextChangedListener(textWatcher); - - baseTitle.setOnFocusChangeListener((v, hasFocus) -> { - if (!hasFocus) { - ViewUtil.hideKeyboard(v); - } - }); - - return view; - } - - @Override - public void onDestroyView() { - baseTitle.removeTextChangedListener(textWatcher); - super.onDestroyView(); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - menu.clear(); - inflater.inflate(R.menu.fragment_multiple_upload_list, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_upload_multiple: - if (baseTitle.getText().toString().trim().isEmpty()) { - Toast.makeText(getContext(), R.string.add_set_name_toast, Toast.LENGTH_LONG).show(); - return false; - } - multipleUploadInitiatedHandler.OnMultipleUploadInitiated(); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity(); - multipleUploadInitiatedHandler = (OnMultipleUploadInitiatedHandler) getActivity(); - - setHasOptionsMenu(true); - } - - private class TitleTextWatcher implements TextWatcher { - @Override - public void beforeTextChanged(CharSequence charSequence, int i1, int i2, int i3) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int i1, int i2, int i3) { - for (int i = 0; i < detailProvider.getTotalMediaCount(); i++) { - Contribution up = (Contribution) detailProvider.getMediaAtPosition(i); - Boolean isDirty = (Boolean) up.getTag("isDirty"); - if (isDirty == null || !isDirty) { - if (!TextUtils.isEmpty(charSequence)) { - up.setFilename(charSequence.toString() + " - " + ((Integer) up.getTag("sequence") + 1)); - } else { - up.setFilename(""); - } - } - } - detailProvider.notifyDatasetChanged(); - } - - @Override - public void afterTextChanged(Editable editable) { - } - } -} 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 deleted file mode 100644 index 6757c8419..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java +++ /dev/null @@ -1,674 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.Manifest; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.app.Activity; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.graphics.Point; -import android.graphics.Rect; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.support.annotation.NonNull; -import android.support.annotation.RequiresApi; -import android.support.design.widget.FloatingActionButton; -import android.support.graphics.drawable.VectorDrawableCompat; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; -import android.view.KeyEvent; -import android.view.MenuItem; -import android.view.View; -import android.view.animation.DecelerateInterpolator; -import android.widget.FrameLayout; -import android.widget.Toast; - -import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; -import com.facebook.drawee.view.SimpleDraweeView; -import com.github.chrisbanes.photoview.PhotoView; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.lang.ref.WeakReference; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Named; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.AuthenticatedActivity; -import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.caching.CacheController; -import fr.free.nrw.commons.category.CategorizationFragment; -import fr.free.nrw.commons.category.OnCategoriesSaveHandler; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.modifications.CategoryModifier; -import fr.free.nrw.commons.modifications.ModifierSequence; -import fr.free.nrw.commons.modifications.ModifierSequenceDao; -import fr.free.nrw.commons.modifications.TemplateRemoveModifier; -import fr.free.nrw.commons.mwapi.CategoryApi; -import fr.free.nrw.commons.mwapi.MediaWikiApi; -import fr.free.nrw.commons.utils.ContributionUtils; -import fr.free.nrw.commons.utils.ExternalStorageUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import timber.log.Timber; - -import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.DUPLICATE_PROCEED; -import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE; -import static fr.free.nrw.commons.upload.FileUtils.getSHA1; -import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF; - -/** - * Activity for the title/desc screen after image is selected. Also starts processing image - * GPS coordinates or user location (if enabled in Settings) for category suggestions. - */ -public class ShareActivity - extends AuthenticatedActivity - implements SingleUploadFragment.OnUploadActionInitiated, - OnCategoriesSaveHandler, - ActivityCompat.OnRequestPermissionsResultCallback { - - private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4; - //Had to make them class variables, to extract out the click listeners, also I see no harm in this - final Rect startBounds = new Rect(); - final Rect finalBounds = new Rect(); - final Point globalOffset = new Point(); - @Inject - MediaWikiApi mwApi; - @Inject - CacheController cacheController; - @Inject - SessionManager sessionManager; - @Inject - UploadController uploadController; - @Inject - ModifierSequenceDao modifierSequenceDao; - @Inject - CategoryApi apiCall; - @Inject @Named("application_preferences") SharedPreferences applicationPrefs; - @Inject - @Named("default_preferences") - SharedPreferences prefs; - @Inject - GpsCategoryModel gpsCategoryModel; - - @BindView(R.id.container) - FrameLayout flContainer; - @BindView(R.id.backgroundImage) - SimpleDraweeView backgroundImageView; - @BindView(R.id.media_map) - FloatingActionButton mapButton; - @BindView(R.id.media_upload_zoom_in) - FloatingActionButton zoomInButton; - @BindView(R.id.media_upload_zoom_out) - FloatingActionButton zoomOutButton; - @BindView(R.id.main_fab) - FloatingActionButton mainFab; - @BindView(R.id.expanded_image) - PhotoView expandedImageView; - - private String source; - private String mimeType; - private CategorizationFragment categorizationFragment; - private Uri mediaUri; - private Uri contentProviderUri; - private Contribution contribution; - private GPSExtractor gpsObj; - private String decimalCoords; - private FileProcessor fileObj; - private boolean useNewPermissions = false; - private boolean storagePermitted = false; - private boolean locationPermitted = false; - private String title; - private String description; - private String wikiDataEntityId; - private boolean duplicateCheckPassed = false; - private boolean isNearbyUpload = false; - private Animator CurrentAnimator; - private long ShortAnimationDuration; - private boolean isFABOpen = false; - private float startScaleFinal; - private Bundle savedInstanceState; - private boolean isUploadFinalised = false; // Checks is user clicked to upload button or regret before this phase - private boolean isZoom = false; - - - - /** - * Called when user taps the submit button. - * Requests Storage permission, if needed. - */ - - @Override - public void uploadActionInitiated(String title, String description) { - - this.title = title; - this.description = description; - - - if (sessionManager.getCurrentAccount() != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // Check for Storage permission that is required for upload. - // Do not allow user to proceed without permission, otherwise will crash - if (needsToRequestStoragePermission()) { - requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_PERM_ON_SUBMIT_STORAGE); - } else { - uploadBegins(); - } - } else { - uploadBegins(); - } - } - else //Send user to login activity - { - Toast.makeText(this, "You need to login first!", Toast.LENGTH_SHORT).show(); - Intent loginIntent = new Intent(ShareActivity.this, LoginActivity.class); - startActivity(loginIntent); - } - } - - /** - * Checks whether storage permissions need to be requested. - * Permissions are needed if the file is not owned by this application, (e.g. shared from the Gallery) - * - * @return true if file is not owned by this application and permission hasn't been granted beforehand - */ - @RequiresApi(16) - private boolean needsToRequestStoragePermission() { - return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri) - && (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED); - //return false; - } - - - /** - * Called after permission checks are done. - * Gets file metadata for category suggestions, displays toast, caches categories found, calls uploadController - */ - - private void uploadBegins() { - fileObj.processFileCoordinates(locationPermitted); - - Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG); - startingToast.show(); - - if (!fileObj.isCacheFound()) { - //Has to be called after apiCall.request() - cacheController.cacheCategory(); - Timber.d("Cache the categories found"); - } - - uploadController.startUpload(title, contentProviderUri, mediaUri, description, mimeType, source, decimalCoords, wikiDataEntityId, c -> { - ShareActivity.this.contribution = c; - showPostUpload(); - }); - isUploadFinalised = true; - } - - /** - * Starts CategorizationFragment after uploadBegins. - */ - - private void showPostUpload() { - if (categorizationFragment == null) { - categorizationFragment = new CategorizationFragment(); - } - getSupportFragmentManager().beginTransaction() - .replace(R.id.single_upload_fragment_container, categorizationFragment, "categorization") - .commit(); - } - - /** - * Send categories to modifications queue after they are selected - * - * @param categories categories selected - */ - @Override - public void onCategoriesSave(List categories) { - if (categories.size() > 0) { - ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri()); - - categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{}))); - categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized")); - modifierSequenceDao.save(categoriesSequence); - } - - // FIXME: Make sure that the content provider is up - // This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin - ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default! - - finish(); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (contribution != null) { - outState.putParcelable("contribution", contribution); - } - } - - @Override - protected void onAuthCookieAcquired(String authCookie) { - mwApi.setAuthCookie(authCookie); - } - - @Override - protected void onAuthFailure() { - Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG); - failureToast.show(); - finish(); - } - - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - isUploadFinalised = false; - setContentView(R.layout.activity_share); - ButterKnife.bind(this); - initBack(); - backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder - .newInstance(getResources()) - .setPlaceholderImage(VectorDrawableCompat.create(getResources(), - R.drawable.ic_image_black_24dp, getTheme())) - .setFailureImage(VectorDrawableCompat.create(getResources(), - R.drawable.ic_error_outline_black_24dp, getTheme())) - .build()); - if (!ExternalStorageUtils.isStoragePermissionGranted(this)) { - this.savedInstanceState = savedInstanceState; - ExternalStorageUtils.requestExternalStoragePermission(this); - return; // Postpone operation to do after getting permission - } else { - receiveImageIntent(); - createContributionWithReceivedIntent(savedInstanceState); - } - } - - @Override - protected void onStop() { - // If upload is not finalised with failure or success, but contribution is created, - // we have to remove temp file, to prevent using unnecessary memory - if (!isUploadFinalised) { - if (mediaUri != null) { - ContributionUtils.removeTemporaryFile(mediaUri); - } - } - super.onStop(); - } - - private void createContributionWithReceivedIntent(Bundle savedInstanceState) { - if (savedInstanceState != null) { - contribution = savedInstanceState.getParcelable("contribution"); - } - - requestAuthToken(); - Timber.d("Uri: %s", mediaUri.toString()); - Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory()); - - SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView"); - categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization"); - if (shareView == null && categorizationFragment == null) { - shareView = new SingleUploadFragment(); - getSupportFragmentManager() - .beginTransaction() - .add(R.id.single_upload_fragment_container, shareView, "shareView") - .commitAllowingStateLoss(); - } - uploadController.prepareService(); - - ContentResolver contentResolver = this.getContentResolver(); - fileObj = new FileProcessor(mediaUri, contentResolver, this); - checkIfFileExists(); - gpsObj = fileObj.processFileCoordinates(locationPermitted); - decimalCoords = fileObj.getDecimalCoords(); - if (sessionManager.getCurrentAccount() == null) { - Toast.makeText(this, getString(R.string.login_alert_message), Toast.LENGTH_SHORT).show(); - applicationPrefs.edit().putBoolean("login_skipped", false).apply(); - Intent loginIntent = new Intent(ShareActivity.this, LoginActivity.class); - startActivity(loginIntent); - } - } - - /** - * Receive intent from ContributionController.java when user selects picture to upload - */ - private void receiveImageIntent() { - Intent intent = getIntent(); - - if (Intent.ACTION_SEND.equals(intent.getAction())) { - mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); - contentProviderUri = mediaUri; - mediaUri = ContributionUtils.saveFileBeingUploadedTemporarily(this, mediaUri); - - if (intent.hasExtra(UploadService.EXTRA_SOURCE)) { - source = intent.getStringExtra(UploadService.EXTRA_SOURCE); - } else { - source = Contribution.SOURCE_EXTERNAL; - } - - boolean isDirectUpload = intent.getBooleanExtra("isDirectUpload", false); - - if (isDirectUpload) { - Timber.d("This was initiated by a direct upload from Nearby"); - isNearbyUpload = true; - wikiDataEntityId = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF); - Timber.d("Received wikiDataEntityId from contribution controller %s", wikiDataEntityId); - } - mimeType = intent.getType(); - } - - if (mediaUri != null) { - backgroundImageView.setImageURI(mediaUri); - } - } - - /** - * Function to display the zoom and map FAB - */ - private void showFABMenu() { - isFABOpen = true; - - if (gpsObj != null && gpsObj.imageCoordsExists) - mapButton.setVisibility(View.VISIBLE); - zoomInButton.setVisibility(View.VISIBLE); - - mainFab.animate().rotationBy(180); - mapButton.animate().translationY(-getResources().getDimension(R.dimen.second_fab)); - zoomInButton.animate().translationY(-getResources().getDimension(R.dimen.first_fab)); - } - - /** - * Function to close the zoom and map FAB - */ - private void closeFABMenu() { - isFABOpen = false; - mainFab.animate().rotationBy(-180); - mapButton.animate().translationY(0); - zoomInButton.animate().translationY(0).setListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animator) { - } - - @Override - public void onAnimationEnd(Animator animator) { - if (!isFABOpen) { - mapButton.setVisibility(View.GONE); - zoomInButton.setVisibility(View.GONE); - } - } - - @Override - public void onAnimationCancel(Animator animator) { - } - - @Override - public void onAnimationRepeat(Animator animator) { - } - }); - } - - /** - * Checks if upload was initiated via Nearby - * - * @return true if upload was initiated via Nearby - */ - protected boolean isNearbyUpload() { - return isNearbyUpload; - } - - /** - * Handles submit button permission request (for storage) - * - * @param requestCode type of request - * @param permissions permissions requested - * @param grantResults grant results - */ - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Timber.d("onRequestPermissionsResult external storage permission granted"); - // You can receive image intent and save image to a temp file only if ext storage permission is granted - receiveImageIntent(); - createContributionWithReceivedIntent(savedInstanceState); - - if (requestCode == REQUEST_PERM_ON_SUBMIT_STORAGE) { - checkIfFileExists(); - //Uploading only begins if storage permission granted from arrow icon - uploadBegins(); - } - - } else { - finish(); - } - } - - /** - * Check if file user wants to upload already exists on Commons - */ - private void checkIfFileExists() { - if (!useNewPermissions || storagePermitted) { - if (!duplicateCheckPassed) { - //Test SHA1 of image to see if it matches SHA1 of a file on Commons - try { - InputStream inputStream = getContentResolver().openInputStream(mediaUri); - String fileSHA1 = getSHA1(inputStream); - Timber.d("Input stream created from %s", mediaUri.toString()); - Timber.d("File SHA1 is: %s", fileSHA1); - - ExistingFileAsync fileAsyncTask = - new ExistingFileAsync(new WeakReference(this), fileSHA1, new WeakReference(this), result -> { - Timber.d("%s duplicate check: %s", mediaUri.toString(), result); - duplicateCheckPassed = (result == DUPLICATE_PROCEED || result == NO_DUPLICATE); - if (duplicateCheckPassed) { - //image is not a duplicate, so now check if its a unwanted picture or not - fileObj.detectUnwantedPictures(); - } - }, mwApi); - fileAsyncTask.execute(); - } catch (IOException e) { - Timber.e(e, "IO Exception: "); - } - } - } else { - Timber.w("not ready for preprocessing: useNewPermissions=%s storage=%s location=%s", - useNewPermissions, storagePermitted, locationPermitted); - } - } - - @Override - public void onPause() { - super.onPause(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - uploadController.cleanup(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - if (categorizationFragment != null && categorizationFragment.isVisible()) { - categorizationFragment.showBackButtonDialog(); - } else { - onBackPressed(); - } - return true; - } - return super.onOptionsItemSelected(item); - } - - /** - * Allows zooming in to the image about to be uploaded. Called when zoom FAB is tapped - */ - private void zoomImageFromThumb(final View thumbView, Uri imageuri) { - // If there's an animation in progress, cancel it immediately and proceed with this one. - if (CurrentAnimator != null) { - CurrentAnimator.cancel(); - } - isZoom = true; - ViewUtil.hideKeyboard(ShareActivity.this.findViewById(R.id.titleEdit)); - closeFABMenu(); - mainFab.setVisibility(View.GONE); - - InputStream input = null; - try { - input = this.getContentResolver().openInputStream(imageuri); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - - Zoom zoomObj = new Zoom(thumbView, flContainer, this.getContentResolver()); - Bitmap scaledImage = zoomObj.createScaledImage(input, imageuri); - - // Load the high-resolution "zoomed-in" image. - expandedImageView.setImageBitmap(scaledImage); - float startScale = zoomObj.adjustStartEndBounds(startBounds, finalBounds, globalOffset); - - // Hide the thumbnail and show the zoomed-in view. When the animation - // begins, it will position the zoomed-in view in the place of the - // thumbnail. - thumbView.setAlpha(0f); - expandedImageView.setVisibility(View.VISIBLE); - zoomOutButton.setVisibility(View.VISIBLE); - zoomInButton.setVisibility(View.GONE); - - // Set the pivot point for SCALE_X and SCALE_Y transformations - // to the top-left corner of the zoomed-in view (the default - // is the center of the view). - expandedImageView.setPivotX(0f); - expandedImageView.setPivotY(0f); - - // Construct and run the parallel animation of the four translation and - // scale properties (X, Y, SCALE_X, and SCALE_Y). - AnimatorSet set = new AnimatorSet(); - set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left, finalBounds.left)) - .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top, finalBounds.top)) - .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f)) - .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f)); - set.setDuration(ShortAnimationDuration); - set.setInterpolator(new DecelerateInterpolator()); - set.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - CurrentAnimator = null; - } - - @Override - public void onAnimationCancel(Animator animation) { - CurrentAnimator = null; - } - }); - set.start(); - CurrentAnimator = set; - - // Upon clicking the zoomed-in image, it should zoom back down - // to the original bounds and show the thumbnail instead of - // the expanded image. - startScaleFinal = startScale; - } - - /** - * Called when user taps the ^ FAB button, expands to show Zoom and Map - */ - @OnClick(R.id.main_fab) - public void onMainFabClicked() { - if (!isFABOpen) { - showFABMenu(); - } else { - closeFABMenu(); - } - } - - @OnClick(R.id.media_upload_zoom_in) - public void onZoomInFabClicked() { - try { - zoomImageFromThumb(backgroundImageView, mediaUri); - } catch (Exception e) { - Timber.e(e); - } - } - - @OnClick(R.id.media_upload_zoom_out) - public void onZoomOutFabClicked() { - if (CurrentAnimator != null) { - CurrentAnimator.cancel(); - } - isZoom = false; - zoomOutButton.setVisibility(View.GONE); - mainFab.setVisibility(View.VISIBLE); - - // Animate the four positioning/sizing properties in parallel, - // back to their original values. - AnimatorSet set = new AnimatorSet(); - set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left)) - .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top)) - .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScaleFinal)) - .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScaleFinal)); - - set.setDuration(ShortAnimationDuration); - set.setInterpolator(new DecelerateInterpolator()); - set.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - //background image view is thumbView - backgroundImageView.setAlpha(1f); - expandedImageView.setVisibility(View.GONE); - CurrentAnimator = null; - } - - @Override - public void onAnimationCancel(Animator animation) { - //background image view is thumbView - backgroundImageView.setAlpha(1f); - expandedImageView.setVisibility(View.GONE); - CurrentAnimator = null; - } - }); - set.start(); - CurrentAnimator = set; - } - - @OnClick(R.id.media_map) - public void onFabShowMapsClicked() { - if (gpsObj != null && gpsObj.imageCoordsExists) { - Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude()); - Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); - mapIntent.setPackage("com.google.android.apps.maps"); - startActivity(mapIntent); - } - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_BACK: - if(isZoom) { - onZoomOutFabClicked(); - return true; - } - } - return super.onKeyDown(keyCode,event); - - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.java b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.java new file mode 100644 index 000000000..6436f621b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.java @@ -0,0 +1,5 @@ +package fr.free.nrw.commons.upload; + +public interface SimilarImageInterface { + void showSimilarImageFragment(String originalFilePath, String possibleFilePath); +} 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 deleted file mode 100644 index 9ea2f23f4..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java +++ /dev/null @@ -1,389 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.annotation.SuppressLint; -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.Color; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.v4.view.ViewCompat; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.Editable; -import android.text.Html; -import android.text.TextWatcher; -import android.text.method.LinkMovementMethod; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.EditText; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -import javax.inject.Inject; -import javax.inject.Named; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import butterknife.OnItemSelected; -import butterknife.OnTouch; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.settings.Prefs; -import fr.free.nrw.commons.utils.ViewUtil; -import timber.log.Timber; - -import static android.view.MotionEvent.ACTION_UP; - -public class SingleUploadFragment extends CommonsDaggerSupportFragment { - - @BindView(R.id.titleEdit) EditText titleEdit; - @BindView(R.id.rv_descriptions) RecyclerView rvDescriptions; - @BindView(R.id.titleDescButton) Button titleDescButton; - @BindView(R.id.share_license_summary) TextView licenseSummaryView; - @BindView(R.id.licenseSpinner) Spinner licenseSpinner; - - - @Inject @Named("default_preferences") SharedPreferences prefs; - @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; - - private String license; - private OnUploadActionInitiated uploadActionInitiatedHandler; - private TitleTextWatcher textWatcher = new TitleTextWatcher(); - private DescriptionsAdapter descriptionsAdapter; - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.activity_share, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - //What happens when the 'submit' icon is tapped - case R.id.menu_upload_single: - - if (titleEdit.getText().toString().trim().isEmpty()) { - Toast.makeText(getContext(), R.string.add_title_toast, Toast.LENGTH_LONG).show(); - return false; - } - - String title = titleEdit.getText().toString(); - String descriptionsInVariousLanguages = getDescriptionsInAppropriateFormat(); - - //Save the title/desc in short-lived cache so next time this fragment is loaded, we can access these - prefs.edit() - .putString("Title", title) - .putString("Desc", new Gson().toJson(descriptionsAdapter - .getDescriptions()))//Description, now is not just a string, its a list of description objects - .apply(); - - uploadActionInitiatedHandler - .uploadActionInitiated(title, descriptionsInVariousLanguages); - return true; - } - return super.onOptionsItemSelected(item); - } - - private String getDescriptionsInAppropriateFormat() { - List descriptions = descriptionsAdapter.getDescriptions(); - StringBuilder descriptionsInAppropriateFormat = new StringBuilder(); - for (Description description : descriptions) { - String individualDescription = String.format("{{%s|1=%s}}", description.getLanguageId(), - description.getDescriptionText()); - descriptionsInAppropriateFormat.append(individualDescription); - } - return descriptionsInAppropriateFormat.toString(); - - } - - private List getDescriptions() { - List descriptions = descriptionsAdapter.getDescriptions(); - return descriptions; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_single_upload, container, false); - ButterKnife.bind(this, rootView); - - initRecyclerView(); - - Intent activityIntent = getActivity().getIntent(); - if (activityIntent.hasExtra("title")) { - titleEdit.setText(activityIntent.getStringExtra("title")); - } - if (activityIntent.hasExtra("description") && descriptionsAdapter.getDescriptions() != null - && descriptionsAdapter.getDescriptions().size() > 0) { - descriptionsAdapter.getDescriptions().get(0) - .setDescriptionText(activityIntent.getStringExtra("description")); - descriptionsAdapter.notifyItemChanged(0); - } - - - ArrayList licenseItems = new ArrayList<>(); - licenseItems.add(getString(R.string.license_name_cc0)); - licenseItems.add(getString(R.string.license_name_cc_by)); - licenseItems.add(getString(R.string.license_name_cc_by_sa)); - licenseItems.add(getString(R.string.license_name_cc_by_four)); - licenseItems.add(getString(R.string.license_name_cc_by_sa_four)); - - license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); - - // If this is a direct upload from Nearby, autofill title and desc fields with the Place's values - boolean isNearbyUpload = ((ShareActivity) getActivity()).isNearbyUpload(); - - if (isNearbyUpload) { - String imageTitle = directPrefs.getString("Title", ""); - String imageDesc = directPrefs.getString("Desc", ""); - String imageCats = directPrefs.getString("Category", ""); - Timber.d("Image title: " + imageTitle + ", image desc: " + imageDesc + ", image categories: " + imageCats); - titleEdit.setText(imageTitle); - if (descriptionsAdapter.getDescriptions() != null - && descriptionsAdapter.getDescriptions().size() > 0) { - descriptionsAdapter.getDescriptions().get(0).setDescriptionText(imageDesc); - descriptionsAdapter.notifyItemChanged(0); - } - } - - // check if this is the first time we have uploaded - if (prefs.getString("Title", "").trim().length() == 0 - && prefs.getString("Desc", "").trim().length() == 0) { - titleDescButton.setVisibility(View.GONE); - } - - Timber.d(license); - - ArrayAdapter adapter; - if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme", false)) { - // dark theme - adapter = new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_dropdown_item, licenseItems); - } else { - // light theme - adapter = new ArrayAdapter<>(getActivity(), R.layout.light_simple_spinner_dropdown_item, licenseItems); - } - - licenseSpinner.setAdapter(adapter); - - int position = licenseItems.indexOf(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); - - titleEdit.addTextChangedListener(textWatcher); - - titleEdit.setOnFocusChangeListener((v, hasFocus) -> { - if (!hasFocus) { - ViewUtil.hideKeyboard(v); - } - }); - - setLicenseSummary(license); - - return rootView; - } - - private void initRecyclerView() { - descriptionsAdapter = new DescriptionsAdapter(); - descriptionsAdapter.setCallback(this::showInfoAlert); - descriptionsAdapter.setLanguages(getLocaleSupportedByDevice()); - rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext())); - rvDescriptions.setAdapter(descriptionsAdapter); - } - - private List getLocaleSupportedByDevice() { - List languages = new ArrayList<>(); - Locale[] localesArray = Locale.getAvailableLocales(); - List locales = Arrays.asList(localesArray); - for (Locale locale : locales) { - languages.add(new Language(locale)); - } - return languages; - } - - @Override - public void onDestroyView() { - titleEdit.removeTextChangedListener(textWatcher); - super.onDestroyView(); - } - - @OnItemSelected(R.id.licenseSpinner) - void onLicenseSelected(AdapterView parent, View view, int position, long id) { - String licenseName = parent.getItemAtPosition(position).toString(); - - // Set selected color to white because it should be readable on random images. - TextView selectedText = (TextView) licenseSpinner.getChildAt(0); - if (selectedText != null) { - selectedText.setTextColor(Color.WHITE); - selectedText.setBackgroundColor(Color.TRANSPARENT); - } - - String license; - if (getString(R.string.license_name_cc0).equals(licenseName)) { - license = Prefs.Licenses.CC0; - } else if (getString(R.string.license_name_cc_by).equals(licenseName)) { - license = Prefs.Licenses.CC_BY_3; - } else if (getString(R.string.license_name_cc_by_sa).equals(licenseName)) { - license = Prefs.Licenses.CC_BY_SA_3; - } else if (getString(R.string.license_name_cc_by_four).equals(licenseName)) { - license = Prefs.Licenses.CC_BY_4; - } else if (getString(R.string.license_name_cc_by_sa_four).equals(licenseName)) { - license = Prefs.Licenses.CC_BY_SA_4; - } else { - throw new IllegalStateException("Unknown licenseName: " + licenseName); - } - - setLicenseSummary(license); - prefs.edit() - .putString(Prefs.DEFAULT_LICENSE, license) - .apply(); - } - - - @OnClick(R.id.titleDescButton) - void setTitleDescButton() { - //Retrieve last title and desc entered - String title = prefs.getString("Title", ""); - String descriptionJson = prefs.getString("Desc", ""); - Timber.d("Title: %s, Desc: %s", title, descriptionJson); - - titleEdit.setText(title); - Type typeOfDest = new TypeToken>() { - }.getType(); - - List descriptions = new Gson().fromJson(descriptionJson, typeOfDest); - descriptionsAdapter.setDescriptions(descriptions); - - } - - /** - * Copied from https://stackoverflow.com/a/26269435/8065933 - */ - @OnTouch(R.id.titleEdit) - boolean titleInfo(View view, MotionEvent motionEvent) { - final int value; - if (ViewCompat.getLayoutDirection(getView()) == ViewCompat.LAYOUT_DIRECTION_LTR) { - value = titleEdit.getRight() - titleEdit.getCompoundDrawables()[2].getBounds().width(); - if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) { - showInfoAlert(R.string.media_detail_title, R.string.title_info); - return true; - } - } - else { - value = titleEdit.getLeft() + titleEdit.getCompoundDrawables()[0].getBounds().width(); - if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) { - showInfoAlert(R.string.media_detail_title, R.string.title_info); - return true; - } - } - return false; - } - - @SuppressLint("StringFormatInvalid") - private void setLicenseSummary(String license) { - String licenseHyperLink = ""+ getString(Utils.licenseNameFor(license)) + "
"; - licenseSummaryView.setMovementMethod(LinkMovementMethod.getInstance()); - licenseSummaryView.setText(Html.fromHtml(getString(R.string.share_license_summary, licenseHyperLink))); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(true); - uploadActionInitiatedHandler = (OnUploadActionInitiated) getActivity(); - } - - @Override - public void onStop() { - super.onStop(); - - // FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next - View target = getActivity().getCurrentFocus(); - ViewUtil.hideKeyboard(target); - } - - @NonNull - private String licenseUrlFor(String license) { - switch (license) { - case Prefs.Licenses.CC_BY_3: - return "https://creativecommons.org/licenses/by/3.0/"; - case Prefs.Licenses.CC_BY_4: - return "https://creativecommons.org/licenses/by/4.0/"; - case Prefs.Licenses.CC_BY_SA_3: - return "https://creativecommons.org/licenses/by-sa/3.0/"; - case Prefs.Licenses.CC_BY_SA_4: - return "https://creativecommons.org/licenses/by-sa/4.0/"; - case Prefs.Licenses.CC0: - return "https://creativecommons.org/publicdomain/zero/1.0/"; - } - throw new RuntimeException("Unrecognized license value: " + license); - } - - public interface OnUploadActionInitiated { - - void uploadActionInitiated(String title, String description); - } - - private class TitleTextWatcher implements TextWatcher { - - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - } - - @Override - public void afterTextChanged(Editable editable) { - if (getActivity() != null) { - getActivity().invalidateOptionsMenu(); - } - } - } - - - private void showInfoAlert (int titleStringID, int messageStringID){ - new AlertDialog.Builder(getContext()) - .setTitle(titleStringID) - .setMessage(messageStringID) - .setCancelable(true) - .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel()) - .create() - .show(); - } - - @OnClick(R.id.ll_add_description) - public void onLLAddDescriptionClicked() { - descriptionsAdapter.addDescription(new Description()); - rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java index eebe6b4fa..97908fa67 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/SpinnerLanguagesAdapter.java @@ -1,48 +1,88 @@ package fr.free.nrw.commons.upload; import android.content.Context; +import android.graphics.Color; +import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.TextView; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Locale; import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.utils.BiMap; public class SpinnerLanguagesAdapter extends ArrayAdapter { private final int resource; private final LayoutInflater layoutInflater; - List languages; + private List languageNamesList; + private List languageCodesList; + private final BiMap selectedLanguages; + public String selectedLangCode=""; + + public SpinnerLanguagesAdapter(@NonNull Context context, - int resource) { + int resource, BiMap selectedLanguages) { super(context, resource); this.resource = resource; this.layoutInflater = LayoutInflater.from(context); - languages = new ArrayList<>(); + languageNamesList = new ArrayList<>(); + languageCodesList = new ArrayList<>(); + prepareLanguages(); + this.selectedLanguages = selectedLanguages; } - public void setLanguages(List languages) { - this.languages = languages; + private void prepareLanguages() { + List languages = getLocaleSupportedByDevice(); + + for(Language language: languages) { + if(!languageCodesList.contains(language.getLocale().getLanguage())) { + languageNamesList.add(language.getLocale().getDisplayName()); + languageCodesList.add(language.getLocale().getLanguage()); + } + } + } + + private List getLocaleSupportedByDevice() { + List languages = new ArrayList<>(); + Locale[] localesArray = Locale.getAvailableLocales(); + for (Locale locale : localesArray) { + languages.add(new Language(locale)); + } + + Collections.sort(languages, (language, t1) -> language.getLocale().getDisplayName() + .compareTo(t1.getLocale().getDisplayName())); + return languages; + } + + @Override + public boolean isEnabled(int position) { + return !languageCodesList.get(position).isEmpty()&& + (!selectedLanguages.containsKey(languageCodesList.get(position)) || + languageCodesList.get(position).equals(selectedLangCode)); } @Override public int getCount() { - return languages.size(); + return languageNamesList.size(); } @Override public View getDropDownView(int position, @Nullable View convertView, - @NonNull ViewGroup parent) { + @NonNull ViewGroup parent) { View view = layoutInflater.inflate(resource, parent, false); ViewHolder holder = new ViewHolder(view); holder.init(position, true); @@ -75,19 +115,40 @@ public class SpinnerLanguagesAdapter extends ArrayAdapter { } public void init(int position, boolean isDropDownView) { - Language language = languages.get(position); if (!isDropDownView) { view.setVisibility(View.GONE); - tvLanguage.setText( - language.getLocale().getLanguage()); + if(languageCodesList.get(position).length()>2) + tvLanguage.setText(languageCodesList.get(position).subSequence(0,2)); + else + tvLanguage.setText(languageCodesList.get(position)); + } else { view.setVisibility(View.VISIBLE); - tvLanguage.setText( - String.format("%s [%s]", language.getLocale().getDisplayName(), - language.getLocale().getLanguage())); + if (languageCodesList.get(position).isEmpty()) { + tvLanguage.setText(languageNamesList.get(position)); + tvLanguage.setTextColor(Color.GRAY); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + tvLanguage.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); + } + } else { + tvLanguage.setText( + String.format("%s [%s]", languageNamesList.get(position), languageCodesList.get(position))); + if(selectedLanguages.containsKey(languageCodesList.get(position))&& + !languageCodesList.get(position).equals(selectedLangCode)) + tvLanguage.setTextColor(Color.GRAY); + else + tvLanguage.setTextColor(Color.BLACK); + } } - } } + String getLanguageCode(int position) { + return languageCodesList.get(position); + } + + int getIndexOfUserDefaultLocale(Context context) { + return languageCodesList.indexOf(context.getResources().getConfiguration().locale.getLanguage()); + } + } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java new file mode 100644 index 000000000..8963d0e25 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java @@ -0,0 +1,5 @@ +package fr.free.nrw.commons.upload; + +public interface ThumbnailClickedListener { + void thumbnailClicked(UploadModel.UploadItem content); +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Title.java b/app/src/main/java/fr/free/nrw/commons/upload/Title.java new file mode 100644 index 000000000..e0781ce67 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/Title.java @@ -0,0 +1,37 @@ +package fr.free.nrw.commons.upload; + +import android.text.TextUtils; + +import io.reactivex.subjects.BehaviorSubject; +import timber.log.Timber; + +class Title{ + + private String titleText; + private boolean set; + + @Override + public String toString() { + return titleText; + } + + public void setTitleText(String titleText) { + this.titleText = titleText; + + if (!TextUtils.isEmpty(titleText)) { + set = true; + } + } + + public boolean isSet() { + return set; + } + + public void setSet(boolean set) { + this.set = set; + } + + public boolean isEmpty() { + return titleText==null || titleText.isEmpty(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java new file mode 100644 index 000000000..2ea89e8e6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java @@ -0,0 +1,607 @@ +package fr.free.nrw.commons.upload; + +import android.Manifest; +import android.animation.LayoutTransition; +import android.annotation.SuppressLint; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.constraint.ConstraintLayout; +import android.support.design.widget.TextInputLayout; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.CardView; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.Html; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.ViewFlipper; + +import com.github.chrisbanes.photoview.PhotoView; +import com.jakewharton.rxbinding2.view.RxView; +import com.jakewharton.rxbinding2.widget.RxTextView; +import com.pedrogomez.renderers.RVRendererAdapter; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Named; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.auth.AuthenticatedActivity; +import fr.free.nrw.commons.auth.LoginActivity; +import fr.free.nrw.commons.category.CategoriesModel; +import fr.free.nrw.commons.category.CategoryItem; +import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.utils.DialogUtil; +import fr.free.nrw.commons.utils.StringUtils; +import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +import static fr.free.nrw.commons.utils.ImageUtils.Result; +import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult; +import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF; + +public class UploadActivity extends AuthenticatedActivity implements UploadView, SimilarImageInterface { + @Inject InputMethodManager inputMethodManager; + @Inject MediaWikiApi mwApi; + @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; + @Inject UploadPresenter presenter; + @Inject CategoriesModel categoriesModel; + + // Main GUI + @BindView(R.id.backgroundImage) PhotoView background; + @BindView(R.id.activity_upload_cards) ConstraintLayout cardLayout; + @BindView(R.id.view_flipper) ViewFlipper viewFlipper; + + // Top Card + @BindView(R.id.top_card) CardView topCard; + @BindView(R.id.top_card_expand_button) ImageView topCardExpandButton; + @BindView(R.id.top_card_title) TextView topCardTitle; + @BindView(R.id.top_card_thumbnails) RecyclerView topCardThumbnails; + + // Bottom Card + @BindView(R.id.bottom_card) CardView bottomCard; + @BindView(R.id.bottom_card_expand_button) ImageView bottomCardExpandButton; + @BindView(R.id.bottom_card_title) TextView bottomCardTitle; + @BindView(R.id.bottom_card_subtitle) TextView bottomCardSubtitle; + @BindView(R.id.bottom_card_next) Button next; + @BindView(R.id.bottom_card_previous) Button previous; + @BindView(R.id.bottom_card_add_desc) Button bottomCardAddDescription; + + //Right Card + @BindView(R.id.right_card) CardView rightCard; + @BindView(R.id.right_card_expand_button) ImageView rightCardExpandButton; + @BindView(R.id.right_card_map_button) View rightCardMapButton; + + // Category Search + @BindView(R.id.categories_title) TextView categoryTitle; + @BindView(R.id.category_next) Button categoryNext; + @BindView(R.id.category_previous) Button categoryPrevious; + @BindView(R.id.categoriesSearchInProgress) ProgressBar categoriesSearchInProgress; + @BindView(R.id.category_search) EditText categoriesSearch; + @BindView(R.id.category_search_container) TextInputLayout categoriesSearchContainer; + @BindView(R.id.categories) RecyclerView categoriesList; + + // Final Submission + @BindView(R.id.license_title) TextView licenseTitle; + @BindView(R.id.share_license_summary) TextView licenseSummary; + @BindView(R.id.media_upload_policy) TextView licensePolicy; + @BindView(R.id.license_list) Spinner licenseSpinner; + @BindView(R.id.submit) Button submit; + @BindView(R.id.license_previous) Button licensePrevious; + @BindView(R.id.rv_descriptions) RecyclerView rvDescriptions; + + private DescriptionsAdapter descriptionsAdapter; + private RVRendererAdapter categoriesAdapter; + private CompositeDisposable compositeDisposable; + + DexterPermissionObtainer dexterPermissionObtainer; + + + @SuppressLint("CheckResult") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_upload); + ButterKnife.bind(this); + compositeDisposable = new CompositeDisposable(); + + configureLayout(); + configureTopCard(); + configureBottomCard(); + initRecyclerView(); + configureRightCard(); + configureNavigationButtons(); + configureCategories(); + configureLicenses(); + + presenter.init(); + + dexterPermissionObtainer = new DexterPermissionObtainer(this, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + getString(R.string.storage_permission), + getString(R.string.write_storage_permission_rationale_for_image_share)); + + dexterPermissionObtainer.confirmStoragePermissions().subscribe(this::receiveSharedItems); + } + + @Override + public boolean checkIfLoggedIn() { + if (!sessionManager.isUserLoggedIn()) { + Timber.d("Current account is null"); + ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in)); + Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class); + startActivity(loginIntent); + return false; + } + return true; + } + + @Override + protected void onDestroy() { + presenter.cleanup(); + super.onDestroy(); + } + + @Override + protected void onResume() { + super.onResume(); + checkIfLoggedIn(); + compositeDisposable.add( + dexterPermissionObtainer.confirmStoragePermissions() + .subscribe(() -> presenter.addView(this))); + compositeDisposable.add( + RxTextView.textChanges(categoriesSearch) + .doOnEach(v -> categoriesSearchContainer.setError(null)) + .takeUntil(RxView.detaches(categoriesSearch)) + .debounce(500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(filter -> updateCategoryList(filter.toString()), Timber::e) + ); + } + + @Override + protected void onPause() { + presenter.removeView(); + compositeDisposable.dispose(); + compositeDisposable = new CompositeDisposable(); + super.onPause(); + } + + @Override + public void updateThumbnails(List uploads) { + int uploadCount = uploads.size(); + topCardThumbnails.setAdapter(new UploadThumbnailsAdapterFactory(presenter::thumbnailClicked).create(uploads)); + topCardTitle.setText(getResources().getQuantityString(R.plurals.upload_count_title, uploadCount, uploadCount)); + } + + @Override + public void updateRightCardContent(boolean gpsPresent) { + if(gpsPresent){ + rightCardMapButton.setVisibility(View.VISIBLE); + }else{ + rightCardMapButton.setVisibility(View.GONE); + } + //The card should be disabled if it has no buttons. + setRightCardVisibility(gpsPresent); + } + + @Override + public void updateBottomCardContent(int currentStep, + int stepCount, + UploadModel.UploadItem uploadItem, + boolean isShowingItem) { + String cardTitle = getResources().getString(R.string.step_count, currentStep, stepCount); + String cardSubTitle = getResources().getString(R.string.image_in_set_label, currentStep); + bottomCardTitle.setText(cardTitle); + bottomCardSubtitle.setText(cardSubTitle); + categoryTitle.setText(cardTitle); + licenseTitle.setText(cardTitle); + if(isShowingItem) { + descriptionsAdapter.setItems(uploadItem.title, uploadItem.descriptions); + rvDescriptions.setAdapter(descriptionsAdapter); + } + } + + @Override + public void updateLicenses(List licenses, String selectedLicense) { + ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, licenses); + licenseSpinner.setAdapter(adapter); + + int position = licenses.indexOf(getString(Utils.licenseNameFor(selectedLicense))); + + // Check position is valid + if (position < 0) { + Timber.d("Invalid position: %d. Using default license", position); + position = licenses.size() - 1; + } + + Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(selectedLicense))); + licenseSpinner.setSelection(position); + } + + @SuppressLint("StringFormatInvalid") + @Override + public void updateLicenseSummary(String selectedLicense) { + String licenseHyperLink = "" + + getString(Utils.licenseNameFor(selectedLicense)) + "
"; + licenseSummary.setMovementMethod(LinkMovementMethod.getInstance()); + licenseSummary.setText( + Html.fromHtml( + getString(R.string.share_license_summary, licenseHyperLink))); + } + + @Override + public void updateTopCardContent() { + RecyclerView.Adapter adapter = topCardThumbnails.getAdapter(); + if (adapter != null) { + adapter.notifyDataSetChanged(); + } + } + + @Override + public void setNextEnabled(boolean available) { + next.setEnabled(available); + categoryNext.setEnabled(available); + } + + @Override + public void setSubmitEnabled(boolean available) { + submit.setEnabled(available); + } + + @Override + public void setPreviousEnabled(boolean available) { + previous.setEnabled(available); + categoryPrevious.setEnabled(available); + licensePrevious.setEnabled(available); + } + + @Override + public void setTopCardState(boolean state) { + updateCardState(state, topCardExpandButton, topCardThumbnails); + } + + @Override + public void setTopCardVisibility(boolean visible) { + topCard.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + @Override + public void setBottomCardVisibility(boolean visible) { + bottomCard.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + @Override + public void setRightCardVisibility(boolean visible) { + rightCard.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + @Override + public void setBottomCardVisibility(@UploadPage int page) { + if (page == TITLE_CARD) { + viewFlipper.setDisplayedChild(0); + } else if (page == CATEGORIES) { + viewFlipper.setDisplayedChild(1); + } else if (page == LICENSE) { + viewFlipper.setDisplayedChild(2); + dismissKeyboard(); + } else if (page == PLEASE_WAIT) { + viewFlipper.setDisplayedChild(3); + } + } + + @Override + public void setBottomCardState(boolean state) { + updateCardState(state, bottomCardExpandButton, rvDescriptions, previous, next, bottomCardAddDescription); + } + + @Override + public void setRightCardState(boolean state) { + rightCardExpandButton.animate().rotation(rightCardExpandButton.getRotation() + (state ? -180 : 180)).start(); + //Add all items in rightCard here + rightCardMapButton.setVisibility(state ? View.VISIBLE : View.GONE); + } + + @Override + public void setBackground(Uri mediaUri) { + background.setImageURI(mediaUri); + } + + + @Override + public void dismissKeyboard() { + InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + + // verify if the soft keyboard is open + if (imm != null && imm.isAcceptingText() && getCurrentFocus() != null) { + imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0); + } + } + + @Override + public void showBadPicturePopup(@Result int result) { + String errorMessageForResult = getErrorMessageForResult(this, result); + if (StringUtils.isNullOrWhiteSpace(errorMessageForResult)) { + return; + } + + DialogUtil.showAlertDialog(this, + getString(R.string.warning), + errorMessageForResult, + () -> presenter.deletePicture(), + () -> presenter.keepPicture()); + } + + @Override + public void showDuplicatePicturePopup() { + DialogUtil.showAlertDialog(this, + getString(R.string.warning), + String.format(getString(R.string.upload_title_duplicate), presenter.getCurrentImageFileName()), + null, + () -> { + presenter.keepPicture(); + presenter.handleNext(descriptionsAdapter.getTitle(), getDescriptions()); + }); + } + + public void showNoCategorySelectedWarning() { + DialogUtil.showAlertDialog(this, + getString(R.string.no_categories_selected), + getString(R.string.no_categories_selected_warning_desc), + getString(R.string.no_go_back), + getString(R.string.yes_submit), + null, + () -> presenter.handleCategoryNext(categoriesModel, true)); + } + + @Override + public void launchMapActivity(String decCoords) { + Utils.handleGeoCoordinates(this, decCoords); + } + + @Override + public void showErrorMessage(int resourceId) { + ViewUtil.showShortToast(this, resourceId); + } + + @Override + public void initDefaultCategories() { + updateCategoryList(""); + } + + @Override + protected void onAuthCookieAcquired(String authCookie) { + mwApi.setAuthCookie(authCookie); + } + + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) { + dexterPermissionObtainer.onManualPermissionReturned(); + } + } + + + @Override + protected void onAuthFailure() { + Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG).show(); + finish(); + } + + private void configureLicenses() { + licenseSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + String licenseName = parent.getItemAtPosition(position).toString(); + presenter.selectLicense(licenseName); + } + + @Override + public void onNothingSelected(AdapterView parent) { + presenter.selectLicense(null); + } + }); + } + + private void configureLayout() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + cardLayout.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING); + } + background.setScaleType(ImageView.ScaleType.CENTER_CROP); + background.setOnScaleChangeListener((scaleFactor, x, y) -> presenter.closeAllCards()); + } + + private void configureTopCard() { + topCardExpandButton.setOnClickListener(v -> presenter.toggleTopCardState()); + topCardThumbnails.setLayoutManager(new LinearLayoutManager(this, + LinearLayoutManager.HORIZONTAL, false)); + } + + private void configureBottomCard() { + bottomCardExpandButton.setOnClickListener(v -> presenter.toggleBottomCardState()); + bottomCardAddDescription.setOnClickListener(v -> addNewDescription()); + } + + private void addNewDescription() { + descriptionsAdapter.addDescription(new Description()); + rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1); + } + + private void configureRightCard() { + rightCardExpandButton.setOnClickListener(v -> presenter.toggleRightCardState()); + rightCardMapButton.setOnClickListener(v -> presenter.openCoordinateMap()); + } + + private void configureNavigationButtons() { + // Navigation next / previous for each image as we're collecting title + description + next.setOnClickListener(v -> { + setTitleAndDescriptions(); + presenter.handleNext(descriptionsAdapter.getTitle(), + descriptionsAdapter.getDescriptions()); + }); + previous.setOnClickListener(v -> presenter.handlePrevious()); + + // Next / previous for the category selection currentPage + categoryNext.setOnClickListener(v -> presenter.handleCategoryNext(categoriesModel, false)); + categoryPrevious.setOnClickListener(v -> presenter.handlePrevious()); + + // Finally, the previous / submit buttons on the final currentPage of the wizard + licensePrevious.setOnClickListener(v -> presenter.handlePrevious()); + submit.setOnClickListener(v -> { + Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG).show(); + presenter.handleSubmit(categoriesModel); + finish(); + }); + + } + + private void setTitleAndDescriptions() { + List descriptions = descriptionsAdapter.getDescriptions(); + Timber.d("Descriptions size is %d are %s", descriptions.size(), descriptions); + } + + private void configureCategories() { + categoriesAdapter = new UploadCategoriesAdapterFactory(categoriesModel).create(new ArrayList<>()); + categoriesList.setLayoutManager(new LinearLayoutManager(this)); + categoriesList.setAdapter(categoriesAdapter); + } + + @SuppressLint("CheckResult") + private void updateCategoryList(String filter) { + List imageTitleList = presenter.getImageTitleList(); + Observable.fromIterable(categoriesModel.getSelectedCategories()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe(disposable -> { + categoriesSearchInProgress.setVisibility(View.VISIBLE); + categoriesSearchContainer.setError(null); + categoriesAdapter.clear(); + }) + .observeOn(Schedulers.io()) + .concatWith( + categoriesModel.searchAll(filter, imageTitleList) + .mergeWith(categoriesModel.searchCategories(filter, imageTitleList)) + .concatWith(TextUtils.isEmpty(filter) + ? categoriesModel.defaultCategories(imageTitleList) : Observable.empty()) + ) + .filter(categoryItem -> !categoriesModel.containsYear(categoryItem.getName())) + .distinct() + .sorted(categoriesModel.sortBySimilarity(filter)) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + s -> categoriesAdapter.add(s), + Timber::e, + () -> { + categoriesAdapter.notifyDataSetChanged(); + categoriesSearchInProgress.setVisibility(View.GONE); + + if (categoriesAdapter.getItemCount() == categoriesModel.selectedCategoriesCount() + && !categoriesSearch.getText().toString().isEmpty()) { + categoriesSearchContainer.setError("No categories found"); + } + } + ); + } + + private void receiveSharedItems() { + Intent intent = getIntent(); + String mimeType = intent.getType(); + String source; + + if (intent.hasExtra(UploadService.EXTRA_SOURCE)) { + source = intent.getStringExtra(UploadService.EXTRA_SOURCE); + } else { + source = Contribution.SOURCE_EXTERNAL; + } + + if (Intent.ACTION_SEND.equals(intent.getAction())) { + Uri mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); + if (intent.getBooleanExtra("isDirectUpload", false)) { + String imageTitle = directPrefs.getString("Title", ""); + String imageDesc = directPrefs.getString("Desc", ""); + Timber.i("Received direct upload with title %s and description %s", imageTitle, imageDesc); + String wikidataEntityIdPref = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF); + presenter.receiveDirect(mediaUri, mimeType, source, wikidataEntityIdPref, imageTitle, imageDesc); + } else { + Timber.i("Received single upload"); + presenter.receive(mediaUri, mimeType, source); + } + } else if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) { + ArrayList urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + Timber.i("Received multiple upload %s", urisList.size()); + presenter.receive(urisList, mimeType, source); + } + } + + private void updateCardState(boolean state, ImageView button, View... content) { + button.animate().rotation(button.getRotation() + (state ? 180 : -180)).start(); + if (content != null) { + for (View view : content) { + view.setVisibility(state ? View.VISIBLE : View.GONE); + } + } + } + + @Override + public List getDescriptions() { + return descriptionsAdapter.getDescriptions(); + } + + private void initRecyclerView() { + descriptionsAdapter = new DescriptionsAdapter(this); + descriptionsAdapter.setCallback(this::showInfoAlert); + rvDescriptions.setLayoutManager(new LinearLayoutManager(getApplicationContext())); + rvDescriptions.setAdapter(descriptionsAdapter); + addNewDescription(); + } + + + private void showInfoAlert(int titleStringID, int messageStringId, String... formatArgs) { + new AlertDialog.Builder(this) + .setTitle(titleStringID) + .setMessage(getString(messageStringId, (Object[]) formatArgs)) + .setCancelable(true) + .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel()) + .create() + .show(); + } + + @Override + public void showSimilarImageFragment(String originalFilePath, String possibleFilePath) { + SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment(); + Bundle args = new Bundle(); + args.putString("originalImagePath", originalFilePath); + args.putString("possibleImagePath", possibleFilePath); + newFragment.setArguments(args); + newFragment.show(getSupportFragmentManager(), "dialog"); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesAdapterFactory.java new file mode 100644 index 000000000..1797cbe80 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesAdapterFactory.java @@ -0,0 +1,27 @@ +package fr.free.nrw.commons.upload; + +import com.pedrogomez.renderers.ListAdapteeCollection; +import com.pedrogomez.renderers.RVRendererAdapter; +import com.pedrogomez.renderers.RendererBuilder; + +import java.util.Collections; +import java.util.List; + +import fr.free.nrw.commons.category.CategoryClickedListener; +import fr.free.nrw.commons.category.CategoryItem; + +public class UploadCategoriesAdapterFactory { + private final CategoryClickedListener listener; + + public UploadCategoriesAdapterFactory(CategoryClickedListener listener) { + this.listener = listener; + } + + public RVRendererAdapter create(List placeList) { + RendererBuilder builder = new RendererBuilder() + .bind(CategoryItem.class, new UploadCategoriesRenderer(listener)); + ListAdapteeCollection collection = new ListAdapteeCollection<>( + placeList != null ? placeList : Collections.emptyList()); + return new RVRendererAdapter<>(builder, collection); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesRenderer.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesRenderer.java new file mode 100644 index 000000000..d0862b964 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadCategoriesRenderer.java @@ -0,0 +1,52 @@ +package fr.free.nrw.commons.upload; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; + +import com.pedrogomez.renderers.Renderer; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.category.CategoryClickedListener; +import fr.free.nrw.commons.category.CategoryItem; + +public class UploadCategoriesRenderer extends Renderer { + @BindView(R.id.tvName) CheckBox checkedView; + private final CategoryClickedListener listener; + + UploadCategoriesRenderer(CategoryClickedListener listener) { + this.listener = listener; + } + + @Override + protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) { + return layoutInflater.inflate(R.layout.layout_upload_categories_item, viewGroup, false); + } + + @Override + protected void setUpView(View view) { + ButterKnife.bind(this, view); + } + + @Override + protected void hookListeners(View view) { + view.setOnClickListener(v -> { + CategoryItem item = getContent(); + item.setSelected(!item.isSelected()); + checkedView.setChecked(item.isSelected()); + if (listener != null) { + listener.categoryClicked(item); + } + }); + } + + @Override + public void render() { + CategoryItem item = getContent(); + checkedView.setChecked(item.isSelected()); + checkedView.setText(item.getName()); + } +} 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 222757f54..fd0563ab3 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 @@ -23,7 +23,6 @@ import java.io.InputStream; import java.util.Date; import java.util.concurrent.Executors; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.SessionManager; @@ -87,49 +86,11 @@ public class UploadController { /** * Starts a new upload task. - * @param title the title of the contribution - * @param mediaUri the media URI of the contribution - * @param description the description of the contribution - * @param mimeType the MIME type of the contribution - * @param source the source of the contribution - * @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615") - * @param wikiDataEntityId - * @param onComplete the progress tracker + * + * @param contribution the contribution object */ - public void startUpload(String title, Uri contentProviderUri, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, String wikiDataEntityId, ContributionUploadProgress onComplete) { - Contribution contribution; - - - //TODO: Modify this to include coords - contribution = new Contribution(mediaUri, null, title, description, -1, - null, null, sessionManager.getCurrentAccount().name, - CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords); - - - contribution.setTag("mimeType", mimeType); - contribution.setSource(source); - - Timber.d("Wikidata entity ID received from Share activity is %s", wikiDataEntityId); - //TODO: Modify this to include coords - Account currentAccount = sessionManager.getCurrentAccount(); - if(currentAccount == null) { - Timber.d("Current account is null"); - ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in)); - sessionManager.forceLogin(context); - return; - } - contribution = new Contribution(mediaUri, null, title, description, -1, - null, null, currentAccount.name, - CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords); - - - contribution.setTag("mimeType", mimeType); - contribution.setSource(source); - contribution.setWikiDataEntityId(wikiDataEntityId); - contribution.setContentProviderUri(contentProviderUri); - - //Calls the next overloaded method - startUpload(contribution, onComplete); + public void startUpload(Contribution contribution) { + startUpload(contribution, c -> {}); } /** @@ -142,7 +103,14 @@ public class UploadController { public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) { //Set creator, desc, and license if (TextUtils.isEmpty(contribution.getCreator())) { - contribution.setCreator(sessionManager.getCurrentAccount().name); + Account currentAccount = sessionManager.getCurrentAccount(); + if (currentAccount == null) { + Timber.d("Current account is null"); + ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in)); + sessionManager.forceLogin(context); + return; + } + contribution.setCreator(currentAccount.name); } if (contribution.getDescription() == null) { @@ -163,8 +131,6 @@ public class UploadController { long length; ContentResolver contentResolver = context.getContentResolver(); try { - - //TODO: understand do we really need this code if (contribution.getDataLength() <= 0) { Timber.d("UploadController/doInBackground, contribution.getLocalUri():" + contribution.getLocalUri()); AssetFileDescriptor assetFileDescriptor = contentResolver @@ -218,7 +184,7 @@ public class UploadController { contribution.setDateCreated(new Date()); } } - return contribution; + return contribution; } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java new file mode 100644 index 000000000..46193e958 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java @@ -0,0 +1,400 @@ +package fr.free.nrw.commons.upload; + +import android.annotation.SuppressLint; +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.graphics.BitmapRegionDecoder; +import android.net.Uri; +import android.support.annotation.Nullable; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Named; + +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.settings.Prefs; +import fr.free.nrw.commons.utils.ImageUtils; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subjects.BehaviorSubject; +import timber.log.Timber; + +public class UploadModel { + + private MediaWikiApi mwApi; + private static UploadItem DUMMY = new UploadItem(Uri.EMPTY, "", "", GPSExtractor.DUMMY, "", null,-1l) { + }; + private final SharedPreferences prefs; + private final List licenses; + private String license; + private final Map licensesByName; + private List items = new ArrayList<>(); + private boolean topCardState = true; + private boolean bottomCardState = true; + private boolean rightCardState = true; + private int currentStepIndex = 0; + private Context context; + private ContentResolver contentResolver; + private boolean useExtStorage; + private Disposable badImageSubscription; + + @Inject + SessionManager sessionManager; + private Uri currentMediaUri; + + @Inject + UploadModel(@Named("licenses") List licenses, + @Named("default_preferences") SharedPreferences prefs, + @Named("licenses_by_name") Map licensesByName, + Context context, + MediaWikiApi mwApi) { + this.licenses = licenses; + this.prefs = prefs; + this.license = Prefs.Licenses.CC_BY_SA_3; + this.licensesByName = licensesByName; + this.context = context; + this.mwApi = mwApi; + this.contentResolver = context.getContentResolver(); + useExtStorage = this.prefs.getBoolean("useExternalStorage", false); + } + + @SuppressLint("CheckResult") + void receive(List mediaUri, String mimeType, String source, SimilarImageInterface similarImageInterface) { + initDefaultValues(); + Observable itemObservable = Observable.fromIterable(mediaUri) + .map(media -> { + currentMediaUri=media; + return cacheFileUpload(media); + }) + .map(filePath -> { + long fileCreatedDate = getFileCreatedDate(currentMediaUri); + Uri uri = Uri.fromFile(new File(filePath)); + FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context); + UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface), + FileUtils.getFileExt(filePath), null,fileCreatedDate); + Single.zip( + Single.fromCallable(() -> + new FileInputStream(filePath)) + .map(FileUtils::getSHA1) + .map(mwApi::existingFile) + .map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK), + Single.fromCallable(() -> + new FileInputStream(filePath)) + .map(file -> BitmapRegionDecoder.newInstance(file, false)) + .map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK + (dupe, dark) -> dupe | dark) + .observeOn(Schedulers.io()) + .subscribe(item.imageQuality::onNext, Timber::e); + return item; + }); + items = itemObservable.toList().blockingGet(); + items.get(0).selected = true; + items.get(0).first = true; + } + + @SuppressLint("CheckResult") + void receiveDirect(Uri media, String mimeType, String source, String wikidataEntityIdPref, String title, String desc, SimilarImageInterface similarImageInterface) { + initDefaultValues(); + long fileCreatedDate = getFileCreatedDate(media); + String filePath = this.cacheFileUpload(media); + Uri uri = Uri.fromFile(new File(filePath)); + FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context); + UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface), + FileUtils.getFileExt(filePath), wikidataEntityIdPref,fileCreatedDate); + item.title.setTitleText(title); + item.descriptions.get(0).setDescriptionText(desc); + //TODO figure out if default descriptions in other languages exist + item.descriptions.get(0).setLanguageCode("en"); + Single.zip( + Single.fromCallable(() -> + new FileInputStream(filePath)) + .map(FileUtils::getSHA1) + .map(mwApi::existingFile) + .map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK), + Single.fromCallable(() -> + new FileInputStream(filePath)) + .map(file -> BitmapRegionDecoder.newInstance(file, false)) + .map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK + (dupe, dark) -> dupe | dark).subscribe(item.imageQuality::onNext); + items.add(item); + items.get(0).selected = true; + items.get(0).first = true; + } + + private void initDefaultValues() { + currentStepIndex = 0; + topCardState = true; + bottomCardState = true; + rightCardState = true; + items = new ArrayList<>(); + } + + /** + * Get file creation date from uri from all possible content providers + * @param media + * @return + */ + private long getFileCreatedDate(Uri media) { + try { + Cursor cursor = contentResolver.query(media, null, null, null, null); + if (cursor == null) { + return -1;//Could not fetch last_modified + } + //Content provider contracts for opening gallery from the app and that by sharing from gallery from outside are different and we need to handle both the cases + int lastModifiedColumnIndex = cursor.getColumnIndex("last_modified");//If gallery is opened from in app + if(lastModifiedColumnIndex==-1){ + lastModifiedColumnIndex=cursor.getColumnIndex("datetaken"); + } + //If both the content providers do not give the data, lets leave it to Jesus + if(lastModifiedColumnIndex==-1){ + return -1l; + } + cursor.moveToFirst(); + return cursor.getLong(lastModifiedColumnIndex); + } catch (Exception e) { + return -1;////Could not fetch last_modified + } + } + + boolean isPreviousAvailable() { + return currentStepIndex > 0; + } + + boolean isNextAvailable() { + return currentStepIndex < (items.size() + 1); + } + + boolean isSubmitAvailable() { + int count = items.size(); + boolean hasError = license == null; + for (int i = 0; i < count; i++) { + UploadItem item = items.get(i); + hasError |= item.error; + } + return !hasError; + } + + int getCurrentStep() { + return currentStepIndex + 1; + } + + int getStepCount() { + return items.size() + 2; + } + + public int getCount() { + return items.size(); + } + + public List getUploads() { + return items; + } + + boolean isTopCardState() { + return topCardState; + } + + void setTopCardState(boolean topCardState) { + this.topCardState = topCardState; + } + + boolean isBottomCardState() { + return bottomCardState; + } + + void setRightCardState(boolean rightCardState) { + this.rightCardState = rightCardState; + } + + boolean isRightCardState() { + return rightCardState; + } + + void setBottomCardState(boolean bottomCardState) { + this.bottomCardState = bottomCardState; + } + + public void next() { + if (badImageSubscription != null) + badImageSubscription.dispose(); + markCurrentUploadVisited(); + if (currentStepIndex < items.size() + 1) { + currentStepIndex++; + } + updateItemState(); + } + + public void setCurrentTitleAndDescriptions(Title title, List descriptions) { + setCurrentUploadTitle(title); + setCurrentUploadDescriptions(descriptions); + } + + private void setCurrentUploadTitle(Title title) { + if (currentStepIndex < items.size() && currentStepIndex >= 0) { + items.get(currentStepIndex).title = title; + } + } + + private void setCurrentUploadDescriptions(List descriptions) { + if (currentStepIndex < items.size() && currentStepIndex >= 0) { + items.get(currentStepIndex).descriptions = descriptions; + } + } + + public void previous() { + if (badImageSubscription != null) + badImageSubscription.dispose(); + markCurrentUploadVisited(); + if (currentStepIndex > 0) { + currentStepIndex--; + } + updateItemState(); + } + + void jumpTo(UploadItem item) { + currentStepIndex = items.indexOf(item); + item.visited = true; + updateItemState(); + } + + UploadItem getCurrentItem() { + return isShowingItem() ? items.get(currentStepIndex) : DUMMY; + } + + boolean isShowingItem() { + return currentStepIndex < items.size(); + } + + private void updateItemState() { + int count = items.size(); + for (int i = 0; i < count; i++) { + UploadItem item = items.get(i); + item.selected = (currentStepIndex >= count || i == currentStepIndex); + item.error = item.title == null || item.title.isEmpty(); + } + } + + private void markCurrentUploadVisited() { + if (currentStepIndex < items.size() && currentStepIndex >= 0) { + items.get(currentStepIndex).visited = true; + } + } + + public List getLicenses() { + return licenses; + } + + String getSelectedLicense() { + return license; + } + + void setSelectedLicense(String licenseName) { + this.license = licensesByName.get(licenseName); + } + + Observable buildContributions(List categoryStringList) { + return Observable.fromIterable(items).map(item -> + { + Contribution contribution = new Contribution(item.mediaUri, null, item.title + "." + item.fileExt, + Description.formatList(item.descriptions), -1, + null, null, sessionManager.getUserName(), + CommonsApplication.DEFAULT_EDIT_SUMMARY, item.gpsCoords.getCoords()); + contribution.setWikiDataEntityId(item.wikidataEntityId); + contribution.setCategories(categoryStringList); + contribution.setTag("mimeType", item.mimeType); + contribution.setSource(item.source); + contribution.setContentProviderUri(item.mediaUri); + if (item.createdTimestamp != -1l) { + contribution.setDateCreated(new Date(item.createdTimestamp)); + //Set the date only if you have it, else the upload service is gonna try it the other way + } + return contribution; + }); + } + + /** + * Copy files into local storage and return file path + * + * @param media Uri of the file + * @return path of the enw file + */ + private String cacheFileUpload(Uri media) { + try { + String copyPath; + if (useExtStorage) + copyPath = FileUtils.createExternalCopyPathAndCopy(media, contentResolver); + else + copyPath = FileUtils.createCopyPathAndCopy(media, context); + Timber.i("File path is " + copyPath); + return copyPath; + } catch (IOException e) { + Timber.w(e, "Error in copying URI " + media.getPath()); + return null; + } + } + + void keepPicture() { + items.get(currentStepIndex).imageQuality.onNext(ImageUtils.IMAGE_KEEP); + } + + void deletePicture() { + badImageSubscription.dispose(); + items.remove(currentStepIndex).imageQuality.onComplete(); + updateItemState(); + } + + void subscribeBadPicture(Consumer consumer) { + badImageSubscription = getCurrentItem().imageQuality.subscribe(consumer, Timber::e); + } + + + @SuppressWarnings("WeakerAccess") + static class UploadItem { + public final Uri mediaUri; + public final String mimeType; + public final String source; + public final GPSExtractor gpsCoords; + + public boolean selected = false; + public boolean first = false; + public String fileExt; + public BehaviorSubject imageQuality; + Title title; + List descriptions; + public String wikidataEntityId; + public boolean visited; + public boolean error; + public long createdTimestamp; + + @SuppressLint("CheckResult") + UploadItem(Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords, String fileExt, @Nullable String wikidataEntityId, long createdTimestamp) { + title = new Title(); + descriptions = new ArrayList<>(); + descriptions.add(new Description()); + this.wikidataEntityId = wikidataEntityId; + this.mediaUri = mediaUri; + this.mimeType = mimeType; + this.source = source; + this.gpsCoords = gpsCoords; + this.fileExt = fileExt; + imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT); + this.createdTimestamp=createdTimestamp; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java new file mode 100644 index 000000000..74e3192bd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java @@ -0,0 +1,430 @@ +package fr.free.nrw.commons.upload; + +import android.annotation.SuppressLint; +import android.net.Uri; + +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.category.CategoriesModel; +import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.utils.ImageUtils; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +import static fr.free.nrw.commons.upload.UploadModel.UploadItem; +import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE; +import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS; +import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP; +import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; + +/** + * The MVP pattern presenter of Upload GUI + */ +@Singleton +public class UploadPresenter { + + private final UploadModel uploadModel; + private final UploadController uploadController; + private final MediaWikiApi mediaWikiApi; + + private static final UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(), + new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null); + private UploadView view = DUMMY; + + private static final SimilarImageInterface SIMILAR_IMAGE = (SimilarImageInterface) Proxy.newProxyInstance(SimilarImageInterface.class.getClassLoader(), + new Class[]{SimilarImageInterface.class}, (proxy, method, methodArgs) -> null); + private SimilarImageInterface similarImageInterface = SIMILAR_IMAGE; + + @UploadView.UploadPage + private int currentPage = UploadView.PLEASE_WAIT; + + + @Inject + UploadPresenter(UploadModel uploadModel, + UploadController uploadController, + MediaWikiApi mediaWikiApi) { + this.uploadModel = uploadModel; + this.uploadController = uploadController; + this.mediaWikiApi = mediaWikiApi; + } + + void receive(Uri mediaUri, String mimeType, String source) { + receive(Collections.singletonList(mediaUri), mimeType, source); + } + + /** + * Passes the items received to {@link #uploadModel} and displays the items. + * + * @param media The Uri's of the media being uploaded. + * @param mimeType the mimeType of the files. + * @param source File source from {@link Contribution.FileSource} + */ + @SuppressLint("CheckResult") + void receive(List media, String mimeType, @Contribution.FileSource String source) { + Completable.fromRunnable(() -> uploadModel.receive(media, mimeType, source, similarImageInterface)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + updateCards(); + updateLicenses(); + updateContent(); + if (uploadModel.isShowingItem()) + uploadModel.subscribeBadPicture(this::handleBadPicture); + }, Timber::e); + } + + /** + * Passes the direct upload item received to {@link #uploadModel} and displays the items. + * + * @param media The Uri's of the media being uploaded. + * @param mimeType the mimeType of the files. + * @param source File source from {@link Contribution.FileSource} + */ + @SuppressLint("CheckResult") + void receiveDirect(Uri media, String mimeType, @Contribution.FileSource String source, String wikidataEntityIdPref, String title, String desc) { + Completable.fromRunnable(() -> uploadModel.receiveDirect(media, mimeType, source, wikidataEntityIdPref, title, desc, similarImageInterface)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + updateCards(); + updateLicenses(); + updateContent(); + if (uploadModel.isShowingItem()) + uploadModel.subscribeBadPicture(this::handleBadPicture); + }, Timber::e); + } + /** + * Sets the license to parameter and updates {@link UploadActivity} + * + * @param licenseName license name + */ + void selectLicense(String licenseName) { + uploadModel.setSelectedLicense(licenseName); + view.updateLicenseSummary(uploadModel.getSelectedLicense()); + } + + //region Wizard step management + + /** + * Called by the next button in {@link UploadActivity} + */ + @SuppressLint("CheckResult") + void handleNext(Title title, + List descriptions) { + validateCurrentItemTitle() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(errorCode -> handleImage(errorCode, title, descriptions)); + } + + /** + * Called by the next button in {@link UploadActivity} + */ + @SuppressLint("CheckResult") + void handleCategoryNext(CategoriesModel categoriesModel, + boolean noCategoryWarningShown) { + if (categoriesModel.selectedCategoriesCount() < 1 && !noCategoryWarningShown) { + view.showNoCategorySelectedWarning(); + } else { + nextUploadedItem(); + } + } + + private void handleImage(Integer errorCode, Title title, List descriptions) { + switch (errorCode) { + case EMPTY_TITLE: + view.showErrorMessage(R.string.add_title_toast); + break; + case FILE_NAME_EXISTS: + if(getCurrentItem().imageQuality.getValue().equals(IMAGE_KEEP)) { + setTitleAndDescription(title, descriptions); + nextUploadedItem(); + } else { + view.showDuplicatePicturePopup(); + } + break; + case IMAGE_OK: + default: + setTitleAndDescription(title, descriptions); + nextUploadedItem(); + } + } + + private void nextUploadedItem() { + uploadModel.next(); + updateContent(); + if (uploadModel.isShowingItem()) { + uploadModel.subscribeBadPicture(this::handleBadPicture); + } + view.dismissKeyboard(); + } + + private void setTitleAndDescription(Title title, List descriptions) { + uploadModel.setCurrentTitleAndDescriptions(title, descriptions); + } + + private Title getCurrentImageTitle() { + return getCurrentItem().title; + } + + String getCurrentImageFileName() { + UploadItem currentItem = getCurrentItem(); + return currentItem.title + "." + uploadModel.getCurrentItem().fileExt; + } + + @SuppressLint("CheckResult") + private Observable validateCurrentItemTitle() { + Title title = getCurrentImageTitle(); + if (title.isEmpty()) { + view.showErrorMessage(R.string.add_title_toast); + return Observable.just(EMPTY_TITLE); + } + + return Observable.fromCallable(() -> mediaWikiApi.fileExistsWithName(getCurrentImageFileName())) + .subscribeOn(Schedulers.io()) + .map(doesFileExist -> { + if (doesFileExist) { + return FILE_NAME_EXISTS; + } + return IMAGE_OK; + }); + } + + /** + * Called by the previous button in {@link UploadActivity} + */ + void handlePrevious() { + uploadModel.previous(); + updateContent(); + if (uploadModel.isShowingItem()) { + uploadModel.subscribeBadPicture(this::handleBadPicture); + } + view.dismissKeyboard(); + } + + /** + * Called when one of the pictures on the top card is clicked on in {@link UploadActivity} + */ + void thumbnailClicked(UploadItem item) { + uploadModel.jumpTo(item); + updateContent(); + } + + /** + * Called by the submit button in {@link UploadActivity} + */ + @SuppressLint("CheckResult") + void handleSubmit(CategoriesModel categoriesModel) { + if (view.checkIfLoggedIn()) + uploadModel.buildContributions(categoriesModel.getCategoryStringList()) + .observeOn(Schedulers.io()) + .subscribe(uploadController::startUpload); + } + + /** + * Called by the map button on the right card in {@link UploadActivity} + */ + void openCoordinateMap() { + GPSExtractor gpsObj = uploadModel.getCurrentItem().gpsCoords; + if (gpsObj != null && gpsObj.imageCoordsExists) { + view.launchMapActivity(gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude()); + } + } + + + /** + * Called by the image processors when a result is obtained. + * + * @param result the result returned by the image procesors. + */ + private void handleBadPicture(@ImageUtils.Result int result) { + view.showBadPicturePopup(result); + } + + void keepPicture() { + uploadModel.keepPicture(); + } + + void deletePicture() { + if (uploadModel.getCount() == 1) + view.finish(); + else { + uploadModel.deletePicture(); + updateCards(); + updateContent(); + if (uploadModel.isShowingItem()) + uploadModel.subscribeBadPicture(this::handleBadPicture); + view.dismissKeyboard(); + } + } + //endregion + + //region Top Bottom and Right card state management + + + /** + * Toggles the top card's state between open and closed. + */ + void toggleTopCardState() { + uploadModel.setTopCardState(!uploadModel.isTopCardState()); + view.setTopCardState(uploadModel.isTopCardState()); + } + + /** + * Toggles the bottom card's state between open and closed. + */ + void toggleBottomCardState() { + uploadModel.setBottomCardState(!uploadModel.isBottomCardState()); + view.setBottomCardState(uploadModel.isBottomCardState()); + } + + /** + * Toggles the right card's state between open and closed. + */ + void toggleRightCardState() { + uploadModel.setRightCardState(!uploadModel.isRightCardState()); + view.setRightCardState(uploadModel.isRightCardState()); + } + + /** + * Sets all the cards' states to closed. + */ + void closeAllCards() { + if (uploadModel.isTopCardState()) { + uploadModel.setTopCardState(false); + view.setTopCardState(false); + } + if (uploadModel.isRightCardState()) { + uploadModel.setRightCardState(false); + view.setRightCardState(false); + } + if (uploadModel.isBottomCardState()) { + uploadModel.setBottomCardState(false); + view.setBottomCardState(false); + } + } + //endregion + + //region View / Lifecycle management + public void init() { + uploadController.prepareService(); + } + + void cleanup() { + uploadController.cleanup(); + } + + void removeView() { + this.view = DUMMY; + } + + void addView(UploadView view) { + this.view = view; + + updateCards(); + updateLicenses(); + updateContent(); + } + + + /** + * Updates the cards for when there is a change to the amount of items being uploaded. + */ + private void updateCards() { + Timber.i("uploadModel.getCount():" + uploadModel.getCount()); + view.updateThumbnails(uploadModel.getUploads()); + view.setTopCardVisibility(uploadModel.getCount() > 1); + view.setBottomCardVisibility(uploadModel.getCount() > 0); + view.setTopCardState(uploadModel.isTopCardState()); + view.setBottomCardState(uploadModel.isBottomCardState()); + } + + /** + * Sets the list of licences and the default license. + */ + private void updateLicenses() { + String selectedLicense = uploadModel.getSelectedLicense(); + view.updateLicenses(uploadModel.getLicenses(), selectedLicense); + view.updateLicenseSummary(selectedLicense); + } + + /** + * Updates the cards and the background when a new currentPage is selected. + */ + private void updateContent() { + Timber.i("Updating content for currentPage" + uploadModel.getCurrentStep()); + view.setNextEnabled(uploadModel.isNextAvailable()); + view.setPreviousEnabled(uploadModel.isPreviousAvailable()); + view.setSubmitEnabled(uploadModel.isSubmitAvailable()); + + view.setBackground(uploadModel.getCurrentItem().mediaUri); + + view.updateBottomCardContent(uploadModel.getCurrentStep(), + uploadModel.getStepCount(), + uploadModel.getCurrentItem(), + uploadModel.isShowingItem()); + + view.updateTopCardContent(); + + GPSExtractor gpsObj = uploadModel.getCurrentItem().gpsCoords; + view.updateRightCardContent(gpsObj != null && gpsObj.imageCoordsExists); + + showCorrectCards(uploadModel.getCurrentStep(), uploadModel.getCount()); + } + + /** + * Updates the layout to show the correct bottom card. + * + * @param currentStep the current step + * @param uploadCount how many items are being uploaded + */ + private void showCorrectCards(int currentStep, int uploadCount) { + if (uploadCount == 0) { + currentPage = UploadView.PLEASE_WAIT; + } else if (currentStep <= uploadCount) { + currentPage = UploadView.TITLE_CARD; + view.setTopCardVisibility(uploadModel.getCount() > 1); + } else if (currentStep == uploadCount + 1) { + currentPage = UploadView.CATEGORIES; + view.setTopCardVisibility(false); + view.setRightCardVisibility(false); + view.initDefaultCategories(); + } else { + currentPage = UploadView.LICENSE; + view.setTopCardVisibility(false); + view.setRightCardVisibility(false); + } + view.setBottomCardVisibility(currentPage); + } + + //endregion + + /** + * @return the item currently being displayed + */ + private UploadItem getCurrentItem() { + return uploadModel.getCurrentItem(); + } + + List getImageTitleList() { + List titleList = new ArrayList<>(); + for (UploadItem item : uploadModel.getUploads()) { + if (item.title.isSet()) { + titleList.add(item.title.toString()); + } + } + return titleList; + } + +} \ No newline at end of file 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 488517121..e7920f317 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 @@ -36,7 +36,7 @@ import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.ContributionDao; -import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.UploadResult; @@ -62,7 +62,9 @@ public class UploadService extends HandlerService { private NotificationCompat.Builder curProgressNotification; private int toUpload; - // The file names of unfinished uploads, used to prevent overwriting + /** + * The file names of unfinished uploads, used to prevent overwriting + */ private Set unfinishedUploads = new HashSet<>(); // DO NOT HAVE NOTIFICATION ID OF 0 FOR ANYTHING @@ -193,7 +195,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(this, 0, new Intent(this, ContributionsActivity.class), 0)) + .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0)) .setTicker(getString(R.string.upload_progress_notification_title_in_progress, contribution.getDisplayTitle())); } @@ -230,7 +232,7 @@ public class UploadService extends HandlerService { //As the fileInputStream is null there's no point in continuing the upload process //mwapi.upload accepts a NonNull input stream - if(fileInputStream == null) { + if (fileInputStream == null) { Timber.d("File not found"); return; } @@ -314,11 +316,12 @@ public class UploadService extends HandlerService { } @SuppressLint("StringFormatInvalid") + @SuppressWarnings("deprecation") private void showFailedNotification(Contribution contribution) { Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true) .setSmallIcon(R.drawable.ic_launcher) .setAutoCancel(true) - .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0)) + .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0)) .setTicker(getString(R.string.upload_failed_notification_title, contribution.getDisplayTitle())) .setContentTitle(getString(R.string.upload_failed_notification_title, contribution.getDisplayTitle())) .setContentText(getString(R.string.upload_failed_notification_subtitle)) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java new file mode 100644 index 000000000..64b873da0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailRenderer.java @@ -0,0 +1,49 @@ +package fr.free.nrw.commons.upload; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.facebook.drawee.view.SimpleDraweeView; +import com.pedrogomez.renderers.Renderer; + +import fr.free.nrw.commons.R; + +class UploadThumbnailRenderer extends Renderer { + private ThumbnailClickedListener listener; + private SimpleDraweeView background; + private View space; + private ImageView error; + + public UploadThumbnailRenderer(ThumbnailClickedListener listener) { + this.listener = listener; + } + + @Override + protected View inflate(LayoutInflater inflater, ViewGroup parent) { + return inflater.inflate(R.layout.item_upload_thumbnail, parent, false); + } + + @Override + protected void setUpView(View rootView) { + error = rootView.findViewById(R.id.error); + space = rootView.findViewById(R.id.left_space); + background = rootView.findViewById(R.id.thumbnail); + } + + @Override + protected void hookListeners(View rootView) { + background.setOnClickListener(v -> listener.thumbnailClicked(getContent())); + } + + @Override + public void render() { + UploadModel.UploadItem content = getContent(); + background.setImageURI(content.mediaUri); + background.setAlpha(content.selected ? 1.0f : 0.5f); + space.setVisibility(content.first ? View.VISIBLE : View.GONE); + error.setVisibility(content.visited && content.error ? View.VISIBLE : View.GONE); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java new file mode 100644 index 000000000..bc0a79c80 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadThumbnailsAdapterFactory.java @@ -0,0 +1,26 @@ +package fr.free.nrw.commons.upload; + +import com.pedrogomez.renderers.ListAdapteeCollection; +import com.pedrogomez.renderers.RVRendererAdapter; +import com.pedrogomez.renderers.RendererBuilder; + +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; + +public class UploadThumbnailsAdapterFactory { + private ThumbnailClickedListener listener; + + UploadThumbnailsAdapterFactory(ThumbnailClickedListener listener) { + this.listener = listener; + } + + public RVRendererAdapter create(List placeList) { + RendererBuilder builder = new RendererBuilder() + .bind(UploadModel.UploadItem.class, new UploadThumbnailRenderer(listener)); + ListAdapteeCollection collection = new ListAdapteeCollection<>( + placeList != null ? placeList : Collections.emptyList()); + return new RVRendererAdapter<>(builder, collection); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java new file mode 100644 index 000000000..410914446 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java @@ -0,0 +1,82 @@ +package fr.free.nrw.commons.upload; + +import android.net.Uri; +import android.support.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.util.List; + +import fr.free.nrw.commons.utils.ImageUtils; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +public interface UploadView { + // Dummy implementation of the view interface to allow us to have a 'null object pattern' + // in the presenter and avoid constant NULL checking. +// UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(), +// new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null); + + List getDescriptions(); + + @Retention(SOURCE) + @IntDef({PLEASE_WAIT, TITLE_CARD, CATEGORIES, LICENSE}) + @interface UploadPage {} + + int PLEASE_WAIT = 0; + + int TITLE_CARD = 1; + int CATEGORIES = 2; + int LICENSE = 3; + + boolean checkIfLoggedIn(); + + void updateThumbnails(List uploads); + + void setNextEnabled(boolean available); + + void setSubmitEnabled(boolean available); + + void setPreviousEnabled(boolean available); + + void setTopCardState(boolean state); + + void setRightCardVisibility(boolean visible); + + void setBottomCardState(boolean state); + + void setRightCardState(boolean bottomCardState); + + void setBackground(Uri mediaUri); + + void setTopCardVisibility(boolean visible); + + void setBottomCardVisibility(boolean visible); + + void setBottomCardVisibility(@UploadPage int page); + + void updateRightCardContent(boolean gpsPresent); + + void updateBottomCardContent(int currentStep, int stepCount, UploadModel.UploadItem uploadItem, boolean isShowingItem); + + void updateLicenses(List licenses, String selectedLicense); + + void updateLicenseSummary(String selectedLicense); + + void updateTopCardContent(); + + void dismissKeyboard(); + + void showBadPicturePopup(@ImageUtils.Result int errorMessage); + + void showDuplicatePicturePopup(); + + void finish(); + + void launchMapActivity(String decCoords); + + void showErrorMessage(int resourceId); + + void initDefaultCategories(); + + void showNoCategorySelectedWarning(); +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java b/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java index d3274bf2c..a28fde579 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java @@ -7,62 +7,62 @@ import java.util.HashMap; * info in the user language */ public class UrlLicense { - HashMap urlLicense = new HashMap(); - public void initialize(){ - urlLicense.put("en","https://commons.wikimedia.org/wiki/Commons:Licensing"); - urlLicense.put("ar","https://commons.wikimedia.org/wiki/Commons:Licensing/ar"); - urlLicense.put("ast","https://commons.wikimedia.org/wiki/Commons:Licensing/ast"); - urlLicense.put("az","https://commons.wikimedia.org/wiki/Commons:Licensing/az"); - urlLicense.put("be","https://commons.wikimedia.org/wiki/Commons:Licensing/be"); - urlLicense.put("bg","https://commons.wikimedia.org/wiki/Commons:Licensing/bg"); - urlLicense.put("bn","https://commons.wikimedia.org/wiki/Commons:Licensing/bn"); - urlLicense.put("ca","https://commons.wikimedia.org/wiki/Commons:Licensing/ca"); - urlLicense.put("cs","https://commons.wikimedia.org/wiki/Commons:Licensing/cs"); - urlLicense.put("da","https://commons.wikimedia.org/wiki/Commons:Licensing/da"); - urlLicense.put("de","https://commons.wikimedia.org/wiki/Commons:Licensing/de"); - urlLicense.put("el","https://commons.wikimedia.org/wiki/Commons:Licensing/el"); - urlLicense.put("eo","https://commons.wikimedia.org/wiki/Commons:Licensing/eo"); - urlLicense.put("es","https://commons.wikimedia.org/wiki/Commons:Licensing/es"); - urlLicense.put("eu","https://commons.wikimedia.org/wiki/Commons:Licensing/eu"); - urlLicense.put("fa","https://commons.wikimedia.org/wiki/Commons:Licensing/fa"); - urlLicense.put("fi","https://commons.wikimedia.org/wiki/Commons:Licensing/fi"); - urlLicense.put("fr","https://commons.wikimedia.org/wiki/Commons:Licensing/fr"); - urlLicense.put("gl","https://commons.wikimedia.org/wiki/Commons:Licensing/gl"); - urlLicense.put("gsw","https://commons.wikimedia.org/wiki/Commons:Licensing/gsw"); - urlLicense.put("he","https://commons.wikimedia.org/wiki/Commons:Licensing/he"); - urlLicense.put("hi","https://commons.wikimedia.org/wiki/Commons:Licensing/hi"); - urlLicense.put("hu","https://commons.wikimedia.org/wiki/Commons:Licensing/hu"); - urlLicense.put("id","https://commons.wikimedia.org/wiki/Commons:Licensing/id"); - urlLicense.put("is","https://commons.wikimedia.org/wiki/Commons:Licensing/is"); - urlLicense.put("it","https://commons.wikimedia.org/wiki/Commons:Licensing/it"); - urlLicense.put("ja","https://commons.wikimedia.org/wiki/Commons:Licensing/ja"); - urlLicense.put("ka","https://commons.wikimedia.org/wiki/Commons:Licensing/ka"); - urlLicense.put("km","https://commons.wikimedia.org/wiki/Commons:Licensing/km"); - urlLicense.put("ko","https://commons.wikimedia.org/wiki/Commons:Licensing/ko"); - urlLicense.put("ku","https://commons.wikimedia.org/wiki/Commons:Licensing/ku"); - urlLicense.put("mk","https://commons.wikimedia.org/wiki/Commons:Licensing/mk"); - urlLicense.put("mr","https://commons.wikimedia.org/wiki/Commons:Licensing/mr"); - urlLicense.put("ms","https://commons.wikimedia.org/wiki/Commons:Licensing/ms"); - urlLicense.put("my","https://commons.wikimedia.org/wiki/Commons:Licensing/my"); - urlLicense.put("nl","https://commons.wikimedia.org/wiki/Commons:Licensing/nl"); - urlLicense.put("oc","https://commons.wikimedia.org/wiki/Commons:Licensing/oc"); - urlLicense.put("pl","https://commons.wikimedia.org/wiki/Commons:Licensing/pl"); - urlLicense.put("pt","https://commons.wikimedia.org/wiki/Commons:Licensing/pt"); - urlLicense.put("pt-br","https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br"); - urlLicense.put("ro","https://commons.wikimedia.org/wiki/Commons:Licensing/ro"); - urlLicense.put("ru","https://commons.wikimedia.org/wiki/Commons:Licensing/ru"); - urlLicense.put("scn","https://commons.wikimedia.org/wiki/Commons:Licensing/scn"); - urlLicense.put("sk","https://commons.wikimedia.org/wiki/Commons:Licensing/sk"); - urlLicense.put("sl","https://commons.wikimedia.org/wiki/Commons:Licensing/sl"); - urlLicense.put("sv","https://commons.wikimedia.org/wiki/Commons:Licensing/sv"); - urlLicense.put("tr","https://commons.wikimedia.org/wiki/Commons:Licensing/tr"); - urlLicense.put("uk","https://commons.wikimedia.org/wiki/Commons:Licensing/uk"); - urlLicense.put("ur","https://commons.wikimedia.org/wiki/Commons:Licensing/ur"); - urlLicense.put("vi","https://commons.wikimedia.org/wiki/Commons:Licensing/vi"); - urlLicense.put("zh","https://commons.wikimedia.org/wiki/Commons:Licensing/zh"); + public static HashMap urlLicense = new HashMap<>(); + static { + urlLicense.put("en", "https://commons.wikimedia.org/wiki/Commons:Licensing"); + urlLicense.put("ar", "https://commons.wikimedia.org/wiki/Commons:Licensing/ar"); + urlLicense.put("ast", "https://commons.wikimedia.org/wiki/Commons:Licensing/ast"); + urlLicense.put("az", "https://commons.wikimedia.org/wiki/Commons:Licensing/az"); + urlLicense.put("be", "https://commons.wikimedia.org/wiki/Commons:Licensing/be"); + urlLicense.put("bg", "https://commons.wikimedia.org/wiki/Commons:Licensing/bg"); + urlLicense.put("bn", "https://commons.wikimedia.org/wiki/Commons:Licensing/bn"); + urlLicense.put("ca", "https://commons.wikimedia.org/wiki/Commons:Licensing/ca"); + urlLicense.put("cs", "https://commons.wikimedia.org/wiki/Commons:Licensing/cs"); + urlLicense.put("da", "https://commons.wikimedia.org/wiki/Commons:Licensing/da"); + urlLicense.put("de", "https://commons.wikimedia.org/wiki/Commons:Licensing/de"); + urlLicense.put("el", "https://commons.wikimedia.org/wiki/Commons:Licensing/el"); + urlLicense.put("eo", "https://commons.wikimedia.org/wiki/Commons:Licensing/eo"); + urlLicense.put("es", "https://commons.wikimedia.org/wiki/Commons:Licensing/es"); + urlLicense.put("eu", "https://commons.wikimedia.org/wiki/Commons:Licensing/eu"); + urlLicense.put("fa", "https://commons.wikimedia.org/wiki/Commons:Licensing/fa"); + urlLicense.put("fi", "https://commons.wikimedia.org/wiki/Commons:Licensing/fi"); + urlLicense.put("fr", "https://commons.wikimedia.org/wiki/Commons:Licensing/fr"); + urlLicense.put("gl", "https://commons.wikimedia.org/wiki/Commons:Licensing/gl"); + urlLicense.put("gsw", "https://commons.wikimedia.org/wiki/Commons:Licensing/gsw"); + urlLicense.put("he", "https://commons.wikimedia.org/wiki/Commons:Licensing/he"); + urlLicense.put("hi", "https://commons.wikimedia.org/wiki/Commons:Licensing/hi"); + urlLicense.put("hu", "https://commons.wikimedia.org/wiki/Commons:Licensing/hu"); + urlLicense.put("id", "https://commons.wikimedia.org/wiki/Commons:Licensing/id"); + urlLicense.put("is", "https://commons.wikimedia.org/wiki/Commons:Licensing/is"); + urlLicense.put("it", "https://commons.wikimedia.org/wiki/Commons:Licensing/it"); + urlLicense.put("ja", "https://commons.wikimedia.org/wiki/Commons:Licensing/ja"); + urlLicense.put("ka", "https://commons.wikimedia.org/wiki/Commons:Licensing/ka"); + urlLicense.put("km", "https://commons.wikimedia.org/wiki/Commons:Licensing/km"); + urlLicense.put("ko", "https://commons.wikimedia.org/wiki/Commons:Licensing/ko"); + urlLicense.put("ku", "https://commons.wikimedia.org/wiki/Commons:Licensing/ku"); + urlLicense.put("mk", "https://commons.wikimedia.org/wiki/Commons:Licensing/mk"); + urlLicense.put("mr", "https://commons.wikimedia.org/wiki/Commons:Licensing/mr"); + urlLicense.put("ms", "https://commons.wikimedia.org/wiki/Commons:Licensing/ms"); + urlLicense.put("my", "https://commons.wikimedia.org/wiki/Commons:Licensing/my"); + urlLicense.put("nl", "https://commons.wikimedia.org/wiki/Commons:Licensing/nl"); + urlLicense.put("oc", "https://commons.wikimedia.org/wiki/Commons:Licensing/oc"); + urlLicense.put("pl", "https://commons.wikimedia.org/wiki/Commons:Licensing/pl"); + urlLicense.put("pt", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt"); + urlLicense.put("pt-br", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br"); + urlLicense.put("ro", "https://commons.wikimedia.org/wiki/Commons:Licensing/ro"); + urlLicense.put("ru", "https://commons.wikimedia.org/wiki/Commons:Licensing/ru"); + urlLicense.put("scn", "https://commons.wikimedia.org/wiki/Commons:Licensing/scn"); + urlLicense.put("sk", "https://commons.wikimedia.org/wiki/Commons:Licensing/sk"); + urlLicense.put("sl", "https://commons.wikimedia.org/wiki/Commons:Licensing/sl"); + urlLicense.put("sv", "https://commons.wikimedia.org/wiki/Commons:Licensing/sv"); + urlLicense.put("tr", "https://commons.wikimedia.org/wiki/Commons:Licensing/tr"); + urlLicense.put("uk", "https://commons.wikimedia.org/wiki/Commons:Licensing/uk"); + urlLicense.put("ur", "https://commons.wikimedia.org/wiki/Commons:Licensing/ur"); + urlLicense.put("vi", "https://commons.wikimedia.org/wiki/Commons:Licensing/vi"); + urlLicense.put("zh", "https://commons.wikimedia.org/wiki/Commons:Licensing/zh"); } - public String getLicenseUrl ( String language){ - if(urlLicense.containsKey(language)) { + public static String getLicenseUrl ( String language){ + if (urlLicense.containsKey(language)) { return urlLicense.get(language); } else { return urlLicense.get("en"); diff --git a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java new file mode 100644 index 000000000..92f9f1935 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java @@ -0,0 +1,30 @@ +package fr.free.nrw.commons.utils; + +import android.support.annotation.NonNull; +import android.text.Editable; +import android.text.TextWatcher; + +public class AbstractTextWatcher implements TextWatcher { + private final TextChange textChange; + + public AbstractTextWatcher(@NonNull TextChange textChange) { + this.textChange = textChange; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + textChange.onTextChanged(s.toString()); + } + + @Override + public void afterTextChanged(Editable s) { + } + + public interface TextChange { + void onTextChanged(String value); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/BiMap.java b/app/src/main/java/fr/free/nrw/commons/utils/BiMap.java new file mode 100644 index 000000000..227f5f024 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/BiMap.java @@ -0,0 +1,41 @@ +package fr.free.nrw.commons.utils; + +import java.util.HashMap; +import java.util.Set; + +/** + * HashMap that can be searched in both the forward and reverse directions. + */ +public class BiMap { + + private HashMap map = new HashMap(); + private HashMap inversedMap = new HashMap(); + + public void put(K k, V v) { + map.put(k, v); + inversedMap.put(v, k); + } + + public V get(K k) { + return map.get(k); + } + + public K getKey(V v) { + return inversedMap.get(v); + } + + public Set getEntrySet(){ + return inversedMap.keySet(); + } + + public void remove(K k){ + inversedMap.remove(map.remove(k)); + } + + + public boolean containsKey(V v){ + return inversedMap.containsKey(v); + } + +} + diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ContributionListViewUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ContributionListViewUtils.java new file mode 100644 index 000000000..6dc9625d7 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ContributionListViewUtils.java @@ -0,0 +1,39 @@ +package fr.free.nrw.commons.utils; + +import android.util.Log; +import android.view.View; + +/** + * This class includes utilities for contribution list fragment indicators, such as number of + * uploads, notification and nearby cards and their progress bar behind them. + */ +public class ContributionListViewUtils { + + /** + * Sets indicator and progress bar visibility according to 3 states, data is ready to display, + * data still loading, both should be invisible because media details fragment is visible + * @param indicator this can be numOfUploads text view, notification/nearby card views + * @param progressBar this is the progress bar behind indicators, displays they are loading + * @param isIndicatorReady is indicator fetched the information will be displayed + * @param isBothInvisible true if contribution list fragment is not active (ie. Media Details Fragment is active) + */ + public static void setIndicatorVisibility(View indicator, View progressBar, boolean isIndicatorReady, boolean isBothInvisible) { + if (indicator!=null && progressBar!=null) { + if (isIndicatorReady) { + // Indicator ready, display them + indicator.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.GONE); + } else { + if (isBothInvisible) { + // Media Details Fragment is visible, hide both + indicator.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + } else { + // Indicator is not ready, still loading + indicator.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + } + } + } + } +} 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 index 2e4592e40..f68037488 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java @@ -5,6 +5,7 @@ import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.Dialog; import android.content.Context; +import android.content.DialogInterface; import android.os.Build; import android.support.annotation.Nullable; import android.support.v4.app.DialogFragment; @@ -114,7 +115,49 @@ public class DialogUtil { .setIcon(iconResourceId).create(); return alertDialog; + } + public static void showAlertDialog(Activity activity, + String title, + String message, + final Runnable onPositiveBtnClick, + final Runnable onNegativeBtnClick) { + showAlertDialog(activity, + title, + message, + activity.getString(R.string.no), + activity.getString(R.string.yes), + onPositiveBtnClick, + onNegativeBtnClick); + } + + public static void showAlertDialog(Activity activity, + String title, + String message, + String positiveButtonText, + String negativeButtonText, + final Runnable onPositiveBtnClick, + final Runnable onNegativeBtnClick) { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(title); + builder.setMessage(message); + + builder.setPositiveButton(positiveButtonText, (dialogInterface, i) -> { + dialogInterface.dismiss(); + if (onPositiveBtnClick != null) { + onPositiveBtnClick.run(); + } + }); + + builder.setNegativeButton(negativeButtonText, (DialogInterface dialogInterface, int i) -> { + dialogInterface.dismiss(); + if (onNegativeBtnClick != null) { + onNegativeBtnClick.run(); + } + }); + + AlertDialog dialog = builder.create(); + showSafely(activity, dialog); } public interface Callback { diff --git a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java index d8e569564..26ab5b2ca 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java @@ -66,7 +66,7 @@ public class FileUtils { */ public static boolean checkIfDirectoryExists(String pathToCheck) { File director = new File(pathToCheck); - if(director.exists() && director.isDirectory()) { + if (director.exists() && director.isDirectory()) { return true; } else { return false; diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java index 460046bab..e6cc2fc5d 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java @@ -7,6 +7,7 @@ import android.graphics.BitmapRegionDecoder; import android.graphics.Color; import android.graphics.Rect; import android.net.Uri; +import android.support.annotation.IntDef; import android.support.annotation.Nullable; import com.facebook.common.executors.CallerThreadExecutor; @@ -20,6 +21,8 @@ import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequestBuilder; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import fr.free.nrw.commons.R; import timber.log.Timber; @@ -30,20 +33,44 @@ import timber.log.Timber; public class ImageUtils { - public enum Result { - IMAGE_DARK, - IMAGE_OK + public static final int IMAGE_DARK = 1; + public static final int IMAGE_BLURRY = 1 << 1; + public static final int IMAGE_DUPLICATE = 1 << 2; + public static final int IMAGE_OK = 0; + public static final int IMAGE_KEEP = -1; + public static final int IMAGE_WAIT = -2; + public static final int EMPTY_TITLE = -3; + public static final int FILE_NAME_EXISTS = -4; + public static final int NO_CATEGORY_SELECTED = -5; + + @IntDef( + flag = true, + value = { + IMAGE_DARK, + IMAGE_BLURRY, + IMAGE_DUPLICATE, + IMAGE_OK, + IMAGE_KEEP, + IMAGE_WAIT, + EMPTY_TITLE, + FILE_NAME_EXISTS, + NO_CATEGORY_SELECTED + } + ) + @Retention(RetentionPolicy.SOURCE) + public @interface Result { } /** * @param bitmapRegionDecoder BitmapRegionDecoder for the image we wish to process - * @return Result.IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null - * Result.IMAGE_DARK if image is too dark + * @return IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null + * IMAGE_DARK if image is too dark */ - public static Result checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) { + public static @Result + int checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) { if (bitmapRegionDecoder == null) { Timber.e("Expected bitmapRegionDecoder was null"); - return Result.IMAGE_OK; + return IMAGE_OK; } int loadImageHeight = bitmapRegionDecoder.getHeight(); @@ -59,10 +86,10 @@ public class ImageUtils { Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect,null); if (checkIfImageIsDark(processBitmap)) { - return Result.IMAGE_DARK; + return IMAGE_DARK; } - return Result.IMAGE_OK; + return IMAGE_OK; } /** @@ -132,8 +159,9 @@ public class ImageUtils { /** * Downloads the image from the URL and sets it as the phone's wallpaper * Fails silently if download or setting wallpaper fails. - * @param context - * @param imageUrl + * + * @param context context + * @param imageUrl Url of the image */ public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) { Timber.d("Trying to set wallpaper from url %s", imageUrl.toString()); @@ -150,7 +178,7 @@ public class ImageUtils { @Override public void onNewResultImpl(@Nullable Bitmap bitmap) { - if (dataSource.isFinished() && bitmap != null){ + if (dataSource.isFinished() && bitmap != null) { Timber.d("Bitmap loaded from url %s", imageUrl.toString()); setWallpaper(context, Bitmap.createBitmap(bitmap)); dataSource.close(); @@ -173,7 +201,29 @@ public class ImageUtils { wallpaperManager.setBitmap(bitmap); ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_successfully)); } catch (IOException e) { - Timber.e(e,"Error setting wallpaper"); + Timber.e(e, "Error setting wallpaper"); } } + + public static String getErrorMessageForResult(Context context, @Result int result) { + String errorMessage; + if (result == ImageUtils.IMAGE_DARK) + errorMessage = context.getString(R.string.upload_image_problem_dark); + else if (result == ImageUtils.IMAGE_BLURRY) + errorMessage = context.getString(R.string.upload_image_problem_blurry); + else if (result == ImageUtils.IMAGE_DUPLICATE) + errorMessage = context.getString(R.string.upload_image_problem_duplicate); + else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY)) + errorMessage = context.getString(R.string.upload_image_problem_dark_blurry); + else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_DUPLICATE)) + errorMessage = context.getString(R.string.upload_image_problem_dark_duplicate); + else if (result == (ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE)) + errorMessage = context.getString(R.string.upload_image_problem_blurry_duplicate); + else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE)) + errorMessage = context.getString(R.string.upload_image_problem_dark_blurry_duplicate); + else + return ""; + + return errorMessage; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java index 3429ef403..d76e2ff5f 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java @@ -9,8 +9,14 @@ import android.support.v4.content.ContextCompat; import fr.free.nrw.commons.CommonsApplication; + public class PermissionUtils { + public static final int CAMERA_PERMISSION_FROM_CONTRIBUTION_LIST = 100; + public static final int GALLERY_PERMISSION_FROM_CONTRIBUTION_LIST = 101; + public static final int CAMERA_PERMISSION_FROM_NEARBY_MAP = 102; + public static final int GALLERY_PERMISSION_FROM_NEARBY_MAP = 103; + /** * This method can be used by any activity which requires a permission which has been blocked(marked never ask again by the user) It open the app settings from where the user can manually give us the required permission. diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java index e409b856d..1b321ec07 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java @@ -44,4 +44,4 @@ public class StringSortingUtils { double distanceBetweenStrings = new Levenshtein().distance(longer, shorter); return (longerLength - distanceBetweenStrings) / (double) longerLength; } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java index 0eb8216e4..0f93e65ef 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/StringUtils.java @@ -12,4 +12,8 @@ public class StringUtils { return Html.fromHtml(source).toString(); } } + + public static boolean isNullOrWhiteSpace(String value) { + return value == null || value.trim().isEmpty(); + } } 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 index ad37b27fc..9be11d295 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/UriDeserializer.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/UriDeserializer.java @@ -15,4 +15,4 @@ public class UriDeserializer implements JsonDeserializer { 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/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java index af71e826d..aef3dddb0 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java @@ -2,10 +2,13 @@ package fr.free.nrw.commons.utils; import android.app.Activity; import android.content.Context; +import android.support.annotation.StringRes; import android.support.design.widget.Snackbar; import android.view.Display; import android.view.View; +import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; +import android.widget.PopupWindow; import android.widget.Toast; public class ViewUtil { @@ -15,7 +18,7 @@ public class ViewUtil { public static final String SHOWCASE_VIEW_ID_3 = "SHOWCASE_VIEW_ID_3"; public static void showSnackbar(View view, int messageResourceId) { - if(view.getContext() == null) { + if (view.getContext() == null) { return; } @@ -30,9 +33,33 @@ public class ViewUtil { ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_LONG).show()); } + public static void showLongToast(Context context, @StringRes int stringResourceId) { + if (context == null) { + return; + } + + ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_LONG).show()); + } + + public static void showShortToast(Context context, String text) { + if (context == null) { + return; + } + + ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_SHORT).show()); + } + + public static void showShortToast(Context context, @StringRes int stringResourceId) { + if (context == null) { + return; + } + + ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_SHORT).show()); + } + public static boolean isPortrait(Context context) { Display orientation = ((Activity)context).getWindowManager().getDefaultDisplay(); - if(orientation.getWidth() < orientation.getHeight()){ + if (orientation.getWidth() < orientation.getHeight()){ return true; } else { return false; @@ -49,4 +76,15 @@ public class ViewUtil { } } + public static void displayPopupWindow(View anchorView, Context context, View popupWindowLayout, String text) { + + PopupWindow popup = new PopupWindow(context); + popup.setContentView(popupWindowLayout); + // Closes the popup window when touch outside of it - when looses focus + popup.setOutsideTouchable(true); + popup.setFocusable(true); + // Show anchored to button + popup.showAsDropDown(anchorView); + } + } diff --git a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java index 22bea67b3..fc87f7abb 100644 --- a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java +++ b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java @@ -128,4 +128,4 @@ public class PicOfDayAppWidget extends AppWidgetProvider { public void onDisabled(Context context) { // Enter relevant functionality for when the last widget is disabled } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java index 407c24711..a97d0eded 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java @@ -17,4 +17,4 @@ public class WikidataEditListenerImpl extends WikidataEditListener { wikidataP18EditListener.onWikidataEditSuccessful(); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java index 0a7c0b8ec..6b0c52cb3 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java @@ -48,12 +48,12 @@ public class WikidataEditService { * @param fileName */ public void createClaimWithLogging(String wikidataEntityId, String fileName) { - if(wikidataEntityId == null) { + if (wikidataEntityId == null) { Timber.d("Skipping creation of claim as Wikidata entity ID is null"); return; } - if(fileName == null) { + if (fileName == null) { Timber.d("Skipping creation of claim as fileName entity ID is null"); return; } diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-hdpi/ic_arrow_back_white.png new file mode 100644 index 000000000..0b78dc8c4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_arrow_back_white.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art.png new file mode 100644 index 000000000..d974d19d5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art_dot.png new file mode 100644 index 000000000..b03310f55 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art_dot.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-mdpi/ic_arrow_back_white.png new file mode 100644 index 000000000..8d1142f31 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_arrow_back_white.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art.png new file mode 100644 index 000000000..f9cb42735 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art_dot.png new file mode 100644 index 000000000..ebb4f86c0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art_dot.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-xhdpi/ic_arrow_back_white.png new file mode 100644 index 000000000..f782543d3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_arrow_back_white.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art.png new file mode 100644 index 000000000..62369be0a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art_dot.png new file mode 100644 index 000000000..c6be29890 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art_dot.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png new file mode 100644 index 000000000..2fe75945f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art.png new file mode 100644 index 000000000..907c922fc Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art_dot.png new file mode 100644 index 000000000..239de1d73 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art_dot.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white.png new file mode 100644 index 000000000..575d5075c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art.png new file mode 100644 index 000000000..88ba092c1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art_dot.png new file mode 100644 index 000000000..c5a5e3f1f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art_dot.png differ diff --git a/app/src/main/res/drawable/ic_error_red_24dp.xml b/app/src/main/res/drawable/ic_error_red_24dp.xml new file mode 100644 index 000000000..e1569395b --- /dev/null +++ b/app/src/main/res/drawable/ic_error_red_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_less_black_24dp.xml b/app/src/main/res/drawable/ic_expand_less_black_24dp.xml new file mode 100644 index 000000000..3afdf9682 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_less_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_less_white_24dp.xml b/app/src/main/res/drawable/ic_expand_less_white_24dp.xml new file mode 100644 index 000000000..d58421a2f --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_less_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_more_black_24dp.xml b/app/src/main/res/drawable/ic_expand_more_black_24dp.xml new file mode 100644 index 000000000..8d57dbc10 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_more_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_more_white_24dp.xml b/app/src/main/res/drawable/ic_expand_more_white_24dp.xml new file mode 100644 index 000000000..fd3ce4a46 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_more_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_location_white_24dp.xml b/app/src/main/res/drawable/ic_location_white_24dp.xml new file mode 100644 index 000000000..aea56cb5a --- /dev/null +++ b/app/src/main/res/drawable/ic_location_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_active_white_24dp.xml b/app/src/main/res/drawable/ic_notifications_active_white_24dp.xml new file mode 100644 index 000000000..5d6921643 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_active_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_white_24dp.xml b/app/src/main/res/drawable/ic_notifications_white_24dp.xml new file mode 100644 index 000000000..120895c4f --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_white_24dp.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/layout/activity_contributions.xml b/app/src/main/res/layout/activity_contributions.xml index 51a48f0a5..2e6e92728 100644 --- a/app/src/main/res/layout/activity_contributions.xml +++ b/app/src/main/res/layout/activity_contributions.xml @@ -1,34 +1,51 @@ + android:layout_height="match_parent" + android:background="?attr/contributionsListBackground" + > + android:layout_height="match_parent" + android:gravity="center_horizontal" + > + + + android:layout_below="@id/tab_layout"> - + android:layout_height="match_parent" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_upload_bottom_card.xml b/app/src/main/res/layout/activity_upload_bottom_card.xml new file mode 100644 index 000000000..273835b5d --- /dev/null +++ b/app/src/main/res/layout/activity_upload_bottom_card.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + +