Merge branch 'master' into addedReasonList

This commit is contained in:
Vivek Maskara 2018-11-20 21:16:51 +05:30 committed by GitHub
commit 2bcc8c10b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
248 changed files with 7414 additions and 4619 deletions

View file

@ -2,24 +2,28 @@
Summarize your issue in one sentence (what goes wrong, what did you expect to happen) 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:** **Steps to reproduce:**
How can we reproduce the issue? How can we reproduce the issue?
What did you expect the app to do, and what did you see instead? 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). 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:** **Device and Android version:**
What make and model device (e.g., Samsung J7) did you encounter this on? What Android What make and model device (e.g., Samsung J7) did you encounter this on?
version (e.g., Android 4.0 Ice Cream Sandwich or Android 6.0 Marshmallow) are you running? Is it What Android version (e.g., Android 4.0 Ice Cream Sandwich or Android 6.0 Marshmallow) are you running?
the stock version from the manufacturer or a custom ROM ? 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). 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).

View file

@ -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._ _Note: Please ensure that you have read CONTRIBUTING.md if this is your first pull request._

View file

@ -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]. 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 ## 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] * [User Documentation][5]
* [Contributor Documentation][7] * [Contributor Documentation][6]
* [Volunteers Welcome!][9] * [Volunteers Welcome!][7]
* [Developer Documentation][8] * [Developer Documentation][8]
* [Libraries Used][9]
## 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]
## Contributors ## ## Contributors ##
@ -60,37 +36,18 @@ Thank you all for your work!
## License ## ## 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 [1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
[2]: https://commons-app.github.io/ [2]: https://commons-app.github.io/
[3]: https://github.com/commons-app/apps-android-commons/issues [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 [4]: https://github.com/commons-app/apps-android-commons/wiki
[6]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation [5]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation
[7]: https://github.com/commons-app/apps-android-commons/wiki#contributor-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 [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 [9]: https://github.com/commons-app/apps-android-commons/wiki/Libraries-used
[10]: https://meta.wikimedia.org/wiki/Grants:Project/Improve_%27Upload_to_Commons%27_Android_App/Renewal
[11]: https://github.com/square/picasso [10]: https://www.apache.org/licenses/LICENSE-2.0
[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

View file

@ -13,7 +13,6 @@ dependencies {
implementation 'ch.acra:acra:4.9.2' implementation 'ch.acra:acra:4.9.2'
implementation 'org.mediawiki:api:1.3'
implementation 'commons-codec:commons-codec:1.10' implementation 'commons-codec:commons-codec:1.10'
implementation 'com.github.pedrovgs:renderers:3.3.3' implementation 'com.github.pedrovgs:renderers:3.3.3'
implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.google.code.gson:gson:2.8.5'
@ -32,6 +31,7 @@ dependencies {
transitive = true transitive = true
} }
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0' implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
//noinspection GradleCompatible //noinspection GradleCompatible
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION" implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
implementation "com.android.support:appcompat-v7:$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 'com.squareup.okio:okio:1.14.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
// Because RxAndroid releases are few and far between, it is recommended you also // 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. // explicitly depend on RxJava's latest version for bug fixes and new features.
implementation 'io.reactivex.rxjava2:rxjava:2.2.0' implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
@ -131,7 +132,7 @@ android {
flavorDimensions 'tier' flavorDimensions 'tier'
productFlavors { productFlavors {
prod { prod {
applicationId 'fr.free.nrw.commons' 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\"" 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", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\""
buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.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", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\""
buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\"" buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.contentprovider\""
buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\"" buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.locations.contentprovider\""
dimension 'tier' dimension 'tier'
} }

View file

@ -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));
}
}

View file

