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 43debf323..8f6012720 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 000000000..0b78dc8c4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_arrow_back_white.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art.png new file mode 100644 index 000000000..d974d19d5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art_dot.png new file mode 100644 index 000000000..b03310f55 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification_white_clip_art_dot.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-mdpi/ic_arrow_back_white.png new file mode 100644 index 000000000..8d1142f31 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_arrow_back_white.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art.png new file mode 100644 index 000000000..f9cb42735 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art_dot.png new file mode 100644 index 000000000..ebb4f86c0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification_white_clip_art_dot.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-xhdpi/ic_arrow_back_white.png new file mode 100644 index 000000000..f782543d3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_arrow_back_white.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art.png new file mode 100644 index 000000000..62369be0a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art_dot.png new file mode 100644 index 000000000..c6be29890 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification_white_clip_art_dot.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png new file mode 100644 index 000000000..2fe75945f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art.png new file mode 100644 index 000000000..907c922fc Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art_dot.png new file mode 100644 index 000000000..239de1d73 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification_white_clip_art_dot.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white.png b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white.png new file mode 100644 index 000000000..575d5075c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art.png b/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art.png new file mode 100644 index 000000000..88ba092c1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art_dot.png b/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art_dot.png new file mode 100644 index 000000000..c5a5e3f1f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_notification_white_clip_art_dot.png differ diff --git a/app/src/main/res/drawable/ic_location_white_24dp.xml b/app/src/main/res/drawable/ic_location_white_24dp.xml new file mode 100644 index 000000000..aea56cb5a --- /dev/null +++ b/app/src/main/res/drawable/ic_location_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_active_white_24dp.xml b/app/src/main/res/drawable/ic_notifications_active_white_24dp.xml new file mode 100644 index 000000000..5d6921643 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_active_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_white_24dp.xml b/app/src/main/res/drawable/ic_notifications_white_24dp.xml new file mode 100644 index 000000000..120895c4f --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_white_24dp.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/layout/activity_contributions.xml b/app/src/main/res/layout/activity_contributions.xml index 51a48f0a5..2e6e92728 100644 --- a/app/src/main/res/layout/activity_contributions.xml +++ b/app/src/main/res/layout/activity_contributions.xml @@ -1,34 +1,51 @@ + android:layout_height="match_parent" + android:background="?attr/contributionsListBackground" + > + android:layout_height="match_parent" + android:gravity="center_horizontal" + > + + + android:layout_below="@id/tab_layout"> - + android:layout_height="match_parent" /> + + + + + \ 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 @@ + + +