From 02908a678b64628291a0845c67c0aba4ad3058ee Mon Sep 17 00:00:00 2001 From: neslihanturan Date: Sat, 10 Nov 2018 18:26:01 +0200 Subject: [PATCH 01/12] Main screen ui changes, fixes #725 Main screen UI overhaul (#1922) * Delete Contributions Activity content to rewrite it * Add layout for new Contributions Activity design * Bind views * Override auth cookie required * Add tabs and fragments method * Create ContributionsFragment which will hold ContributionsListFragment and MediaDetailsFragment * Add NearbyFragment which will hold NearbyMapFragment and NearbyListFragment * Add ContributionsActivityPagerAdapter inner class to manage view pager * Create strings will be written on tabs for contributions and nearby * Create setTabAndViewPagerSynchronisation method to sycn view pager and tab layout. If user swipe pages, tabs will also change (and vice versa) * Add theme dependent background color for Drawer Layout of activity_contributions layout file * Add theme dependent background color for tabs in main * Create Contributions Fragment structure which will hold Media Detail Fragment and Contributions List Fragment * Inifilate contributions list fragment view * Create variables and methods to reuse and create Media Detils Fragment and Contributions List Fragment which will be inside Contribution Fragment * Override cursor loader methods * set MediaDetilsView fragment or ContributionListFragment according to users state * Show details of an image when item is clicked * Add delete and retry functionality, note: not tested yet * Override media count methods * Implement onBack Pressed settings * Register and unregister datasetObservers * Add contributin list fragment * Add contribution list layout with FABs for camera and galerry * Make sure we called onAuthAcquired from fragment after is is attached * Create ContributionListViewUtils class to change visibility of views according to MediaDetailsFragment visiblity or their loading state * Make number of uploads visible if contribution list is visible and number of uploads is uploaded. Progress bar is visible if contribution list is visible and number of uploads are uploading. Both invisible if Media Details Fragment is visible * Return parent fragment instead of parent activity * GetPagerFragment instead of getActivity since currently ContributionsFragment take over responsibility from ContributionsActivity * Add contribution number next to tab text for contribution, as discussed in thread * Remove number of uploads from contributions fragment since we moved it text of tab layout * Add unread notifications asynctask to check unread notifications on background * Save latest time user notification activity viewed * Add shared preferences provider for latest notification activity visit time * Add shared preferences provider for latest notification activity visit time * Change notification icon (add blue dot) whenever a notification comes * Recover notifications state on come back to contributions list from media details fragment * Add date with year parameter to Notification class, because we will use it on comparasion of dates * Check if user visited noifications activity after last notification came * Add ation to notification icon * Add nearby custom card view class * Add card view to activity * Add a button which will be displayed when nearby permission is not granted thus closest point can't be displayed on main screen. Besides, theme dependent click styles are added to button * Add button click and permission request logic. Not: solve why location manager is null * Inject location manager to activity instead * Make card view dismissable with swip * Add preference to disable or display closest nearby location * Add a bugfix to set visibility of nearby notification cardview * Add UI modifier methods to display notifications * Modify getFromWikidataQuery method, so that based on the restunClosestResult boolean, we get only the closest nearby place, instead of fetching bunch of nearby places each time * Make inner class vaariables public to reach them out of package * Temporarily comment out icon setter methods since it crashes under API19 * Inject location manager * Register location manager accoring to permission is given, then call nearby card view updater methods * Change method calls loadAttractionsFromLocation by considering new parameter to decide between closest nearby call or an usual nearby call * Add progress bar to nearby cardview * Fix notifications string * Hide nearby card view when Media Details is visible * Change tab on nerby card view click * Add hardcoded strings to strings.xml * Move nearby activity to new nearby frament * Add fragments for nearby list and map into outer nearby fragment * Change options menu item according to tab view * Make nearby card view invisible on swipe to nearby tab * Use retained nearby fragment * Add action to list sheet button * This commit caused contrib list become invisible thus, Revert "Use retained nearby fragment" This reverts commit 86b3633b232c1c5bba6b2c506e5511bdfdab596c. * Make sure retained fragments are used for -both- nearby and contrib fragments * Remove unrelated part added because of confusion, sorry * Make sure nearbyNotificationCardView visibility works corrent * Move nearby methods from nerby activity to nearby fragment, and add a lastLocationDuplicate variable to distinguish first time location from nearby fragment and nearby notification card on contributions activity * Change activity.findViewByID lines with parentFragment.view.findViewById * Remove toolbar from nearby fragment, since contributions activity already has * Disable view pager swipe, since using map is very hard with swipe option of view pager * Place progress bar inside nearby card notification to center * Make sure using retaied nearby map fragment and nearby list fragment inside nearby fragment * Update nearby notification content on slight location updates too, if it is first update after on resume. This prevented very long time loading progress bar * Add case for no nearest pleace found, to prevent bug * Prevent a possible bug can be caused from activity already killed * Add click actions to FAB buttons in contributions list fragment. And arrange FAB margins * Try to use a new location manager instance instead of using single object for both nearby map and nearby notification card view. Because location manager has a state mechanism which is designed to be called from a single point. When I call same methods from both nearby card view notification and nearby map, next update time of map etc. gets confused. * Set radius to initial value on getFromWikidataQuery (when it is called for getting closest result to be used in nearby card view notification). Normally, algorithm increase radius, this technique works for nearby map but when it comes to finding nearest point, it can return null * Add an enum to make card view visibility more stable, however, still there is a bug. * Prevent some more nearby card view visilbility bugs, however still there is a bug * Add some nullchecks for precaution * Check nearby permission and refresh nearby view if nearby tab selected, othervise do nothing * Send user to contrib tab if permission is denied after masrhmallow * Change nearby fragment background so that progress bar is visible now * Reduce code duplicate * request location and gps permission from contribution nearby car view too * Make sure using retained fragments * Make sure Contrib list fragment is retained on orientation change * Fix NPE at options menu * Make fragment flag fancier, define it per fragment instance, instead of activity * Fix Service leak and onsavedInstance NPE errrors both occured on orientation change * Refresh nearby map on orientation change * Remove unused imports, organise logs and add comments for NearbyMapFragment class * Remove all references of nearby activity, since we don't use it anymore * Remove unused imports, organise logs and add comments for Nearby Controller * Remove unused imports, organise logs and add comments for NearbyFragment * Remove unused imports, organise logs and add comments for NearbyNotificationCardView * Change class name from Contributions Activity to Main Activity. Remove unused imports, organise logs and add comments for MainAtivity * Remove extra spaces * Remove unused imports and logs * Remove unused imports, organise logs and add comments for LocationServiceManager * Remove unused imports, organise logs and add comments for NotificationsActivity * Remove unused imports, organise logs and add comments for Contributions Fragment * bug fix nearby notification card dismiss/restore issue * Change display_nearby_notification_summary varibale with Tap here to see the nearest place that needs pictures * Add nearest place notification card dismiss toast * Fix mistake made on previous commit, while fixing conflicts * Set no data yet message invisible after contributions list is loaded * Change FAB margins, according to Josephine's review * Change FAB margins, according to Josephine's review * Change contributions list background to white, to make FAB more visible * Add infobutton with popup window next to nearby tba, to explain what does this tab do * Change hambuger icon to back arrow when media details activity visible * On back button clicked from nearby fragment, switch back to contributions fragment, instead of closing the app * Check nearby card view visibility on coming back from media details activity * Change notification icon with default vector supplied by android vector repos. If we use the one I drawn on inkscape, produced vector is not compatible with API level 19 and below. I couldn't find a proper solution, and decided to change icon * Fix a possible NPE, caused by loation manager has Main activity reference after it is destroyed * Change hardcoded string with var from string xml * Make sure you listen storage permissions from contribution list framgent FABs * Make sure you listened storage permissions for Neaby fragment buttons too * Check NPEs causing crashes. Now it does not crash after coming back from settings activity * Make notification icon compatible with NearbyFragment.java} | 783 +++++++++--------- .../commons/nearby/NearbyListFragment.java | 2 +- .../nrw/commons/nearby/NearbyMapFragment.java | 150 ++-- .../nearby/NearbyNoificationCardView.java | 278 +++++++ .../free/nrw/commons/nearby/NearbyPlaces.java | 20 +- .../commons/notification/Notification.java | 4 +- .../notification/NotificationActivity.java | 8 +- .../notification/NotificationUtils.java | 10 +- .../UnreadNotificationsCheckAsync.java | 81 ++ .../nrw/commons/quiz/QuizResultActivity.java | 16 +- .../commons/theme/NavigationBaseActivity.java | 30 +- .../upload/DetectUnwantedPicturesAsync.java | 6 +- .../nrw/commons/upload/ExistingFileAsync.java | 6 +- .../nrw/commons/upload/UploadService.java | 6 +- .../utils/ContributionListViewUtils.java | 39 + .../nrw/commons/utils/PermissionUtils.java | 5 + .../fr/free/nrw/commons/utils/ViewUtil.java | 13 + .../res/drawable-hdpi/ic_arrow_back_white.png | Bin 0 -> 197 bytes .../ic_notification_white_clip_art.png | Bin 0 -> 312 bytes .../ic_notification_white_clip_art_dot.png | Bin 0 -> 429 bytes .../res/drawable-mdpi/ic_arrow_back_white.png | Bin 0 -> 136 bytes .../ic_notification_white_clip_art.png | Bin 0 -> 233 bytes .../ic_notification_white_clip_art_dot.png | Bin 0 -> 2206 bytes .../drawable-xhdpi/ic_arrow_back_white.png | Bin 0 -> 233 bytes .../ic_notification_white_clip_art.png | Bin 0 -> 410 bytes .../ic_notification_white_clip_art_dot.png | Bin 0 -> 4895 bytes .../drawable-xxhdpi/ic_arrow_back_white.png | Bin 0 -> 355 bytes .../ic_notification_white_clip_art.png | Bin 0 -> 608 bytes .../ic_notification_white_clip_art_dot.png | Bin 0 -> 7817 bytes .../drawable-xxxhdpi/ic_arrow_back_white.png | Bin 0 -> 398 bytes .../ic_notification_white_clip_art.png | Bin 0 -> 839 bytes .../ic_notification_white_clip_art_dot.png | Bin 0 -> 10898 bytes .../res/drawable/ic_location_white_24dp.xml | 5 + .../ic_notifications_active_white_24dp.xml | 5 + .../drawable/ic_notifications_white_24dp.xml | 6 + .../res/layout/activity_contributions.xml | 35 +- .../res/layout/custom_nearby_tab_layout.xml | 24 + .../res/layout/fragment_contributions.xml | 61 +- .../layout/fragment_contributions_list.xml | 107 +++ app/src/main/res/layout/fragment_nearby.xml | 170 +++- .../main/res/layout/fragment_nearby_list.xml | 13 + app/src/main/res/layout/nearby_card_view.xml | 96 +++ .../res/layout/nearby_info_popup_layout.xml | 19 + ...ontribution_activity_notification_menu.xml | 14 + app/src/main/res/menu/drawer.xml | 5 - .../res/menu/fragment_contributions_list.xml | 34 - app/src/main/res/values/attrs.xml | 6 + app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 14 +- app/src/main/res/values/styles.xml | 18 + app/src/main/res/xml/preferences.xml | 6 + 67 files changed, 2949 insertions(+), 1186 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/contributions/UnswipableViewPager.java rename app/src/main/java/fr/free/nrw/commons/nearby/{NearbyActivity.java => NearbyFragment.java} (65%) create mode 100644 app/src/main/java/fr/free/nrw/commons/nearby/NearbyNoificationCardView.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/UnreadNotificationsCheckAsync.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ContributionListViewUtils.java create mode 100644 app/src/main/res/drawable-hdpi/ic_arrow_back_white.png create mode 100644 app/src/main/res/drawable-hdpi/ic_notification_white_clip_art.png create mode 100644 app/src/main/res/drawable-hdpi/ic_notification_white_clip_art_dot.png create mode 100644 app/src/main/res/drawable-mdpi/ic_arrow_back_white.png create mode 100644 app/src/main/res/drawable-mdpi/ic_notification_white_clip_art.png create mode 100644 app/src/main/res/drawable-mdpi/ic_notification_white_clip_art_dot.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_arrow_back_white.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art_dot.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art_dot.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art_dot.png create mode 100644 app/src/main/res/drawable/ic_location_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_notifications_active_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_notifications_white_24dp.xml create mode 100644 app/src/main/res/layout/custom_nearby_tab_layout.xml create mode 100644 app/src/main/res/layout/fragment_contributions_list.xml create mode 100644 app/src/main/res/layout/fragment_nearby_list.xml create mode 100644 app/src/main/res/layout/nearby_card_view.xml create mode 100644 app/src/main/res/layout/nearby_info_popup_layout.xml create mode 100644 app/src/main/res/menu/contribution_activity_notification_menu.xml delete mode 100644 app/src/main/res/menu/fragment_contributions_list.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2d9c9a438..6cbead479 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -69,7 +69,7 @@ @@ -80,17 +80,12 @@ + android:parentActivityName=".contributions.MainActivity" /> - - @@ -104,18 +99,18 @@ + android:parentActivityName=".contributions.MainActivity" /> + android:parentActivityName=".contributions.MainActivity" /> , - AdapterView.OnItemClickListener, - MediaDetailPagerFragment.MediaDetailProvider, - FragmentManager.OnBackStackChangedListener, - ContributionsListFragment.SourceRefresher { - - @Inject MediaWikiApi mediaWikiApi; - @Inject SessionManager sessionManager; - @Inject @Named("default_preferences") SharedPreferences prefs; - @Inject ContributionDao contributionDao; - - private Cursor allContributions; - private ContributionsListFragment contributionsList; - private MediaDetailPagerFragment mediaDetails; - private UploadService uploadService; - private boolean isUploadServiceConnected; - private ArrayList observersWaitingForLoad = new ArrayList<>(); - - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - - private ServiceConnection uploadServiceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName componentName, IBinder binder) { - uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder) - .getService(); - isUploadServiceConnected = true; - } - - @Override - public void onServiceDisconnected(ComponentName componentName) { - // this should never happen - Timber.e(new RuntimeException("UploadService died but the rest of the process did not!")); - } - }; - - @Override - protected void onDestroy() { - compositeDisposable.clear(); - getSupportFragmentManager().removeOnBackStackChangedListener(this); - super.onDestroy(); - if (isUploadServiceConnected) { - unbindService(uploadServiceConnection); - } - } - - @Override - protected void onResume() { - super.onResume(); - boolean isSettingsChanged = prefs.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false); - prefs.edit().putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false).apply(); - if (isSettingsChanged) { - refreshSource(); - } - } - - @Override - protected void onAuthCookieAcquired(String authCookie) { - // Do a sync everytime we get here! - requestSync(sessionManager.getCurrentAccount(), BuildConfig.CONTRIBUTION_AUTHORITY, new Bundle()); - Intent uploadServiceIntent = new Intent(this, UploadService.class); - uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); - startService(uploadServiceIntent); - bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE); - - allContributions = contributionDao.loadAllContributions(); - - getSupportLoaderManager().initLoader(0, null, this); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_contributions); - ButterKnife.bind(this); - - // Activity can call methods in the fragment by acquiring a - // reference to the Fragment from FragmentManager, using findFragmentById() - FragmentManager supportFragmentManager = getSupportFragmentManager(); - contributionsList = (ContributionsListFragment)supportFragmentManager - .findFragmentById(R.id.contributionsListFragment); - - supportFragmentManager.addOnBackStackChangedListener(this); - if (savedInstanceState != null) { - mediaDetails = (MediaDetailPagerFragment)supportFragmentManager - .findFragmentById(R.id.contributionsFragmentContainer); - - getSupportLoaderManager().initLoader(0, null, this); - } - - requestAuthToken(); - initDrawer(); - setTitle(getString(R.string.title_activity_contributions)); - - - if (checkAccount()) { - new QuizChecker(this, - sessionManager.getCurrentAccount().name, - mediaWikiApi); - } - if (!BuildConfig.FLAVOR.equalsIgnoreCase("beta")){ - setUploadCount(); - } - - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - boolean mediaDetailsVisible = mediaDetails != null && mediaDetails.isVisible(); - outState.putBoolean("mediaDetailsVisible", mediaDetailsVisible); - } - - /** - * Replace whatever is in the current contributionsFragmentContainer view with - * mediaDetailPagerFragment, and preserve previous state in back stack. - * Called when user selects a contribution. - */ - private void showDetail(int i) { - if (mediaDetails == null || !mediaDetails.isVisible()) { - mediaDetails = new MediaDetailPagerFragment(); - FragmentManager supportFragmentManager = getSupportFragmentManager(); - supportFragmentManager - .beginTransaction() - .replace(R.id.contributionsFragmentContainer, mediaDetails) - .addToBackStack(null) - .commit(); - supportFragmentManager.executePendingTransactions(); - } - mediaDetails.showImage(i); - } - - public void retryUpload(int i) { - allContributions.moveToPosition(i); - Contribution c = contributionDao.fromCursor(allContributions); - if (c.getState() == STATE_FAILED) { - uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c); - Timber.d("Restarting for %s", c.toString()); - } else { - Timber.d("Skipping re-upload for non-failed %s", c.toString()); - } - } - - public void deleteUpload(int i) { - allContributions.moveToPosition(i); - Contribution c = contributionDao.fromCursor(allContributions); - if (c.getState() == STATE_FAILED) { - Timber.d("Deleting failed contrib %s", c.toString()); - // If upload fails and then user decides to cancel upload at all, which means contribution - // object will be deleted. So we have to delete temp file for that contribution. - ContributionUtils.removeTemporaryFile(c.getLocalUri()); - contributionDao.delete(c); - } else { - Timber.d("Skipping deletion for non-failed contrib %s", c.toString()); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - if (mediaDetails.isVisible()) { - getSupportFragmentManager().popBackStack(); - } - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - protected void onAuthFailure() { - finish(); // If authentication failed, we just exit - } - - @Override - public void onItemClick(AdapterView adapterView, View view, int position, long item) { - showDetail(position); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - return super.onCreateOptionsMenu(menu); - } - - @Override - public Loader onCreateLoader(int i, Bundle bundle) { - int uploads = prefs.getInt(UPLOADS_SHOWING, 100); - return new CursorLoader(this, BASE_URI, - ALL_FIELDS, "", null, - ContributionDao.CONTRIBUTION_SORT + "LIMIT " + uploads); - } - - @Override - public void onLoadFinished(Loader cursorLoader, Cursor cursor) { - contributionsList.changeProgressBarVisibility(false); - - if (contributionsList.getAdapter() == null) { - contributionsList.setAdapter(new ContributionsListAdapter(getApplicationContext(), - cursor, 0, contributionDao)); - } else { - ((CursorAdapter) contributionsList.getAdapter()).swapCursor(cursor); - } - - if (contributionsList.getAdapter().getCount()>0){ - contributionsList.changeEmptyScreen(false); - } - contributionsList.clearSyncMessage(); - notifyAndMigrateDataSetObservers(); - } - - @Override - public void onLoaderReset(Loader cursorLoader) { - ((CursorAdapter) contributionsList.getAdapter()).swapCursor(null); - } - - //FIXME: Potential cause of wrong image display bug - @Override - public Media getMediaAtPosition(int i) { - if (contributionsList.getAdapter() == null) { - // not yet ready to return data - return null; - } else { - return contributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i)); - } - } - - @Override - public int getTotalMediaCount() { - if (contributionsList.getAdapter() == null) { - return 0; - } - return contributionsList.getAdapter().getCount(); - } - - @SuppressWarnings("ConstantConditions") - private void setUploadCount() { - compositeDisposable.add(mediaWikiApi - .getUploadCount(sessionManager.getCurrentAccount().name) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::displayUploadCount, - t -> Timber.e(t, "Fetching upload count failed") - )); - } - - private void displayUploadCount(Integer uploadCount) { - if (isFinishing() - || getSupportActionBar() == null - || getResources() == null) { - return; - } - - getSupportActionBar().setSubtitle(getResources() - .getQuantityString(R.plurals.contributions_subtitle, - uploadCount, uploadCount)); - } - - public void betaSetUploadCount(int betaUploadCount) { - displayUploadCount(betaUploadCount); - } - - - @Override - public void notifyDatasetChanged() { - // Do nothing for now - } - - private void notifyAndMigrateDataSetObservers() { - Adapter adapter = contributionsList.getAdapter(); - - // First, move the observers over to the adapter now that we have it. - for (DataSetObserver observer : observersWaitingForLoad) { - adapter.registerDataSetObserver(observer); - } - observersWaitingForLoad.clear(); - - // Now fire off a first notification... - for (DataSetObserver observer : observersWaitingForLoad) { - observer.onChanged(); - } - } - - @Override - public void registerDataSetObserver(DataSetObserver observer) { - Adapter adapter = contributionsList.getAdapter(); - if (adapter == null) { - observersWaitingForLoad.add(observer); - } else { - adapter.registerDataSetObserver(observer); - } - } - - @Override - public void unregisterDataSetObserver(DataSetObserver observer) { - Adapter adapter = contributionsList.getAdapter(); - if (adapter == null) { - observersWaitingForLoad.remove(observer); - } else { - adapter.unregisterDataSetObserver(observer); - } - } - - /** - * to ensure user is logged in - * @return - */ - private boolean checkAccount() { - Account currentAccount = sessionManager.getCurrentAccount(); - if (currentAccount == null) { - Timber.d("Current account is null"); - ViewUtil.showLongToast(this, getResources().getString(R.string.user_not_logged_in)); - sessionManager.forceLogin(this); - return false; - } - return true; - } - - @Override - public void onBackStackChanged() { - initBackButton(); - } - - @Override - public void refreshSource() { - getSupportLoaderManager().restartLoader(0, null, this); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java new file mode 100644 index 000000000..962a24ce3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -0,0 +1,626 @@ +package fr.free.nrw.commons.contributions; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; + +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.app.LoaderManager; +import android.support.v4.widget.CursorAdapter; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Adapter; +import android.widget.AdapterView; + +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; + +import javax.inject.Inject; +import javax.inject.Named; + +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.HandlerService; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.location.LocationServiceManager; +import fr.free.nrw.commons.location.LocationUpdateListener; +import fr.free.nrw.commons.media.MediaDetailPagerFragment; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.nearby.NearbyController; +import fr.free.nrw.commons.nearby.NearbyNoificationCardView; +import fr.free.nrw.commons.nearby.Place; +import fr.free.nrw.commons.notification.NotificationController; +import fr.free.nrw.commons.notification.UnreadNotificationsCheckAsync; +import fr.free.nrw.commons.settings.Prefs; +import fr.free.nrw.commons.upload.UploadService; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; +import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS; +import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; +import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUEST; +import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING; +import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; + +public class ContributionsFragment + extends CommonsDaggerSupportFragment + implements LoaderManager.LoaderCallbacks, + AdapterView.OnItemClickListener, + MediaDetailPagerFragment.MediaDetailProvider, + FragmentManager.OnBackStackChangedListener, + ContributionsListFragment.SourceRefresher, + LocationUpdateListener + { + @Inject + @Named("default_preferences") + SharedPreferences prefs; + @Inject + ContributionDao contributionDao; + @Inject + MediaWikiApi mediaWikiApi; + @Inject + NotificationController notificationController; + @Inject + NearbyController nearbyController; + + private ArrayList observersWaitingForLoad = new ArrayList<>(); + private Cursor allContributions; + private UploadService uploadService; + private boolean isUploadServiceConnected; + private CompositeDisposable compositeDisposable = new CompositeDisposable(); + CountDownLatch waitForContributionsListFragment = new CountDownLatch(1); + + private ContributionsListFragment contributionsListFragment; + private MediaDetailPagerFragment mediaDetailPagerFragment; + public static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag"; + public static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag"; + + public NearbyNoificationCardView nearbyNoificationCardView; + private Disposable placesDisposable; + private LatLng curLatLng; + + private boolean firstLocationUpdate = true; + private LocationServiceManager locationManager; + + private boolean isFragmentAttachedBefore = false; + + + /** + * Since we will need to use parent activity on onAuthCookieAcquired, we have to wait + * fragment to be attached. Latch will be responsible for this sync. + */ + private ServiceConnection uploadServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName componentName, IBinder binder) { + uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder) + .getService(); + isUploadServiceConnected = true; + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + // this should never happen + Timber.e(new RuntimeException("UploadService died but the rest of the process did not!")); + } + }; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_contributions, container, false); + nearbyNoificationCardView = view.findViewById(R.id.card_view_nearby); + + if (savedInstanceState != null) { + mediaDetailPagerFragment = (MediaDetailPagerFragment)getChildFragmentManager().findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG); + contributionsListFragment = (ContributionsListFragment) getChildFragmentManager().findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG); + + if (savedInstanceState.getBoolean("mediaDetailsVisible")) { + setMediaDetailPagerFragment(); + } else { + setContributionsListFragment(); + } + } else { + setContributionsListFragment(); + } + + if(!BuildConfig.FLAVOR.equalsIgnoreCase("beta")){ + setUploadCount(); + } + + return view; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + /* + - There are some operations we need auth, so we need to make sure isAuthCookieAcquired. + - And since we use same retained fragment doesn't want to make all network operations + all over again on same fragment attached to recreated activity, we do this network + operations on first time fragment atached to an activity. Then they will be retained + until fragment life time ends. + */ + if (((MainActivity)getActivity()).isAuthCookieAcquired && !isFragmentAttachedBefore) { + onAuthCookieAcquired(((MainActivity)getActivity()).uploadServiceIntent); + isFragmentAttachedBefore = true; + new UnreadNotificationsCheckAsync((MainActivity) getActivity(), notificationController).execute(); + + } + } + + /** + * Replace FrameLayout with ContributionsListFragment, user will see contributions list. + * Creates new one if null. + */ + public void setContributionsListFragment() { + // show tabs on contribution list is visible + ((MainActivity)getActivity()).showTabs(); + // show nearby card view on contributions list is visible + if (nearbyNoificationCardView != null) { + if (prefs.getBoolean("displayNearbyCardView", true)) { + nearbyNoificationCardView.setVisibility(View.VISIBLE); + } else { + nearbyNoificationCardView.setVisibility(View.GONE); + } + } + + // Create if null + if (getContributionsListFragment() == null) { + contributionsListFragment = new ContributionsListFragment(); + } + FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); + // When this container fragment is created, we fill it with our ContributionsListFragment + transaction.replace(R.id.root_frame, contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG); + transaction.addToBackStack(CONTRIBUTION_LIST_FRAGMENT_TAG); + transaction.commit(); + getChildFragmentManager().executePendingTransactions(); + } + + /** + * Replace FrameLayout with MediaDetailPagerFragment, user will see details of selected media. + * Creates new one if null. + */ + public void setMediaDetailPagerFragment() { + // hide tabs on media detail view is visible + ((MainActivity)getActivity()).hideTabs(); + // hide nearby card view on media detail is visible + nearbyNoificationCardView.setVisibility(View.GONE); + + // Create if null + if (getMediaDetailPagerFragment() == null) { + mediaDetailPagerFragment = new MediaDetailPagerFragment(); + } + FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); + // When this container fragment is created, we fill it with our MediaDetailPagerFragment + //transaction.addToBackStack(null); + transaction.add(R.id.root_frame, mediaDetailPagerFragment, MEDIA_DETAIL_PAGER_FRAGMENT_TAG); + transaction.addToBackStack(MEDIA_DETAIL_PAGER_FRAGMENT_TAG); + transaction.commit(); + getChildFragmentManager().executePendingTransactions(); + + } + + /** + * Just getter method of ContributionsListFragment child of ContributionsFragment + * @return contributionsListFragment, if any created + */ + public ContributionsListFragment getContributionsListFragment() { + return contributionsListFragment; + } + + /** + * Just getter method of MediaDetailPagerFragment child of ContributionsFragment + * @return mediaDetailsFragment, if any created + */ + public MediaDetailPagerFragment getMediaDetailPagerFragment() { + return mediaDetailPagerFragment; + } + + @Override + public void onBackStackChanged() { + ((MainActivity)getActivity()).initBackButton(); + } + + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + int uploads = prefs.getInt(UPLOADS_SHOWING, 100); + return new CursorLoader(getActivity(), BASE_URI, //TODO find out the reason we pass activity here + ALL_FIELDS, "", null, + ContributionDao.CONTRIBUTION_SORT + "LIMIT " + uploads); + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + if (contributionsListFragment != null) { + contributionsListFragment.changeProgressBarVisibility(false); + + if (contributionsListFragment.getAdapter() == null) { + contributionsListFragment.setAdapter(new ContributionsListAdapter(getActivity().getApplicationContext(), + cursor, 0, contributionDao)); + } else { + ((CursorAdapter) contributionsListFragment.getAdapter()).swapCursor(cursor); + } + + contributionsListFragment.clearSyncMessage(); + notifyAndMigrateDataSetObservers(); + } + } + + @Override + public void onLoaderReset(Loader cursorLoader) { + ((CursorAdapter) contributionsListFragment.getAdapter()).swapCursor(null); + } + + private void notifyAndMigrateDataSetObservers() { + Adapter adapter = contributionsListFragment.getAdapter(); + + // First, move the observers over to the adapter now that we have it. + for (DataSetObserver observer : observersWaitingForLoad) { + adapter.registerDataSetObserver(observer); + } + observersWaitingForLoad.clear(); + + // Now fire off a first notification... + for (DataSetObserver observer : observersWaitingForLoad) { + observer.onChanged(); + } + } + + /** + * Called when onAuthCookieAcquired is called on authenticated parent activity + * @param uploadServiceIntent + */ + public void onAuthCookieAcquired(Intent uploadServiceIntent) { + // Since we call onAuthCookieAcquired method from onAttach, isAdded is still false. So don't use it + + if (getActivity() != null) { // If fragment is attached to parent activity + getActivity().bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE); + isUploadServiceConnected = true; + allContributions = contributionDao.loadAllContributions(); + getActivity().getSupportLoaderManager().initLoader(0, null, ContributionsFragment.this); + } + + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case LOCATION_REQUEST: { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Timber.d("Location permission granted, refreshing view"); + // No need to display permission request button anymore + nearbyNoificationCardView.displayPermissionRequestButton(false); + locationManager.registerLocationManager(); + } else { + // Still ask for permission + nearbyNoificationCardView.displayPermissionRequestButton(true); + } + } + break; + + default: + // This is needed to allow the request codes from the Fragments to be routed appropriately + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + @Override + public void onItemClick(AdapterView adapterView, View view, int i, long l) { + // show detail at a position + showDetail(i); + } + + + /** + * Replace whatever is in the current contributionsFragmentContainer view with + * mediaDetailPagerFragment, and preserve previous state in back stack. + * Called when user selects a contribution. + */ + private void showDetail(int i) { + if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) { + mediaDetailPagerFragment = new MediaDetailPagerFragment(); + setMediaDetailPagerFragment(); + } + mediaDetailPagerFragment.showImage(i); + } + + /** + * Retry upload when it is failed + * @param i position of upload which will be retried + */ + public void retryUpload(int i) { + allContributions.moveToPosition(i); + Contribution c = contributionDao.fromCursor(allContributions); + if (c.getState() == STATE_FAILED) { + uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c); + Timber.d("Restarting for %s", c.toString()); + } else { + Timber.d("Skipping re-upload for non-failed %s", c.toString()); + } + } + + /** + * Delete a failed upload attempt + * @param i position of upload attempt which will be deteled + */ + public void deleteUpload(int i) { + allContributions.moveToPosition(i); + Contribution c = contributionDao.fromCursor(allContributions); + if (c.getState() == STATE_FAILED) { + Timber.d("Deleting failed contrib %s", c.toString()); + contributionDao.delete(c); + } else { + Timber.d("Skipping deletion for non-failed contrib %s", c.toString()); + } + } + + @Override + public void refreshSource() { + getActivity().getSupportLoaderManager().restartLoader(0, null, this); + } + + @Override + public Media getMediaAtPosition(int i) { + if (contributionsListFragment.getAdapter() == null) { + // not yet ready to return data + return null; + } else { + return contributionDao.fromCursor((Cursor) contributionsListFragment.getAdapter().getItem(i)); + } + } + + @Override + public int getTotalMediaCount() { + if (contributionsListFragment.getAdapter() == null) { + return 0; + } + return contributionsListFragment.getAdapter().getCount(); + } + + @Override + public void notifyDatasetChanged() { + + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + Adapter adapter = contributionsListFragment.getAdapter(); + if (adapter == null) { + observersWaitingForLoad.add(observer); + } else { + adapter.registerDataSetObserver(observer); + } + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + Adapter adapter = contributionsListFragment.getAdapter(); + if (adapter == null) { + observersWaitingForLoad.remove(observer); + } else { + adapter.unregisterDataSetObserver(observer); + } + } + + @SuppressWarnings("ConstantConditions") + private void setUploadCount() { + + compositeDisposable.add(mediaWikiApi + .getUploadCount(((MainActivity)getActivity()).sessionManager.getCurrentAccount().name) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::displayUploadCount, + t -> Timber.e(t, "Fetching upload count failed") + )); + } + + private void displayUploadCount(Integer uploadCount) { + if (getActivity().isFinishing() + || getResources() == null) { + return; + } + + ((MainActivity)getActivity()).setNumOfUploads(uploadCount); + + } + + public void betaSetUploadCount(int betaUploadCount) { + displayUploadCount(betaUploadCount); + } + + /** + * Updates notification indicator on toolbar to indicate there are unread notifications + * @param isThereUnreadNotifications true if user checked notifications before last notification date + */ + public void updateNotificationsNotification(boolean isThereUnreadNotifications) { + ((MainActivity)getActivity()).updateNotificationIcon(isThereUnreadNotifications); + } + + @Override + public void onStart() { + super.onStart(); + } + + @Override + public void onPause() { + super.onPause(); + locationManager.removeLocationListener(this); + locationManager.unregisterLocationManager(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + boolean mediaDetailsVisible = mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible(); + outState.putBoolean("mediaDetailsVisible", mediaDetailsVisible); + } + + @Override + public void onResume() { + super.onResume(); + locationManager = new LocationServiceManager(getActivity()); + + firstLocationUpdate = true; + locationManager.addLocationListener(this); + + boolean isSettingsChanged = prefs.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false); + prefs.edit().putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false).apply(); + if (isSettingsChanged) { + refreshSource(); + } + + + if (prefs.getBoolean("displayNearbyCardView", true)) { + nearbyNoificationCardView.cardViewVisibilityState = NearbyNoificationCardView.CardViewVisibilityState.LOADING; + nearbyNoificationCardView.setVisibility(View.VISIBLE); + checkGPS(); + + } else { + // Hide nearby notification card view if related shared preferences is false + nearbyNoificationCardView.setVisibility(View.GONE); + } + + + } + + + /** + * Check GPS to decide displaying request permission button or not. + */ + private void checkGPS() { + if (!locationManager.isProviderEnabled()) { + Timber.d("GPS is not enabled"); + nearbyNoificationCardView.permissionType = NearbyNoificationCardView.PermissionType.ENABLE_GPS; + nearbyNoificationCardView.displayPermissionRequestButton(true); + } else { + Timber.d("GPS is enabled"); + checkLocationPermission(); + } + } + + private void checkLocationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (locationManager.isLocationPermissionGranted()) { + nearbyNoificationCardView.permissionType = NearbyNoificationCardView.PermissionType.NO_PERMISSION_NEEDED; + nearbyNoificationCardView.displayPermissionRequestButton(false); + locationManager.registerLocationManager(); + } else { + nearbyNoificationCardView.permissionType = NearbyNoificationCardView.PermissionType.ENABLE_LOCATION_PERMISSON; + nearbyNoificationCardView.displayPermissionRequestButton(true); + } + } else { + // If device is under Marshmallow, we already checked for GPS + nearbyNoificationCardView.permissionType = NearbyNoificationCardView.PermissionType.NO_PERMISSION_NEEDED; + nearbyNoificationCardView.displayPermissionRequestButton(false); + locationManager.registerLocationManager(); + } + } + + + private void updateClosestNearbyCardViewInfo() { + + curLatLng = locationManager.getLastLocation(); + + placesDisposable = Observable.fromCallable(() -> nearbyController + .loadAttractionsFromLocation(curLatLng, true)) // thanks to boolean, it will only return closest result + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::updateNearbyNotification, + throwable -> { + Timber.d(throwable); + updateNearbyNotification(null); + }); + } + + private void updateNearbyNotification(@Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { + + if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null && nearbyPlacesInfo.placeList.size() > 0) { + Place closestNearbyPlace = nearbyPlacesInfo.placeList.get(0); + String distance = formatDistanceBetween(curLatLng, closestNearbyPlace.location); + closestNearbyPlace.setDistance(distance); + nearbyNoificationCardView.updateContent (true, closestNearbyPlace); + } else { + // Means that no close nearby place is found + nearbyNoificationCardView.updateContent (false, null); + } + } + + @Override + public void onDestroy() { + compositeDisposable.clear(); + getChildFragmentManager().removeOnBackStackChangedListener(this); + locationManager.unregisterLocationManager(); + locationManager.removeLocationListener(this); + // Try to prevent a possible NPE + locationManager.context = null; + super.onDestroy(); + + if (isUploadServiceConnected) { + if (getActivity() != null) { + getActivity().unbindService(uploadServiceConnection); + isUploadServiceConnected = false; + } + } + + if (placesDisposable != null) { + placesDisposable.dispose(); + } + } + + @Override + public void onLocationChangedSignificantly(LatLng latLng) { + // Will be called if location changed more than 1000 meter + // Do nothing on slight changes for using network efficiently + firstLocationUpdate = false; + updateClosestNearbyCardViewInfo(); + } + + @Override + public void onLocationChangedSlightly(LatLng latLng) { + /* Update closest nearby notification card onLocationChangedSlightly + If first time to update location after onResume, then no need to wait for significant + location change. Any closest location is better than no location + */ + if (firstLocationUpdate) { + updateClosestNearbyCardViewInfo(); + // Turn it to false, since it is not first location update anymore. To change closest location + // notifiction, we need to wait for a significant location change. + firstLocationUpdate = false; + } + } + + @Override + public void onLocationChangedMedium(LatLng latLng) { + // Update closest nearby card view if location changed more than 500 meters + updateClosestNearbyCardViewInfo(); + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index a17db5e80..7a4294863 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -1,23 +1,26 @@ package fr.free.nrw.commons.contributions; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.widget.FloatingActionButton; import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; import android.widget.AdapterView; import android.widget.GridView; import android.widget.ListAdapter; import android.widget.ProgressBar; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import android.widget.TextView; import java.util.Arrays; @@ -30,15 +33,18 @@ import butterknife.ButterKnife; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.R; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.nearby.NearbyActivity; +import fr.free.nrw.commons.utils.PermissionUtils; import timber.log.Timber; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; import static android.app.Activity.RESULT_OK; -import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.view.View.GONE; +/** + * Created by root on 01.06.2018. + */ + public class ContributionsListFragment extends CommonsDaggerSupportFragment { @BindView(R.id.contributionsList) @@ -47,101 +53,122 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { TextView waitingMessage; @BindView(R.id.loadingContributionsProgressBar) ProgressBar progressBar; + @BindView(R.id.fab_plus) + FloatingActionButton fabPlus; + @BindView(R.id.fab_camera) + FloatingActionButton fabCamera; + @BindView(R.id.fab_galery) + FloatingActionButton fabGalery; @BindView(R.id.noDataYet) TextView noDataYet; - @Inject - @Named("prefs") - SharedPreferences prefs; @Inject @Named("default_preferences") SharedPreferences defaultPrefs; - private ContributionController controller; + private Animation fab_close; + private Animation fab_open; + private Animation rotate_forward; + private Animation rotate_backward; - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.fragment_contributions, container, false); - ButterKnife.bind(this, v); + private boolean isFabOpen = false; + public ContributionController controller; - contributionsList.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity()); - if (savedInstanceState != null) { - Timber.d("Scrolling to %d", savedInstanceState.getInt("grid-position")); - contributionsList.setSelection(savedInstanceState.getInt("grid-position")); - } + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_contributions_list, container, false); + ButterKnife.bind(this, view); - //TODO: Should this be in onResume? - String lastModified = prefs.getString("lastSyncTimestamp", ""); - Timber.d("Last Sync Timestamp: %s", lastModified); - - if (lastModified.equals("")) { - waitingMessage.setVisibility(View.VISIBLE); - } else { - waitingMessage.setVisibility(GONE); - } + contributionsList.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment()); changeEmptyScreen(true); changeProgressBarVisibility(true); - return v; + return view; } - public ListAdapter getAdapter() { - return contributionsList.getAdapter(); - } - - public void setAdapter(ListAdapter adapter) { - this.contributionsList.setAdapter(adapter); - - if (BuildConfig.FLAVOR.equalsIgnoreCase("beta")){ - ((ContributionsActivity) getActivity()).betaSetUploadCount(adapter.getCount()); + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (controller == null) { + controller = new ContributionController(this); } + controller.loadState(savedInstanceState); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (controller != null) { + controller.saveState(outState); + } else { + controller = new ContributionController(this); + } + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + initializeAnimations(); + setListeners(); } public void changeEmptyScreen(boolean isEmpty){ this.noDataYet.setVisibility(isEmpty ? View.VISIBLE : View.GONE); } - public void changeProgressBarVisibility(boolean isVisible) { - this.progressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE); + private void initializeAnimations() { + fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open); + fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close); + rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward); + rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward); } - @Override - public void onSaveInstanceState(Bundle outState) { - if (outState == null) { - outState = new Bundle(); - } - super.onSaveInstanceState(outState); - controller.saveState(outState); - outState.putInt("grid-position", contributionsList.getFirstVisiblePosition()); - } + private void setListeners() { - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - //FIXME: must get the file data for Google Photos when receive the intent answer, in the onActivityResult method - super.onActivityResult(requestCode, resultCode, data); - - if (resultCode == RESULT_OK) { - Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", - requestCode, resultCode, data); - if (requestCode == ContributionController.SELECT_FROM_CAMERA) { - // If coming from camera, pass null as uri. Because camera photos get saved to a - // fixed directory - controller.handleImagePicked(requestCode, null, false, null); - } else { - controller.handleImagePicked(requestCode, data.getData(), false, null); + fabPlus.setOnClickListener(view -> animateFAB(isFabOpen)); + fabCamera.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) { + // Here, thisActivity is the current activity + if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + if (shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { + // Show an explanation to the user *asynchronously* -- don't block + // this thread waiting for the user's response! After the user + // sees the explanation, try again to request the permission. + new AlertDialog.Builder(getParentFragment().getActivity()) + .setMessage(getString(R.string.write_storage_permission_rationale)) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + getActivity().requestPermissions + (new String[]{WRITE_EXTERNAL_STORAGE}, PermissionUtils.CAMERA_PERMISSION_FROM_CONTRIBUTION_LIST); + dialog.dismiss(); + }) + .setNegativeButton(android.R.string.cancel, null) + .create() + .show(); + } else { + // No explanation needed, we can request the permission. + requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, + 3); + // MY_PERMISSIONS_WRITE_EXTERNAL_STORAGE is an + // app-defined int constant. The callback method gets the + // result of the request. + } + } else { + controller.startCameraCapture(); + } + } else { + controller.startCameraCapture(); + } } - } else { - Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", - requestCode, resultCode, data); - } - } + }); - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_from_gallery: - //Gallery crashes before reach ShareActivity screen so must implement permissions check here + fabGalery.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + //Gallery crashes before reach ShareActivity screen so must implement permissions check here if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Here, thisActivity is the current activity @@ -156,10 +183,11 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { // this thread waiting for the user's response! After the user // sees the explanation, try again to request the permission. - new AlertDialog.Builder(getActivity()) + new AlertDialog.Builder(getParentFragment().getActivity()) .setMessage(getString(R.string.read_storage_permission_rationale)) .setPositiveButton(android.R.string.ok, (dialog, which) -> { - requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, 1); + getActivity().requestPermissions + (new String[]{READ_EXTERNAL_STORAGE}, PermissionUtils.GALLERY_PERMISSION_FROM_CONTRIBUTION_LIST); dialog.dismiss(); }) .setNegativeButton(android.R.string.cancel, null) @@ -179,59 +207,62 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { } } else { controller.startGalleryPick(); - return true; } } else { controller.startGalleryPick(); - return true; } + } + }); + } - return true; - case R.id.menu_from_camera: - boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) { - // Here, thisActivity is the current activity - if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - if (shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { - // Show an explanation to the user *asynchronously* -- don't block - // this thread waiting for the user's response! After the user - // sees the explanation, try again to request the permission. - new AlertDialog.Builder(getActivity()) - .setMessage(getString(R.string.write_storage_permission_rationale)) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 3); - dialog.dismiss(); - }) - .setNegativeButton(android.R.string.cancel, null) - .create() - .show(); - } else { - // No explanation needed, we can request the permission. - requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, - 3); - // MY_PERMISSIONS_WRITE_EXTERNAL_STORAGE is an - // app-defined int constant. The callback method gets the - // result of the request. - } - } else { - controller.startCameraCapture(); - return true; - } - } else { - controller.startCameraCapture(); - return true; - } - return true; - default: - return super.onOptionsItemSelected(item); + private void animateFAB(boolean isFabOpen) { + this.isFabOpen = !isFabOpen; + if (fabPlus.isShown()){ + if (isFabOpen) { + fabPlus.startAnimation(rotate_backward); + fabCamera.startAnimation(fab_close); + fabGalery.startAnimation(fab_close); + fabCamera.hide(); + fabGalery.hide(); + } else { + fabPlus.startAnimation(rotate_forward); + fabCamera.startAnimation(fab_open); + fabGalery.startAnimation(fab_open); + fabCamera.show(); + fabGalery.show(); + } + this.isFabOpen=!isFabOpen; } } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { + public void onAttach(Context context) { + super.onAttach(context); + ContributionsFragment parentFragment = (ContributionsFragment)getParentFragment(); + parentFragment.waitForContributionsListFragment.countDown(); + } + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK) { + Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", + requestCode, resultCode, data); + if (requestCode == ContributionController.SELECT_FROM_CAMERA) { + // If coming from camera, pass null as uri. Because camera photos get saved to a + // fixed directory + controller.handleImagePicked(requestCode, null, false, null); + } else if (requestCode == ContributionController.SELECT_FROM_GALLERY){ + controller.handleImagePicked(requestCode, data.getData(), false, null); + } + } else { + Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", + requestCode, resultCode, data); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); Timber.d("onRequestPermissionsResult: req code = " + " perm = " + Arrays.toString(permissions) + " grant =" + Arrays.toString(grantResults)); @@ -246,11 +277,12 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { break; // 2 = Location allowed when 'nearby places' selected case 2: { - if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) { + // TODO: understand and fix + /*if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) { Timber.d("Location permission granted"); - Intent nearbyIntent = new Intent(getActivity(), NearbyActivity.class); + Intent nearbyIntent = new Intent(getActivity(), MainActivity.class); startActivity(nearbyIntent); - } + }*/ } break; case 3: { @@ -262,42 +294,38 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { } } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - menu.clear(); // See http://stackoverflow.com/a/8495697/17865 - inflater.inflate(R.menu.fragment_contributions_list, menu); - if (!deviceHasCamera()) { - menu.findItem(R.id.menu_from_camera).setEnabled(false); - } - } - - public boolean deviceHasCamera() { - PackageManager pm = getContext().getPackageManager(); - return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) || - pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - controller = new ContributionController(this); - setHasOptionsMenu(true); - } - - @Override - public void onDestroy() { - super.onDestroy(); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - controller.loadState(savedInstanceState); + /** + * Responsible to set progress bar invisible and visible + * @param isVisible True when contributions list should be hidden. + */ + public void changeProgressBarVisibility(boolean isVisible) { + this.progressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE); } + /** + * Clears sync message displayed with progress bar before contributions list became visible + */ protected void clearSyncMessage() { waitingMessage.setVisibility(GONE); + noDataYet.setVisibility(GONE); + } + + public ListAdapter getAdapter() { + return contributionsList.getAdapter(); + } + + /** + * Sets adapter to contributions list. If beta mode, sets upload count for beta explicitly. + * @param adapter List adapter for uploads of contributor + */ + public void setAdapter(ListAdapter adapter) { + this.contributionsList.setAdapter(adapter); + + if(BuildConfig.FLAVOR.equalsIgnoreCase("beta")){ + //TODO: add betaSetUploadCount method + ((ContributionsFragment) getParentFragment()).betaSetUploadCount(adapter.getCount()); + } } public interface SourceRefresher { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java new file mode 100644 index 000000000..e3ed7f18e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -0,0 +1,550 @@ +package fr.free.nrw.commons.contributions; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.ViewPager; + +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.ImageView; + + +import javax.inject.Inject; +import javax.inject.Named; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.auth.AuthenticatedActivity; +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.location.LocationServiceManager; +import fr.free.nrw.commons.nearby.NearbyFragment; +import fr.free.nrw.commons.nearby.NearbyMapFragment; +import fr.free.nrw.commons.notification.NotificationActivity; +import fr.free.nrw.commons.theme.NavigationBaseActivity; +import fr.free.nrw.commons.upload.UploadService; +import fr.free.nrw.commons.utils.PermissionUtils; +import fr.free.nrw.commons.utils.ViewUtil; +import timber.log.Timber; + +import static android.content.ContentResolver.requestSync; +import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUEST; + +public class MainActivity extends AuthenticatedActivity implements FragmentManager.OnBackStackChangedListener { + + @Inject + SessionManager sessionManager; + @BindView(R.id.tab_layout) + TabLayout tabLayout; + @BindView(R.id.pager) + public UnswipableViewPager viewPager; + @Inject + public LocationServiceManager locationManager; + @Inject + @Named("default_preferences") + public SharedPreferences prefs; + + + public Intent uploadServiceIntent; + public boolean isAuthCookieAcquired = false; + + public ContributionsActivityPagerAdapter contributionsActivityPagerAdapter; + public final int CONTRIBUTIONS_TAB_POSITION = 0; + public final int NEARBY_TAB_POSITION = 1; + + public boolean isContributionsFragmentVisible = true; // False means nearby fragment is visible + private Menu menu; + private boolean isThereUnreadNotifications = false; + + private boolean onOrientationChanged = false; + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_contributions); + ButterKnife.bind(this); + + requestAuthToken(); + initDrawer(); + setTitle(getString(R.string.navigation_item_home)); // Should I create a new string variable with another name instead? + + if (savedInstanceState != null ) { + onOrientationChanged = true; // Will be used in nearby fragment to determine significant update of map + + //If nearby map was visible, call on Tab Selected to call all nearby operations + if (savedInstanceState.getInt("viewPagerCurrentItem") == 1) { + ((NearbyFragment)contributionsActivityPagerAdapter.getItem(1)).onTabSelected(onOrientationChanged); + } + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt("viewPagerCurrentItem", viewPager.getCurrentItem()); + } + + @Override + protected void onAuthCookieAcquired(String authCookie) { + // Do a sync everytime we get here! + requestSync(sessionManager.getCurrentAccount(), BuildConfig.CONTRIBUTION_AUTHORITY, new Bundle()); + uploadServiceIntent = new Intent(this, UploadService.class); + uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); + startService(uploadServiceIntent); + + addTabsAndFragments(); + isAuthCookieAcquired = true; + if (contributionsActivityPagerAdapter.getItem(0) != null) { + ((ContributionsFragment)contributionsActivityPagerAdapter.getItem(0)).onAuthCookieAcquired(uploadServiceIntent); + } + } + + private void addTabsAndFragments() { + contributionsActivityPagerAdapter = new ContributionsActivityPagerAdapter(getSupportFragmentManager()); + viewPager.setAdapter(contributionsActivityPagerAdapter); + + tabLayout.addTab(tabLayout.newTab().setText(getResources().getString(R.string.contributions_fragment))); + tabLayout.addTab(tabLayout.newTab().setText(getResources().getString(R.string.nearby_fragment))); + + // Set custom view to add nearby info icon next to text + View nearbyTabLinearLayout = LayoutInflater.from(this).inflate(R.layout.custom_nearby_tab_layout, null); + View nearbyInfoPopupWindowLayout = LayoutInflater.from(this).inflate(R.layout.nearby_info_popup_layout, null); + ImageView nearbyInfo = nearbyTabLinearLayout.findViewById(R.id.nearby_info_image); + tabLayout.getTabAt(1).setCustomView(nearbyTabLinearLayout); + + nearbyInfo.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + /*new AlertDialog.Builder(MainActivity.this) + .setTitle(R.string.title_activity_nearby) + .setMessage(R.string.showcase_view_whole_nearby_activity) + .setCancelable(true) + .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel()) + .create() + .show();*/ + String popupText = getResources().getString(R.string.showcase_view_whole_nearby_activity); + ViewUtil.displayPopupWindow(nearbyInfo, MainActivity.this, nearbyInfoPopupWindowLayout, popupText); + } + }); + + if (uploadServiceIntent != null) { + // If auth cookie already acquired notify contrib fragmnet so that it san operate auth required actions + ((ContributionsFragment)contributionsActivityPagerAdapter.getItem(CONTRIBUTIONS_TAB_POSITION)).onAuthCookieAcquired(uploadServiceIntent); + } + setTabAndViewPagerSynchronisation(); + } + + /** + * Adds number of uploads next to tab text "Contributions" then it will look like + * "Contributions (NUMBER)" + * @param uploadCount + */ + public void setNumOfUploads(int uploadCount) { + tabLayout.getTabAt(0).setText(getResources().getString(R.string.contributions_fragment) +" "+ getResources() + .getQuantityString(R.plurals.contributions_subtitle, + uploadCount, uploadCount)); + } + + /** + * Normally tab layout and view pager has no relation, which means when you swipe view pager + * tab won't change and vice versa. So we have to notify each of them. + */ + private void setTabAndViewPagerSynchronisation() { + //viewPager.canScrollHorizontally(false); + viewPager.setFocusableInTouchMode(true); + + viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + + } + + @Override + public void onPageSelected(int position) { + switch (position) { + case CONTRIBUTIONS_TAB_POSITION: + Timber.d("Contributions tab selected"); + tabLayout.getTabAt(CONTRIBUTIONS_TAB_POSITION).select(); + isContributionsFragmentVisible = true; + updateMenuItem(); + + break; + case NEARBY_TAB_POSITION: + Timber.d("Nearby tab selected"); + tabLayout.getTabAt(NEARBY_TAB_POSITION).select(); + isContributionsFragmentVisible = false; + updateMenuItem(); + // Do all permission and GPS related tasks on tab selected, not on create + ((NearbyFragment)contributionsActivityPagerAdapter.getItem(1)).onTabSelected(onOrientationChanged); + + break; + default: + tabLayout.getTabAt(CONTRIBUTIONS_TAB_POSITION).select(); + break; + } + } + + @Override + public void onPageScrollStateChanged(int state) { + + } + }); + + tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + viewPager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + + } + }); + } + + public void hideTabs() { + changeDrawerIconToBakcButton(); + if (tabLayout != null) { + tabLayout.setVisibility(View.GONE); + } + } + + public void showTabs() { + changeDrawerIconToDefault(); + if (tabLayout != null) { + tabLayout.setVisibility(View.VISIBLE); + } + } + + @Override + protected void onAuthFailure() { + + } + + @Override + public void onBackPressed() { + String contributionsFragmentTag = ((ContributionsActivityPagerAdapter) viewPager.getAdapter()).makeFragmentName(R.id.pager, 0); + String nearbyFragmentTag = ((ContributionsActivityPagerAdapter) viewPager.getAdapter()).makeFragmentName(R.id.pager, 1); + if (getSupportFragmentManager().findFragmentByTag(contributionsFragmentTag) != null && isContributionsFragmentVisible) { + // Meas that contribution fragment is visible (not nearby fragment) + ContributionsFragment contributionsFragment = (ContributionsFragment) getSupportFragmentManager().findFragmentByTag(contributionsFragmentTag); + + if (contributionsFragment.getChildFragmentManager().findFragmentByTag(ContributionsFragment.MEDIA_DETAIL_PAGER_FRAGMENT_TAG) != null) { + // Means that media details fragment is visible to uer instead of contributions list fragment (As chils fragment) + // Then we want to go back to contributions list fragment on backbutton pressed from media detail fragment + contributionsFragment.getChildFragmentManager().popBackStack(); + // Tabs were invisible when Media Details Fragment is active, make them visible again on Contrib List Fragment active + showTabs(); + // Nearby Notification Card View was invisible when Media Details Fragment is active, make it visible again on Contrib List Fragment active, according to preferences + if (prefs.getBoolean("displayNearbyCardView", true)) { + contributionsFragment.nearbyNoificationCardView.setVisibility(View.VISIBLE); + } else { + contributionsFragment.nearbyNoificationCardView.setVisibility(View.GONE); + } + } else { + finish(); + } + } else if (getSupportFragmentManager().findFragmentByTag(nearbyFragmentTag) != null && !isContributionsFragmentVisible) { + // Meas that nearby fragment is visible (not contributions fragment) + // Set current item to contributions activity instead of closing the activity + viewPager.setCurrentItem(0); + } else { + super.onBackPressed(); + } + } + + @Override + public void onBackStackChanged() { + initBackButton(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.contribution_activity_notification_menu, menu); + + if (!isThereUnreadNotifications) { + // TODO: used vectors are not compatible with API 19 and below, change them + menu.findItem(R.id.notifications).setIcon(ContextCompat.getDrawable(this, R.drawable.ic_notification_white_clip_art)); + } else { + menu.findItem(R.id.notifications).setIcon(ContextCompat.getDrawable(this, R.drawable.ic_notification_white_clip_art_dot)); + } + + this.menu = menu; + + updateMenuItem(); + + return true; + } + + /** + * Responsible with displaying required menu items according to displayed fragment. + * Notifications icon when contributions list is visible, list sheet icon when nearby is visible + */ + private void updateMenuItem() { + if (menu != null) { + if (isContributionsFragmentVisible) { + // Display notifications menu item + menu.findItem(R.id.notifications).setVisible(true); + menu.findItem(R.id.list_sheet).setVisible(false); + Timber.d("Contributions activity notifications menu item is visible"); + } else { + // Display bottom list menu item + menu.findItem(R.id.notifications).setVisible(false); + menu.findItem(R.id.list_sheet).setVisible(true); + Timber.d("Contributions activity list sheet menu item is visible"); + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.notifications: + // Starts notification activity on click to notification icon + NavigationBaseActivity.startActivityWithFlags(this, NotificationActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); + finish(); + return true; + + case R.id.list_sheet: + if (contributionsActivityPagerAdapter.getItem(1) != null) { + ((NearbyFragment)contributionsActivityPagerAdapter.getItem(1)).listOptionMenuIteClicked(); + } + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private boolean deviceHasCamera() { + PackageManager pm = getPackageManager(); + return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) || + pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); + } + + /** + * Updte notification icon if there is an unread notification + * @param isThereUnreadNotifications true if user didn't visit notifications activity since + * latest notification came to account + */ + public void updateNotificationIcon(boolean isThereUnreadNotifications) { + if (!isThereUnreadNotifications) { + this.isThereUnreadNotifications = false; + menu.findItem(R.id.notifications).setIcon(ContextCompat.getDrawable(this, R.drawable.ic_notification_white_clip_art)); + } else { + this.isThereUnreadNotifications = true; + menu.findItem(R.id.notifications).setIcon(ContextCompat.getDrawable(this, R.drawable.ic_notification_white_clip_art_dot)); + } + } + + public class ContributionsActivityPagerAdapter extends FragmentPagerAdapter { + FragmentManager fragmentManager; + private boolean isContributionsListFragment = true; // to know what to put in first tab, Contributions of Media Details + + + public ContributionsActivityPagerAdapter(FragmentManager fragmentManager) { + super(fragmentManager); + this.fragmentManager = fragmentManager; + } + + @Override + public int getCount() { + return 2; + } + + /* + * Do not use getItem method to access fragments on pager adapter. User reference vairables + * instead. + * */ + @Override + public Fragment getItem(int position) { + switch (position){ + case 0: + ContributionsFragment retainedContributionsFragment = getContributionsFragment(0); + if (retainedContributionsFragment != null) { + /** + * ContributionsFragment is parent of ContributionsListFragment and + * MediaDetailsFragment. If below decides which child will be visible. + */ + if (isContributionsListFragment) { + retainedContributionsFragment.setContributionsListFragment(); + } else { + retainedContributionsFragment.setMediaDetailPagerFragment(); + } + return retainedContributionsFragment; + } else { + // If we reach here, retainedContributionsFragment is null + return new ContributionsFragment(); + + } + + case 1: + NearbyFragment retainedNearbyFragment = getNearbyFragment(1); + if (retainedNearbyFragment != null) { + return retainedNearbyFragment; + } else { + // If we reach here, retainedNearbyFragment is null + return new NearbyFragment(); + } + default: + return null; + } + } + + /** + * Generates fragment tag with makeFragmentName method to get retained contributions fragment + * @param position index of tabs, in our case 0 or 1 + * @return + */ + private ContributionsFragment getContributionsFragment(int position) { + String tag = makeFragmentName(R.id.pager, position); + return (ContributionsFragment)fragmentManager.findFragmentByTag(tag); + } + + /** + * Generates fragment tag with makeFragmentName method to get retained nearby fragment + * @param position index of tabs, in our case 0 or 1 + * @return + */ + private NearbyFragment getNearbyFragment(int position) { + String tag = makeFragmentName(R.id.pager, position); + return (NearbyFragment)fragmentManager.findFragmentByTag(tag); + } + + /** + * A simple hack to use retained fragment when getID is called explicitly, if we don't use + * this method, a new fragment will be initialized on each explicit calls of getID + * @param viewId id of view pager + * @param index index of tabs, in our case 0 or 1 + * @return + */ + public String makeFragmentName(int viewId, int index) { + return "android:switcher:" + viewId + ":" + index; + } + + /** + * In first tab we can have ContributionsFragment or Media details fragment. This method + * is responsible to update related boolean + * @param isContributionsListFragment true when contribution fragment should be visible, false + * means user clicked to MediaDetails + */ + private void updateContributionFragmentTabContent(boolean isContributionsListFragment) { + this.isContributionsListFragment = isContributionsListFragment; + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + ContributionsListFragment contributionsListFragment = + (ContributionsListFragment) contributionsActivityPagerAdapter + .getItem(0).getChildFragmentManager() + .findFragmentByTag(ContributionsFragment.CONTRIBUTION_LIST_FRAGMENT_TAG); + contributionsListFragment.onActivityResult(requestCode, resultCode, data); + } + + @Override + public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + switch (requestCode) { + case LOCATION_REQUEST: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Timber.d("Location permission given"); + } else { + // If nearby fragment is visible and location permission is not given, send user back to contrib fragment + if (!isContributionsFragmentVisible) { + viewPager.setCurrentItem(CONTRIBUTIONS_TAB_POSITION); + + // TODO: If contrib fragment is visible and location permission is not given, display permission request button + } else { + + } + } + return; + } + // Storage permission for gallery + case PermissionUtils.GALLERY_PERMISSION_FROM_CONTRIBUTION_LIST: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Storage permission given + ContributionsListFragment contributionsListFragment = + (ContributionsListFragment) contributionsActivityPagerAdapter + .getItem(0).getChildFragmentManager() + .findFragmentByTag(ContributionsFragment.CONTRIBUTION_LIST_FRAGMENT_TAG); + contributionsListFragment.controller.startGalleryPick(); + } + return; + } + + case PermissionUtils.CAMERA_PERMISSION_FROM_CONTRIBUTION_LIST: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Storage permission given + ContributionsListFragment contributionsListFragment = + (ContributionsListFragment) contributionsActivityPagerAdapter + .getItem(0).getChildFragmentManager() + .findFragmentByTag(ContributionsFragment.CONTRIBUTION_LIST_FRAGMENT_TAG); + contributionsListFragment.controller.startCameraCapture(); + } + return; + } + + case PermissionUtils.CAMERA_PERMISSION_FROM_NEARBY_MAP: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Storage permission given + NearbyMapFragment nearbyMapFragment = + ((NearbyFragment) contributionsActivityPagerAdapter + .getItem(1)).nearbyMapFragment; + nearbyMapFragment.controller.startCameraCapture(); + } + return; + } + + case PermissionUtils.GALLERY_PERMISSION_FROM_NEARBY_MAP: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Storage permission given + NearbyMapFragment nearbyMapFragment = + ((NearbyFragment) contributionsActivityPagerAdapter + .getItem(1)).nearbyMapFragment; + nearbyMapFragment.controller.startGalleryPick(); + } + return; + } + + default: + return; + } + } + + @Override + protected void onDestroy() { + locationManager.unregisterLocationManager(); + // Remove ourself from hashmap to prevent memory leaks + locationManager = null; + super.onDestroy(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/UnswipableViewPager.java b/app/src/main/java/fr/free/nrw/commons/contributions/UnswipableViewPager.java new file mode 100644 index 000000000..46c8f3502 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/contributions/UnswipableViewPager.java @@ -0,0 +1,30 @@ +package fr.free.nrw.commons.contributions; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; + +public class UnswipableViewPager extends ViewPager{ + public UnswipableViewPager(@NonNull Context context) { + super(context); + } + + public UnswipableViewPager(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + // Unswipable + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Unswipable + return false; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java index d26bae178..19cb09dc6 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -9,10 +9,10 @@ import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.SignupActivity; import fr.free.nrw.commons.bookmarks.BookmarksActivity; import fr.free.nrw.commons.category.CategoryDetailsActivity; +import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.category.CategoryImagesActivity; -import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.explore.SearchActivity; -import fr.free.nrw.commons.nearby.NearbyActivity; + import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.settings.SettingsActivity; import fr.free.nrw.commons.upload.MultipleShareActivity; @@ -35,7 +35,7 @@ public abstract class ActivityBuilderModule { abstract MultipleShareActivity bindMultipleShareActivity(); @ContributesAndroidInjector - abstract ContributionsActivity bindContributionsActivity(); + abstract MainActivity bindContributionsActivity(); @ContributesAndroidInjector abstract SettingsActivity bindSettingsActivity(); @@ -46,9 +46,6 @@ public abstract class ActivityBuilderModule { @ContributesAndroidInjector abstract SignupActivity bindSignupActivity(); - @ContributesAndroidInjector - abstract NearbyActivity bindNearbyActivity(); - @ContributesAndroidInjector abstract NotificationActivity bindNotificationActivity(); diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index a52f6ddab..d54b556c4 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -120,6 +120,17 @@ public class CommonsApplicationModule { return context.getSharedPreferences("direct_nearby_upload_prefs", MODE_PRIVATE); } + /** + * Is used to determine when user is viewed notifications activity last + * @param context + * @return date of lastReadNotificationDate + */ + @Provides + @Named("last_read_notification_date") + public SharedPreferences providesLastReadNotificationDatePreferences(Context context) { + return context.getSharedPreferences("last_read_notification_date", MODE_PRIVATE); + } + @Provides public UploadController providesUploadController(SessionManager sessionManager, @Named("default_preferences") SharedPreferences sharedPreferences, Context context) { return new UploadController(sessionManager, context, sharedPreferences); diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java index 9868f3576..04804dab0 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -7,12 +7,14 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.category.CategoryImagesListFragment; import fr.free.nrw.commons.category.SubCategoryListFragment; +import fr.free.nrw.commons.contributions.ContributionsFragment; import fr.free.nrw.commons.contributions.ContributionsListFragment; import fr.free.nrw.commons.explore.categories.SearchCategoryFragment; import fr.free.nrw.commons.explore.images.SearchImageFragment; import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment; import fr.free.nrw.commons.media.MediaDetailFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment; +import fr.free.nrw.commons.nearby.NearbyFragment; import fr.free.nrw.commons.nearby.NearbyListFragment; import fr.free.nrw.commons.nearby.NearbyMapFragment; import fr.free.nrw.commons.nearby.NoPermissionsFragment; @@ -69,6 +71,12 @@ public abstract class FragmentBuilderModule { @ContributesAndroidInjector abstract RecentSearchesFragment bindRecentSearchesFragment(); + @ContributesAndroidInjector + abstract ContributionsFragment bindContributionsFragment(); + + @ContributesAndroidInjector + abstract NearbyFragment bindNearbyFragment(); + @ContributesAndroidInjector abstract BookmarkPicturesFragment bindBookmarkPictureListFragment(); diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java index 4a137beed..c58aa1e10 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java @@ -26,12 +26,13 @@ public class LocationServiceManager implements LocationListener { private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 2 * 60 * 100; private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 10; - private Context context; + public Context context; private LocationManager locationManager; private Location lastLocation; + //private Location lastLocationDuplicate; // Will be used for nearby card view on contributions activity private final List locationListeners = new CopyOnWriteArrayList<>(); private boolean isLocationManagerRegistered = false; - private Set locationExplanationDisplayed = new HashSet<>(); + public Set locationExplanationDisplayed = new HashSet<>(); /** * Constructs a new instance of LocationServiceManager. @@ -253,15 +254,25 @@ public class LocationServiceManager implements LocationListener { @Override public void onLocationChanged(Location location) { + Timber.d("on location changed"); if (isBetterLocation(location, lastLocation) .equals(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) { lastLocation = location; + //lastLocationDuplicate = location; for (LocationUpdateListener listener : locationListeners) { listener.onLocationChangedSignificantly(LatLng.from(lastLocation)); } - } else if (isBetterLocation(location, lastLocation) + } else if (location.distanceTo(lastLocation) >= 500) { + // Update nearby notification card at every 500 meters. + for (LocationUpdateListener listener : locationListeners) { + listener.onLocationChangedMedium(LatLng.from(lastLocation)); + } + } + + else if (isBetterLocation(location, lastLocation) .equals(LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) { lastLocation = location; + //lastLocationDuplicate = location; for (LocationUpdateListener listener : locationListeners) { listener.onLocationChangedSlightly(LatLng.from(lastLocation)); } @@ -286,6 +297,7 @@ public class LocationServiceManager implements LocationListener { public enum LocationChangeType{ LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving + LOCATION_MEDIUM_CHANGED, //Between slight and significant changes, will be used for nearby card view updates. LOCATION_NOT_CHANGED, PERMISSION_JUST_GRANTED, MAP_UPDATED diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java index f3e920e18..61ff26b11 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.location; public interface LocationUpdateListener { - void onLocationChangedSignificantly(LatLng latLng); - void onLocationChangedSlightly(LatLng latLng); + void onLocationChangedSignificantly(LatLng latLng); // Will be used to update all nearby markers on the map + void onLocationChangedSlightly(LatLng latLng); // Will be used to track users motion + void onLocationChangedMedium(LatLng latLng); // Will be used updating nearby card view notification } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 8feed101e..8b988a06a 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -145,7 +145,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity(); + detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) (getParentFragment().getParentFragment()); if (savedInstanceState != null) { editable = savedInstanceState.getBoolean("editable"); diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index 824e5f4df..7caa9454a 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -38,7 +38,6 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao; import fr.free.nrw.commons.category.CategoryDetailsActivity; import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.explore.SearchActivity; import fr.free.nrw.commons.mwapi.MediaWikiApi; @@ -137,7 +136,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple Timber.d("Returning as activity is destroyed!"); return true; } - MediaDetailProvider provider = (MediaDetailProvider) getActivity(); + MediaDetailProvider provider = (MediaDetailProvider) getParentFragment(); Media m = provider.getMediaAtPosition(pager.getCurrentItem()); switch (item.getItemId()) { case R.id.menu_bookmark_current_image: @@ -174,12 +173,12 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple return true; case R.id.menu_retry_current_image: // Retry - ((ContributionsActivity) getActivity()).retryUpload(pager.getCurrentItem()); + //((MainActivity) getActivity()).retryUpload(pager.getCurrentItem()); getActivity().getSupportFragmentManager().popBackStack(); return true; case R.id.menu_cancel_current_image: // todo: delete image - ((ContributionsActivity) getActivity()).deleteUpload(pager.getCurrentItem()); + //((MainActivity) getActivity()).deleteUpload(pager.getCurrentItem()); getActivity().getSupportFragmentManager().popBackStack(); return true; default: @@ -254,8 +253,8 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple menu.clear(); // see http://stackoverflow.com/a/8495697/17865 inflater.inflate(R.menu.fragment_image_detail, menu); if (pager != null) { - MediaDetailProvider provider = (MediaDetailProvider) getActivity(); - if (provider == null) { + MediaDetailProvider provider = (MediaDetailProvider) getParentFragment(); + if(provider == null) { return; } @@ -326,7 +325,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple @Override public void onPageScrolled(int i, float v, int i2) { - if (getActivity() == null) { + if(getParentFragment().getActivity() == null) { Timber.d("Returning as activity is destroyed!"); return; } @@ -347,7 +346,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple e.printStackTrace(); } } - getActivity().supportInvalidateOptionsMenu(); + getParentFragment().getActivity().supportInvalidateOptionsMenu(); } @Override @@ -381,11 +380,11 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple public Fragment getItem(int i) { if (i == 0) { // See bug https://code.google.com/p/android/issues/detail?id=27526 - if (getActivity() == null) { + if(getParentFragment().getActivity() == null) { Timber.d("Skipping getItem. Returning as activity is destroyed!"); return null; } - pager.postDelayed(() -> getActivity().supportInvalidateOptionsMenu(), 5); + pager.postDelayed(() -> getParentFragment().getActivity().supportInvalidateOptionsMenu(), 5); } return MediaDetailFragment.forMedia(i, editable, isFeaturedImage); } @@ -396,7 +395,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple Timber.d("Skipping getCount. Returning as activity is destroyed!"); return 0; } - return ((MediaDetailProvider) getActivity()).getTotalMediaCount(); + return ((MediaDetailProvider) getParentFragment()).getTotalMediaCount(); } } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java index ca25fccf3..e7ccd97d5 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java @@ -4,9 +4,11 @@ import android.os.Build; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; +import android.util.Log; import fr.free.nrw.commons.R; import fr.free.nrw.commons.contributions.ContributionController; +import fr.free.nrw.commons.utils.PermissionUtils; import timber.log.Timber; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; @@ -24,8 +26,9 @@ class DirectUpload { } // These permission requests will be handled by the Fragments. - // Do not use requestCode 1 as it will conflict with NearbyActivity's requestCodes + // Do not use requestCode 1 as it will conflict with NearbyFragment's requestCodes void initiateGalleryUpload() { + Log.d("deneme7","initiateGalleryUpload"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (ContextCompat.checkSelfPermission(fragment.getActivity(), READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED) { if (fragment.shouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE)) { @@ -33,15 +36,15 @@ class DirectUpload { .setMessage(fragment.getActivity().getString(R.string.read_storage_permission_rationale)) .setPositiveButton(android.R.string.ok, (dialog, which) -> { Timber.d("Requesting permissions for read external storage"); - fragment.requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, 4); + fragment.getActivity().requestPermissions + (new String[]{READ_EXTERNAL_STORAGE}, PermissionUtils.GALLERY_PERMISSION_FROM_NEARBY_MAP); dialog.dismiss(); }) .setNegativeButton(android.R.string.cancel, null) .create() .show(); } else { - fragment.requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, - 4); + fragment.getActivity().requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, PermissionUtils.GALLERY_PERMISSION_FROM_NEARBY_MAP); } } else { controller.startGalleryPick(); @@ -53,20 +56,22 @@ class DirectUpload { } void initiateCameraUpload() { + Log.d("deneme7","initiateCameraUpload"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (ContextCompat.checkSelfPermission(fragment.getActivity(), WRITE_EXTERNAL_STORAGE) != PERMISSION_GRANTED) { if (fragment.shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { new AlertDialog.Builder(fragment.getActivity()) .setMessage(fragment.getActivity().getString(R.string.write_storage_permission_rationale)) .setPositiveButton(android.R.string.ok, (dialog, which) -> { - fragment.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 5); + fragment.getActivity().requestPermissions + (new String[]{WRITE_EXTERNAL_STORAGE}, PermissionUtils.CAMERA_PERMISSION_FROM_NEARBY_MAP); dialog.dismiss(); }) .setNegativeButton(android.R.string.cancel, null) .create() .show(); } else { - fragment.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 5); + fragment.getActivity().requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, PermissionUtils.CAMERA_PERMISSION_FROM_NEARBY_MAP); } } else { controller.startCameraCapture(); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index 39deee7ad..a84e86218 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -39,6 +39,7 @@ public class NearbyController { this.prefs = prefs; } + /** * Prepares Place list to make their distance information update later. * @@ -46,15 +47,15 @@ public class NearbyController { * @return NearbyPlacesInfo a variable holds Place list without distance information * and boundary coordinates of current Place List */ - public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) throws IOException { - + public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng, boolean returnClosestResult) throws IOException { Timber.d("Loading attractions near %s", curLatLng); NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo(); if (curLatLng == null) { + Timber.d("Loading attractions neari, but curLatLng is null"); return null; } - List places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage()); + List places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage(), returnClosestResult); if (null != places && places.size() > 0) { LatLng[] boundaryCoordinates = {places.get(0).location, // south @@ -168,7 +169,7 @@ public class NearbyController { } public class NearbyPlacesInfo { - List placeList; // List of nearby places - LatLng[] boundaryCoordinates; // Corners of nearby area + public List placeList; // List of nearby places + public LatLng[] boundaryCoordinates; // Corners of nearby area } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java similarity index 65% rename from app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java rename to app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java index ca4d7adef..5cafedcbc 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFragment.java @@ -5,21 +5,19 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.graphics.Typeface; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Handler; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.design.widget.BottomSheetBehavior; import android.support.design.widget.Snackbar; import android.support.v4.app.FragmentTransaction; import android.support.v7.app.AlertDialog; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; +import android.view.LayoutInflater; + import android.view.View; +import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.ProgressBar; @@ -34,11 +32,11 @@ import javax.inject.Named; import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.contributions.MainActivity; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType; import fr.free.nrw.commons.location.LocationUpdateListener; -import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.UriSerializer; import fr.free.nrw.commons.utils.ViewUtil; @@ -48,23 +46,18 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; -import uk.co.deanwild.materialshowcaseview.IShowcaseListener; -import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView; import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED; import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED; import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED; - -public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener, - WikidataEditListener.WikidataP18EditListener { - - private static final int LOCATION_REQUEST = 1; +public class NearbyFragment extends CommonsDaggerSupportFragment + implements LocationUpdateListener, + WikidataEditListener.WikidataP18EditListener { @BindView(R.id.progressBar) ProgressBar progressBar; - @BindView(R.id.bottom_sheet) LinearLayout bottomSheet; @BindView(R.id.bottom_sheet_details) @@ -78,59 +71,127 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp LocationServiceManager locationManager; @Inject NearbyController nearbyController; - @Inject WikidataEditListener wikidataEditListener; - @Inject - @Named("application_preferences") SharedPreferences applicationPrefs; - private LatLng curLatLng; - private Bundle bundle; - private Disposable placesDisposable; - private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed - private BottomSheetBehavior bottomSheetBehavior; // Behavior for list bottom sheet - private BottomSheetBehavior bottomSheetBehaviorForDetails; // Behavior for details bottom sheet + WikidataEditListener wikidataEditListener; + @Inject + @Named("application_preferences") + SharedPreferences applicationPrefs; + public NearbyMapFragment nearbyMapFragment; private NearbyListFragment nearbyListFragment; private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName(); private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName(); - private View listButton; // Reference to list button to use in tutorial + private Bundle bundle; + private BottomSheetBehavior bottomSheetBehavior; // Behavior for list bottom sheet + private BottomSheetBehavior bottomSheetBehaviorForDetails; // Behavior for details bottom sheet + + private LatLng curLatLng; + private Disposable placesDisposable; + private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed + public View view; + private Snackbar snackbar; + + private LatLng lastKnownLocation; private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; private BroadcastReceiver broadcastReceiver; - private boolean isListShowcaseAdded = false; - private boolean isMapShowCaseAdded = false; - - private LatLng lastKnownLocation; - - private MaterialShowcaseView secondSingleShowCaseView; + private boolean onOrientationChanged = false; @Override - protected void onCreate(Bundle savedInstanceState) { + public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_nearby); - ButterKnife.bind(this); - resumeFragment(); - bundle = new Bundle(); - - initBottomSheetBehaviour(); - initDrawer(); - wikidataEditListener.setAuthenticationStateListener(this); + setRetainInstance(true); } + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_nearby, container, false); + ButterKnife.bind(this, view); + + /*// Resume the fragment if exist + resumeFragment();*/ + bundle = new Bundle(); + initBottomSheetBehaviour(); + wikidataEditListener.setAuthenticationStateListener(this); + this.view = view; + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + if (savedInstanceState != null) { + onOrientationChanged = true; + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); + } + } + + /** + * Hide or expand bottom sheet according to states of all sheets + */ + public void listOptionMenuIteClicked() { + if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_COLLAPSED || bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_HIDDEN){ + bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + }else if(bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_EXPANDED){ + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + + } + + /** + * Resume fragments if they exists + */ private void resumeFragment() { // Find the retained fragment on activity restarts nearbyMapFragment = getMapFragment(); nearbyListFragment = getListFragment(); } + /** + * Returns the map fragment added to child fragment manager previously, if exists. + */ + private NearbyMapFragment getMapFragment() { + return (NearbyMapFragment) getChildFragmentManager().findFragmentByTag(TAG_RETAINED_MAP_FRAGMENT); + } + + private void removeMapFragment() { + if (nearbyMapFragment != null) { + android.support.v4.app.FragmentManager fm = getFragmentManager(); + fm.beginTransaction().remove(nearbyMapFragment).commit(); + nearbyMapFragment = null; + } + } + + + /** + * Returns the list fragment added to child fragment manager previously, if exists. + */ + private NearbyListFragment getListFragment() { + return (NearbyListFragment) getChildFragmentManager().findFragmentByTag(TAG_RETAINED_LIST_FRAGMENT); + } + + private void removeListFragment() { + if (nearbyListFragment != null) { + android.support.v4.app.FragmentManager fm = getFragmentManager(); + fm.beginTransaction().remove(nearbyListFragment).commit(); + nearbyListFragment = null; + } + } + + /** + * Initialize bottom sheet behaviour (sheet for map list.) Set height 9/16 of all window. + * Add callback for bottom sheet changes, so that we can sync it with bottom sheet for details + * (sheet for nearby details) + */ private void initBottomSheetBehaviour() { transparentView.setAlpha(0); - - bottomSheet.getLayoutParams().height = getWindowManager() + bottomSheet.getLayoutParams().height = getActivity().getWindowManager() .getDefaultDisplay().getHeight() / 16 * 9; bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet); - // TODO initProperBottomSheetBehavior(); bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { @Override @@ -149,259 +210,44 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_nearby, menu); - - new Handler().post(() -> { - - listButton = findViewById(R.id.action_display_list); - - secondSingleShowCaseView = new MaterialShowcaseView.Builder(this) - .setTarget(listButton) - .setDismissText(getString(R.string.showcase_view_got_it_button)) - .setContentText(getString(R.string.showcase_view_list_icon)) - .setDelay(500) // optional but starting animations immediately in onCreate can make them choppy - .singleUse(ViewUtil.SHOWCASE_VIEW_ID_1) // provide a unique ID used to ensure it is only shown once - .setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD)) - .setListener(new IShowcaseListener() { - @Override - public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) { - - } - - // If dismissed, we can inform fragment to start showcase sequence there - @Override - public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) { - nearbyMapFragment.onNearbyMaterialShowcaseDismissed(); - } - }) - .build(); - - isListShowcaseAdded = true; - - if (isMapShowCaseAdded) { // If map showcase is also ready, start ShowcaseSequence - // Probably this case is not possible. Just added to be careful - setMapViewTutorialShowCase(); - } - }); - - return super.onCreateOptionsMenu(menu); + public void prepareViewsForSheetPosition(int bottomSheetState) { + // TODO } @Override - public boolean onOptionsItemSelected(MenuItem item) { - - // Handle item selection - switch (item.getItemId()) { - case R.id.action_display_list: - if (bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_COLLAPSED || bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_HIDDEN){ - bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); - }else if (bottomSheetBehavior.getState()==BottomSheetBehavior.STATE_EXPANDED){ - bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - } - - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private void requestLocationPermissions() { - if (!isFinishing()) { - locationManager.requestPermissions(this); - } + public void onLocationChangedSignificantly(LatLng latLng) { + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - switch (requestCode) { - case LOCATION_REQUEST: { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Timber.d("Location permission granted, refreshing view"); - //Still need to check if GPS is enabled - checkGps(); - lastKnownLocation = locationManager.getLKL(); - refreshView(PERMISSION_JUST_GRANTED); - } else { - //If permission not granted, go to page that says Nearby Places cannot be displayed - hideProgressBar(); - showLocationPermissionDeniedErrorDialog(); - } - } - break; - - default: - // This is needed to allow the request codes from the Fragments to be routed appropriately - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - } + public void onLocationChangedSlightly(LatLng latLng) { + refreshView(LOCATION_SLIGHTLY_CHANGED); } - private void showLocationPermissionDeniedErrorDialog() { - new AlertDialog.Builder(this) - .setMessage(R.string.nearby_needs_permissions) - .setCancelable(false) - .setPositiveButton(R.string.give_permission, (dialog, which) -> { - //will ask for the location permission again - checkGps(); - }) - .setNegativeButton(R.string.cancel, (dialog, which) -> { - //dismiss dialog and finish activity - dialog.cancel(); - finish(); - }) - .create() - .show(); - } - private void checkGps() { - if (!locationManager.isProviderEnabled()) { - Timber.d("GPS is not enabled"); - new AlertDialog.Builder(this) - .setMessage(R.string.gps_disabled) - .setCancelable(false) - .setPositiveButton(R.string.enable_gps, - (dialog, id) -> { - Intent callGPSSettingIntent = new Intent( - android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS); - Timber.d("Loaded settings page"); - startActivityForResult(callGPSSettingIntent, 1); - }) - .setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> { - showLocationPermissionDeniedErrorDialog(); - dialog.cancel(); - }) - .create() - .show(); - } else { - Timber.d("GPS is enabled"); - checkLocationPermission(); - } - } - - private void checkLocationPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (locationManager.isLocationPermissionGranted()) { - refreshView(LOCATION_SIGNIFICANTLY_CHANGED); - } else { - // Should we show an explanation? - if (locationManager.isPermissionExplanationRequired(this)) { - // Show an explanation to the user *asynchronously* -- don't block - // this thread waiting for the user's response! After the user - // sees the explanation, try again to request the permission. - new AlertDialog.Builder(this) - .setMessage(getString(R.string.location_permission_rationale_nearby)) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - requestLocationPermissions(); - dialog.dismiss(); - }) - .setNegativeButton(android.R.string.cancel, (dialog, id) -> { - showLocationPermissionDeniedErrorDialog(); - dialog.cancel(); - }) - .create() - .show(); - - } else { - // No explanation needed, we can request the permission. - requestLocationPermissions(); - } - } - } else { - refreshView(LOCATION_SIGNIFICANTLY_CHANGED); - } + @Override + public void onLocationChangedMedium(LatLng latLng) { + // For nearby map actions, there are no differences between 500 meter location change (aka medium change) and slight change + refreshView(LOCATION_SLIGHTLY_CHANGED); } @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == 1) { - Timber.d("User is back from Settings page"); - refreshView(LOCATION_SIGNIFICANTLY_CHANGED); - } + public void onWikidataEditSuccessful() { + refreshView(MAP_UPDATED); } - @Override - protected void onStart() { - super.onStart(); - locationManager.addLocationListener(this); - registerLocationUpdates(); - } - - @Override - protected void onStop() { - super.onStop(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (placesDisposable != null) { - placesDisposable.dispose(); - } - } - - @Override - protected void onResume() { - super.onResume(); - lockNearbyView = false; - checkGps(); - addNetworkBroadcastReceiver(); - } - - @Override - public void onPause() { - super.onPause(); - // this means that this activity will not be recreated now, user is leaving it - // or the activity is otherwise finishing - if (isFinishing()) { - // we will not need this fragment anymore, this may also be a good place to signal - // to the retained fragment object to perform its own cleanup. - removeMapFragment(); - removeListFragment(); - - } - unregisterReceiver(broadcastReceiver); - broadcastReceiver = null; - locationManager.removeLocationListener(this); - locationManager.unregisterLocationManager(); - - } - - private void addNetworkBroadcastReceiver() { - IntentFilter intentFilter = new IntentFilter(NETWORK_INTENT_ACTION); - Snackbar snackbar = Snackbar.make(transparentView , R.string.no_internet, Snackbar.LENGTH_INDEFINITE); - - broadcastReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (NetworkUtils.isInternetConnectionEstablished(NearbyActivity.this)) { - refreshView(LOCATION_SIGNIFICANTLY_CHANGED); - snackbar.dismiss(); - } else { - snackbar.show(); - } - } - }; - - this.registerReceiver(broadcastReceiver, intentFilter); - } - - /** * This method should be the single point to load/refresh nearby places * * @param locationChangeType defines if location shanged significantly or slightly */ - private void refreshView(LocationChangeType locationChangeType) { + private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) { + Timber.d("Refreshing nearby places"); if (lockNearbyView) { return; } - if (!NetworkUtils.isInternetConnectionEstablished(this)) { + if (!NetworkUtils.isInternetConnectionEstablished(getActivity())) { hideProgressBar(); return; } @@ -411,7 +257,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp if (curLatLng != null && curLatLng.equals(lastLocation) && !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed - return; + if (!onOrientationChanged) { + return; + } } curLatLng = lastLocation; @@ -424,9 +272,15 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp return; } + /* + onOrientation changed is true whenever activities orientation changes. After orientation + change we want to refresh map significantly, doesn't matter if location changed significantly + or not. Thus, we included onOrientatinChanged boolean to if clause + */ if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED) || locationChangeType.equals(PERMISSION_JUST_GRANTED) - || locationChangeType.equals(MAP_UPDATED)) { + || locationChangeType.equals(MAP_UPDATED) + || onOrientationChanged) { progressBar.setVisibility(View.VISIBLE); //TODO: This hack inserts curLatLng before populatePlaces is called (see #1440). Ideally a proper fix should be found @@ -438,7 +292,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp bundle.putString("CurLatLng", gsonCurLatLng); placesDisposable = Observable.fromCallable(() -> nearbyController - .loadAttractionsFromLocation(curLatLng)) + .loadAttractionsFromLocation(curLatLng, false)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::populatePlaces, @@ -458,40 +312,8 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp } } - /** - * This method first checks if the location permissions has been granted and then register the location manager for updates. - */ - private void registerLocationUpdates() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (locationManager.isLocationPermissionGranted()) { - locationManager.registerLocationManager(); - } else { - // Should we show an explanation? - if (locationManager.isPermissionExplanationRequired(this)) { - new AlertDialog.Builder(this) - .setMessage(getString(R.string.location_permission_rationale_nearby)) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - requestLocationPermissions(); - dialog.dismiss(); - }) - .setNegativeButton(android.R.string.cancel, (dialog, id) -> { - showLocationPermissionDeniedErrorDialog(); - dialog.cancel(); - }) - .create() - .show(); - - } else { - // No explanation needed, we can request the permission. - requestLocationPermissions(); - } - } - } else { - locationManager.registerLocationManager(); - } - } - private void populatePlaces(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { + Timber.d("Populating nearby places"); List placeList = nearbyPlacesInfo.placeList; LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates; Gson gson = new GsonBuilder() @@ -502,7 +324,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates); if (placeList.size() == 0) { - ViewUtil.showSnackbar(findViewById(R.id.container), R.string.no_nearby); + ViewUtil.showSnackbar(view.findViewById(R.id.container), R.string.no_nearby); } bundle.putString("PlaceList", gsonPlaceList); @@ -523,47 +345,13 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp updateMapFragment(false); updateListFragment(); } - - isMapShowCaseAdded = true; - } - - public void setMapViewTutorialShowCase() { - /* - *This showcase view will be the first step of our nearbyMaterialShowcaseSequence. The reason we use a - * single item instead of adding another step to nearbyMaterialShowcaseSequence is that we are not able to - * call withoutShape() method on steps. For mapView we need an showcase view without - * any circle on it, it should cover the whole page. - * */ - MaterialShowcaseView firstSingleShowCaseView = new MaterialShowcaseView.Builder(this) - .setTarget(nearbyMapFragment.mapView) - .setDismissText(getString(R.string.showcase_view_got_it_button)) - .setContentText(getString(R.string.showcase_view_whole_nearby_activity)) - .setDelay(500) // optional but starting animations immediately in onCreate can make them choppy - .singleUse(ViewUtil.SHOWCASE_VIEW_ID_2) // provide a unique ID used to ensure it is only shown once - .withoutShape() // no shape on map view since there are no view to focus on - .setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD)) - .setListener(new IShowcaseListener() { - @Override - public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) { - - } - - @Override - public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) { - /* Add other nearbyMaterialShowcaseSequence here, it will make the user feel as they are a - * nearbyMaterialShowcaseSequence whole together. - * */ - secondSingleShowCaseView.show(NearbyActivity.this); - } - }) - .build(); - - if (applicationPrefs.getBoolean("firstRunNearby", true)) { - applicationPrefs.edit().putBoolean("firstRunNearby", false).apply(); - firstSingleShowCaseView.show(this); - } } + /** + * Lock nearby view updates while updating map or list. Because we don't want new update calls + * when we already updating for old location update. + * @param lock + */ private void lockNearbyView(boolean lock) { if (lock) { lockNearbyView = true; @@ -576,53 +364,22 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp } } - private void hideProgressBar() { - if (progressBar != null) { - progressBar.setVisibility(View.GONE); - } - } - - private NearbyMapFragment getMapFragment() { - return (NearbyMapFragment) getSupportFragmentManager().findFragmentByTag(TAG_RETAINED_MAP_FRAGMENT); - } - - private void removeMapFragment() { - if (nearbyMapFragment != null) { - android.support.v4.app.FragmentManager fm = getSupportFragmentManager(); - fm.beginTransaction().remove(nearbyMapFragment).commit(); - nearbyMapFragment = null; - } - } - - private NearbyListFragment getListFragment() { - return (NearbyListFragment) getSupportFragmentManager().findFragmentByTag(TAG_RETAINED_LIST_FRAGMENT); - } - - private void removeListFragment() { - if (nearbyListFragment != null) { - android.support.v4.app.FragmentManager fm = getSupportFragmentManager(); - fm.beginTransaction().remove(nearbyListFragment).commit(); - nearbyListFragment = null; - } - } - private void updateMapFragment(boolean isSlightUpdate) { /* - * Significant update means updating nearby place markers. Slightly update means only - * updating current location marker and camera target. - * We update our map Significantly on each 1000 meter change, but we can't never know - * the frequency of nearby places. Thus we check if we are close to the boundaries of - * our nearby markers, we update our map Significantly. - * */ - + Significant update means updating nearby place markers. Slightly update means only + updating current location marker and camera target. + We update our map Significantly on each 1000 meter change, but we can't never know + the frequency of nearby places. Thus we check if we are close to the boundaries of + our nearby markers, we update our map Significantly. + */ NearbyMapFragment nearbyMapFragment = getMapFragment(); if (nearbyMapFragment != null && curLatLng != null) { hideProgressBar(); // In case it is visible (this happens, not an impossible case) /* - * If we are close to nearby places boundaries, we need a significant update to - * get new nearby places. Check order is south, north, west, east - * */ + * If we are close to nearby places boundaries, we need a significant update to + * get new nearby places. Check order is south, north, west, east + * */ if (nearbyMapFragment.boundaryCoordinates != null && (curLatLng.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude() || curLatLng.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude() @@ -630,7 +387,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp || curLatLng.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) { // populate places placesDisposable = Observable.fromCallable(() -> nearbyController - .loadAttractionsFromLocation(curLatLng)) + .loadAttractionsFromLocation(curLatLng, false)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::populatePlaces, @@ -645,6 +402,15 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp return; } + /* + If this is the map update just after orientation change, then it is not a slight update + anymore. We want to significantly update map after each orientation change + */ + if (onOrientationChanged) { + isSlightUpdate = false; + onOrientationChanged = false; + } + if (isSlightUpdate) { nearbyMapFragment.setBundleForUpdtes(bundle); nearbyMapFragment.updateMapSlightly(); @@ -671,7 +437,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp * Calls fragment for map view. */ private void setMapFragment() { - FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction(); nearbyMapFragment = new NearbyMapFragment(); nearbyMapFragment.setArguments(bundle); fragmentTransaction.replace(R.id.container, nearbyMapFragment, TAG_RETAINED_MAP_FRAGMENT); @@ -682,7 +448,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp * Calls fragment for list view. */ private void setListFragment() { - FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction(); nearbyListFragment = new NearbyListFragment(); nearbyListFragment.setArguments(bundle); fragmentTransaction.replace(R.id.container_sheet, nearbyListFragment, TAG_RETAINED_LIST_FRAGMENT); @@ -691,26 +457,235 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp fragmentTransaction.commitAllowingStateLoss(); } - @Override - public void onLocationChangedSignificantly(LatLng latLng) { - refreshView(LOCATION_SIGNIFICANTLY_CHANGED); + private void hideProgressBar() { + if (progressBar != null) { + progressBar.setVisibility(View.GONE); + } } - @Override - public void onLocationChangedSlightly(LatLng latLng) { - refreshView(LOCATION_SLIGHTLY_CHANGED); + /** + * This method first checks if the location permissions has been granted and then register the location manager for updates. + */ + private void registerLocationUpdates() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (locationManager.isLocationPermissionGranted()) { + locationManager.registerLocationManager(); + } else { + // Should we show an explanation? + if (locationManager.isPermissionExplanationRequired(getActivity())) { + new AlertDialog.Builder(getActivity()) + .setMessage(getString(R.string.location_permission_rationale_nearby)) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + requestLocationPermissions(); + dialog.dismiss(); + }) + .setNegativeButton(android.R.string.cancel, (dialog, id) -> { + showLocationPermissionDeniedErrorDialog(); + dialog.cancel(); + }) + .create() + .show(); + + } else { + // No explanation needed, we can request the permission. + requestLocationPermissions(); + } + } + } else { + locationManager.registerLocationManager(); + } } - public void prepareViewsForSheetPosition(int bottomSheetState) { - // TODO + private void requestLocationPermissions() { + if (!getActivity().isFinishing()) { + locationManager.requestPermissions(getActivity()); + } + } + + private void showLocationPermissionDeniedErrorDialog() { + new AlertDialog.Builder(getActivity()) + .setMessage(R.string.nearby_needs_permissions) + .setCancelable(false) + .setPositiveButton(R.string.give_permission, (dialog, which) -> { + //will ask for the location permission again + checkGps(); + }) + .setNegativeButton(R.string.cancel, (dialog, which) -> { + //dismiss dialog and send user to contributions tab instead + dialog.cancel(); + ((MainActivity)getActivity()).viewPager.setCurrentItem(((MainActivity)getActivity()).CONTRIBUTIONS_TAB_POSITION); + }) + .create() + .show(); + } + + /** + * Checks device GPS permission first for all API levels + */ + private void checkGps() { + Timber.d("checking GPS"); + if (!locationManager.isProviderEnabled()) { + Timber.d("GPS is not enabled"); + new AlertDialog.Builder(getActivity()) + .setMessage(R.string.gps_disabled) + .setCancelable(false) + .setPositiveButton(R.string.enable_gps, + (dialog, id) -> { + Intent callGPSSettingIntent = new Intent( + android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS); + Timber.d("Loaded settings page"); + startActivityForResult(callGPSSettingIntent, 1); + }) + .setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> { + showLocationPermissionDeniedErrorDialog(); + dialog.cancel(); + }) + .create() + .show(); + } else { + Timber.d("GPS is enabled"); + checkLocationPermission(); + } + } + + /** + * This method ideally should be called from inside of CheckGPS method. If device GPS is enabled + * then we need to control app specific permissions for >=M devices. For other devices, enabled + * GPS is enough for nearby, so directly call refresh view. + */ + private void checkLocationPermission() { + Timber.d("Checking location permission"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (locationManager.isLocationPermissionGranted()) { + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); + } else { + // Should we show an explanation? + if (locationManager.isPermissionExplanationRequired(getActivity())) { + // Show an explanation to the user *asynchronously* -- don't block + // this thread waiting for the user's response! After the user + // sees the explanation, try again to request the permission. + new AlertDialog.Builder(getActivity()) + .setMessage(getString(R.string.location_permission_rationale_nearby)) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + requestLocationPermissions(); + dialog.dismiss(); + }) + .setNegativeButton(android.R.string.cancel, (dialog, id) -> { + showLocationPermissionDeniedErrorDialog(); + dialog.cancel(); + }) + .create() + .show(); + + } else { + // No explanation needed, we can request the permission. + requestLocationPermissions(); + } + } + } else { + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); + } } private void showErrorMessage(String message) { - ViewUtil.showLongToast(NearbyActivity.this, message); + ViewUtil.showLongToast(getActivity(), message); + } + + private void addNetworkBroadcastReceiver() { + IntentFilter intentFilter = new IntentFilter(NETWORK_INTENT_ACTION); + snackbar = Snackbar.make(transparentView , R.string.no_internet, Snackbar.LENGTH_INDEFINITE); + + broadcastReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (snackbar != null) { + if (NetworkUtils.isInternetConnectionEstablished(getActivity())) { + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); + snackbar.dismiss(); + } else { + snackbar.show(); + } + } + } + }; + + getActivity().registerReceiver(broadcastReceiver, intentFilter); + } @Override - public void onWikidataEditSuccessful() { - refreshView(MAP_UPDATED); + public void onResume() { + super.onResume(); + // Resume the fragment if exist + resumeFragment(); + } + + public void onTabSelected(boolean onOrientationChanged) { + Timber.d("On nearby tab selected"); + this.onOrientationChanged = onOrientationChanged; + performNearbyOperations(); + + } + + /** + * Calls nearby operations in required order. + */ + private void performNearbyOperations() { + locationManager.addLocationListener(this); + registerLocationUpdates(); + lockNearbyView = false; + checkGps(); + addNetworkBroadcastReceiver(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (placesDisposable != null) { + placesDisposable.dispose(); + } + } + + @Override + public void onDetach() { + super.onDetach(); + snackbar = null; + broadcastReceiver = null; + } + + @Override + public void onStart() { + super.onStart(); + } + + @Override + public void onPause() { + super.onPause(); + // this means that this activity will not be recreated now, user is leaving it + // or the activity is otherwise finishing + if(getActivity().isFinishing()) { + // we will not need this fragment anymore, this may also be a good place to signal + // to the retained fragment object to perform its own cleanup. + //removeMapFragment(); + removeListFragment(); + + } + if (broadcastReceiver != null) { + getActivity().unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + if (locationManager != null) { + locationManager.removeLocationListener(this); + locationManager.unregisterLocationManager(); + } } } + + diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java index b094cbb41..3cf60c31d 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java @@ -73,7 +73,7 @@ public class NearbyListFragment extends DaggerFragment { ViewGroup container, Bundle savedInstanceState) { Timber.d("NearbyListFragment created"); - View view = inflater.inflate(R.layout.fragment_nearby, container, false); + View view = inflater.inflate(R.layout.fragment_nearby_list, container, false); recyclerView = view.findViewById(R.id.listView); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java index 69a05a339..628ab72f9 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java @@ -7,7 +7,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Color; -import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; @@ -16,6 +15,7 @@ import android.support.design.widget.BottomSheetBehavior; import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.FloatingActionButton; import android.support.v7.app.AlertDialog; +import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -109,7 +109,7 @@ public class NearbyMapFragment extends DaggerFragment { private Animation fab_close; private Animation fab_open; private Animation rotate_forward; - private ContributionController controller; + public ContributionController controller; private DirectUpload directUpload; private Place place; @@ -122,9 +122,7 @@ public class NearbyMapFragment extends DaggerFragment { private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.06; private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04; - private boolean isSecondMaterialShowcaseDismissed; private boolean isMapReady; - private MaterialShowcaseView thirdSingleShowCaseView; private Bundle bundleForUpdtes;// Carry information from activity about changed nearby places and current location @@ -177,6 +175,13 @@ public class NearbyMapFragment extends DaggerFragment { setRetainInstance(true); } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -214,7 +219,12 @@ public class NearbyMapFragment extends DaggerFragment { }); } + /** + * Updates map slightly means it doesn't updates all nearby markers around. It just updates + * location tracker marker of user. + */ public void updateMapSlightly() { + Timber.d("updateMapSlightly called, bundle is:"+bundleForUpdtes); if (mapboxMap != null) { Gson gson = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriDeserializer()) @@ -229,7 +239,13 @@ public class NearbyMapFragment extends DaggerFragment { } + /** + * Updates map significantly means it updates nearby markers and location tracker marker. It is + * called when user is out of boundaries (south, north, east or west) of markers drawn by + * previous nearby call. + */ public void updateMapSignificantly() { + Timber.d("updateMapSignificantly called, bundle is:"+bundleForUpdtes); if (mapboxMap != null) { if (bundleForUpdtes != null) { Gson gson = new GsonBuilder() @@ -309,6 +325,12 @@ public class NearbyMapFragment extends DaggerFragment { } } + /** + * Updates camera position according to list sheet status. If list sheet is collapsed, camera + * focus should be in the center. If list sheet is expanded, camera focus should be visible + * on the gap between list sheet and tab layout. + * @param isBottomListSheetExpanded + */ private void updateMapCameraAccordingToBottomSheet(boolean isBottomListSheetExpanded) { CameraPosition position; this.isBottomListSheetExpanded = isBottomListSheetExpanded; @@ -345,39 +367,40 @@ public class NearbyMapFragment extends DaggerFragment { } private void initViews() { - bottomSheetList = getActivity().findViewById(R.id.bottom_sheet); + Timber.d("initViews called"); + bottomSheetList = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.bottom_sheet); bottomSheetListBehavior = BottomSheetBehavior.from(bottomSheetList); - bottomSheetDetails = getActivity().findViewById(R.id.bottom_sheet_details); + bottomSheetDetails = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.bottom_sheet_details); bottomSheetDetailsBehavior = BottomSheetBehavior.from(bottomSheetDetails); bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); bottomSheetDetails.setVisibility(View.VISIBLE); - fabPlus = getActivity().findViewById(R.id.fab_plus); - fabCamera = getActivity().findViewById(R.id.fab_camera); - fabGallery = getActivity().findViewById(R.id.fab_galery); - fabRecenter = getActivity().findViewById(R.id.fab_recenter); + fabPlus = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_plus); + fabCamera = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_camera); + fabGallery = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_galery); + fabRecenter = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.fab_recenter); - fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open); - fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close); - rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward); - rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward); + fab_open = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.fab_open); + fab_close = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.fab_close); + rotate_forward = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.rotate_forward); + rotate_backward = AnimationUtils.loadAnimation(getParentFragment().getActivity(), R.anim.rotate_backward); - transparentView = getActivity().findViewById(R.id.transparentView); + transparentView = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.transparentView); - description = getActivity().findViewById(R.id.description); - title = getActivity().findViewById(R.id.title); - distance = getActivity().findViewById(R.id.category); - icon = getActivity().findViewById(R.id.icon); + description = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.description); + title = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.title); + distance = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.category); + icon = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.icon); - wikidataButton = getActivity().findViewById(R.id.wikidataButton); - wikipediaButton = getActivity().findViewById(R.id.wikipediaButton); - directionsButton = getActivity().findViewById(R.id.directionsButton); - commonsButton = getActivity().findViewById(R.id.commonsButton); + wikidataButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikidataButton); + wikipediaButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikipediaButton); + directionsButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.directionsButton); + commonsButton = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.commonsButton); - wikidataButtonText = getActivity().findViewById(R.id.wikidataButtonText); - wikipediaButtonText = getActivity().findViewById(R.id.wikipediaButtonText); - directionsButtonText = getActivity().findViewById(R.id.directionsButtonText); - commonsButtonText = getActivity().findViewById(R.id.commonsButtonText); + wikidataButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikidataButtonText); + wikipediaButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.wikipediaButtonText); + directionsButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.directionsButtonText); + commonsButtonText = ((NearbyFragment)getParentFragment()).view.findViewById(R.id.commonsButtonText); bookmarkButton = getActivity().findViewById(R.id.bookmarkButton); bookmarkButtonImage = getActivity().findViewById(R.id.bookmarkButtonImage); @@ -494,6 +517,7 @@ public class NearbyMapFragment extends DaggerFragment { } private void setupMapView(Bundle savedInstanceState) { + Timber.d("setupMapView called"); MapboxMapOptions options = new MapboxMapOptions() .compassGravity(Gravity.BOTTOM | Gravity.LEFT) .compassMargins(new int[]{12, 0, 0, 24}) @@ -505,15 +529,19 @@ public class NearbyMapFragment extends DaggerFragment { .zoom(11) .build()); - // create map - mapView = new MapView(getActivity(), options); - mapView.onCreate(savedInstanceState); - mapView.getMapAsync(mapboxMap -> { - ((NearbyActivity)getActivity()).setMapViewTutorialShowCase(); - NearbyMapFragment.this.mapboxMap = mapboxMap; - updateMapSignificantly(); - }); - mapView.setStyleUrl("asset://mapstyle.json"); + if (!getParentFragment().getActivity().isFinishing()) { + mapView = new MapView(getParentFragment().getActivity(), options); + // create map + mapView.onCreate(savedInstanceState); + mapView.getMapAsync(new OnMapReadyCallback() { + @Override + public void onMapReady(MapboxMap mapboxMap) { + NearbyMapFragment.this.mapboxMap = mapboxMap; + updateMapSignificantly(); + } + }); + mapView.setStyleUrl("asset://mapstyle.json"); + } } /** @@ -542,6 +570,7 @@ public class NearbyMapFragment extends DaggerFragment { * move. */ private void addCurrentLocationMarker(MapboxMap mapboxMap) { + Timber.d("addCurrentLocationMarker is called"); if (currentLocationMarker != null) { currentLocationMarker.remove(); // Remove previous marker, we are not Hansel and Gretel } @@ -564,8 +593,11 @@ public class NearbyMapFragment extends DaggerFragment { mapboxMap.addPolygon(currentLocationPolygonOptions); } + /** + * Adds markers for nearby places to mapbox map + */ private void addNearbyMarkerstoMapBoxMap() { - + Timber.d("addNearbyMarkerstoMapBoxMap is called"); mapboxMap.addMarkers(baseMarkerOptions); mapboxMap.setOnInfoWindowCloseListener(marker -> { @@ -624,6 +656,12 @@ public class NearbyMapFragment extends DaggerFragment { return circle; } + /** + * If nearby details bottom sheet state is collapsed: show fab plus + * If nearby details bottom sheet state is expanded: show fab plus + * If nearby details bottom sheet state is hidden: hide all fabs + * @param bottomSheetState + */ public void prepareViewsForSheetPosition(int bottomSheetState) { switch (bottomSheetState) { @@ -648,6 +686,9 @@ public class NearbyMapFragment extends DaggerFragment { } } + /** + * Hides all fabs + */ private void hideFAB() { removeAnchorFromFABs(fabPlus); @@ -679,25 +720,14 @@ public class NearbyMapFragment extends DaggerFragment { private void showFAB() { - addAnchorToBigFABs(fabPlus, getActivity().findViewById(R.id.bottom_sheet_details).getId()); + addAnchorToBigFABs(fabPlus, ((NearbyFragment)getParentFragment()).view.findViewById(R.id.bottom_sheet_details).getId()); fabPlus.show(); - addAnchorToSmallFABs(fabGallery, getActivity().findViewById(R.id.empty_view).getId()); + addAnchorToSmallFABs(fabGallery, ((NearbyFragment)getParentFragment()).view.findViewById(R.id.empty_view).getId()); - addAnchorToSmallFABs(fabCamera, getActivity().findViewById(R.id.empty_view1).getId()); - thirdSingleShowCaseView = new MaterialShowcaseView.Builder(this.getActivity()) - .setTarget(fabPlus) - .setDismissText(getString(R.string.showcase_view_got_it_button)) - .setContentText(getString(R.string.showcase_view_plus_fab)) - .setDelay(500) // optional but starting animations immediately in onCreate can make them choppy - .singleUse(ViewUtil.SHOWCASE_VIEW_ID_3) // provide a unique ID used to ensure it is only shown once - .setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD)) - .build(); + addAnchorToSmallFABs(fabCamera, ((NearbyFragment)getParentFragment()).view.findViewById(R.id.empty_view1).getId()); isMapReady = true; - if (isSecondMaterialShowcaseDismissed) { - thirdSingleShowCaseView.show(getActivity()); - } } @@ -724,6 +754,11 @@ public class NearbyMapFragment extends DaggerFragment { floatingActionButton.setLayoutParams(params); } + /** + * Same botom sheet carries information for all nearby places, so we need to pass information + * (title, description, distance and links) to view on nearby marker click + * @param place Place of clicked nearby marker + */ private void passInfoToSheet(Place place) { this.place = place; @@ -795,7 +830,7 @@ public class NearbyMapFragment extends DaggerFragment { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Timber.d("onRequestPermissionsResult: req code = " + " perm = " + permissions + " grant =" + grantResults); - // Do not use requestCode 1 as it will conflict with NearbyActivity's requestCodes + // Do not use requestCode 1 as it will conflict with NearbyFragment's requestCodes switch (requestCode) { // 4 = "Read external storage" allowed when gallery selected case 4: { @@ -873,18 +908,13 @@ public class NearbyMapFragment extends DaggerFragment { } } + /** + * This bundle is sent whenever and updte for nearby map comes, not for recreation, for updates + */ public void setBundleForUpdtes(Bundle bundleForUpdtes) { this.bundleForUpdtes = bundleForUpdtes; } - public void onNearbyMaterialShowcaseDismissed() { - isSecondMaterialShowcaseDismissed = true; - if (isMapReady) { - thirdSingleShowCaseView.show(getActivity()); - } - } - - @Override public void onStart() { if (mapView != null) { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNoificationCardView.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNoificationCardView.java new file mode 100644 index 000000000..6df87388a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNoificationCardView.java @@ -0,0 +1,278 @@ +package fr.free.nrw.commons.nearby; + +import android.content.Context; +import android.content.Intent; +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 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; + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + x1 = event.getX(); + break; + case MotionEvent.ACTION_UP: + x2 = event.getX(); + float 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) { + 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; + }); + } + + /** + * 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) { + if (this.getVisibility() == GONE) { + return; // If nearby card view is invisible because of preferences, do nothing + } + cardViewVisibilityState = CardViewVisibilityState.READY; + permissionRequestButton.setVisibility(GONE); + contentLayout.setVisibility(VISIBLE); + // Make progress bar invisible once data is ready + progressBar.setVisibility(GONE); + // And content views visible since they are ready + notificationTitle.setVisibility(VISIBLE); + notificationDistance.setVisibility(VISIBLE); + notificationIcon.setVisibility(VISIBLE); + + if (isClosestNearbyPlaceFound) { + notificationTitle.setText(place.name); + notificationDistance.setText(place.distance); + } else { + notificationDistance.setText(""); + notificationTitle.setText(R.string.no_close_nearby); + } + } + + @Override + protected void onVisibilityChanged(@NonNull View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + if (visibility == VISIBLE) { + /** + * Sometimes we need to preserve previous state of notification card view without getting + * any data from user. Ie. wen user came back from Media Details fragment to Contrib List + * fragment, we need to know what was the state of card view, and set it to exact same state. + */ + switch (cardViewVisibilityState) { + case READY: + permissionRequestButton.setVisibility(GONE); + contentLayout.setVisibility(VISIBLE); + // Make progress bar invisible once data is ready + progressBar.setVisibility(GONE); + // And content views visible since they are ready + notificationTitle.setVisibility(VISIBLE); + notificationDistance.setVisibility(VISIBLE); + notificationIcon.setVisibility(VISIBLE); + break; + case LOADING: + permissionRequestButton.setVisibility(GONE); + contentLayout.setVisibility(VISIBLE); + // Set visibility of elements in content layout once it become visible + progressBar.setVisibility(VISIBLE); + notificationTitle.setVisibility(GONE); + notificationDistance.setVisibility(GONE); + notificationIcon.setVisibility(GONE); + permissionRequestButton.setVisibility(GONE); + break; + case ASK_PERMISSION: + contentLayout.setVisibility(GONE); + permissionRequestButton.setVisibility(VISIBLE); + break; + default: + break; + } + } + } + + /** + * This states will help us to preserve progress bar and content layout states + */ + public enum CardViewVisibilityState { + LOADING, + READY, + INVISIBLE, + ASK_PERMISSION, + } + + /** + * We need to know which kind of permission we need to request, then update permission request + * button action accordingly + */ + public enum PermissionType { + ENABLE_GPS, + ENABLE_LOCATION_PERMISSON, // For only after Marsmallow + NO_PERMISSION_NEEDED + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java index 9e10ca8c4..6ca4af318 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java @@ -23,9 +23,9 @@ import timber.log.Timber; public class NearbyPlaces { - private static final int MIN_RESULTS = 40; + private static int MIN_RESULTS = 40; private static final double INITIAL_RADIUS = 1.0; // in kilometers - private static final double MAX_RADIUS = 300.0; // in kilometers + private static double MAX_RADIUS = 300.0; // in kilometers private static final double RADIUS_MULTIPLIER = 1.618; private static final Uri WIKIDATA_QUERY_URL = Uri.parse("https://query.wikidata.org/sparql"); private static final Uri WIKIDATA_QUERY_UI_URL = Uri.parse("https://query.wikidata.org/"); @@ -41,9 +41,22 @@ public class NearbyPlaces { } } - List getFromWikidataQuery(LatLng curLatLng, String lang) throws IOException { + List getFromWikidataQuery(LatLng curLatLng, String lang, boolean returnClosestResult) throws IOException { List places = Collections.emptyList(); + /** + * If returnClosestResult is true, then this means that we are trying to get closest point + * to use in cardView above contributions list + */ + if (returnClosestResult) { + MIN_RESULTS = 1; // Return closest nearby place + MAX_RADIUS = 5; // Return places only in 5 km area + radius = INITIAL_RADIUS; // refresh radius again, otherwise increased radius is grater than MAX_RADIUS, thus returns null + } else { + MIN_RESULTS = 40; + MAX_RADIUS = 300.0; // in kilometers + } + // increase the radius gradually to find a satisfactory number of nearby places while (radius <= MAX_RADIUS) { try { @@ -150,4 +163,5 @@ public class NearbyPlaces { return places; } + } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/Notification.java b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java index e6d759f66..0116d024c 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/Notification.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java @@ -11,13 +11,15 @@ public class Notification { public String description; public String link; public String iconUrl; + public String dateWithYear; - public Notification(NotificationType notificationType, String notificationText, String date, String description, String link, String iconUrl) { + public Notification(NotificationType notificationType, String notificationText, String date, String description, String link, String iconUrl, String dateWithYear) { this.notificationType = notificationType; this.notificationText = notificationText; this.date = date; this.description = description; this.link = link; this.iconUrl = iconUrl; + this.dateWithYear = dateWithYear; } } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java index 5af23c340..2108b166e 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -17,6 +17,7 @@ import android.widget.RelativeLayout; import com.pedrogomez.renderers.RVRendererAdapter; import java.util.Collections; +import java.util.Date; import java.util.List; import javax.inject.Inject; @@ -85,7 +86,12 @@ public class NotificationActivity extends NavigationBaseActivity { private void addNotifications() { Timber.d("Add notifications"); - if (mNotificationWorkerFragment == null){ + // Store when add notification is called last + long currentDate = new Date(System.currentTimeMillis()).getTime(); + getSharedPreferences("prefs", MODE_PRIVATE).edit().putLong("last_read_notification_date", currentDate).apply(); + Timber.d("Set last notification read date to current date:"+ currentDate); + + if(mNotificationWorkerFragment == null){ Observable.fromCallable(() -> { progressBar.setVisibility(View.VISIBLE); return controller.getNotifications(); diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java index 095a8a666..7c19c516c 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java @@ -144,7 +144,7 @@ public class NotificationUtils { notificationText = getWelcomeMessage(context, document); break; } - return new Notification(type, notificationText, getTimestamp(document), description, link, iconUrl); + return new Notification(type, notificationText, getTimestamp(document), description, link, iconUrl, getTimestampWithYear(document)); } private static String getNotificationText(Node document) { @@ -247,6 +247,14 @@ public class NotificationUtils { return ""; } + private static String getTimestampWithYear(Node document) { + Element timestampElement = (Element) getNode(document, "timestamp"); + if (timestampElement != null) { + return timestampElement.getAttribute("utcunix"); + } + return ""; + } + private static String getNotificationDescription(Node document) { Element titleElement = (Element) getNode(document, "title"); if (titleElement != null) { diff --git a/app/src/main/java/fr/free/nrw/commons/notification/UnreadNotificationsCheckAsync.java b/app/src/main/java/fr/free/nrw/commons/notification/UnreadNotificationsCheckAsync.java new file mode 100644 index 000000000..cb504f9c5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/UnreadNotificationsCheckAsync.java @@ -0,0 +1,81 @@ +package fr.free.nrw.commons.notification; + +import android.os.AsyncTask; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.Date; +import java.util.List; + +import fr.free.nrw.commons.contributions.MainActivity; +import fr.free.nrw.commons.contributions.ContributionsFragment; +import timber.log.Timber; + +/** + * This asynctask will check unread notifications after a date (date user check notifications last) + */ + +public class UnreadNotificationsCheckAsync extends AsyncTask { + + WeakReference context; + NotificationController notificationController; + + + public UnreadNotificationsCheckAsync(MainActivity context, NotificationController notificationController) { + this.context = new WeakReference<>(context); + this.notificationController = notificationController; + } + + @Override + protected Notification doInBackground(Void... voids) { + Notification lastNotification = null; + + try { + lastNotification = findLastNotification(notificationController.getNotifications()); + } catch (IOException e) { + e.printStackTrace(); + } + + return lastNotification; + } + + @Override + protected void onPostExecute(Notification lastNotification) { + super.onPostExecute(lastNotification); + + if (lastNotification == null) { + return; + } + + Date lastNotificationCheckDate = new Date(context.get() + .getSharedPreferences("prefs",0) + .getLong("last_read_notification_date", 0)); + Timber.d("You may have unread notifications since"+lastNotificationCheckDate); + + boolean isThereUnreadNotifications; + + Date lastReadNotificationDate = new java.util.Date(Long.parseLong(lastNotification.dateWithYear)*1000); + + if (lastNotificationCheckDate.before(lastReadNotificationDate)) { + isThereUnreadNotifications = true; + } else { + isThereUnreadNotifications = false; + } + + // Check if activity is still running + if (context.get().getWindow().getDecorView().isShown() && !context.get().isFinishing()) { + // Check if fragment is not null and visible + if (context.get().isContributionsFragmentVisible && context.get().contributionsActivityPagerAdapter.getItem(0) != null) { + ((ContributionsFragment)(context.get().contributionsActivityPagerAdapter.getItem(0))).updateNotificationsNotification(isThereUnreadNotifications); + } + } + } + + private Notification findLastNotification(List allNotifications) { + if (allNotifications.size() > 0) { + return allNotifications.get(allNotifications.size()-1); + } else { + return null; + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java index 18441146e..6b806d248 100644 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java @@ -8,7 +8,6 @@ import android.net.Uri; import android.os.Bundle; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; -import android.os.Bundle; import com.dinuscxj.progressbar.CircleProgressBar; @@ -16,7 +15,7 @@ import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.contributions.MainActivity; import android.support.v7.widget.Toolbar; import android.view.LayoutInflater; @@ -26,16 +25,9 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; -import com.dinuscxj.progressbar.CircleProgressBar; - import java.io.File; import java.io.FileOutputStream; -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.ContributionsActivity; /** * Displays the final score of quiz and congratulates the user @@ -63,7 +55,7 @@ public class QuizResultActivity extends AppCompatActivity { setScore(score); }else{ startActivityWithFlags( - this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, + this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP); super.onBackPressed(); } @@ -87,14 +79,14 @@ public class QuizResultActivity extends AppCompatActivity { @OnClick(R.id.quiz_result_next) public void launchContributionActivity(){ startActivityWithFlags( - this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, + this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP); } @Override public void onBackPressed() { startActivityWithFlags( - this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, + this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP); super.onBackPressed(); } diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index 77c94ca1d..763a6d09a 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -13,6 +13,7 @@ import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -32,11 +33,9 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.achievements.AchievementsActivity; import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.bookmarks.BookmarksActivity; -import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.category.CategoryImagesActivity; -import fr.free.nrw.commons.contributions.ContributionsActivity; -import fr.free.nrw.commons.nearby.NearbyActivity; +import fr.free.nrw.commons.bookmarks.BookmarksActivity; import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.settings.SettingsActivity; import timber.log.Timber; @@ -91,6 +90,23 @@ public abstract class NavigationBaseActivity extends BaseActivity } } + public void changeDrawerIconToBakcButton() { + toggle.setDrawerIndicatorEnabled(false); + toggle.setHomeAsUpIndicator(R.drawable.ic_arrow_back_white); + toggle.setToolbarNavigationClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onBackPressed(); + } + }); + } + + public void changeDrawerIconToDefault() { + if (toggle != null) { + toggle.setDrawerIndicatorEnabled(true); + } + } + /** * Set the username in navigationHeader. */ @@ -156,13 +172,9 @@ public abstract class NavigationBaseActivity extends BaseActivity case R.id.action_home: drawerLayout.closeDrawer(navigationView); startActivityWithFlags( - this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, + this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP); return true; - case R.id.action_nearby: - drawerLayout.closeDrawer(navigationView); - startActivityWithFlags(this, NearbyActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - return true; case R.id.action_about: drawerLayout.closeDrawer(navigationView); startActivityWithFlags(this, AboutActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java index 5a413e49a..342869074 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java @@ -10,7 +10,7 @@ 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.contributions.MainActivity; import fr.free.nrw.commons.utils.ImageUtils; import timber.log.Timber; @@ -63,8 +63,8 @@ public class DetectUnwantedPicturesAsync extends AsyncTask { - //user does not wish to upload the picture, take them back to ContributionsActivity - Intent intent = new Intent(activity, ContributionsActivity.class); + //user does not wish to upload the picture, take them back to MainActivity + Intent intent = new Intent(activity, MainActivity.class); dialogInterface.dismiss(); activity.startActivity(intent); }); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java index 7d59a7568..08669ed9e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java @@ -10,7 +10,7 @@ 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.contributions.MainActivity; import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; @@ -77,8 +77,8 @@ public class ExistingFileAsync extends AsyncTask { 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); + //Go back to MainActivity + Intent intent = new Intent(context.get(), MainActivity.class); context.get().startActivity(intent); callback.onResult(Result.DUPLICATE_CANCELLED); }); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 2eae7b8f5..823a4b91f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -36,7 +36,7 @@ import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.ContributionDao; -import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.UploadResult; @@ -193,7 +193,7 @@ public class UploadService extends HandlerService { .setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload)) .setOngoing(true) .setProgress(100, 0, true) - .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0)) + .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0)) .setTicker(getString(R.string.upload_progress_notification_title_in_progress, contribution.getDisplayTitle())); } @@ -318,7 +318,7 @@ public class UploadService extends HandlerService { Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true) .setSmallIcon(R.drawable.ic_launcher) .setAutoCancel(true) - .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ContributionsActivity.class), 0)) + .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0)) .setTicker(getString(R.string.upload_failed_notification_title, contribution.getDisplayTitle())) .setContentTitle(getString(R.string.upload_failed_notification_title, contribution.getDisplayTitle())) .setContentText(getString(R.string.upload_failed_notification_subtitle)) diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ContributionListViewUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ContributionListViewUtils.java new file mode 100644 index 000000000..6dc9625d7 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ContributionListViewUtils.java @@ -0,0 +1,39 @@ +package fr.free.nrw.commons.utils; + +import android.util.Log; +import android.view.View; + +/** + * This class includes utilities for contribution list fragment indicators, such as number of + * uploads, notification and nearby cards and their progress bar behind them. + */ +public class ContributionListViewUtils { + + /** + * Sets indicator and progress bar visibility according to 3 states, data is ready to display, + * data still loading, both should be invisible because media details fragment is visible + * @param indicator this can be numOfUploads text view, notification/nearby card views + * @param progressBar this is the progress bar behind indicators, displays they are loading + * @param isIndicatorReady is indicator fetched the information will be displayed + * @param isBothInvisible true if contribution list fragment is not active (ie. Media Details Fragment is active) + */ + public static void setIndicatorVisibility(View indicator, View progressBar, boolean isIndicatorReady, boolean isBothInvisible) { + if (indicator!=null && progressBar!=null) { + if (isIndicatorReady) { + // Indicator ready, display them + indicator.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.GONE); + } else { + if (isBothInvisible) { + // Media Details Fragment is visible, hide both + indicator.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + } else { + // Indicator is not ready, still loading + indicator.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + } + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java index 3429ef403..8595634d5 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java @@ -11,6 +11,11 @@ import fr.free.nrw.commons.CommonsApplication; public class PermissionUtils { + public static final int CAMERA_PERMISSION_FROM_CONTRIBUTION_LIST = 100; + public static final int GALLERY_PERMISSION_FROM_CONTRIBUTION_LIST = 101; + public static final int CAMERA_PERMISSION_FROM_NEARBY_MAP = 102; + public static final int GALLERY_PERMISSION_FROM_NEARBY_MAP = 103; + /** * This method can be used by any activity which requires a permission which has been blocked(marked never ask again by the user) It open the app settings from where the user can manually give us the required permission. diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java index ed6513ca2..a2c25c948 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java @@ -5,7 +5,9 @@ import android.content.Context; import android.support.design.widget.Snackbar; import android.view.Display; import android.view.View; +import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; +import android.widget.PopupWindow; import android.widget.Toast; public class ViewUtil { @@ -49,4 +51,15 @@ public class ViewUtil { } } + public static void displayPopupWindow(View anchorView, Context context, View popupWindowLayout, String text) { + + PopupWindow popup = new PopupWindow(context); + popup.setContentView(popupWindowLayout); + // Closes the popup window when touch outside of it - when looses focus + popup.setOutsideTouchable(true); + popup.setFocusable(true); + // Show anchored to button + popup.showAsDropDown(anchorView); + } + } diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-hdpi/ic_arrow_back_white.png new file mode 100644 index 0000000000000000000000000000000000000000..0b78dc8c4d0e01106b63e11476cbe9381e04748e GIT binary patch literal 197 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBdOTemLn>~)o$kxkpdjGZd+42{ zh|Bbq)~?cfC-?5oxX90DmuXk=oBjTZ#fDP7%Nkk)+BiIqamGn_8cmGMC_43nxjUh_ zBgAE9v9JB~oe~M&k`FX4Pf9T`U3*C7OJ|AGT&>Tlw_l$sblP!hf^)CUw-eohKTD=4 wOs)UxpqIhVws)C_<*oD0iiFVdQ&MBb@02-oBIsgCw literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art.png new file mode 100644 index 0000000000000000000000000000000000000000..d974d19d5aa423d77352bf2e05e538bce7a8ceda GIT binary patch literal 312 zcmV-80muG{P)BOx|35j=J(PV_PQKKG(5pg!Kd(5PwyfPzqrH0000< KMNUMnLSTZJ34e+J literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art_dot.png new file mode 100644 index 0000000000000000000000000000000000000000..b03310f557eb65a272ed2cb7bbc3cacb12fbc72b GIT binary patch literal 429 zcmV;e0aE^nP)^ z0AImDv*;^iGMfr@DoqiKLn+ZlV@%C;=wdat*IX+I`Nq3H{NM;*2nZoK*Cfs+#Td#2 ztJVKvf#bY1n-96%eKI*=*$Kxzl}d|t8w5dUPelG2*M>1w)%W@Q_$npKx4KS=;@@Cv z!?21)=y|6CN|LgsNv6~Lhf$s}aGa^Cej@<@e9L;S)ea7$V6UxK=<6NKTfgs`CN8$@ zb`i@(r-O$VN!UETY+bv?evu!La!7vGyMT|*7z`_6kOg@iVDhe)kRjs0A z1WogrhKuo{LuZ&{}Twp_mJ^$3#OKa9ADDN)RtCReE( zUlBBY(`Xz^0A~?@jyo7Eb$v4&?rfVB1g22nHI0&_&=?o{)m-MEI>urw#$qhSE&%%g Xpbmdw>f??A00000NkvXXu0mjfH3PTW literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-mdpi/ic_arrow_back_white.png new file mode 100644 index 0000000000000000000000000000000000000000..8d1142f31d3090ca65b14f780477a6f070a17342 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gj{+=$5Ar-fh6C_xjx&HkBZ_nG3 znfXIf!H)BY))tdHxmOByN!4+6>}{CQ8sXFRP@zVuqknP(KeOTM15pm26&87`^hY`I idQWMKE41axU}K0WEI(m;{KYAt(F~rhelF{r5}E+a@-4jp literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art.png new file mode 100644 index 0000000000000000000000000000000000000000..f9cb427356e001b42f5de11b78e643bd5fb481ef GIT binary patch literal 233 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gj8$4YcLn>}1CrB7R`2YXEkHZ_z zBli@{bafacYuLJMSLRx<3tU%>WJoFwI9*_`aIZPVf~WaNji#v$U-!Wm7Hu(yMH_#x z2#Pga(U_3zC@I)>xIofUY>ncV%<<-EpYqd grns~e2_FWAT{Xgz(LL**0o~5v>FVdQ&MBb@03dW#mjD0& literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art_dot.png new file mode 100644 index 0000000000000000000000000000000000000000..ebb4f86c0b89725949e78a6f34f079dc16916722 GIT binary patch literal 2206 zcmV;P2x0e$P)QoQwW60#BHF45-_QpCc%m$f5>04 zWLqOy(rD&%ao)>FvgCOm_stu%^Mkk1+;?@}|DAj8z31Nhph=S^O`0@m5)USB1GYj$ zQa}rkbpBKUR*f;G_^jHRRNMw^LmFs9MKr(~umCiifDPq=BGLuy0(Jx4Kn}HW8Mp!5 z0_K5vV~oUQ=Xx5(14SeO^a4Y`0B`{4aDFZV7lAK;Pk=i>Q$Uw>F|P+00Uie)1@caj zBI=E;s0UsYkriW1IRL3y3=CFu}LBmJ}jdAX<#l2h%V3y9F%}*aX^pn+@nHNHe2k>3s zUsLdMm`dvIfg+MbMa!d}w+D@V{b&z71Y|egSA6fi{FM_Y2Ft}_Sm7SPz^_K&=K=4n zFCx-{#;m=-x6n@GG3V+)JBSS;EuJ~kc4=bbo0X*{>*Bc>cx?oJ5#YZ1AR>8GAlKu1 zfj)HT@`!V_IM>&qr8GO6nm&AZxP0qYI&vG~hhy+(KljvAz<$)ro&ZKs@p8_UrW*ee z>+SmT*s;eKPoFN-$;^3yp)sBHcp}nHL_~~;w2Md&+EkxJ2g-x!KzR?^WOHa-OtMbA zyQfZd)=@lwE(5>ub3>#a=#23W0Q*s|dy=YXSsv_dH!wXmHnco7m5b8G%@`;rV0Z{V z_fkF*4|F`Q1sxFgqv8#rQ?wq`<2E$a%@p5%KVNV000fd1__5dWQ8nQ-I=kpYd)h;& zkS$&-xclatyP`3zHUcNbqc%|rxC`wx27rTT)AihwE`IPqN9|IPvG+f4*h@j{0y?vc z9Pka)h#od|4z(yI*fc62i(U%Ze$!dB`S#%P4Q@qh-@Y|#76n9f0jnmq1 z+Ma&;o)wFNabpP1c`0aHz$7YMD{N8JM+XA|aL}?p!?|${w+0YjGMBtBJQ&|w5fGHbwcb+_Xx9yo{?u53= zJM#iR^>afM0&cY|8JNB|-k3>tch_TAmI%D;C!G(`q_eDv2RfvA^qqGuByzc^_rbj8 zcR>lF%LiH&v2A>OssHV_r<3`7Bq9p@dK7*cs`}fvtdSE*nlfGXO@Z6m5hjls(_h7W^c?Zyq&f=2he?QmmY7HwG%J-f0Mo&_|TVf(}BboPW5}r-K#ex{5xV zuin_N2UgIfz**qm*jxV%T8E9SvpCmP^w~T&U+^iQriZw3{YPMjls6?<#{f#2Y(UCp~qhbP$6q&bIQ*31u$)l@wA&hDqgaNX~?%{H+L=)_d04v#v}DdKisSO(_Mbd+)Hh~CoeX@o&= z2^HlMD%`^atN{0%vU4=R*W4OKz#HgXG>K+P+R=fnD`Xk%^%sE4PT3nY1q3vvT|os} zLwn<1bdK#NDBREAT|>v8AEPIUVm(U&4IrQ~rsQOX@1Pw>8a=0WpXw)|7SU!q1AK~p zK&;gKdRvtW>V30lv%H9QC~lT#6^(s&P|sV#<2jC~iG3Wn9nKKvYvS&0cO=f&KN|KW gH)+zO$^Se51?h(~-zQA>$9l)%%mq z^mETW6^|Tp_gScr>Ga9(x!xRwZ_+=`NlI9($xHq7a28OvLdTrXmqYJVWb-}SZt=@> zDdV=Y|GW0Ro4V;{{=K0=gyoHZ6LJjY?`6YXg)Jve&hhiKRFAix!e$}&jxVTvcl(?# feb*To9NI+X|48m$S$u36(CrMKu6{1-oD!MQ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art.png new file mode 100644 index 0000000000000000000000000000000000000000..62369be0ac2fea751adc1c99a5a671f16d75e6c9 GIT binary patch literal 410 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png new file mode 100644 index 0000000000000000000000000000000000000000..2fe75945f8ce349e27be78bda406d186a9b08cf9 GIT binary patch literal 355 zcmV-p0i6DcP)DcQEyHxN>wqAye20X6{;0UdQ48H#@tlyd2K#Pnw)2IDqYp2$Z~RWk2#gL zqM6m?q%G#`MGKPSH)n2nl#`s6QBHDtBxgr*nv;`I0{zKJtR^QTIsHeu>EtlVDS?zg zN+2bW68Qfm@F-e}CV^=Dl?7bK66kahodVK9G!0>thpO{O4eD3|F$jbBit!`OnE&ON zrzrBO!OJS^4i;vtTv^&%pKko^^QJOMk|>juNy;Q;k}^q|G?}E0+v89s>rE>3GutN# z4W|ehM=kXgpwMTISv(bhbEuXiksJU30000OI(OceGx>Z?H*^31002ovPDHLkV1l!O Blgt1B literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art.png new file mode 100644 index 0000000000000000000000000000000000000000..907c922fc3861573c50e3e379968c722c9401019 GIT binary patch literal 608 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 b4hH8~Mc2GLB0KS?J4nRS)z4*}Q$iB}$=0OT literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art.png new file mode 100644 index 0000000000000000000000000000000000000000..88ba092c1848d8de55b853bd886ba62dcbe4ea39 GIT binary patch literal 839 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>VCM65aSW-r^>*&r>?;lu$99K? zzfPPORwW>^g<~)GqvHo>i`%YX=l}YwDMG_(^$+-zMNz&Zf_QHoebLn|tztfD?ye3y7E!o;hpV(cedPFWtC# zVSR^u_x=Q&j=nJhDcW_F2%4)A;7bMb9eb#`JPwbc8$Hb3Q zexx4!HNW!i-m+6~xg{(QUpM}~@5gI~h54y>uiM@==ghC4YOVb*F6qPnUHbb}?K~dU zx7T0leXU~sr2LUE%lu`!A6pr-_pJXqZ_O6Y(*Kza5C2QLUj3dSdB8Hf*qL>~ zb%pe`ZR^Xo++&ledp574-<4sy5{La~*X)Py#rAh_94HR1Z8UjSaWvT`v`12QGfRTn zq7TORx0E<96<2IX6?PW6)ZzR{Uy$j?n)i2^8jPzZ+c+^;1(e@nYRGGOn7%KM@xb8@ zMw<-`35rP!++qx0l-Mq8W=JqiV$cS1l-Vw9XHe+835c&8|3&6^{keLd6W zTAZ5rag%VFWtPWH&h0WM4<0`|TiNbH zKzW3KnBwN5>yonPg$@c&vtj7zh_5c0^y`-3q%X}I-h4Fu$jkYQdB$f31NWwX3~F}| zt!maeX64!RG$xzplje-2%zFHX?7lZ_vw6+;((L?^YR`2s9KSA{+x=hi*R@v0_b;}o zgvoYriht7n$e#Z9=eCto`IH$hmWfBn&Xer~C(;1LzijNABDDe?^uB>Y$kWx&Wt~$( F69A2tYYqSa literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art_dot.png new file mode 100644 index 0000000000000000000000000000000000000000..c5a5e3f1f88bcd1d963f550fe1977a0101e2b690 GIT binary patch literal 10898 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ðulH_3WxHdj@YXk;?YYTWV2L-+T-26+n@6VZVhc`( zcdlO8N3XZj9Wu;8`ypGK*Q|p+;TqnKVKo*#K@9&xu3L{^Cw5(M=Fo{$tXPRWTZXA%0aF)H{Zv}SdcmtZ-If4>b*C0F9WYdvE(=Gl$C{z?xiyR5M<(poT$v&N5Vfr zNy1-gcdB!QHNmKin$q({27hKUx7**I<=a9Zjv1G zQEBQ+tkErtT%oQj+SQEX`>4(cFmXq1^CKr)U#liGKz*Ns^uGsy9b4kU6 z7XDI&aphX{<-KuX^QTfe;`zdKihJv?Pabt#pK^3M64DUEY}VIhop%W`+30N#8P!Pj z?=V}_j5JMFJYqv|&7uMPprDMJ8 zU6zXSA8vs10NsXXNi4wOY!dSL1~cy!IV{0EnD`F?$bO&Y4gSO&RlM4exX_*)ok0cG zGmQ~4L4p6=YLlc7R=CIR+KfvHFec93$EIH6`EinaW){032BsbNp7H=f7{+8Kh;#nZ z5rJY$!|+rJH2rTe+%Etsu#%3_F#PaXCn+LyLKbs{n$Gx@Zj{r8#Vuw0=d@JEX ziFvf)*D$vsj6e)rC>+Ln5MIoxw6{{Z_Ho!TCS@33tzu*KemrNDhb;Gda6rDoI{p6p zR4!I!1VEkeb&Pq|Y`;O(YM)miF^^rO zKbjuHgj9w<1UzPmF)_e<5(<8zKzpsQ`vCFtbM_e>DIvQ=7VzNSAb!jGwkOT1ft8WCV%SA+WpsTYU} z+7Rj(cAn)f${LYxJwbG4mz^Au2Adi=_r#pe^fd_B*SltjPyjr@i1DJq5P7?f)Sc(^ z+GGc|4@R1g{+<$2E)M*rF|Go?rr*>r4O?gI4lZ#0hcgmZ zWZAEmqz!UOkyUhkG*oXrYnz;yo2E7>a*X(7?x%NFq5Jol9XoXbDt}(kq)CXkW8e$) z?bE9mQF&Pl)sGHUF`QDgX=Mo0uHN*;?>u#O%||OFWIkiE@pP_0fUvDb+W8d_K7I3=-Xq5+>b?bj;HS>6a`Ax zPCEjTAyHs6)lEMX!kAT6)kjWvXj%B^>!+kcQ)`zCjr-l?_i@PmVQHAsp|8u%VsaJ+ zlB8BreLE;zW53G81Ma=ctlY;X{j8GyX_LNl3}f~+r6d?#gn;g^tieX5Aj*Q(V?>N^ zlxQt`8ovA9n^J$VXkHct?u+0)X}{bJ!-wmoo73*k#XGz26Tdx-2k${O8=@H;EDW5B zENTpkk`i)M4ijC!AQHT?eF`z1@m_4y343Ap_%#n1{x7>Ili+9QvCQu#)HL$@VE-t9 NRnt?gxNY + + diff --git a/app/src/main/res/drawable/ic_notifications_active_white_24dp.xml b/app/src/main/res/drawable/ic_notifications_active_white_24dp.xml new file mode 100644 index 000000000..5d6921643 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_active_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_white_24dp.xml b/app/src/main/res/drawable/ic_notifications_white_24dp.xml new file mode 100644 index 000000000..120895c4f --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_white_24dp.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/layout/activity_contributions.xml b/app/src/main/res/layout/activity_contributions.xml index 51a48f0a5..2e6e92728 100644 --- a/app/src/main/res/layout/activity_contributions.xml +++ b/app/src/main/res/layout/activity_contributions.xml @@ -1,34 +1,51 @@ + android:layout_height="match_parent" + android:background="?attr/contributionsListBackground" + > + android:layout_height="match_parent" + android:gravity="center_horizontal" + > + + + android:layout_below="@id/tab_layout"> - + android:layout_height="match_parent" /> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_contributions.xml b/app/src/main/res/layout/fragment_contributions.xml index a016d752c..dd1959178 100644 --- a/app/src/main/res/layout/fragment_contributions.xml +++ b/app/src/main/res/layout/fragment_contributions.xml @@ -1,51 +1,22 @@ - + xmlns:app="http://schemas.android.com/apk/res-auto" + android:orientation="vertical"> - + - + + - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_contributions_list.xml b/app/src/main/res/layout/fragment_contributions_list.xml new file mode 100644 index 000000000..719283336 --- /dev/null +++ b/app/src/main/res/layout/fragment_contributions_list.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_nearby.xml b/app/src/main/res/layout/fragment_nearby.xml index ef580fe99..4269f135b 100644 --- a/app/src/main/res/layout/fragment_nearby.xml +++ b/app/src/main/res/layout/fragment_nearby.xml @@ -1,13 +1,161 @@ - - - + + android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/app/src/main/res/layout/fragment_nearby_list.xml b/app/src/main/res/layout/fragment_nearby_list.xml new file mode 100644 index 000000000..ef580fe99 --- /dev/null +++ b/app/src/main/res/layout/fragment_nearby_list.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/layout/nearby_card_view.xml b/app/src/main/res/layout/nearby_card_view.xml new file mode 100644 index 000000000..fe52e219a --- /dev/null +++ b/app/src/main/res/layout/nearby_card_view.xml @@ -0,0 +1,96 @@ + + +