@ -15,7 +15,7 @@
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" /> <uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" /> <uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
<uses-permission android:name="android.permission.READ_LOGS"/> <uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.SET_WALLPAPER"/> <uses-permission android:name="android.permission.SET_WALLPAPER"/>
@ -41,56 +41,44 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".WelcomeActivity" /> <activity android:name=".WelcomeActivity" />
<activity <activity android:name=".upload.UploadActivity"
android:name=".upload.ShareActivity"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="@string/app_name"> android:label="@string/app_name">
<intent-filter android:label="@string/intent_share_upload_label"> <intent-filter android:label="@string/intent_share_upload_label">
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" /> <data android:mimeType="image/*" />
<data android:mimeType="audio/ogg" /> <data android:mimeType="audio/ogg" />
</intent-filter> </intent-filter>
</activity> <intent-filter>
<activity
android:name=".upload.MultipleShareActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
<intent-filter android:label="@string/intent_share_upload_label">
<action android:name="android.intent.action.SEND_MULTIPLE" /> <action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" /> <data android:mimeType="image/*" />
<data android:mimeType="audio/ogg" /> <data android:mimeType="audio/ogg" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".contributions.ContributionsActivity" android:name=".contributions.MainActivity"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="@string/app_name" /> android:label="@string/app_name" />
<activity <activity
android:name=".settings.SettingsActivity" android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings" /> android:label="@string/title_activity_settings" />
<activity <activity
android:name=".AboutActivity" android:name=".AboutActivity"
android:label="@string/title_activity_about" android:label="@string/title_activity_about"
android:parentActivityName=".contributions.ContributionsActivity" /> android:parentActivityName=".contributions.MainActivity" />
<activity <activity
android:name=".auth.SignupActivity" android:name=".auth.SignupActivity"
android:label="@string/title_activity_signup" /> android:label="@string/title_activity_signup" />
<activity
android:name=".nearby.NearbyActivity"
android:label="@string/title_activity_nearby"
android:parentActivityName=".contributions.ContributionsActivity" />
<activity <activity
android:name=".notification.NotificationActivity" android:name=".notification.NotificationActivity"
android:label="@string/navigation_item_notification" /> android:label="@string/navigation_item_notification" />
@ -104,18 +92,18 @@
<activity <activity
android:name=".category.CategoryImagesActivity" android:name=".category.CategoryImagesActivity"
android:label="@string/title_activity_featured_images" android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.ContributionsActivity" /> android:parentActivityName=".contributions.MainActivity" />
<activity <activity
android:name=".category.CategoryDetailsActivity" android:name=".category.CategoryDetailsActivity"
android:label="@string/title_activity_featured_images" android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.ContributionsActivity" /> android:parentActivityName=".contributions.MainActivity" />
<activity <activity
android:name=".explore.SearchActivity" android:name=".explore.SearchActivity"
android:label="@string/title_activity_search" android:label="@string/title_activity_search"
android:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
android:parentActivityName=".contributions.ContributionsActivity" android:parentActivityName=".contributions.MainActivity"
/> />
<activity <activity
@ -140,24 +128,24 @@
android:name="android.accounts.AccountAuthenticator" android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" /> android:resource="@xml/authenticator" />
</service> </service>
<service <service
android:name=".contributions.ContributionsSyncService" android:name=".contributions.ContributionsSyncService"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.content.SyncAdapter" /> <action android:name="android.content.SyncAdapter" />
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.content.SyncAdapter" android:name="android.content.SyncAdapter"
android:resource="@xml/contributions_sync_adapter" /> android:resource="@xml/contributions_sync_adapter" />
</service> </service>
<service <service
android:name=".modifications.ModificationsSyncService" android:name=".modifications.ModificationsSyncService"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.content.SyncAdapter" /> <action android:name="android.content.SyncAdapter" />
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.content.SyncAdapter" android:name="android.content.SyncAdapter"
android:resource="@xml/modifications_sync_adapter" /> android:resource="@xml/modifications_sync_adapter" />
@ -177,21 +165,18 @@
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
<provider <provider
android:name=".contributions.ContributionsContentProvider" android:name=".contributions.ContributionsContentProvider"
android:authorities="${applicationId}.contributions.contentprovider" android:authorities="${applicationId}.contributions.contentprovider"
android:exported="false" android:exported="false"
android:label="@string/provider_contributions" android:label="@string/provider_contributions"
android:syncable="true" /> android:syncable="true" />
<provider <provider
android:name=".modifications.ModificationsContentProvider" android:name=".modifications.ModificationsContentProvider"
android:authorities="${applicationId}.modifications.contentprovider" android:authorities="${applicationId}.modifications.contentprovider"
android:exported="false" android:exported="false"
android:label="@string/provider_modifications" android:label="@string/provider_modifications"
android:syncable="true" /> android:syncable="true" />
<provider <provider
android:name=".category.CategoryContentProvider" android:name=".category.CategoryContentProvider"
android:authorities="${applicationId}.categories.contentprovider" android:authorities="${applicationId}.categories.contentprovider"

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@ -14,12 +15,11 @@ import android.widget.Toast;
import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.LinkedHashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -110,6 +110,31 @@ public class Utils {
throw new RuntimeException("Unrecognized license value: " + license); throw new RuntimeException("Unrecognized license value: " + license);
} }
/**
* Generates license url with given ID
* @param license License ID
* @return Url of license
*/
@NonNull
public static String licenseUrlFor(String license) {
switch (license) {
case Prefs.Licenses.CC_BY_3:
return "https://creativecommons.org/licenses/by/3.0/";
case Prefs.Licenses.CC_BY_4:
return "https://creativecommons.org/licenses/by/4.0/";
case Prefs.Licenses.CC_BY_SA_3:
return "https://creativecommons.org/licenses/by-sa/3.0/";
case Prefs.Licenses.CC_BY_SA_4:
return "https://creativecommons.org/licenses/by-sa/4.0/";
case Prefs.Licenses.CC0:
return "https://creativecommons.org/publicdomain/zero/1.0/";
default:
throw new RuntimeException("Unrecognized license value: " + license);
}
}
/** /**
* Adds extension to filename. Converts to .jpg if system provides .jpeg, adds .jpg if no extension detected * Adds extension to filename. Converts to .jpg if system provides .jpeg, adds .jpg if no extension detected
* @param title File name * @param title File name
@ -176,6 +201,18 @@ public class Utils {
customTabsIntent.launchUrl(context, url); customTabsIntent.launchUrl(context, url);
} }
public static void handleGeoCoordinates(Context context, String coords) {
try {
Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + coords);
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
mapIntent.setPackage("com.google.android.apps.maps");
context.startActivity(mapIntent);
} catch (ActivityNotFoundException ex) {
Toast toast = Toast.makeText(context, context.getString(R.string.map_application_missing), LENGTH_SHORT);
toast.show();
}
}
/** /**
* To take screenshot of the screen and return it in Bitmap format * To take screenshot of the screen and return it in Bitmap format
* *
@ -190,4 +227,14 @@ public class Utils {
return bitmap; return bitmap;
} }
public static <K,V> Map<K,V> arraysToMap(K[] kArray, V[] vArray){
if(kArray.length!=vArray.length)
throw new RuntimeException("arraysToMap array sizes don't match");
Map<K,V> map=new LinkedHashMap<>();
for (int i=0;i<vArray.length;i++){
map.put(kArray[i], vArray[i]);
}
return map;
}
} }

View file

@ -33,7 +33,7 @@ public class WelcomeActivity extends BaseActivity {
moreInformation = this.getString(R.string.welcome_help_button_text); moreInformation = this.getString(R.string.welcome_help_button_text);
if(getIntent() != null) { if (getIntent() != null) {
Bundle bundle = getIntent().getExtras(); Bundle bundle = getIntent().getExtras();
if (bundle != null) { if (bundle != null) {
isQuiz = bundle.getBoolean("isQuiz"); isQuiz = bundle.getBoolean("isQuiz");
@ -54,7 +54,7 @@ public class WelcomeActivity extends BaseActivity {
*/ */
@Override @Override
public void onDestroy() { public void onDestroy() {
if(isQuiz){ if (isQuiz){
Intent i = new Intent(WelcomeActivity.this, QuizActivity.class); Intent i = new Intent(WelcomeActivity.this, QuizActivity.class);
startActivity(i); startActivity(i);
} }

View file

@ -60,15 +60,15 @@ public class WelcomePagerAdapter extends PagerAdapter {
this.container=container; this.container=container;
LayoutInflater inflater = LayoutInflater.from(container.getContext()); LayoutInflater inflater = LayoutInflater.from(container.getContext());
ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false); ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false);
if( BuildConfig.FLAVOR == "beta"){ if (BuildConfig.FLAVOR == "beta") {
TextView textView = (TextView) layout.findViewById(R.id.welcomeYesButton); TextView textView = layout.findViewById(R.id.welcomeYesButton);
if( textView.getVisibility() != View.VISIBLE){ if (textView.getVisibility() != View.VISIBLE) {
textView.setVisibility(View.VISIBLE); textView.setVisibility(View.VISIBLE);
} }
ViewHolder holder = new ViewHolder(layout); ViewHolder holder = new ViewHolder(layout);
layout.setTag(holder); layout.setTag(holder);
if(position == PAGE_FINAL){ if (position == PAGE_FINAL){
TextView moreInfo = layout.findViewById(R.id.welcomeInfo); TextView moreInfo = layout.findViewById(R.id.welcomeInfo);
moreInfo.setText(Html.fromHtml(WelcomeActivity.moreInformation)); moreInfo.setText(Html.fromHtml(WelcomeActivity.moreInformation));
ViewHolder holder1 = new ViewHolder(layout); ViewHolder holder1 = new ViewHolder(layout);

View file

@ -185,7 +185,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
* which then calls parseJson when results are fetched * which then calls parseJson when results are fetched
*/ */
private void setAchievements() { private void setAchievements() {
if(checkAccount()) { if (checkAccount()) {
compositeDisposable.add(mediaWikiApi compositeDisposable.add(mediaWikiApi
.getAchievements(Objects.requireNonNull(sessionManager.getCurrentAccount()).name) .getAchievements(Objects.requireNonNull(sessionManager.getCurrentAccount()).name)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@ -218,7 +218,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
* used to the count of images uploaded by user * used to the count of images uploaded by user
*/ */
private void setUploadCount(Achievements achievements) { private void setUploadCount(Achievements achievements) {
if(checkAccount()) { if (checkAccount()) {
compositeDisposable.add(mediaWikiApi compositeDisposable.add(mediaWikiApi
.getUploadCount(Objects.requireNonNull(sessionManager.getCurrentAccount()).name) .getUploadCount(Objects.requireNonNull(sessionManager.getCurrentAccount()).name)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@ -338,9 +338,9 @@ public class AchievementsActivity extends NavigationBaseActivity {
AlertDialog.Builder alertadd = new AlertDialog.Builder(AchievementsActivity.this); AlertDialog.Builder alertadd = new AlertDialog.Builder(AchievementsActivity.this);
LayoutInflater factory = LayoutInflater.from(AchievementsActivity.this); LayoutInflater factory = LayoutInflater.from(AchievementsActivity.this);
final View view = factory.inflate(R.layout.image_alert_layout, null); 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); screenShotImage.setImageBitmap(screenshot);
TextView shareMessage = (TextView) view.findViewById(R.id.alert_text); TextView shareMessage = view.findViewById(R.id.alert_text);
shareMessage.setText(R.string.achievements_share_message); shareMessage.setText(R.string.achievements_share_message);
alertadd.setView(view); alertadd.setView(view);
alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot)); alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot));
@ -387,7 +387,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
*/ */
private boolean checkAccount(){ private boolean checkAccount(){
Account currentAccount = sessionManager.getCurrentAccount(); Account currentAccount = sessionManager.getCurrentAccount();
if(currentAccount == null) { if (currentAccount == null) {
Timber.d("Current account is null"); Timber.d("Current account is null");
ViewUtil.showLongToast(this, getResources().getString(R.string.user_not_logged_in)); ViewUtil.showLongToast(this, getResources().getString(R.string.user_not_logged_in));
sessionManager.forceLogin(this); sessionManager.forceLogin(this);

View file

@ -19,7 +19,7 @@ public class BitmapUtils {
*/ */
public static BitmapDrawable writeOnDrawable(Bitmap bm, String text, Context context){ public static BitmapDrawable writeOnDrawable(Bitmap bm, String text, Context context){
Bitmap.Config config = bm.getConfig(); Bitmap.Config config = bm.getConfig();
if(config == null){ if (config == null){
config = Bitmap.Config.ARGB_8888; config = Bitmap.Config.ARGB_8888;
} }
Bitmap bitmap = Bitmap.createBitmap(bm.getWidth(),bm.getHeight(),config); Bitmap bitmap = Bitmap.createBitmap(bm.getWidth(),bm.getHeight(),config);

View file

@ -22,4 +22,4 @@ public class FeaturedImages {
public int getFeaturedPicturesOnWikimediaCommons() { public int getFeaturedPicturesOnWikimediaCommons() {
return featuredPicturesOnWikimediaCommons; return featuredPicturesOnWikimediaCommons;
} }
} }

View file

@ -61,4 +61,4 @@ public class FeedbackResponse {
public int getImagesEditedBySomeoneElse() { public int getImagesEditedBySomeoneElse() {
return imagesEditedBySomeoneElse; return imagesEditedBySomeoneElse;
} }
} }

View file

@ -16,7 +16,8 @@ import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
public abstract class AuthenticatedActivity extends NavigationBaseActivity { public abstract class AuthenticatedActivity extends NavigationBaseActivity {
@Inject SessionManager sessionManager; @Inject
protected SessionManager sessionManager;
@Inject @Inject
MediaWikiApi mediaWikiApi; MediaWikiApi mediaWikiApi;
private String authCookie; private String authCookie;

View file

@ -43,7 +43,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity; 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.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity;
@ -139,7 +139,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
.show()); .show());
if(BuildConfig.FLAVOR.equals("beta")){ if (BuildConfig.FLAVOR.equals("beta")){
loginCredentials.setText(getString(R.string.login_credential)); loginCredentials.setText(getString(R.string.login_credential));
} else { } else {
loginCredentials.setVisibility(View.GONE); loginCredentials.setVisibility(View.GONE);
@ -381,10 +381,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
super.onRestoreInstanceState(savedInstanceState); super.onRestoreInstanceState(savedInstanceState);
loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGING_IN, false); loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGING_IN, false);
errorMessageShown = savedInstanceState.getBoolean(ERROR_MESSAGE_SHOWN, false); errorMessageShown = savedInstanceState.getBoolean(ERROR_MESSAGE_SHOWN, false);
if(loginCurrentlyInProgress){ if (loginCurrentlyInProgress){
performLogin(); performLogin();
} }
if(errorMessageShown){ if (errorMessageShown){
resultantError = savedInstanceState.getString(RESULTANT_ERROR); resultantError = savedInstanceState.getString(RESULTANT_ERROR);
handleOtherResults(resultantError); handleOtherResults(resultantError);
} }
@ -399,7 +399,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
public void showMessageAndCancelDialog(@StringRes int resId) { public void showMessageAndCancelDialog(@StringRes int resId) {
showMessage(resId, R.color.secondaryDarkColor); showMessage(resId, R.color.secondaryDarkColor);
if(progressDialog != null){ if (progressDialog != null){
progressDialog.cancel(); progressDialog.cancel();
} }
} }
@ -415,7 +415,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
} }
public void startMainActivity() { public void startMainActivity() {
NavigationBaseActivity.startActivityWithFlags(this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); NavigationBaseActivity.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP);
finish(); finish();
} }

View file

@ -63,4 +63,4 @@ public class BookmarksPagerAdapter extends FragmentPagerAdapter {
BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(pages.get(0).getPage()); BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(pages.get(0).getPage());
fragment.onResume(); fragment.onResume();
} }
} }

View file

@ -105,7 +105,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
*/ */
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
private void initList() { private void initList() {
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet(); handleNoInternet();
return; return;
} }
@ -179,7 +179,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
* @param collection List of new Media to be displayed * @param collection List of new Media to be displayed
*/ */
private void handleSuccess(List<Media> collection) { private void handleSuccess(List<Media> collection) {
if(collection == null) { if (collection == null) {
initErrorView(); initErrorView();
return; return;
} }
@ -188,7 +188,7 @@ public class BookmarkPicturesFragment extends DaggerFragment {
return; return;
} }
if(gridAdapter == null) { if (gridAdapter == null) {
setAdapter(collection); setAdapter(collection);
} else { } else {
if (gridAdapter.containsAll(collection)) { if (gridAdapter.containsAll(collection)) {

View file

@ -1,24 +1,23 @@
package fr.free.nrw.commons.category; package fr.free.nrw.commons.category;
import com.pedrogomez.renderers.ListAdapteeCollection; import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder; import com.pedrogomez.renderers.RendererBuilder;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
class CategoriesAdapterFactory { public class CategoriesAdapterFactory {
private final CategoriesRenderer.CategoryClickedListener listener; private final CategoryClickedListener listener;
CategoriesAdapterFactory(CategoriesRenderer.CategoryClickedListener listener) { public CategoriesAdapterFactory(CategoryClickedListener listener) {
this.listener = listener; this.listener = listener;
} }
public RVRendererAdapter<CategoryItem> create(List<CategoryItem> placeList) { public CategoryRendererAdapter create(List<CategoryItem> placeList) {
RendererBuilder<CategoryItem> builder = new RendererBuilder<CategoryItem>() RendererBuilder<CategoryItem> builder = new RendererBuilder<CategoryItem>()
.bind(CategoryItem.class, new CategoriesRenderer(listener)); .bind(CategoryItem.class, new CategoriesRenderer(listener));
ListAdapteeCollection<CategoryItem> collection = new ListAdapteeCollection<>( ListAdapteeCollection<CategoryItem> collection = new ListAdapteeCollection<>(
placeList != null ? placeList : Collections.<CategoryItem>emptyList()); placeList != null ? placeList : Collections.<CategoryItem>emptyList());
return new RVRendererAdapter<>(builder, collection); return new CategoryRendererAdapter(builder, collection);
} }
} }

View file

@ -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<String, ArrayList<String>> categoriesCache;
private List<CategoryItem> 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<CategoryItem> sortBySimilarity(final String filter) {
Comparator<String> 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<String, ArrayList<String>> categories) {
categoriesCache.putAll(categories);
}
public HashMap<String, ArrayList<String>> getCategoriesCache() {
return categoriesCache;
}
boolean cacheContainsKey(String term) {
return categoriesCache.containsKey(term);
}
//endregion
//region Category searching
public Observable<CategoryItem> searchAll(String term, List<String> 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<CategoryItem> searchCategories(String term, List<String> 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<String> getCachedCategories(String term) {
return categoriesCache.get(term);
}
public Observable<CategoryItem> defaultCategories(List<String> titleList) {
Observable<CategoryItem> 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<CategoryItem> directCategories() {
String directCategory = directPrefs.getString("Category", "");
List<String> 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<CategoryItem> gpsCategories() {
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
.map(name -> new CategoryItem(name, false));
}
private Observable<CategoryItem> titleCategories(List<String> titleList) {
return Observable.fromIterable(titleList)
.concatMap(this::getTitleCategories);
}
private Observable<CategoryItem> getTitleCategories(String title) {
return mwApi.searchTitles(title, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
private Observable<CategoryItem> 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<CategoryItem> getSelectedCategories() {
return selectedCategories;
}
public List<String> getCategoryStringList() {
List<String> output = new ArrayList<>();
for (CategoryItem item : selectedCategories) {
output.add(item.getName());
}
return output;
}
//endregion
}

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.category; package fr.free.nrw.commons.category;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -11,7 +12,7 @@ import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
class CategoriesRenderer extends Renderer<CategoryItem> { public class CategoriesRenderer extends Renderer<CategoryItem> {
@BindView(R.id.tvName) CheckedTextView checkedView; @BindView(R.id.tvName) CheckedTextView checkedView;
private final CategoryClickedListener listener; private final CategoryClickedListener listener;
@ -44,11 +45,8 @@ class CategoriesRenderer extends Renderer<CategoryItem> {
@Override @Override
public void render() { public void render() {
CategoryItem item = getContent(); CategoryItem item = getContent();
Log.e("Commons", "Rendering: "+item);
checkedView.setChecked(item.isSelected()); checkedView.setChecked(item.isSelected());
checkedView.setText(item.getName()); checkedView.setText(item.getName());
} }
interface CategoryClickedListener {
void categoryClicked(CategoryItem item);
}
} }

View file

@ -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<CategoryItem> categoriesAdapter;
private OnCategoriesSaveHandler onCategoriesSaveHandler;
private HashMap<String, ArrayList<String>> categoriesCache;
private List<CategoryItem> 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<CategoryItem> items = new ArrayList<>();
categoriesCache = new HashMap<>();
if (savedInstanceState != null) {
items.addAll(savedInstanceState.getParcelableArrayList("currentCategories"));
//noinspection unchecked
categoriesCache.putAll((HashMap<String, ArrayList<String>>) 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<CategoryItem> 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<CategoryItem> sortBySimilarity(final String filter) {
Comparator<String> stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
return (firstItem, secondItem) -> stringSimilarityComparator
.compare(firstItem.getName(), secondItem.getName());
}
private List<String> getStringList(List<CategoryItem> input) {
List<String> output = new ArrayList<>();
for (CategoryItem item : input) {
output.add(item.getName());
}
return output;
}
private Observable<CategoryItem> defaultCategories() {
Observable<CategoryItem> 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<CategoryItem> directCategories() {
String directCategory = directPrefs.getString("Category", "");
// Strip newlines to prevent blank categories, and to tidy existing categories
directCategory = directCategory.replace("\n", "");
List<String> 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<CategoryItem> gpsCategories() {
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
.map(name -> new CategoryItem(name, false));
}
private Observable<CategoryItem> 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<CategoryItem> recentCategories() {
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
.map(s -> new CategoryItem(s, false));
}
private Observable<CategoryItem> 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<CategoryItem> 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();
}
}
}
}

View file

@ -93,4 +93,4 @@ public class Category {
this.contentUri = contentUri; this.contentUri = contentUri;
} }
} }

View file

@ -0,0 +1,5 @@
package fr.free.nrw.commons.category;
public interface CategoryClickedListener {
void categoryClicked(CategoryItem item);
}

View file

@ -26,4 +26,4 @@ public class CategoryImageController {
public List<Media> getCategoryImages(String categoryName) { public List<Media> getCategoryImages(String categoryName) {
return mediaWikiApi.getCategoryImages(categoryName); return mediaWikiApi.getCategoryImages(categoryName);
} }
} }

View file

@ -101,7 +101,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
*/ */
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
private void initList() { private void initList() {
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet(); handleNoInternet();
return; return;
} }
@ -208,7 +208,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
*/ */
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
private void fetchMoreImages() { private void fetchMoreImages() {
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet(); handleNoInternet();
return; return;
} }
@ -227,13 +227,13 @@ public class CategoryImagesListFragment extends DaggerFragment {
* @param collection List of new Media to be displayed * @param collection List of new Media to be displayed
*/ */
private void handleSuccess(List<Media> collection) { private void handleSuccess(List<Media> collection) {
if(collection == null || collection.isEmpty()) { if (collection == null || collection.isEmpty()) {
initErrorView(); initErrorView();
hasMoreImages = false; hasMoreImages = false;
return; return;
} }
if(gridAdapter == null) { if (gridAdapter == null) {
setAdapter(collection); setAdapter(collection);
} else { } else {
if (gridAdapter.containsAll(collection)) { if (gridAdapter.containsAll(collection)) {

View file

@ -3,7 +3,7 @@ package fr.free.nrw.commons.category;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
class CategoryItem implements Parcelable { public class CategoryItem implements Parcelable {
private final String name; private final String name;
private boolean selected; private boolean selected;
@ -71,4 +71,9 @@ class CategoryItem implements Parcelable {
public int hashCode() { public int hashCode() {
return name.hashCode(); return name.hashCode();
} }
@Override
public String toString() {
return "CategoryItem: '" + name + '\'';
}
} }

View file

@ -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<CategoryItem> {
CategoryRendererAdapter(RendererBuilder<CategoryItem> rendererBuilder, AdapteeCollection<CategoryItem> collection) {
super(rendererBuilder, collection);
}
protected ArrayList<CategoryItem> allItems() {
int itemCount = getItemCount();
ArrayList<CategoryItem> items = new ArrayList<>(itemCount);
for (int i = 0; i < itemCount; i++) {
items.add(getItem(i));
}
return items;
}
}

View file

@ -109,4 +109,4 @@ public class GridViewAdapter extends ArrayAdapter {
author.setVisibility(View.GONE); author.setVisibility(View.GONE);
} }
} }
} }

View file

@ -73,7 +73,7 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
categoryName = getArguments().getString("categoryName"); categoryName = getArguments().getString("categoryName");
isParentCategory = getArguments().getBoolean("isParentCategory"); isParentCategory = getArguments().getBoolean("isParentCategory");
initSubCategoryList(); initSubCategoryList();
if(getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
} }
else{ else{
@ -91,7 +91,7 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
*/ */
public void initSubCategoryList() { public void initSubCategoryList() {
categoriesNotFoundView.setVisibility(GONE); categoriesNotFoundView.setVisibility(GONE);
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet(); handleNoInternet();
return; return;
} }
@ -118,7 +118,7 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
* @param subCategoryList * @param subCategoryList
*/ */
private void handleSuccess(List<String> subCategoryList) { private void handleSuccess(List<String> subCategoryList) {
if(subCategoryList == null || subCategoryList.isEmpty()) { if (subCategoryList == null || subCategoryList.isEmpty()) {
initEmptyView(); initEmptyView();
} }
else { else {

View file

@ -121,4 +121,4 @@ public class ThreadPoolService implements Executor {
return new ThreadPoolService(this); return new ThreadPoolService(this);
} }
} }
} }

View file

@ -2,8 +2,11 @@ package fr.free.nrw.commons.contributions;
import android.net.Uri; import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.StringDef;
import java.lang.annotation.Retention;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Locale; 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.Media;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
import static java.lang.annotation.RetentionPolicy.SOURCE;
public class Contribution extends Media { public class Contribution extends Media {
public static Creator<Contribution> CREATOR = new Creator<Contribution>() { public static Creator<Contribution> CREATOR = new Creator<Contribution>() {
@ -33,6 +38,10 @@ public class Contribution extends Media {
public static final int STATE_QUEUED = 2; public static final int STATE_QUEUED = 2;
public static final int STATE_IN_PROGRESS = 3; 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_CAMERA = "camera";
public static final String SOURCE_GALLERY = "gallery"; public static final String SOURCE_GALLERY = "gallery";
public static final String SOURCE_EXTERNAL = "external"; public static final String SOURCE_EXTERNAL = "external";
@ -40,7 +49,6 @@ public class Contribution extends Media {
private Uri contentUri; private Uri contentUri;
private String source; private String source;
private String editSummary; private String editSummary;
private Date timestamp;
private int state; private int state;
private long transferred; private long transferred;
private String decimalCoords; private String decimalCoords;
@ -48,14 +56,13 @@ public class Contribution extends Media {
private String wikiDataEntityId; private String wikiDataEntityId;
private Uri contentProviderUri; 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, int state, long dataLength, Date dateUploaded, long transferred,
String source, String description, String creator, boolean isMultiple, String source, String description, String creator, boolean isMultiple,
int width, int height, String license) { 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.contentUri = contentUri;
this.state = state; this.state = state;
this.timestamp = timestamp;
this.transferred = transferred; this.transferred = transferred;
this.source = source; this.source = source;
this.isMultiple = isMultiple; this.isMultiple = isMultiple;
@ -69,14 +76,12 @@ public class Contribution extends Media {
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator); super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
this.decimalCoords = decimalCoords; this.decimalCoords = decimalCoords;
this.editSummary = editSummary; this.editSummary = editSummary;
timestamp = new Date(System.currentTimeMillis());
} }
public Contribution(Parcel in) { public Contribution(Parcel in) {
super(in); super(in);
contentUri = in.readParcelable(Uri.class.getClassLoader()); contentUri = in.readParcelable(Uri.class.getClassLoader());
source = in.readString(); source = in.readString();
timestamp = (Date) in.readSerializable();
state = in.readInt(); state = in.readInt();
transferred = in.readLong(); transferred = in.readLong();
isMultiple = in.readInt() == 1; isMultiple = in.readInt() == 1;
@ -87,12 +92,13 @@ public class Contribution extends Media {
super.writeToParcel(parcel, flags); super.writeToParcel(parcel, flags);
parcel.writeParcelable(contentUri, flags); parcel.writeParcelable(contentUri, flags);
parcel.writeString(source); parcel.writeString(source);
parcel.writeSerializable(timestamp);
parcel.writeInt(state); parcel.writeInt(state);
parcel.writeLong(transferred); parcel.writeLong(transferred);
parcel.writeInt(isMultiple ? 1 : 0); parcel.writeInt(isMultiple ? 1 : 0);
} }
public boolean getMultiple() { public boolean getMultiple() {
return isMultiple; return isMultiple;
} }
@ -121,14 +127,6 @@ public class Contribution extends Media {
this.contentUri = contentUri; this.contentUri = contentUri;
} }
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
public int getState() { public int getState() {
return state; return state;
} }
@ -141,10 +139,6 @@ public class Contribution extends Media {
this.dateUploaded = date; this.dateUploaded = date;
} }
public String getTrackingTemplates() {
return "{{subst:unc}}"; // Remove when we have categorization
}
public String getPageContents() { public String getPageContents() {
StringBuilder buffer = new StringBuilder(); StringBuilder buffer = new StringBuilder();
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
@ -169,8 +163,15 @@ public class Contribution extends Media {
buffer.append("== {{int:license-header}} ==\n") buffer.append("== {{int:license-header}} ==\n")
.append(licenseTemplateFor(getLicense())).append("\n\n") .append(licenseTemplateFor(getLicense())).append("\n\n")
.append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n") .append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n");
.append(getTrackingTemplates()); 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(); return buffer.toString();
} }
@ -184,7 +185,7 @@ public class Contribution extends Media {
} }
public Contribution() { public Contribution() {
timestamp = new Date(System.currentTimeMillis());
} }
public String getSource() { 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 * When the corresponding wikidata entity is known as in case of nearby uploads, it can be set
* using the setter method * using the setter method
* @param wikiDataEntityId * @param wikiDataEntityId wikiDataEntityId
*/ */
public void setWikiDataEntityId(String wikiDataEntityId) { public void setWikiDataEntityId(String wikiDataEntityId) {
this.wikiDataEntityId = wikiDataEntityId; this.wikiDataEntityId = wikiDataEntityId;

View file

@ -5,22 +5,26 @@ import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentActivity;
import android.support.v4.content.FileProvider; import android.support.v4.content.FileProvider;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import fr.free.nrw.commons.upload.ShareActivity; import fr.free.nrw.commons.upload.UploadActivity;
import timber.log.Timber; import timber.log.Timber;
import static android.content.Intent.ACTION_GET_CONTENT; import static android.content.Intent.ACTION_GET_CONTENT;
import static android.content.Intent.ACTION_SEND; import static android.content.Intent.ACTION_SEND;
import static android.content.Intent.ACTION_SEND_MULTIPLE;
import static android.content.Intent.EXTRA_STREAM; 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_CAMERA;
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY; 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_GALLERY = 1;
public static final int SELECT_FROM_CAMERA = 2; public static final int SELECT_FROM_CAMERA = 2;
public static final int PICK_IMAGE_MULTIPLE = 3;
private Fragment fragment; private Fragment fragment;
@ -79,6 +84,14 @@ public class ContributionController {
} }
public void startGalleryPick() { 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) //FIXME: Starts gallery (opens Google Photos)
Intent pickImageIntent = new Intent(ACTION_GET_CONTENT); Intent pickImageIntent = new Intent(ACTION_GET_CONTENT);
pickImageIntent.setType("image/*"); pickImageIntent.setType("image/*");
@ -87,15 +100,41 @@ public class ContributionController {
Timber.d("Fragment is not added, startActivityForResult cannot be called"); Timber.d("Fragment is not added, startActivityForResult cannot be called");
return; return;
} }
Timber.d("startGalleryPick() called with pickImageIntent"); Timber.d("startSingleGalleryPick() called with pickImageIntent");
fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY); 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> 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) { public void handleImagePicked(int requestCode, @Nullable Uri uri, boolean isDirectUpload, String wikiDataEntityId) {
FragmentActivity activity = fragment.getActivity(); FragmentActivity activity = fragment.getActivity();
Timber.d("handleImagePicked() called with onActivityResult(). Boolean isDirectUpload: " + isDirectUpload + "String wikiDataEntityId: " + wikiDataEntityId); 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); shareIntent.setAction(ACTION_SEND);
switch (requestCode) { switch (requestCode) {
case SELECT_FROM_GALLERY: case SELECT_FROM_GALLERY:

View file

@ -98,7 +98,8 @@ public class ContributionDao {
cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime()); cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime());
} }
cv.put(Table.COLUMN_LENGTH, contribution.getDataLength()); 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_STATE, contribution.getState());
cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred()); cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred());
cv.put(Table.COLUMN_SOURCE, contribution.getSource()); cv.put(Table.COLUMN_SOURCE, contribution.getSource());

View file

@ -15,10 +15,10 @@ class ContributionViewHolder {
final ProgressBar progressView; final ProgressBar progressView;
ContributionViewHolder(View parent) { ContributionViewHolder(View parent) {
imageView = (MediaWikiImageView) parent.findViewById(R.id.contributionImage); imageView = parent.findViewById(R.id.contributionImage);
titleView = (TextView)parent.findViewById(R.id.contributionTitle); titleView = parent.findViewById(R.id.contributionTitle);
stateView = (TextView)parent.findViewById(R.id.contributionState); stateView = parent.findViewById(R.id.contributionState);
seqNumView = (TextView)parent.findViewById(R.id.contributionSequenceNumber); seqNumView = parent.findViewById(R.id.contributionSequenceNumber);
progressView = (ProgressBar)parent.findViewById(R.id.contributionProgress); progressView = parent.findViewById(R.id.contributionProgress);
} }
} }

View file

@ -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<Cursor>,
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<DataSetObserver> 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<Cursor> 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<Cursor> 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<Cursor> 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);
}
}

View file

@ -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<Cursor>,
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<DataSetObserver> 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<Cursor> 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<Cursor> 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<Cursor> 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();
}
}

View file

@ -1,25 +1,31 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; 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.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.GridView; import android.widget.GridView;
import android.widget.ListAdapter; import android.widget.ListAdapter;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import javax.inject.Inject; import javax.inject.Inject;
@ -30,7 +36,7 @@ import butterknife.ButterKnife;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; 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 timber.log.Timber;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE; 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.app.Activity.RESULT_OK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.view.View.GONE; 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 { public class ContributionsListFragment extends CommonsDaggerSupportFragment {
@ -47,100 +58,121 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
TextView waitingMessage; TextView waitingMessage;
@BindView(R.id.loadingContributionsProgressBar) @BindView(R.id.loadingContributionsProgressBar)
ProgressBar progressBar; 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) @BindView(R.id.noDataYet)
TextView noDataYet; TextView noDataYet;
@Inject
@Named("prefs")
SharedPreferences prefs;
@Inject @Inject
@Named("default_preferences") @Named("default_preferences")
SharedPreferences defaultPrefs; SharedPreferences defaultPrefs;
private ContributionController controller; private Animation fab_close;
private Animation fab_open;
private Animation rotate_forward;
private Animation rotate_backward;
@Override private boolean isFabOpen = false;
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public ContributionController controller;
View v = inflater.inflate(R.layout.fragment_contributions, container, false);
ButterKnife.bind(this, v);
contributionsList.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity()); public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if (savedInstanceState != null) { View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
Timber.d("Scrolling to %d", savedInstanceState.getInt("grid-position")); ButterKnife.bind(this, view);
contributionsList.setSelection(savedInstanceState.getInt("grid-position"));
}
//TODO: Should this be in onResume? contributionsList.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment());
String lastModified = prefs.getString("lastSyncTimestamp", "");
Timber.d("Last Sync Timestamp: %s", lastModified);
if (lastModified.equals("")) {
waitingMessage.setVisibility(View.VISIBLE);
} else {
waitingMessage.setVisibility(GONE);
}
changeEmptyScreen(true); changeEmptyScreen(true);
changeProgressBarVisibility(true); changeProgressBarVisibility(true);
return v; return view;
} }
public ListAdapter getAdapter() { @Override
return contributionsList.getAdapter(); public void onActivityCreated(@Nullable Bundle savedInstanceState) {
} super.onActivityCreated(savedInstanceState);
if (controller == null) {
public void setAdapter(ListAdapter adapter) { controller = new ContributionController(this);
this.contributionsList.setAdapter(adapter);
if(BuildConfig.FLAVOR.equalsIgnoreCase("beta")){
((ContributionsActivity) getActivity()).betaSetUploadCount(adapter.getCount());
} }
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){ public void changeEmptyScreen(boolean isEmpty){
this.noDataYet.setVisibility(isEmpty ? View.VISIBLE : View.GONE); this.noDataYet.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
} }
public void changeProgressBarVisibility(boolean isVisible) { private void initializeAnimations() {
this.progressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE); 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 private void setListeners() {
public void onSaveInstanceState(Bundle outState) {
if (outState == null) {
outState = new Bundle();
}
super.onSaveInstanceState(outState);
controller.saveState(outState);
outState.putInt("grid-position", contributionsList.getFirstVisiblePosition());
}
@Override fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
public void onActivityResult(int requestCode, int resultCode, Intent data) { fabCamera.setOnClickListener(new View.OnClickListener() {
//FIXME: must get the file data for Google Photos when receive the intent answer, in the onActivityResult method @Override
super.onActivityResult(requestCode, resultCode, data); public void onClick(View view) {
boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true);
if (resultCode == RESULT_OK) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", // Here, thisActivity is the current activity
requestCode, resultCode, data); if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE)
if (requestCode == ContributionController.SELECT_FROM_CAMERA) { != PackageManager.PERMISSION_GRANTED) {
// If coming from camera, pass null as uri. Because camera photos get saved to a if (shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) {
// fixed directory // Show an explanation to the user *asynchronously* -- don't block
controller.handleImagePicked(requestCode, null, false, null); // this thread waiting for the user's response! After the user
} else { // sees the explanation, try again to request the permission.
controller.handleImagePicked(requestCode, data.getData(), false, null); 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 fabGalery.setOnClickListener(new View.OnClickListener() {
public boolean onOptionsItemSelected(MenuItem item) { @Override
switch (item.getItemId()) { public void onClick(View view) {
case R.id.menu_from_gallery:
//Gallery crashes before reach ShareActivity screen so must implement permissions check here //Gallery crashes before reach ShareActivity screen so must implement permissions check here
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 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 // this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission. // 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)) .setMessage(getString(R.string.read_storage_permission_rationale))
.setPositiveButton(android.R.string.ok, (dialog, which) -> { .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(); dialog.dismiss();
}) })
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
@ -179,59 +212,64 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
} }
} else { } else {
controller.startGalleryPick(); controller.startGalleryPick();
return true;
} }
} else { } else {
controller.startGalleryPick(); controller.startGalleryPick();
return true;
} }
}
});
}
return true; private void animateFAB(boolean isFabOpen) {
case R.id.menu_from_camera: this.isFabOpen = !isFabOpen;
boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true); if (fabPlus.isShown()){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) { if (isFabOpen) {
// Here, thisActivity is the current activity fabPlus.startAnimation(rotate_backward);
if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE) fabCamera.startAnimation(fab_close);
!= PackageManager.PERMISSION_GRANTED) { fabGalery.startAnimation(fab_close);
if (shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { fabCamera.hide();
// Show an explanation to the user *asynchronously* -- don't block fabGalery.hide();
// this thread waiting for the user's response! After the user } else {
// sees the explanation, try again to request the permission. fabPlus.startAnimation(rotate_forward);
new AlertDialog.Builder(getActivity()) fabCamera.startAnimation(fab_open);
.setMessage(getString(R.string.write_storage_permission_rationale)) fabGalery.startAnimation(fab_open);
.setPositiveButton(android.R.string.ok, (dialog, which) -> { fabCamera.show();
requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 3); fabGalery.show();
dialog.dismiss(); }
}) this.isFabOpen=!isFabOpen;
.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);
} }
} }
@Override @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, public void onAttach(Context context) {
@NonNull int[] grantResults) { 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 = " Timber.d("onRequestPermissionsResult: req code = " + " perm = "
+ Arrays.toString(permissions) + " grant =" + Arrays.toString(grantResults)); + Arrays.toString(permissions) + " grant =" + Arrays.toString(grantResults));
@ -246,11 +284,12 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
break; break;
// 2 = Location allowed when 'nearby places' selected // 2 = Location allowed when 'nearby places' selected
case 2: { 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"); Timber.d("Location permission granted");
Intent nearbyIntent = new Intent(getActivity(), NearbyActivity.class); Intent nearbyIntent = new Intent(getActivity(), MainActivity.class);
startActivity(nearbyIntent); startActivity(nearbyIntent);
} }*/
} }
break; break;
case 3: { case 3: {
@ -262,42 +301,60 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
} }
} }
@Override private void handleMultipleImages(int requestCode, Intent data) {
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { if (getContext() == null) {
menu.clear(); // See http://stackoverflow.com/a/8495697/17865 return;
inflater.inflate(R.menu.fragment_contributions_list, menu); }
if (!deviceHasCamera()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
menu.findItem(R.id.menu_from_camera).setEnabled(false); && data.getClipData() != null) {
ClipData mClipData = data.getClipData();
ArrayList<Uri> mArrayUri = new ArrayList<Uri>();
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) || * Responsible to set progress bar invisible and visible
pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); * @param isVisible True when contributions list should be hidden.
} */
public void changeProgressBarVisibility(boolean isVisible) {
@Override this.progressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE);
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);
} }
/**
* Clears sync message displayed with progress bar before contributions list became visible
*/
protected void clearSyncMessage() { protected void clearSyncMessage() {
waitingMessage.setVisibility(GONE); 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 { public interface SourceRefresher {

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -182,4 +182,4 @@ public class DeleteTask extends AsyncTask<Void, Integer, Boolean> {
.setPriority(PRIORITY_HIGH); .setPriority(PRIORITY_HIGH);
notificationManager.notify(NOTIFICATION_DELETE, notificationBuilder.build()); notificationManager.notify(NOTIFICATION_DELETE, notificationBuilder.build());
} }
} }

View file

@ -9,14 +9,13 @@ import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity; import fr.free.nrw.commons.auth.SignupActivity;
import fr.free.nrw.commons.bookmarks.BookmarksActivity; import fr.free.nrw.commons.bookmarks.BookmarksActivity;
import fr.free.nrw.commons.category.CategoryDetailsActivity; 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.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.explore.SearchActivity; 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.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity; import fr.free.nrw.commons.settings.SettingsActivity;
import fr.free.nrw.commons.upload.MultipleShareActivity; import fr.free.nrw.commons.upload.UploadActivity;
import fr.free.nrw.commons.upload.ShareActivity;
@Module @Module
@SuppressWarnings({"WeakerAccess", "unused"}) @SuppressWarnings({"WeakerAccess", "unused"})
@ -29,13 +28,7 @@ public abstract class ActivityBuilderModule {
abstract WelcomeActivity bindWelcomeActivity(); abstract WelcomeActivity bindWelcomeActivity();
@ContributesAndroidInjector @ContributesAndroidInjector
abstract ShareActivity bindShareActivity(); abstract MainActivity bindContributionsActivity();
@ContributesAndroidInjector
abstract MultipleShareActivity bindMultipleShareActivity();
@ContributesAndroidInjector
abstract ContributionsActivity bindContributionsActivity();
@ContributesAndroidInjector @ContributesAndroidInjector
abstract SettingsActivity bindSettingsActivity(); abstract SettingsActivity bindSettingsActivity();
@ -46,15 +39,15 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract SignupActivity bindSignupActivity(); abstract SignupActivity bindSignupActivity();
@ContributesAndroidInjector
abstract NearbyActivity bindNearbyActivity();
@ContributesAndroidInjector @ContributesAndroidInjector
abstract NotificationActivity bindNotificationActivity(); abstract NotificationActivity bindNotificationActivity();
@ContributesAndroidInjector @ContributesAndroidInjector
abstract CategoryImagesActivity bindFeaturedImagesActivity(); abstract CategoryImagesActivity bindFeaturedImagesActivity();
@ContributesAndroidInjector
abstract UploadActivity bindUploadActivity();
@ContributesAndroidInjector @ContributesAndroidInjector
abstract SearchActivity bindSearchActivity(); abstract SearchActivity bindSearchActivity();

View file

@ -90,4 +90,4 @@ public class ApplicationlessInjection
return instance; return instance;
} }
} }

View file

@ -1,10 +1,17 @@
package fr.free.nrw.commons.di; package fr.free.nrw.commons.di;
import android.app.Activity;
import android.content.ContentProviderClient; import android.content.ContentProviderClient;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v4.util.LruCache; 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.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
@ -12,12 +19,14 @@ import javax.inject.Singleton;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import fr.free.nrw.commons.BuildConfig; 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.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces; 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.upload.UploadController;
import fr.free.nrw.commons.wikidata.WikidataEditListener; import fr.free.nrw.commons.wikidata.WikidataEditListener;
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl; import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
@ -38,6 +47,35 @@ public class CommonsApplicationModule {
return this.applicationContext; return this.applicationContext;
} }
@Provides
public InputMethodManager provideInputMethodManager() {
return (InputMethodManager) applicationContext.getSystemService(Activity.INPUT_METHOD_SERVICE);
}
@Provides
@Named("licenses")
public List<String> provideLicenses(Context context) {
List<String> 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<String, String> provideLicensesByName(Context context) {
Map<String, String> 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 @Provides
public AccountUtil providesAccountUtil(Context context) { public AccountUtil providesAccountUtil(Context context) {
return new AccountUtil(context); return new AccountUtil(context);
@ -120,6 +158,17 @@ public class CommonsApplicationModule {
return context.getSharedPreferences("direct_nearby_upload_prefs", MODE_PRIVATE); 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 @Provides
public UploadController providesUploadController(SessionManager sessionManager, @Named("default_preferences") SharedPreferences sharedPreferences, Context context) { public UploadController providesUploadController(SessionManager sessionManager, @Named("default_preferences") SharedPreferences sharedPreferences, Context context) {
return new UploadController(sessionManager, context, sharedPreferences); return new UploadController(sessionManager, context, sharedPreferences);
@ -173,4 +222,4 @@ public class CommonsApplicationModule {
public boolean provideIsBetaVariant() { public boolean provideIsBetaVariant() {
return BuildConfig.FLAVOR.equals("beta"); return BuildConfig.FLAVOR.equals("beta");
} }
} }

View file

@ -40,4 +40,4 @@ public abstract class CommonsDaggerAppCompatActivity extends AppCompatActivity i
activityInjector.inject(this); activityInjector.inject(this);
} }
} }

View file

@ -28,4 +28,4 @@ public abstract class CommonsDaggerBroadcastReceiver extends BroadcastReceiver {
serviceInjector.inject(this); serviceInjector.inject(this);
} }
} }

View file

@ -29,4 +29,4 @@ public abstract class CommonsDaggerContentProvider extends ContentProvider {
serviceInjector.inject(this); serviceInjector.inject(this);
} }
} }

View file

@ -29,4 +29,4 @@ public abstract class CommonsDaggerIntentService extends IntentService {
serviceInjector.inject(this); serviceInjector.inject(this);
} }
} }

View file

@ -28,4 +28,4 @@ public abstract class CommonsDaggerService extends Service {
serviceInjector.inject(this); serviceInjector.inject(this);
} }
} }

View file

@ -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())); throw new IllegalArgumentException(String.format("No injector was found for %s", getClass().getCanonicalName()));
} }
} }

View file

@ -4,29 +4,25 @@ import dagger.Module;
import dagger.android.ContributesAndroidInjector; import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; 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.CategoryImagesListFragment;
import fr.free.nrw.commons.category.SubCategoryListFragment; 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.contributions.ContributionsListFragment;
import fr.free.nrw.commons.explore.categories.SearchCategoryFragment; import fr.free.nrw.commons.explore.categories.SearchCategoryFragment;
import fr.free.nrw.commons.explore.images.SearchImageFragment; import fr.free.nrw.commons.explore.images.SearchImageFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment; import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailFragment; import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment; 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.NearbyListFragment;
import fr.free.nrw.commons.nearby.NearbyMapFragment; import fr.free.nrw.commons.nearby.NearbyMapFragment;
import fr.free.nrw.commons.nearby.NoPermissionsFragment; import fr.free.nrw.commons.nearby.NoPermissionsFragment;
import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.upload.MultipleUploadListFragment;
import fr.free.nrw.commons.upload.SingleUploadFragment;
@Module @Module
@SuppressWarnings({"WeakerAccess", "unused"}) @SuppressWarnings({"WeakerAccess", "unused"})
public abstract class FragmentBuilderModule { public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract CategorizationFragment bindCategorizationFragment();
@ContributesAndroidInjector @ContributesAndroidInjector
abstract ContributionsListFragment bindContributionsListFragment(); abstract ContributionsListFragment bindContributionsListFragment();
@ -48,12 +44,6 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract SettingsFragment bindSettingsFragment(); abstract SettingsFragment bindSettingsFragment();
@ContributesAndroidInjector
abstract MultipleUploadListFragment bindMultipleUploadListFragment();
@ContributesAndroidInjector
abstract SingleUploadFragment bindSingleUploadFragment();
@ContributesAndroidInjector @ContributesAndroidInjector
abstract CategoryImagesListFragment bindFeaturedImagesListFragment(); abstract CategoryImagesListFragment bindFeaturedImagesListFragment();
@ -69,6 +59,12 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract RecentSearchesFragment bindRecentSearchesFragment(); abstract RecentSearchesFragment bindRecentSearchesFragment();
@ContributesAndroidInjector
abstract ContributionsFragment bindContributionsFragment();
@ContributesAndroidInjector
abstract NearbyFragment bindNearbyFragment();
@ContributesAndroidInjector @ContributesAndroidInjector
abstract BookmarkPicturesFragment bindBookmarkPictureListFragment(); abstract BookmarkPicturesFragment bindBookmarkPictureListFragment();

View file

@ -95,7 +95,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false); View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
ButterKnife.bind(this, rootView); 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())); categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
} }
else{ else{
@ -124,7 +124,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
public void updateCategoryList(String query) { public void updateCategoryList(String query) {
this.query = query; this.query = query;
categoriesNotFoundView.setVisibility(GONE); categoriesNotFoundView.setVisibility(GONE);
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet(); handleNoInternet();
return; return;
} }
@ -173,7 +173,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
*/ */
private void handleSuccess(List<String> mediaList) { private void handleSuccess(List<String> mediaList) {
queryList = mediaList; queryList = mediaList;
if(mediaList == null || mediaList.isEmpty()) { if (mediaList == null || mediaList.isEmpty()) {
initErrorView(); initErrorView();
} }
else { else {

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.explore.images; package fr.free.nrw.commons.explore.images;
import android.annotation.SuppressLint;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
@ -97,7 +98,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false); View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
ButterKnife.bind(this, rootView); 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())); imagesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
} }
else{ 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 * 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 * Clearing imageAdapter every time new keyword is searched so that user can see only new results
*/ */
@SuppressLint("CheckResult")
public void updateImageList(String query) { public void updateImageList(String query) {
this.query = query; this.query = query;
if(imagesNotFoundView != null) { if (imagesNotFoundView != null) {
imagesNotFoundView.setVisibility(GONE); imagesNotFoundView.setVisibility(GONE);
} }
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet(); handleNoInternet();
return; return;
} }
@ -146,6 +148,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
/** /**
* Adds more results to existing search results * Adds more results to existing search results
*/ */
@SuppressLint("CheckResult")
public void addImagesToList(String query) { public void addImagesToList(String query) {
this.query = query; this.query = query;
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
@ -163,13 +166,11 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
*/ */
private void handlePaginationSuccess(List<Media> mediaList) { private void handlePaginationSuccess(List<Media> mediaList) {
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
if (mediaList.size()!=0){ if (mediaList.size() != 0 || !queryList.get(queryList.size() - 1).getFilename().equals(mediaList.get(mediaList.size() - 1).getFilename())) {
if (!queryList.get(queryList.size()-1).getFilename().equals(mediaList.get(mediaList.size()-1).getFilename())) { queryList.addAll(mediaList);
queryList.addAll(mediaList); imagesAdapter.addAll(mediaList);
imagesAdapter.addAll(mediaList); imagesAdapter.notifyDataSetChanged();
imagesAdapter.notifyDataSetChanged(); ((SearchActivity) getContext()).viewPagerNotifyDataSetChanged();
((SearchActivity)getContext()).viewPagerNotifyDataSetChanged();
}
} }
} }
@ -182,7 +183,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
*/ */
private void handleSuccess(List<Media> mediaList) { private void handleSuccess(List<Media> mediaList) {
queryList = mediaList; queryList = mediaList;
if(mediaList == null || mediaList.isEmpty()) { if (mediaList == null || mediaList.isEmpty()) {
initErrorView(); initErrorView();
} }
else { else {

View file

@ -68,4 +68,4 @@ public class RecentSearch {
this.contentUri = contentUri; this.contentUri = contentUri;
} }
} }

View file

@ -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_TIME_IN_MILLIS = 2 * 60 * 100;
private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 10; private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 10;
private Context context; public Context context;
private LocationManager locationManager; private LocationManager locationManager;
private Location lastLocation; private Location lastLocation;
//private Location lastLocationDuplicate; // Will be used for nearby card view on contributions activity
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>(); private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
private boolean isLocationManagerRegistered = false; private boolean isLocationManagerRegistered = false;
private Set<Activity> locationExplanationDisplayed = new HashSet<>(); public Set<Activity> locationExplanationDisplayed = new HashSet<>();
/** /**
* Constructs a new instance of LocationServiceManager. * Constructs a new instance of LocationServiceManager.
@ -253,15 +254,25 @@ public class LocationServiceManager implements LocationListener {
@Override @Override
public void onLocationChanged(Location location) { public void onLocationChanged(Location location) {
Timber.d("on location changed");
if (isBetterLocation(location, lastLocation) if (isBetterLocation(location, lastLocation)
.equals(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) { .equals(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) {
lastLocation = location; lastLocation = location;
//lastLocationDuplicate = location;
for (LocationUpdateListener listener : locationListeners) { for (LocationUpdateListener listener : locationListeners) {
listener.onLocationChangedSignificantly(LatLng.from(lastLocation)); 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)) { .equals(LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) {
lastLocation = location; lastLocation = location;
//lastLocationDuplicate = location;
for (LocationUpdateListener listener : locationListeners) { for (LocationUpdateListener listener : locationListeners) {
listener.onLocationChangedSlightly(LatLng.from(lastLocation)); listener.onLocationChangedSlightly(LatLng.from(lastLocation));
} }
@ -286,6 +297,7 @@ public class LocationServiceManager implements LocationListener {
public enum LocationChangeType{ public enum LocationChangeType{
LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers
LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving 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, LOCATION_NOT_CHANGED,
PERMISSION_JUST_GRANTED, PERMISSION_JUST_GRANTED,
MAP_UPDATED MAP_UPDATED

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.location; package fr.free.nrw.commons.location;
public interface LocationUpdateListener { public interface LocationUpdateListener {
void onLocationChangedSignificantly(LatLng latLng); void onLocationChangedSignificantly(LatLng latLng); // Will be used to update all nearby markers on the map
void onLocationChangedSlightly(LatLng latLng); void onLocationChangedSlightly(LatLng latLng); // Will be used to track users motion
void onLocationChangedMedium(LatLng latLng); // Will be used updating nearby card view notification
} }

View file

@ -42,4 +42,4 @@ public class CommonsLogSender extends LogsSender {
return builder.toString(); return builder.toString();
} }
} }

View file

@ -141,4 +141,4 @@ public class FileLoggingTree extends Timber.DebugTree implements LogLevelSettabl
LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
logger.addAppender(rollingFileAppender); logger.addAppender(rollingFileAppender);
} }
} }

View file

@ -5,4 +5,4 @@ package fr.free.nrw.commons.logging;
*/ */
public interface LogLevelSettableTree { public interface LogLevelSettableTree {
void setLogLevel(int logLevel); void setLogLevel(int logLevel);
} }

View file

@ -16,10 +16,10 @@ public final class LogUtils {
* @return * @return
*/ */
public static String getLogDirectory(boolean isBeta) { public static String getLogDirectory(boolean isBeta) {
if(isBeta) { if (isBeta) {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/beta"; return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/beta";
} else { } else {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/prod"; return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/prod";
} }
} }
} }

View file

@ -180,4 +180,4 @@ public abstract class LogsSender implements ReportSender {
zos.flush(); zos.flush();
zos.close(); zos.close();
} }
} }

View file

@ -49,6 +49,7 @@ import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.category.CategoryDetailsActivity; 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.DeleteTask;
import fr.free.nrw.commons.delete.ReasonBuilder; import fr.free.nrw.commons.delete.ReasonBuilder;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
@ -156,7 +157,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity(); detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) (getParentFragment().getParentFragment());
if (savedInstanceState != null) { if (savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable"); editable = savedInstanceState.getBoolean("editable");
@ -222,12 +223,14 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}; };
view.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener); view.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener);
locale = getResources().getConfiguration().locale; locale = getResources().getConfiguration().locale;
return view; return view;
} }
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
((ContributionsFragment)(getParentFragment().getParentFragment())).nearbyNoificationCardView.setVisibility(View.GONE);
media = detailProvider.getMediaAtPosition(index); media = detailProvider.getMediaAtPosition(index);
if (media == null) { if (media == null) {
// Ask the detail provider to ping us when we're ready // Ask the detail provider to ping us when we're ready
@ -271,7 +274,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@Override @Override
protected Boolean doInBackground(Void... voids) { protected Boolean doInBackground(Void... voids) {
// Local files have no filename yet // Local files have no filename yet
if(media.getFilename() == null) { if (media.getFilename() == null) {
return Boolean.FALSE; return Boolean.FALSE;
} }
try { try {
@ -343,7 +346,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
} }
rebuildCatList(); rebuildCatList();
if(media.getCreator() == null || media.getCreator().equals("")) { if (media.getCreator() == null || media.getCreator().equals("")) {
authorLayout.setVisibility(GONE); authorLayout.setVisibility(GONE);
} else { } else {
author.setText(media.getCreator()); author.setText(media.getCreator());
@ -357,7 +360,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if (!TextUtils.isEmpty(licenseLink(media))) { if (!TextUtils.isEmpty(licenseLink(media))) {
openWebBrowser(licenseLink(media)); openWebBrowser(licenseLink(media));
} else { } else {
if(isCategoryImage) { if (isCategoryImage) {
Timber.d("Unable to fetch license URL for %s", media.getLicense()); Timber.d("Unable to fetch license URL for %s", media.getLicense());
} else { } else {
Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT); 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) @OnClick(R.id.seeMore)
public void onSeeMoreClicked(){ public void onSeeMoreClicked(){
if(nominatedForDeletion.getVisibility()== VISIBLE) { if (nominatedForDeletion.getVisibility()== VISIBLE) {
openWebBrowser(media.getFilePageTitle().getMobileUri().toString()); openWebBrowser(media.getFilePageTitle().getMobileUri().toString());
} }
} }
private void enableDeleteButton(boolean visibility) { private void enableDeleteButton(boolean visibility) {
delete.setEnabled(visibility); delete.setEnabled(visibility);
if(visibility) { if (visibility) {
delete.setTextColor(getResources().getColor(R.color.primaryTextColor)); delete.setTextColor(getResources().getColor(R.color.primaryTextColor));
} else { } else {
delete.setTextColor(getResources().getColor(R.color.deleteButtonLight)); delete.setTextColor(getResources().getColor(R.color.deleteButtonLight));
@ -444,15 +447,26 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private void rebuildCatList() { private void rebuildCatList() {
categoryContainer.removeAllViews(); categoryContainer.removeAllViews();
// @fixme add the category items // @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); categoryContainer.addView(catLabel);
} }
} }
private View buildCatLabel(final String catName, ViewGroup categoryContainer) { private View buildCatLabel(final String catName, ViewGroup categoryContainer) {
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false); 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); textView.setText(catName);
if (categoriesLoaded && categoriesPresent) { if (categoriesLoaded && categoriesPresent) {

View file

@ -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.CategoryDetailsActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.Contribution; 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.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.SearchActivity; import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.MediaWikiApi;
@ -99,7 +98,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
pager.setAdapter(adapter); pager.setAdapter(adapter);
pager.setCurrentItem(pageNumber, false); pager.setCurrentItem(pageNumber, false);
if(getActivity() == null) { if (getActivity() == null) {
Timber.d("Returning as activity is destroyed!"); Timber.d("Returning as activity is destroyed!");
return; return;
} }
@ -133,11 +132,11 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
if(getActivity() == null) { if (getActivity() == null) {
Timber.d("Returning as activity is destroyed!"); Timber.d("Returning as activity is destroyed!");
return true; return true;
} }
MediaDetailProvider provider = (MediaDetailProvider) getActivity(); MediaDetailProvider provider = (MediaDetailProvider) getParentFragment();
Media m = provider.getMediaAtPosition(pager.getCurrentItem()); Media m = provider.getMediaAtPosition(pager.getCurrentItem());
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.menu_bookmark_current_image: case R.id.menu_bookmark_current_image:
@ -156,7 +155,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
viewIntent.setAction(ACTION_VIEW); viewIntent.setAction(ACTION_VIEW);
viewIntent.setData(m.getFilePageTitle().getMobileUri()); viewIntent.setData(m.getFilePageTitle().getMobileUri());
//check if web browser available //check if web browser available
if(viewIntent.resolveActivity(getActivity().getPackageManager()) != null){ if (viewIntent.resolveActivity(getActivity().getPackageManager()) != null){
startActivity(viewIntent); startActivity(viewIntent);
} else { } else {
Toast toast = Toast.makeText(getContext(), getString(R.string.no_web_browser), LENGTH_SHORT); 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; return true;
case R.id.menu_retry_current_image: case R.id.menu_retry_current_image:
// Retry // Retry
((ContributionsActivity) getActivity()).retryUpload(pager.getCurrentItem()); //((MainActivity) getActivity()).retryUpload(pager.getCurrentItem());
getActivity().getSupportFragmentManager().popBackStack(); getActivity().getSupportFragmentManager().popBackStack();
return true; return true;
case R.id.menu_cancel_current_image: case R.id.menu_cancel_current_image:
// todo: delete image // todo: delete image
((ContributionsActivity) getActivity()).deleteUpload(pager.getCurrentItem()); //((MainActivity) getActivity()).deleteUpload(pager.getCurrentItem());
getActivity().getSupportFragmentManager().popBackStack(); getActivity().getSupportFragmentManager().popBackStack();
return true; return true;
default: default:
@ -193,7 +192,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
* @param media * @param media
*/ */
private void setWallpaper(Media 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"); Timber.d("Media URL not present");
return; return;
} }
@ -254,7 +253,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
menu.clear(); // see http://stackoverflow.com/a/8495697/17865 menu.clear(); // see http://stackoverflow.com/a/8495697/17865
inflater.inflate(R.menu.fragment_image_detail, menu); inflater.inflate(R.menu.fragment_image_detail, menu);
if (pager != null) { if (pager != null) {
MediaDetailProvider provider = (MediaDetailProvider) getActivity(); MediaDetailProvider provider = (MediaDetailProvider) getParentFragment();
if(provider == null) { if(provider == null) {
return; return;
} }
@ -326,7 +325,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
@Override @Override
public void onPageScrolled(int i, float v, int i2) { public void onPageScrolled(int i, float v, int i2) {
if(getActivity() == null) { if(getParentFragment().getActivity() == null) {
Timber.d("Returning as activity is destroyed!"); Timber.d("Returning as activity is destroyed!");
return; return;
} }
@ -347,7 +346,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
e.printStackTrace(); e.printStackTrace();
} }
} }
getActivity().supportInvalidateOptionsMenu(); getParentFragment().getActivity().supportInvalidateOptionsMenu();
} }
@Override @Override
@ -381,22 +380,22 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
public Fragment getItem(int i) { public Fragment getItem(int i) {
if (i == 0) { if (i == 0) {
// See bug https://code.google.com/p/android/issues/detail?id=27526 // 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!"); Timber.d("Skipping getItem. Returning as activity is destroyed!");
return null; return null;
} }
pager.postDelayed(() -> getActivity().supportInvalidateOptionsMenu(), 5); pager.postDelayed(() -> getParentFragment().getActivity().supportInvalidateOptionsMenu(), 5);
} }
return MediaDetailFragment.forMedia(i, editable, isFeaturedImage); return MediaDetailFragment.forMedia(i, editable, isFeaturedImage);
} }
@Override @Override
public int getCount() { public int getCount() {
if(getActivity() == null) { if (getActivity() == null) {
Timber.d("Skipping getCount. Returning as activity is destroyed!"); Timber.d("Skipping getCount. Returning as activity is destroyed!");
return 0; return 0;
} }
return ((MediaDetailProvider) getActivity()).getTotalMediaCount(); return ((MediaDetailProvider) getParentFragment()).getTotalMediaCount();
} }
} }
} }

View file

@ -244,9 +244,9 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
Timber.d("Central auth token isn't valid. Trying to fetch a fresh token"); Timber.d("Central auth token isn't valid. Trying to fetch a fresh token");
api.removeAllCookies(); api.removeAllCookies();
String loginResultCode = login(AccountUtil.getUserName(context), AccountUtil.getPassword(context)); String loginResultCode = login(AccountUtil.getUserName(context), AccountUtil.getPassword(context));
if(loginResultCode.equals("PASS")) { if (loginResultCode.equals("PASS")) {
return getCentralAuthToken(); return getCentralAuthToken();
} else if(loginResultCode.equals("2FA")) { } else if (loginResultCode.equals("2FA")) {
Timber.e("Cannot refresh session for 2FA enabled user. Login required"); Timber.e("Cannot refresh session for 2FA enabled user. Login required");
} else { } else {
Timber.e("Error occurred in refreshing session. Error code is %s", loginResultCode); Timber.e("Error occurred in refreshing session. Error code is %s", loginResultCode);

View file

@ -117,4 +117,4 @@ public class CustomApiResult {
return null; return null;
} }
} }
} }

View file

@ -58,7 +58,7 @@ public class CustomMwApi {
} }
public String getAuthCookie() { public String getAuthCookie() {
if(authCookie == null){ if (authCookie == null){
authCookie = ""; authCookie = "";
List<Cookie> cookies = client.getCookieStore().getCookies(); List<Cookie> cookies = client.getCookieStore().getCookies();
for(Cookie cookie: cookies) { for(Cookie cookie: cookies) {
@ -102,14 +102,14 @@ public class CustomMwApi {
} }
public String getUserID() throws IOException { public String getUserID() throws IOException {
if(this.userID == null || this.userID.equals("0")) { if (this.userID == null || this.userID.equals("0")) {
this.validateLogin(); this.validateLogin();
} }
return userID; return userID;
} }
public String getUserName() throws IOException { public String getUserName() throws IOException {
if(this.userID == null || this.userID.equals("0")) { if (this.userID == null || this.userID.equals("0")) {
this.validateLogin(); this.validateLogin();
} }
return userName; return userName;
@ -122,7 +122,7 @@ public class CustomMwApi {
String token = tokenData.getString("/api/login/@token"); String token = tokenData.getString("/api/login/@token");
CustomApiResult confirmData = this.action("login").param("lgname", username).param("lgpassword", password).param("lgtoken", token).post(); CustomApiResult confirmData = this.action("login").param("lgname", username).param("lgpassword", password).param("lgtoken", token).post();
String finalResult = confirmData.getString("/api/login/@result"); String finalResult = confirmData.getString("/api/login/@result");
if(finalResult.equals("Success")) { if (finalResult.equals("Success")) {
isLoggedIn = true; isLoggedIn = true;
} }
return finalResult; return finalResult;
@ -149,7 +149,7 @@ public class CustomMwApi {
.data("comment", comment) .data("comment", comment)
.data("filename", filename) .data("filename", filename)
.sendProgressListener(uploadProgressListener); .sendProgressListener(uploadProgressListener);
if(length != -1) { if (length != -1) {
builder.file("file", filename, file, length); builder.file("file", filename, file, length);
} else { } else {
builder.file("file", filename, file); builder.file("file", filename, file);
@ -177,4 +177,4 @@ public class CustomMwApi {
return CustomApiResult.fromRequestBuilder(builder, client); return CustomApiResult.fromRequestBuilder(builder, client);
} }
} }
; ;

View file

@ -4,9 +4,11 @@ import android.os.Build;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.util.Log;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.utils.PermissionUtils;
import timber.log.Timber; import timber.log.Timber;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
@ -24,8 +26,9 @@ class DirectUpload {
} }
// These permission requests will be handled by the Fragments. // 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() { void initiateGalleryUpload() {
Log.d("deneme7","initiateGalleryUpload");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(fragment.getActivity(), READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(fragment.getActivity(), READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
if (fragment.shouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE)) { if (fragment.shouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE)) {
@ -33,40 +36,42 @@ class DirectUpload {
.setMessage(fragment.getActivity().getString(R.string.read_storage_permission_rationale)) .setMessage(fragment.getActivity().getString(R.string.read_storage_permission_rationale))
.setPositiveButton(android.R.string.ok, (dialog, which) -> { .setPositiveButton(android.R.string.ok, (dialog, which) -> {
Timber.d("Requesting permissions for read external storage"); 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(); dialog.dismiss();
}) })
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.create() .create()
.show(); .show();
} else { } else {
fragment.requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, fragment.getActivity().requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, PermissionUtils.GALLERY_PERMISSION_FROM_NEARBY_MAP);
4);
} }
} else { } else {
controller.startGalleryPick(); controller.startSingleGalleryPick();
} }
} }
else { else {
controller.startGalleryPick(); controller.startSingleGalleryPick();
} }
} }
void initiateCameraUpload() { void initiateCameraUpload() {
Log.d("deneme7","initiateCameraUpload");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(fragment.getActivity(), WRITE_EXTERNAL_STORAGE) != PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(fragment.getActivity(), WRITE_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
if (fragment.shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { if (fragment.shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) {
new AlertDialog.Builder(fragment.getActivity()) new AlertDialog.Builder(fragment.getActivity())
.setMessage(fragment.getActivity().getString(R.string.write_storage_permission_rationale)) .setMessage(fragment.getActivity().getString(R.string.write_storage_permission_rationale))
.setPositiveButton(android.R.string.ok, (dialog, which) -> { .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(); dialog.dismiss();
}) })
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.create() .create()
.show(); .show();
} else { } else {
fragment.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 5); fragment.getActivity().requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, PermissionUtils.CAMERA_PERMISSION_FROM_NEARBY_MAP);
} }
} else { } else {
controller.startCameraCapture(); controller.startCameraCapture();

View file

@ -45,4 +45,4 @@ public class NearbyAdapterFactory {
rendererAdapter.notifyDataSetChanged(); rendererAdapter.notifyDataSetChanged();
rendererAdapter.diffUpdate(newPlaceList); rendererAdapter.diffUpdate(newPlaceList);
} }
} }

View file

@ -39,6 +39,7 @@ public class NearbyController {
this.prefs = prefs; this.prefs = prefs;
} }
/** /**
* Prepares Place list to make their distance information update later. * 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 * @return NearbyPlacesInfo a variable holds Place list without distance information
* and boundary coordinates of current Place List * 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); Timber.d("Loading attractions near %s", curLatLng);
NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo(); NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo();
if (curLatLng == null) { if (curLatLng == null) {
Timber.d("Loading attractions neari, but curLatLng is null");
return null; return null;
} }
List<Place> places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage()); List<Place> places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage(), returnClosestResult);
if (null != places && places.size() > 0) { if (null != places && places.size() > 0) {
LatLng[] boundaryCoordinates = {places.get(0).location, // south LatLng[] boundaryCoordinates = {places.get(0).location, // south
@ -168,7 +169,7 @@ public class NearbyController {
} }
public class NearbyPlacesInfo { public class NearbyPlacesInfo {
List<Place> placeList; // List of nearby places public List<Place> placeList; // List of nearby places
LatLng[] boundaryCoordinates; // Corners of nearby area public LatLng[] boundaryCoordinates; // Corners of nearby area
} }
} }

View file

@ -5,20 +5,19 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Typeface;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.BottomSheetBehavior; import android.support.design.widget.BottomSheetBehavior;
import android.support.design.widget.Snackbar;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.view.Menu; import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
@ -33,11 +32,11 @@ import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.R; 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.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager; 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.location.LocationUpdateListener;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.UriSerializer; import fr.free.nrw.commons.utils.UriSerializer;
import fr.free.nrw.commons.utils.ViewUtil; 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.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import timber.log.Timber; 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_SIGNIFICANTLY_CHANGED;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_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.MAP_UPDATED;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED; import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED;
public class NearbyFragment extends CommonsDaggerSupportFragment
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener, implements LocationUpdateListener,
WikidataEditListener.WikidataP18EditListener { WikidataEditListener.WikidataP18EditListener {
private static final int LOCATION_REQUEST = 1;
@BindView(R.id.progressBar) @BindView(R.id.progressBar)
ProgressBar progressBar; ProgressBar progressBar;
@BindView(R.id.bottom_sheet) @BindView(R.id.bottom_sheet)
LinearLayout bottomSheet; LinearLayout bottomSheet;
@BindView(R.id.bottom_sheet_details) @BindView(R.id.bottom_sheet_details)
@ -77,59 +71,127 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
LocationServiceManager locationManager; LocationServiceManager locationManager;
@Inject @Inject
NearbyController nearbyController; NearbyController nearbyController;
@Inject WikidataEditListener wikidataEditListener;
@Inject @Inject
@Named("application_preferences") SharedPreferences applicationPrefs; WikidataEditListener wikidataEditListener;
private LatLng curLatLng; @Inject
private Bundle bundle; @Named("application_preferences")
private Disposable placesDisposable; SharedPreferences applicationPrefs;
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
public NearbyMapFragment nearbyMapFragment; public NearbyMapFragment nearbyMapFragment;
private NearbyListFragment nearbyListFragment; private NearbyListFragment nearbyListFragment;
private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName(); private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName();
private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.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 final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
private BroadcastReceiver broadcastReceiver; private BroadcastReceiver broadcastReceiver;
private boolean isListShowcaseAdded = false; private boolean onOrientationChanged = false;
private boolean isMapShowCaseAdded = false;
private LatLng lastKnownLocation;
private MaterialShowcaseView secondSingleShowCaseView;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_nearby); setRetainInstance(true);
ButterKnife.bind(this);
resumeFragment();
bundle = new Bundle();
initBottomSheetBehaviour();
initDrawer();
wikidataEditListener.setAuthenticationStateListener(this);
} }
@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() { private void resumeFragment() {
// Find the retained fragment on activity restarts // Find the retained fragment on activity restarts
nearbyMapFragment = getMapFragment(); nearbyMapFragment = getMapFragment();
nearbyListFragment = getListFragment(); 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() { private void initBottomSheetBehaviour() {
transparentView.setAlpha(0); transparentView.setAlpha(0);
bottomSheet.getLayoutParams().height = getActivity().getWindowManager()
bottomSheet.getLayoutParams().height = getWindowManager()
.getDefaultDisplay().getHeight() / 16 * 9; .getDefaultDisplay().getHeight() / 16 * 9;
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet); bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
// TODO initProperBottomSheetBehavior();
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override @Override
@ -148,257 +210,44 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN);
} }
@Override public void prepareViewsForSheetPosition(int bottomSheetState) {
public boolean onCreateOptionsMenu(Menu menu) { // TODO
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);
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public void onLocationChangedSignificantly(LatLng latLng) {
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
// 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);
}
} }
@Override @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { public void onLocationChangedSlightly(LatLng latLng) {
switch (requestCode) { refreshView(LOCATION_SLIGHTLY_CHANGED);
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);
}
} }
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() { @Override
if (!locationManager.isProviderEnabled()) { public void onLocationChangedMedium(LatLng latLng) {
Timber.d("GPS is not enabled"); // For nearby map actions, there are no differences between 500 meter location change (aka medium change) and slight change
new AlertDialog.Builder(this) refreshView(LOCATION_SLIGHTLY_CHANGED);
.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 @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { public void onWikidataEditSuccessful() {
super.onActivityResult(requestCode, resultCode, data); refreshView(MAP_UPDATED);
if (requestCode == 1) {
Timber.d("User is back from Settings page");
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
} }
@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 * This method should be the single point to load/refresh nearby places
* *
* @param locationChangeType defines if location shanged significantly or slightly * @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) { if (lockNearbyView) {
return; return;
} }
if (!NetworkUtils.isInternetConnectionEstablished(this)) { if (!NetworkUtils.isInternetConnectionEstablished(getActivity())) {
hideProgressBar(); hideProgressBar();
return; return;
} }
@ -408,7 +257,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
if (curLatLng != null && curLatLng.equals(lastLocation) if (curLatLng != null && curLatLng.equals(lastLocation)
&& !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed && !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed
return; if (!onOrientationChanged) {
return;
}
} }
curLatLng = lastLocation; curLatLng = lastLocation;
@ -421,9 +272,15 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
return; 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) if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)
|| locationChangeType.equals(PERMISSION_JUST_GRANTED) || locationChangeType.equals(PERMISSION_JUST_GRANTED)
|| locationChangeType.equals(MAP_UPDATED)) { || locationChangeType.equals(MAP_UPDATED)
|| onOrientationChanged) {
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
//TODO: This hack inserts curLatLng before populatePlaces is called (see #1440). Ideally a proper fix should be found //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); bundle.putString("CurLatLng", gsonCurLatLng);
placesDisposable = Observable.fromCallable(() -> nearbyController placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng)) .loadAttractionsFromLocation(curLatLng, false))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces, .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) { private void populatePlaces(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
Timber.d("Populating nearby places");
List<Place> placeList = nearbyPlacesInfo.placeList; List<Place> placeList = nearbyPlacesInfo.placeList;
LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates; LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates;
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
@ -499,7 +324,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates); String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates);
if (placeList.size() == 0) { 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); bundle.putString("PlaceList", gsonPlaceList);
@ -520,47 +345,13 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
updateMapFragment(false); updateMapFragment(false);
updateListFragment(); 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) { private void lockNearbyView(boolean lock) {
if (lock) { if (lock) {
lockNearbyView = true; 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) { private void updateMapFragment(boolean isSlightUpdate) {
/* /*
* Significant update means updating nearby place markers. Slightly update means only Significant update means updating nearby place markers. Slightly update means only
* updating current location marker and camera target. updating current location marker and camera target.
* We update our map Significantly on each 1000 meter change, but we can't never know 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 the frequency of nearby places. Thus we check if we are close to the boundaries of
* our nearby markers, we update our map Significantly. our nearby markers, we update our map Significantly.
* */ */
NearbyMapFragment nearbyMapFragment = getMapFragment(); NearbyMapFragment nearbyMapFragment = getMapFragment();
if (nearbyMapFragment != null && curLatLng != null) { if (nearbyMapFragment != null && curLatLng != null) {
hideProgressBar(); // In case it is visible (this happens, not an impossible case) 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 * 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 * get new nearby places. Check order is south, north, west, east
* */ * */
if (nearbyMapFragment.boundaryCoordinates != null if (nearbyMapFragment.boundaryCoordinates != null
&& (curLatLng.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude() && (curLatLng.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude()
|| curLatLng.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude() || curLatLng.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude()
@ -627,7 +387,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
|| curLatLng.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) { || curLatLng.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) {
// populate places // populate places
placesDisposable = Observable.fromCallable(() -> nearbyController placesDisposable = Observable.fromCallable(() -> nearbyController
.loadAttractionsFromLocation(curLatLng)) .loadAttractionsFromLocation(curLatLng, false))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces, .subscribe(this::populatePlaces,
@ -642,6 +402,15 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
return; 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) { if (isSlightUpdate) {
nearbyMapFragment.setBundleForUpdtes(bundle); nearbyMapFragment.setBundleForUpdtes(bundle);
nearbyMapFragment.updateMapSlightly(); nearbyMapFragment.updateMapSlightly();
@ -668,7 +437,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
* Calls fragment for map view. * Calls fragment for map view.
*/ */
private void setMapFragment() { private void setMapFragment() {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction();
nearbyMapFragment = new NearbyMapFragment(); nearbyMapFragment = new NearbyMapFragment();
nearbyMapFragment.setArguments(bundle); nearbyMapFragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container, nearbyMapFragment, TAG_RETAINED_MAP_FRAGMENT); 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. * Calls fragment for list view.
*/ */
private void setListFragment() { private void setListFragment() {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction();
nearbyListFragment = new NearbyListFragment(); nearbyListFragment = new NearbyListFragment();
nearbyListFragment.setArguments(bundle); nearbyListFragment.setArguments(bundle);
fragmentTransaction.replace(R.id.container_sheet, nearbyListFragment, TAG_RETAINED_LIST_FRAGMENT); fragmentTransaction.replace(R.id.container_sheet, nearbyListFragment, TAG_RETAINED_LIST_FRAGMENT);
@ -688,26 +457,235 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
fragmentTransaction.commitAllowingStateLoss(); fragmentTransaction.commitAllowingStateLoss();
} }
@Override private void hideProgressBar() {
public void onLocationChangedSignificantly(LatLng latLng) { if (progressBar != null) {
refreshView(LOCATION_SIGNIFICANTLY_CHANGED); progressBar.setVisibility(View.GONE);
}
} }
@Override /**
public void onLocationChangedSlightly(LatLng latLng) { * This method first checks if the location permissions has been granted and then register the location manager for updates.
refreshView(LOCATION_SLIGHTLY_CHANGED); */
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) { private void requestLocationPermissions() {
// TODO 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) { 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 @Override
public void onWikidataEditSuccessful() { public void onResume() {
refreshView(MAP_UPDATED); 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();
}
} }
} }

View file

@ -73,7 +73,7 @@ public class NearbyListFragment extends DaggerFragment {
ViewGroup container, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
Timber.d("NearbyListFragment created"); 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 = view.findViewById(R.id.listView);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
@ -164,4 +164,4 @@ public class NearbyListFragment extends DaggerFragment {
this.bundleForUpdates = bundleForUpdates; this.bundleForUpdates = bundleForUpdates;
} }
} }

View file

@ -7,7 +7,6 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; 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.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton; import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.Gravity; import android.view.Gravity;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -109,7 +109,7 @@ public class NearbyMapFragment extends DaggerFragment {
private Animation fab_close; private Animation fab_close;
private Animation fab_open; private Animation fab_open;
private Animation rotate_forward; private Animation rotate_forward;
private ContributionController controller; public ContributionController controller;
private DirectUpload directUpload; private DirectUpload directUpload;
private Place place; 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_PORTRAIT = 0.06;
private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04; private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04;
private boolean isSecondMaterialShowcaseDismissed;
private boolean isMapReady; private boolean isMapReady;
private MaterialShowcaseView thirdSingleShowCaseView;
private Bundle bundleForUpdtes;// Carry information from activity about changed nearby places and current location 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); setRetainInstance(true);
} }
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { 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() { public void updateMapSlightly() {
Timber.d("updateMapSlightly called, bundle is:"+bundleForUpdtes);
if (mapboxMap != null) { if (mapboxMap != null) {
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriDeserializer()) .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() { public void updateMapSignificantly() {
Timber.d("updateMapSignificantly called, bundle is:"+bundleForUpdtes);
if (mapboxMap != null) { if (mapboxMap != null) {
if (bundleForUpdtes != null) { if (bundleForUpdtes != null) {
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
@ -281,7 +297,7 @@ public class NearbyMapFragment extends DaggerFragment {
// Make camera to follow user on location change // Make camera to follow user on location change
CameraPosition position ; CameraPosition position ;
if(ViewUtil.isPortrait(getActivity())){ if (ViewUtil.isPortrait(getActivity())){
position = new CameraPosition.Builder() position = new CameraPosition.Builder()
.target(isBottomListSheetExpanded ? .target(isBottomListSheetExpanded ?
new LatLng(curMapBoxLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT, 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) { private void updateMapCameraAccordingToBottomSheet(boolean isBottomListSheetExpanded) {
CameraPosition position; CameraPosition position;
this.isBottomListSheetExpanded = isBottomListSheetExpanded; this.isBottomListSheetExpanded = isBottomListSheetExpanded;
if (mapboxMap != null && curLatLng != null) { if (mapboxMap != null && curLatLng != null) {
if (isBottomListSheetExpanded) { if (isBottomListSheetExpanded) {
// Make camera to follow user on location change // Make camera to follow user on location change
if(ViewUtil.isPortrait(getActivity())) { if (ViewUtil.isPortrait(getActivity())) {
position = new CameraPosition.Builder() position = new CameraPosition.Builder()
.target(new LatLng(curLatLng.getLatitude() - CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT, .target(new LatLng(curLatLng.getLatitude() - CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT,
curLatLng.getLongitude())) // Sets the new camera target above curLatLng.getLongitude())) // Sets the new camera target above
@ -345,39 +367,40 @@ public class NearbyMapFragment extends DaggerFragment {
} }
private void initViews() { 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); 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 = BottomSheetBehavior.from(bottomSheetDetails);
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetDetails.setVisibility(View.VISIBLE); bottomSheetDetails.setVisibility(View.VISIBLE);
fabPlus = getActivity().findViewById(R.id.fab_plus); fabPlus = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_plus);
fabCamera = getActivity().findViewById(R.id.fab_camera); fabCamera = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_camera);
fabGallery = getActivity().findViewById(R.id.fab_galery); fabGallery = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_galery);
fabRecenter = getActivity().findViewById(R.id.fab_recenter); fabRecenter = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_recenter);
fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open); fab_open = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.fab_open);
fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close); fab_close = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.fab_close);
rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward); rotate_forward = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.rotate_forward);
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward); 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); description = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.description);
title = getActivity().findViewById(R.id.title); title = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.title);
distance = getActivity().findViewById(R.id.category); distance = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.category);
icon = getActivity().findViewById(R.id.icon); icon = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.icon);
wikidataButton = getActivity().findViewById(R.id.wikidataButton); wikidataButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikidataButton);
wikipediaButton = getActivity().findViewById(R.id.wikipediaButton); wikipediaButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikipediaButton);
directionsButton = getActivity().findViewById(R.id.directionsButton); directionsButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.directionsButton);
commonsButton = getActivity().findViewById(R.id.commonsButton); commonsButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.commonsButton);
wikidataButtonText = getActivity().findViewById(R.id.wikidataButtonText); wikidataButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikidataButtonText);
wikipediaButtonText = getActivity().findViewById(R.id.wikipediaButtonText); wikipediaButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikipediaButtonText);
directionsButtonText = getActivity().findViewById(R.id.directionsButtonText); directionsButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.directionsButtonText);
commonsButtonText = getActivity().findViewById(R.id.commonsButtonText); commonsButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.commonsButtonText);
bookmarkButton = getActivity().findViewById(R.id.bookmarkButton); bookmarkButton = getActivity().findViewById(R.id.bookmarkButton);
bookmarkButtonImage = getActivity().findViewById(R.id.bookmarkButtonImage); bookmarkButtonImage = getActivity().findViewById(R.id.bookmarkButtonImage);
@ -416,7 +439,7 @@ public class NearbyMapFragment extends DaggerFragment {
mapView.getMapAsync(mapboxMap -> { mapView.getMapAsync(mapboxMap -> {
CameraPosition position; CameraPosition position;
if(ViewUtil.isPortrait(getActivity())){ if (ViewUtil.isPortrait(getActivity())){
position = new CameraPosition.Builder() position = new CameraPosition.Builder()
.target(isBottomListSheetExpanded ? .target(isBottomListSheetExpanded ?
new LatLng(curLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT, new LatLng(curLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT,
@ -494,6 +517,7 @@ public class NearbyMapFragment extends DaggerFragment {
} }
private void setupMapView(Bundle savedInstanceState) { private void setupMapView(Bundle savedInstanceState) {
Timber.d("setupMapView called");
MapboxMapOptions options = new MapboxMapOptions() MapboxMapOptions options = new MapboxMapOptions()
.compassGravity(Gravity.BOTTOM | Gravity.LEFT) .compassGravity(Gravity.BOTTOM | Gravity.LEFT)
.compassMargins(new int[]{12, 0, 0, 24}) .compassMargins(new int[]{12, 0, 0, 24})
@ -505,15 +529,19 @@ public class NearbyMapFragment extends DaggerFragment {
.zoom(11) .zoom(11)
.build()); .build());
// create map if (!getParentFragment().getActivity().isFinishing()) {
mapView = new MapView(getActivity(), options); mapView = new MapView(getParentFragment().getActivity(), options);
mapView.onCreate(savedInstanceState); // create map
mapView.getMapAsync(mapboxMap -> { mapView.onCreate(savedInstanceState);
((NearbyActivity)getActivity()).setMapViewTutorialShowCase(); mapView.getMapAsync(new OnMapReadyCallback() {
NearbyMapFragment.this.mapboxMap = mapboxMap; @Override
updateMapSignificantly(); public void onMapReady(MapboxMap mapboxMap) {
}); NearbyMapFragment.this.mapboxMap = mapboxMap;
mapView.setStyleUrl("asset://mapstyle.json"); updateMapSignificantly();
}
});
mapView.setStyleUrl("asset://mapstyle.json");
}
} }
/** /**
@ -542,6 +570,7 @@ public class NearbyMapFragment extends DaggerFragment {
* move. * move.
*/ */
private void addCurrentLocationMarker(MapboxMap mapboxMap) { private void addCurrentLocationMarker(MapboxMap mapboxMap) {
Timber.d("addCurrentLocationMarker is called");
if (currentLocationMarker != null) { if (currentLocationMarker != null) {
currentLocationMarker.remove(); // Remove previous marker, we are not Hansel and Gretel currentLocationMarker.remove(); // Remove previous marker, we are not Hansel and Gretel
} }
@ -564,8 +593,11 @@ public class NearbyMapFragment extends DaggerFragment {
mapboxMap.addPolygon(currentLocationPolygonOptions); mapboxMap.addPolygon(currentLocationPolygonOptions);
} }
/**
* Adds markers for nearby places to mapbox map
*/
private void addNearbyMarkerstoMapBoxMap() { private void addNearbyMarkerstoMapBoxMap() {
Timber.d("addNearbyMarkerstoMapBoxMap is called");
mapboxMap.addMarkers(baseMarkerOptions); mapboxMap.addMarkers(baseMarkerOptions);
mapboxMap.setOnInfoWindowCloseListener(marker -> { mapboxMap.setOnInfoWindowCloseListener(marker -> {
@ -624,6 +656,12 @@ public class NearbyMapFragment extends DaggerFragment {
return circle; 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) { public void prepareViewsForSheetPosition(int bottomSheetState) {
switch (bottomSheetState) { switch (bottomSheetState) {
@ -648,6 +686,9 @@ public class NearbyMapFragment extends DaggerFragment {
} }
} }
/**
* Hides all fabs
*/
private void hideFAB() { private void hideFAB() {
removeAnchorFromFABs(fabPlus); removeAnchorFromFABs(fabPlus);
@ -679,25 +720,14 @@ public class NearbyMapFragment extends DaggerFragment {
private void showFAB() { 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(); 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()); addAnchorToSmallFABs(fabCamera, ((NearbyFragment)getParentFragment()).view.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();
isMapReady = true; isMapReady = true;
if (isSecondMaterialShowcaseDismissed) {
thirdSingleShowCaseView.show(getActivity());
}
} }
@ -724,6 +754,11 @@ public class NearbyMapFragment extends DaggerFragment {
floatingActionButton.setLayoutParams(params); 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) { private void passInfoToSheet(Place place) {
this.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) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Timber.d("onRequestPermissionsResult: req code = " + " perm = " + permissions + " grant =" + 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) { switch (requestCode) {
// 4 = "Read external storage" allowed when gallery selected // 4 = "Read external storage" allowed when gallery selected
case 4: { 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) { public void setBundleForUpdtes(Bundle bundleForUpdtes) {
this.bundleForUpdtes = bundleForUpdtes; this.bundleForUpdtes = bundleForUpdtes;
} }
public void onNearbyMaterialShowcaseDismissed() {
isSecondMaterialShowcaseDismissed = true;
if (isMapReady) {
thirdSingleShowCaseView.show(getActivity());
}
}
@Override @Override
public void onStart() { public void onStart() {
if (mapView != null) { if (mapView != null) {

View file

@ -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
}
}

View file

@ -23,9 +23,9 @@ import timber.log.Timber;
public class NearbyPlaces { 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 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 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_URL = Uri.parse("https://query.wikidata.org/sparql");
private static final Uri WIKIDATA_QUERY_UI_URL = Uri.parse("https://query.wikidata.org/"); private static final Uri WIKIDATA_QUERY_UI_URL = Uri.parse("https://query.wikidata.org/");
@ -41,9 +41,22 @@ public class NearbyPlaces {
} }
} }
List<Place> getFromWikidataQuery(LatLng curLatLng, String lang) throws IOException { List<Place> getFromWikidataQuery(LatLng curLatLng, String lang, boolean returnClosestResult) throws IOException {
List<Place> places = Collections.emptyList(); List<Place> 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 // increase the radius gradually to find a satisfactory number of nearby places
while (radius <= MAX_RADIUS) { while (radius <= MAX_RADIUS) {
try { try {
@ -103,12 +116,19 @@ public class NearbyPlaces {
String point = fields[0]; String point = fields[0];
String wikiDataLink = Utils.stripLocalizedString(fields[1]); String wikiDataLink = Utils.stripLocalizedString(fields[1]);
String name = Utils.stripLocalizedString(fields[2]); 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 type = Utils.stripLocalizedString(fields[4]);
String icon = fields[5]; String icon = fields[5];
String wikipediaSitelink = Utils.stripLocalizedString(fields[7]); String wikipediaSitelink = Utils.stripLocalizedString(fields[7]);
String commonsSitelink = Utils.stripLocalizedString(fields[8]); String commonsSitelink = Utils.stripLocalizedString(fields[8]);
String category = Utils.stripLocalizedString(fields[9]); String category = Utils.stripLocalizedString(fields[9]);
Timber.v("Name: " + name + ", type: " + type + ", category: " + category + ", wikipediaSitelink: " + wikipediaSitelink + ", commonsSitelink: " + commonsSitelink); Timber.v("Name: " + name + ", type: " + type + ", category: " + category + ", wikipediaSitelink: " + wikipediaSitelink + ", commonsSitelink: " + commonsSitelink);
double latitude; double latitude;
@ -127,7 +147,7 @@ public class NearbyPlaces {
places.add(new Place( places.add(new Place(
name, name,
Place.Label.fromText(type), // list Place.Label.fromText(identifier), // list
type, // details type, // details
Uri.parse(icon), Uri.parse(icon),
new LatLng(latitude, longitude, 0), new LatLng(latitude, longitude, 0),
@ -143,4 +163,5 @@ public class NearbyPlaces {
return places; return places;
} }
} }

View file

@ -121,27 +121,30 @@ public class Place {
*/ */
public enum Label { public enum Label {
BUILDING("building", R.drawable.round_icon_generic_building), BUILDING("Q41176", R.drawable.round_icon_generic_building),
HOUSE("house", R.drawable.round_icon_house), HOUSE("Q3947", R.drawable.round_icon_house),
COTTAGE("cottage", R.drawable.round_icon_house), COTTAGE("Q5783996", R.drawable.round_icon_house),
FARMHOUSE("farmhouse", R.drawable.round_icon_house), FARMHOUSE("Q489357", R.drawable.round_icon_house),
CHURCH("church", R.drawable.round_icon_church), CHURCH("Q16970", R.drawable.round_icon_church), //changed from church to church building
RAILWAY_STATION("railway station", R.drawable.round_icon_railway_station), RAILWAY_STATION("Q55488", R.drawable.round_icon_railway_station),
GATEHOUSE("gatehouse", R.drawable.round_icon_gatehouse), GATEHOUSE("Q277760", R.drawable.round_icon_gatehouse),
MILESTONE("milestone", R.drawable.round_icon_milestone), MILESTONE("Q10145", R.drawable.round_icon_milestone),
INN("inn", R.drawable.round_icon_house), INN("Q256020", R.drawable.round_icon_house), //Q27686
CITY("city", R.drawable.round_icon_city), HOTEL("Q27686", R.drawable.round_icon_house),
SECONDARY_SCHOOL("secondary school", R.drawable.round_icon_school), CITY("Q515", R.drawable.round_icon_city),
EDU("edu", R.drawable.round_icon_school), UNIVERSITY("Q3918",R.drawable.round_icon_school), //added university
ISLE("isle", R.drawable.round_icon_island), SCHOOL("Q3914", R.drawable.round_icon_school), //changed from "secondary school" to school
MOUNTAIN("mountain", R.drawable.round_icon_mountain), EDUCATION("Q8434", R.drawable.round_icon_school), //changed from edu to education, there is no id for "edu"
AIRPORT("airport", R.drawable.round_icon_airport), ISLE("Q23442", R.drawable.round_icon_island),
BRIDGE("bridge", R.drawable.round_icon_bridge), MOUNTAIN("Q8502", R.drawable.round_icon_mountain),
ROAD("road", R.drawable.round_icon_road), AIRPORT("Q1248784", R.drawable.round_icon_airport),
FOREST("forest", R.drawable.round_icon_forest), BRIDGE("Q12280", R.drawable.round_icon_bridge),
PARK("park", R.drawable.round_icon_park), ROAD("Q34442", R.drawable.round_icon_road),
RIVER("river", R.drawable.round_icon_river), FOREST("Q4421", R.drawable.round_icon_forest),
WATERFALL("waterfall", R.drawable.round_icon_waterfall), 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); UNKNOWN("?", R.drawable.round_icon_unknown);
private static final Map<String, Label> TEXT_TO_DESCRIPTION private static final Map<String, Label> TEXT_TO_DESCRIPTION

View file

@ -100,4 +100,4 @@ public class Sitelinks implements Parcelable {
return new Sitelinks(this); return new Sitelinks(this);
} }
} }
} }

View file

@ -11,13 +11,15 @@ public class Notification {
public String description; public String description;
public String link; public String link;
public String iconUrl; 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.notificationType = notificationType;
this.notificationText = notificationText; this.notificationText = notificationText;
this.date = date; this.date = date;
this.description = description; this.description = description;
this.link = link; this.link = link;
this.iconUrl = iconUrl; this.iconUrl = iconUrl;
this.dateWithYear = dateWithYear;
} }
} }

View file

@ -17,6 +17,7 @@ import android.widget.RelativeLayout;
import com.pedrogomez.renderers.RVRendererAdapter; import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
@ -25,6 +26,7 @@ import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; 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.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.NetworkUtils;
@ -85,6 +87,11 @@ public class NotificationActivity extends NavigationBaseActivity {
private void addNotifications() { private void addNotifications() {
Timber.d("Add notifications"); 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){ if(mNotificationWorkerFragment == null){
Observable.fromCallable(() -> { Observable.fromCallable(() -> {
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
@ -115,7 +122,7 @@ public class NotificationActivity extends NavigationBaseActivity {
} }
private void setAdapter(List<Notification> notificationList) { private void setAdapter(List<Notification> notificationList) {
if(notificationList == null || notificationList.isEmpty()) { if (notificationList == null || notificationList.isEmpty()) {
ViewUtil.showSnackbar(relativeLayout, R.string.no_notifications); ViewUtil.showSnackbar(relativeLayout, R.string.no_notifications);
return; return;
} }
@ -140,4 +147,10 @@ public class NotificationActivity extends NavigationBaseActivity {
.commit(); .commit();
mNotificationWorkerFragment.setNotificationList(notificationList); mNotificationWorkerFragment.setNotificationList(notificationList);
} }
@Override
public void onBackPressed() {
startActivityWithFlags(
this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
Intent.FLAG_ACTIVITY_SINGLE_TOP); }
} }

View file

@ -1,6 +1,11 @@
package fr.free.nrw.commons.notification; package fr.free.nrw.commons.notification;
import android.graphics.Color;
import android.text.Html; 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.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -60,7 +65,25 @@ public class NotificationRenderer extends Renderer<Notification> {
notificationText = notificationText.trim().replaceAll("(^\\s*)|(\\s*$)", ""); notificationText = notificationText.trim().replaceAll("(^\\s*)|(\\s*$)", "");
notificationText = Html.fromHtml(notificationText).toString(); notificationText = Html.fromHtml(notificationText).toString();
notificationText = notificationText.concat(" "); 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{ public interface NotificationClicked{

View file

@ -24,4 +24,4 @@ public enum NotificationType {
} }
return UNKNOWN; return UNKNOWN;
} }
} }

View file

@ -144,7 +144,7 @@ public class NotificationUtils {
notificationText = getWelcomeMessage(context, document); notificationText = getWelcomeMessage(context, document);
break; 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) { private static String getNotificationText(Node document) {
@ -193,7 +193,7 @@ public class NotificationUtils {
private static String getNotificationIconUrl(Node document) { private static String getNotificationIconUrl(Node document) {
String format = "%s%s"; String format = "%s%s";
Node iconUrl = getNode(getModel(document), "iconUrl"); Node iconUrl = getNode(getModel(document), "iconUrl");
if(iconUrl == null) { if (iconUrl == null) {
return null; return null;
} else { } else {
String url = iconUrl.getTextContent(); String url = iconUrl.getTextContent();
@ -247,6 +247,14 @@ public class NotificationUtils {
return ""; 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) { private static String getNotificationDescription(Node document) {
Element titleElement = (Element) getNode(document, "title"); Element titleElement = (Element) getNode(document, "title");
if (titleElement != null) { if (titleElement != null) {

View file

@ -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<Void, Void, Notification> {
WeakReference<MainActivity> 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<Notification> allNotifications) {
if (allNotifications.size() > 0) {
return allNotifications.get(allNotifications.size()-1);
} else {
return null;
}
}
}

View file

@ -51,7 +51,7 @@ public class QuizActivity extends AppCompatActivity {
*/ */
@OnClick(R.id.next_button) @OnClick(R.id.next_button)
public void setNextQuestion(){ public void setNextQuestion(){
if( questionIndex <= quiz.size() && (positiveAnswer.isChecked() || negativeAnswer.isChecked())) { if ( questionIndex <= quiz.size() && (positiveAnswer.isChecked() || negativeAnswer.isChecked())) {
evaluateScore(); evaluateScore();
} else if ( !positiveAnswer.isChecked() && !negativeAnswer.isChecked()){ } else if ( !positiveAnswer.isChecked() && !negativeAnswer.isChecked()){
AlertDialog.Builder alert = new AlertDialog.Builder(this); 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 * to evaluate score and check whether answer is correct or wrong
*/ */
public void evaluateScore() { public void evaluateScore() {
if((quiz.get(questionIndex).isAnswer() && positiveAnswer.isChecked()) || if ((quiz.get(questionIndex).isAnswer() && positiveAnswer.isChecked()) ||
(!quiz.get(questionIndex).isAnswer() && negativeAnswer.isChecked()) ){ (!quiz.get(questionIndex).isAnswer() && negativeAnswer.isChecked()) ){
customAlert(getResources().getString(R.string.correct),quiz.get(questionIndex).getAnswerMessage() ); customAlert(getResources().getString(R.string.correct),quiz.get(questionIndex).getAnswerMessage() );
score++; score++;
} else{ } else {
customAlert(getResources().getString(R.string.wrong), quiz.get(questionIndex).getAnswerMessage()); customAlert(getResources().getString(R.string.wrong), quiz.get(questionIndex).getAnswerMessage());
} }
} }
@ -127,12 +127,12 @@ public class QuizActivity extends AppCompatActivity {
alert.setMessage(Message); alert.setMessage(Message);
alert.setPositiveButton(R.string.continue_message, (dialog, which) -> { alert.setPositiveButton(R.string.continue_message, (dialog, which) -> {
questionIndex++; questionIndex++;
if(questionIndex == quiz.size()){ if (questionIndex == quiz.size()) {
Intent i = new Intent(QuizActivity.this, QuizResultActivity.class); Intent i = new Intent(QuizActivity.this, QuizResultActivity.class);
dialog.dismiss(); dialog.dismiss();
i.putExtra("QuizResult",score); i.putExtra("QuizResult",score);
startActivity(i); startActivity(i);
}else { } else {
displayQuestion(); displayQuestion();
} }
}); });

View file

@ -73,7 +73,7 @@ public class QuizChecker {
*/ */
private void setTotalUploadCount(int uploadCount) { private void setTotalUploadCount(int uploadCount) {
totalUploadCount = uploadCount - countPref.getInt(UPLOAD_SHARED_PREFERENCE,0); totalUploadCount = uploadCount - countPref.getInt(UPLOAD_SHARED_PREFERENCE,0);
if( totalUploadCount < 0){ if ( totalUploadCount < 0){
totalUploadCount = 0; totalUploadCount = 0;
countPref.edit().putInt(UPLOAD_SHARED_PREFERENCE,0).apply(); countPref.edit().putInt(UPLOAD_SHARED_PREFERENCE,0).apply();
} }
@ -104,7 +104,7 @@ public class QuizChecker {
*/ */
private void setRevertParameter(int revertCountFetched) { private void setRevertParameter(int revertCountFetched) {
revertCount = revertCountFetched - revertPref.getInt(REVERT_SHARED_PREFERENCE,0); revertCount = revertCountFetched - revertPref.getInt(REVERT_SHARED_PREFERENCE,0);
if(revertCount < 0){ if (revertCount < 0){
revertCount = 0; revertCount = 0;
revertPref.edit().putInt(REVERT_SHARED_PREFERENCE, 0).apply(); 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 * to check whether the criterion to call quiz is satisfied
*/ */
private void calculateRevertParameter() { private void calculateRevertParameter() {
if( revertCount < 0 || totalUploadCount < 0){ if ( revertCount < 0 || totalUploadCount < 0){
revertPref.edit().putInt(REVERT_SHARED_PREFERENCE, 0).apply(); revertPref.edit().putInt(REVERT_SHARED_PREFERENCE, 0).apply();
countPref.edit().putInt(UPLOAD_SHARED_PREFERENCE,0).apply(); countPref.edit().putInt(UPLOAD_SHARED_PREFERENCE,0).apply();
return; return;

View file

@ -8,7 +8,6 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import com.dinuscxj.progressbar.CircleProgressBar; import com.dinuscxj.progressbar.CircleProgressBar;
@ -16,7 +15,7 @@ import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
import fr.free.nrw.commons.R; 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.support.v7.widget.Toolbar;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -26,16 +25,9 @@ import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.dinuscxj.progressbar.CircleProgressBar;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; 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 * Displays the final score of quiz and congratulates the user
@ -57,13 +49,13 @@ public class QuizResultActivity extends AppCompatActivity {
ButterKnife.bind(this); ButterKnife.bind(this);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
if( getIntent() != null) { if ( getIntent() != null) {
Bundle extras = getIntent().getExtras(); Bundle extras = getIntent().getExtras();
int score = extras.getInt("QuizResult"); int score = extras.getInt("QuizResult");
setScore(score); setScore(score);
}else{ }else{
startActivityWithFlags( startActivityWithFlags(
this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
Intent.FLAG_ACTIVITY_SINGLE_TOP); Intent.FLAG_ACTIVITY_SINGLE_TOP);
super.onBackPressed(); super.onBackPressed();
} }
@ -87,14 +79,14 @@ public class QuizResultActivity extends AppCompatActivity {
@OnClick(R.id.quiz_result_next) @OnClick(R.id.quiz_result_next)
public void launchContributionActivity(){ public void launchContributionActivity(){
startActivityWithFlags( startActivityWithFlags(
this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
Intent.FLAG_ACTIVITY_SINGLE_TOP); Intent.FLAG_ACTIVITY_SINGLE_TOP);
} }
@Override @Override
public void onBackPressed() { public void onBackPressed() {
startActivityWithFlags( startActivityWithFlags(
this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
Intent.FLAG_ACTIVITY_SINGLE_TOP); Intent.FLAG_ACTIVITY_SINGLE_TOP);
super.onBackPressed(); super.onBackPressed();
} }
@ -186,9 +178,9 @@ public class QuizResultActivity extends AppCompatActivity {
AlertDialog.Builder alertadd = new AlertDialog.Builder(QuizResultActivity.this); AlertDialog.Builder alertadd = new AlertDialog.Builder(QuizResultActivity.this);
LayoutInflater factory = LayoutInflater.from(QuizResultActivity.this); LayoutInflater factory = LayoutInflater.from(QuizResultActivity.this);
final View view = factory.inflate(R.layout.image_alert_layout, null); 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); 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); shareMessage.setText(R.string.quiz_result_share_message);
alertadd.setView(view); alertadd.setView(view);
alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot)); alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot));

View file

@ -44,7 +44,7 @@ public class RadioGroupHelper {
public RadioGroupHelper(View rootView, int... radiosIDs) { public RadioGroupHelper(View rootView, int... radiosIDs) {
super(); super();
for (int radioButtonID : radiosIDs) { for (int radioButtonID : radiosIDs) {
add((RadioButton)rootView.findViewById(radioButtonID)); add(rootView.findViewById(radioButtonID));
} }
} }
@ -58,7 +58,7 @@ public class RadioGroupHelper {
*/ */
View.OnClickListener onClickListener = v -> { View.OnClickListener onClickListener = v -> {
for (CompoundButton rb : radioButtons) { for (CompoundButton rb : radioButtons) {
if(rb != v) rb.setChecked(false); if (rb != v) rb.setChecked(false);
} }
}; };
} }

View file

@ -67,4 +67,4 @@ public class SettingsActivity extends NavigationBaseActivity {
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
} }
} }

View file

@ -8,7 +8,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity; import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity;
public abstract class BaseActivity extends CommonsDaggerAppCompatActivity { public abstract class BaseActivity extends CommonsDaggerAppCompatActivity {
boolean currentTheme; protected boolean currentTheme;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {

View file

@ -13,6 +13,7 @@ import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; 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.WelcomeActivity;
import fr.free.nrw.commons.achievements.AchievementsActivity; import fr.free.nrw.commons.achievements.AchievementsActivity;
import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.bookmarks.BookmarksActivity; import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.bookmarks.BookmarksActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity; import fr.free.nrw.commons.settings.SettingsActivity;
import timber.log.Timber; 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. * Set the username in navigationHeader.
*/ */
@ -156,13 +172,9 @@ public abstract class NavigationBaseActivity extends BaseActivity
case R.id.action_home: case R.id.action_home:
drawerLayout.closeDrawer(navigationView); drawerLayout.closeDrawer(navigationView);
startActivityWithFlags( startActivityWithFlags(
this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
Intent.FLAG_ACTIVITY_SINGLE_TOP); Intent.FLAG_ACTIVITY_SINGLE_TOP);
return true; 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: case R.id.action_about:
drawerLayout.closeDrawer(navigationView); drawerLayout.closeDrawer(navigationView);
startActivityWithFlags(this, AboutActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); startActivityWithFlags(this, AboutActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);

View file

@ -97,4 +97,4 @@ public class CompatTextView extends AppCompatTextView {
a.recycle(); a.recycle();
} }
} }
} }

View file

@ -48,4 +48,4 @@ public class HtmlTextView extends AppCompatTextView {
return Html.fromHtml(source); return Html.fromHtml(source);
} }
} }
} }

View file

@ -1,56 +1,72 @@
package fr.free.nrw.commons.upload; 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 { class Description {
private String languageId; private String languageCode;
private String languageDisplayText;
private String descriptionText; private String descriptionText;
private boolean set;
private int selectedLanguageIndex = -1; 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() { String getDescriptionText() {
return languageDisplayText;
}
public void setLanguageDisplayText(String languageDisplayText) {
this.languageDisplayText = languageDisplayText;
}
public String getDescriptionText() {
return descriptionText; return descriptionText;
} }
public void setDescriptionText(String descriptionText) { void setDescriptionText(String descriptionText) {
this.descriptionText = descriptionText; this.descriptionText = descriptionText;
if (!TextUtils.isEmpty(descriptionText)) {
set = true;
}
} }
public boolean isSet() { /**
return set; * @return the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter}
} */
int getSelectedLanguageIndex() {
public void setSet(boolean set) {
this.set = set;
}
public int getSelectedLanguageIndex() {
return selectedLanguageIndex; 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; 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<Description> 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();
}
} }

View file

@ -2,12 +2,12 @@ package fr.free.nrw.commons.upload;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat;
import android.support.v7.widget.AppCompatSpinner; import android.support.v7.widget.AppCompatSpinner;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
@ -17,182 +17,241 @@ import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.EditText; import android.widget.EditText;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnTouch; import butterknife.OnTouch;
import butterknife.Optional;
import fr.free.nrw.commons.R; 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 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; import static android.view.MotionEvent.ACTION_UP;
class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewHolder> { class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewHolder> {
List<Description> descriptions; private Title title;
List<Language> languages; private List<Description> descriptions;
private Context context; private Context context;
private Callback callback; private Callback callback;
private Subject<String> titleChangedSubject;
public DescriptionsAdapter() { private BiMap<AdapterView, String> selectedLanguages;
private UploadView uploadView;
DescriptionsAdapter(UploadView uploadView) {
title = new Title();
descriptions = new ArrayList<>(); descriptions = new ArrayList<>();
descriptions.add(new Description()); titleChangedSubject = BehaviorSubject.create();
languages = new ArrayList<>(); selectedLanguages = new BiMap<>();
this.uploadView = uploadView;
} }
public void setCallback(Callback callback) { void setCallback(Callback callback) {
this.callback = callback; this.callback = callback;
} }
public void setDescriptions(List<Description> descriptions) { void setItems(Title title, List<Description> descriptions) {
this.descriptions = descriptions; this.descriptions = descriptions;
this.title = title;
selectedLanguages = new BiMap<>();
notifyDataSetChanged(); notifyDataSetChanged();
} }
public void setLanguages(List<Language> languages) { @Override
this.languages = languages; public int getItemViewType(int position) {
if (position == 0) return 1;
else return 2;
} }
@NonNull
@Override @Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()) View view;
.inflate(R.layout.row_item_description, parent, false); 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(); context = parent.getContext();
ViewHolder viewHolder = new ViewHolder(view); return new ViewHolder(view);
return viewHolder;
} }
@Override @Override
public void onBindViewHolder(ViewHolder holder, int position) { public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.init(position); holder.init(position);
} }
@Override @Override
public int getItemCount() { public int getItemCount() {
return descriptions.size(); return descriptions.size() + 1;
} }
public List<Description> getDescriptions() { List<Description> getDescriptions() {
return descriptions; return descriptions;
} }
public void addDescription(Description description) { void addDescription(Description description) {
this.descriptions.add(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 { public class ViewHolder extends RecyclerView.ViewHolder {
@Nullable
@BindView(R.id.spinner_description_languages) @BindView(R.id.spinner_description_languages)
AppCompatSpinner spinnerDescriptionLanguages; 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) { public ViewHolder(View itemView) {
super(itemView); super(itemView);
ButterKnife.bind(this, itemView); ButterKnife.bind(this, itemView);
this.view = itemView; this.view = itemView;
Timber.i("descItemEditText:" + descItemEditText);
} }
public void init(int position) { public void init(int position) {
Description description = descriptions.get(position); if (position == 0) {
if (!TextUtils.isEmpty(description.getDescriptionText())) { Timber.d("Title is " + title);
etDescriptionText.setText(description.getDescriptionText()); if (!title.isEmpty()) {
} else { descItemEditText.setText(title.toString());
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);
} else { } 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 { } 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) { boolean descriptionInfo(View view, MotionEvent motionEvent) {
//Title info is visible only for the title
if (getAdapterPosition() == 0) { 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; final int value;
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) { if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
value = etDescriptionText.getRight() - etDescriptionText value = view.getRight() - descItemEditText.getCompoundDrawables()[2].getBounds().width();
.getCompoundDrawables()[2] if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
.getBounds().width() - etDescriptionText.getPaddingRight();
if (motionEvent.getAction() == ACTION_UP && motionEvent.getX() >= value) {
callback.showAlert(R.string.media_detail_description, callback.showAlert(R.string.media_detail_description,
R.string.description_info); R.string.description_info);
return true; return true;
} }
} else { } else {
value = etDescriptionText.getLeft() + etDescriptionText value = descItemEditText.getLeft() + descItemEditText
.getCompoundDrawables()[0] .getCompoundDrawables()[0]
.getBounds().width(); .getBounds().width();
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) { if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) {
@ -206,27 +265,12 @@ class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewH
} }
} }
private int getIndexOfUserDefaultLocale() { private Drawable getInfoIcon() {
for (int i = 0; i < languages.size(); i++) { return context.getResources()
if (languages.get(i).getLocale() .getDrawable(R.drawable.mapbox_info_icon_default);
.equals(context.getResources().getConfiguration().locale)) {
return i;
}
}
return 0;
}
private void updateDescriptionBasedOnSelectedLanguageIndex(Description description,
int position) {
Language language = languages.get(position);
Locale locale = language.getLocale();
description.setSelectedLanguageIndex(position);
description.setLanguageDisplayText(locale.getDisplayName());
description.setLanguageId(locale.getLanguage());
} }
public interface Callback { public interface Callback {
void showAlert(int mediaDetailDescription, int descriptionInfo); void showAlert(int mediaDetailDescription, int descriptionInfo);
} }
} }

View file

@ -1,82 +0,0 @@
package fr.free.nrw.commons.upload;
import android.app.Activity;
import android.content.Intent;
import android.graphics.BitmapRegionDecoder;
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.utils.ImageUtils;
import timber.log.Timber;
/**
* Created by bluesir9 on 16/9/17.
*
* <p>Responsible 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).
*
* <p>todo: Detect selfies?
*/
public class DetectUnwantedPicturesAsync extends AsyncTask<Void, Void, ImageUtils.Result> {
private final String imageMediaFilePath;
public final WeakReference<Activity> activityWeakReference;
DetectUnwantedPicturesAsync(WeakReference<Activity> 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();
}
}
}
}

View file

@ -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();
}
});
}
}
}

View file

@ -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<Void, Void, Boolean> {
interface Callback {
void onResult(Result result);
}
public enum Result {
NO_DUPLICATE,
DUPLICATE_PROCEED,
DUPLICATE_CANCELLED
}
private final WeakReference<Activity> activity;
private final MediaWikiApi api;
private final String fileSha1;
private final WeakReference<Context> context;
private final Callback callback;
public ExistingFileAsync(WeakReference<Activity> activity, String fileSha1, WeakReference<Context> 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);
}
}
}

View file

@ -1,21 +1,21 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.media.ExifInterface;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@ -44,89 +44,44 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
@Inject @Inject
@Named("default_preferences") @Named("default_preferences")
SharedPreferences prefs; SharedPreferences prefs;
private Uri mediaUri; private String filePath;
private ContentResolver contentResolver; private ContentResolver contentResolver;
private GPSExtractor imageObj; private GPSExtractor imageObj;
private Context context; private Context context;
private String decimalCoords; private String decimalCoords;
private boolean haveCheckedForOtherImages = false; private ExifInterface exifInterface;
private String filePath;
private boolean useExtStorage; private boolean useExtStorage;
private boolean cacheFound; private boolean haveCheckedForOtherImages = false;
private GPSExtractor tempImageObj; private GPSExtractor tempImageObj;
FileProcessor(Uri mediaUri, ContentResolver contentResolver, Context context) { FileProcessor(@NonNull String filePath, ContentResolver contentResolver, Context context) {
this.mediaUri = mediaUri; this.filePath = filePath;
this.contentResolver = contentResolver; this.contentResolver = contentResolver;
this.context = context; this.context = context;
ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this); ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this);
try {
exifInterface=new ExifInterface(filePath);
} catch (IOException e) {
Timber.e(e);
}
useExtStorage = prefs.getBoolean("useExternalStorage", true); 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 * 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"); Timber.d("Calling GPSExtractor");
try { imageObj = new GPSExtractor(exifInterface);
ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r"); decimalCoords = imageObj.getCoords();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (decimalCoords == null || !imageObj.imageCoordsExists) {
if (descriptor != null) { //Find other photos taken around the same time which has gps coordinates
imageObj = new GPSExtractor(descriptor.getFileDescriptor()); if (!haveCheckedForOtherImages)
} findOtherImages(similarImageInterface);// Do not do repeat the process
} else { } else {
String filePath = getPathOfMediaOrCopy(); useImageCoords();
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);
} }
return imageObj; 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 * Find other images around the same location that were taken within the last 20 sec
* * @param similarImageInterface
*/ */
private void findOtherImages() { private void findOtherImages(SimilarImageInterface similarImageInterface) {
Timber.d("filePath" + getPathOfMediaOrCopy()); Timber.d("filePath" + filePath);
long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created
File folder = new File(filePath.substring(0, filePath.lastIndexOf('/'))); 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 tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos
ParcelFileDescriptor descriptor = null; ParcelFileDescriptor descriptor = null;
try { try {
descriptor = contentResolver.openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r"); descriptor = contentResolver.openFileDescriptor(Uri.fromFile(file), "r");
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -173,12 +128,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
if (tempImageObj.getCoords() != null && tempImageObj.imageCoordsExists) { if (tempImageObj.getCoords() != null && tempImageObj.imageCoordsExists) {
// Current image has gps coordinates and it's not current gps locaiton // Current image has gps coordinates and it's not current gps locaiton
Timber.d("This file has image coords:" + file.getAbsolutePath()); Timber.d("This file has image coords:" + file.getAbsolutePath());
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment(); similarImageInterface.showSimilarImageFragment(filePath, file.getAbsolutePath());
Bundle args = new Bundle();
args.putString("originalImagePath", filePath);
args.putString("possibleImagePath", file.getAbsolutePath());
newFragment.setArguments(args);
newFragment.show(((AppCompatActivity) context).getSupportFragmentManager(), "dialog");
break; 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 no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories
if (catListEmpty) { if (catListEmpty) {
cacheFound = false;
apiCall.request(decimalCoords) apiCall.request(decimalCoords)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
@ -223,7 +172,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
); );
Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList); Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList);
} else { } else {
cacheFound = true;
Timber.d("Cache found, setting categoryList in model to %s", displayCatList); Timber.d("Cache found, setting categoryList in model to %s", displayCatList);
gpsCategoryModel.setCategoryList(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>((Activity) context), imageMediaFilePath);
detectUnwantedPicturesAsync.execute();
}
@Override @Override
public void onPositiveResponse() { public void onPositiveResponse() {
imageObj = tempImageObj; imageObj = tempImageObj;
@ -259,4 +193,4 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
Timber.d("EXIF from imageObj"); Timber.d("EXIF from imageObj");
useImageCoords(); useImageCoords();
} }
} }

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -12,6 +13,7 @@ import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.provider.DocumentsContract; import android.provider.DocumentsContract;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -33,6 +35,8 @@ import java.util.Date;
import timber.log.Timber; import timber.log.Timber;
import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext;
public class FileUtils { 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. * 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 * @return path of copy
*/ */
@Nullable @NonNull
static String createCopyPath(ParcelFileDescriptor descriptor) { static String createExternalCopyPathAndCopy(Uri uri, ContentResolver contentResolver) throws IOException {
try { FileDescriptor fileDescriptor = contentResolver.openFileDescriptor(uri, "r").getFileDescriptor();
String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + ".jpg"; String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + "." + getFileExt(uri, contentResolver);
File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp"); File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
newFile.mkdir(); newFile.mkdir();
FileUtils.copy(descriptor.getFileDescriptor(), copyPath); FileUtils.copy(fileDescriptor, copyPath);
Timber.d("Filepath (copied): %s", copyPath); Timber.d("Filepath (copied): %s", copyPath);
return copyPath; return copyPath;
} catch (IOException e) { }
Timber.e(e);
return null; /**
} * 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)) { if ("primary".equalsIgnoreCase(type)) {
returnPath = Environment.getExternalStorageDirectory() + "/" + split[1]; returnPath = Environment.getExternalStorageDirectory() + "/" + split[1];
} }
} else if (isDownloadsDocument(uri)) { // DownloadsProvider } else if (isDownloadsDocument(uri)) { // DownloadsProvider
final String id = DocumentsContract.getDocumentId(uri); final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId( final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/document"), Long.valueOf(id)); 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 } else if (isMediaDocument(uri)) { // MediaProvider
final String docId = DocumentsContract.getDocumentId(uri); final String docId = DocumentsContract.getDocumentId(uri);
@ -166,7 +181,7 @@ public class FileUtils {
returnPath = uri.getPath(); returnPath = uri.getPath();
} }
if(returnPath == null) { if (returnPath == null) {
//fetching path may fail depending on the source URI and all hope is lost //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 //so we will create and use a copy of the file, which seems to work
String copyPath = null; String copyPath = null;
@ -304,6 +319,7 @@ public class FileUtils {
/** /**
* Read and return the content of a resource file as string. * Read and return the content of a resource file as string.
*
* @param fileName asset file's path (e.g. "/queries/nearby_query.rq") * @param fileName asset file's path (e.g. "/queries/nearby_query.rq")
* @return the content of the file * @return the content of the file
*/ */
@ -330,6 +346,7 @@ public class FileUtils {
/** /**
* Deletes files. * Deletes files.
*
* @param file context * @param file context
*/ */
public static boolean deleteFile(File file) { public static boolean deleteFile(File file) {
@ -355,7 +372,7 @@ public class FileUtils {
commonsAppDirectory.mkdir(); commonsAppDirectory.mkdir();
} }
File logsFile = new File(commonsAppDirectory,"logs.txt"); File logsFile = new File(commonsAppDirectory, "logs.txt");
if (logsFile.exists()) { if (logsFile.exists()) {
//old logs file is useless //old logs file is useless
logsFile.delete(); 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));
}
} }

View file

@ -16,11 +16,22 @@ import timber.log.Timber;
*/ */
public class GPSExtractor { public class GPSExtractor {
private ExifInterface exif; public static final GPSExtractor DUMMY= new GPSExtractor();
private double decLatitude; private double decLatitude;
private double decLongitude; private double decLongitude;
public boolean imageCoordsExists; 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). * Construct from the file descriptor of the image (only for API 24 or newer).
* @param fileDescriptor the file descriptor of the image * @param fileDescriptor the file descriptor of the image
@ -28,7 +39,8 @@ public class GPSExtractor {
@RequiresApi(24) @RequiresApi(24)
public GPSExtractor(@NonNull FileDescriptor fileDescriptor) { public GPSExtractor(@NonNull FileDescriptor fileDescriptor) {
try { try {
exif = new ExifInterface(fileDescriptor); ExifInterface exif = new ExifInterface(fileDescriptor);
processCoords(exif);
} catch (IOException | IllegalArgumentException e) { } catch (IOException | IllegalArgumentException e) {
Timber.w(e); Timber.w(e);
} }
@ -41,47 +53,53 @@ public class GPSExtractor {
*/ */
public GPSExtractor(@NonNull String path) { public GPSExtractor(@NonNull String path) {
try { try {
exif = new ExifInterface(path); ExifInterface exif = new ExifInterface(path);
processCoords(exif);
} catch (IOException | IllegalArgumentException e) { } catch (IOException | IllegalArgumentException e) {
Timber.w(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) * 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) * @return coordinates as string (needs to be passed as a String in API query)
*/ */
@Nullable @Nullable
public String getCoords() { public String getCoords() {
String latitude; if(decimalCoords!=null){
String longitude; return decimalCoords;
String latitudeRef; }else if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) {
String longitudeRef; Timber.d("Latitude: %s %s", latitude, latitudeRef);
String decimalCoords; Timber.d("Longitude: %s %s", longitude, longitudeRef);
//If image has no EXIF data and user has enabled GPS setting, get user's location decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef);
//TODO: Always return null as a temporary fix for #1599 return decimalCoords;
if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) {
return null;
} else { } else {
//If image has EXIF data, extract image coords return null;
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;
}
} }
} }

View file

@ -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);
}
}

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.upload;
import java.util.Locale; import java.util.Locale;
class Language { class Language {
private Locale locale; private Locale locale;
private boolean isSet = false; private boolean isSet = false;

Some files were not shown because too many files have changed in this diff Show more