From 1b01c6517f06a9357f8a6374cd00069cfb12313a Mon Sep 17 00:00:00 2001 From: Ashish Kumar Date: Fri, 14 Dec 2018 22:25:53 +0530 Subject: [PATCH] Show campaigns (#2113) * Show campaigns * Added a ui util class SwipableCardView which passes the onSwipe event to its children * NearbyCardView & CampaignView extend SwipableCardView * Fetch campaigns in ContributionsFragment * Added an option to enable disable campaign in Settings/Preferences * synced strings with master * removed duplicate initialsation of CampaignPresenter --- .../fr/free/nrw/commons/BasePresenter.java | 16 +++ .../java/fr/free/nrw/commons/MvpView.java | 8 ++ .../free/nrw/commons/campaigns/Campaign.java | 55 +++++++++ .../nrw/commons/campaigns/CampaignConfig.java | 12 ++ .../campaigns/CampaignResponseDTO.java | 24 ++++ .../nrw/commons/campaigns/CampaignView.java | 110 ++++++++++++++++++ .../commons/campaigns/CampaignsPresenter.java | 99 ++++++++++++++++ .../nrw/commons/campaigns/ICampaignsView.java | 13 +++ .../contributions/ContributionsFragment.java | 78 ++++++++++++- .../mwapi/ApacheHttpClientMediaWikiApi.java | 17 +++ .../free/nrw/commons/mwapi/MediaWikiApi.java | 3 + .../nearby/NearbyNoificationCardView.java | 50 ++------ .../nrw/commons/utils/SwipableCardView.java | 72 ++++++++++++ .../main/res/drawable-hdpi/ic_campaign.png | Bin 0 -> 807 bytes .../main/res/drawable-mdpi/ic_campaign.png | Bin 0 -> 542 bytes .../main/res/drawable-xhdpi/ic_campaign.png | Bin 0 -> 1051 bytes .../main/res/drawable-xxhdpi/ic_campaign.png | Bin 0 -> 1590 bytes .../main/res/drawable-xxxhdpi/ic_campaign.png | Bin 0 -> 2157 bytes .../res/layout/fragment_contributions.xml | 18 ++- app/src/main/res/layout/layout_campagin.xml | 71 +++++++++++ app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/preferences.xml | 5 + 22 files changed, 608 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/BasePresenter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/MvpView.java create mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/Campaign.java create mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.java create mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.java create mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java create mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java create mode 100755 app/src/main/res/drawable-hdpi/ic_campaign.png create mode 100755 app/src/main/res/drawable-mdpi/ic_campaign.png create mode 100755 app/src/main/res/drawable-xhdpi/ic_campaign.png create mode 100755 app/src/main/res/drawable-xxhdpi/ic_campaign.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_campaign.png create mode 100644 app/src/main/res/layout/layout_campagin.xml diff --git a/app/src/main/java/fr/free/nrw/commons/BasePresenter.java b/app/src/main/java/fr/free/nrw/commons/BasePresenter.java new file mode 100644 index 000000000..041fde6b2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/BasePresenter.java @@ -0,0 +1,16 @@ +package fr.free.nrw.commons; + +/** + * Base presenter, enforcing contracts to atach and detach view + */ +public interface BasePresenter { + /** + * Until a view is attached, it is open to listen events from the presenter + */ + void onAttachView(MvpView view); + + /** + * Detaching a view makes sure that the view no more receives events from the presenter + */ + void onDetachView(); +} diff --git a/app/src/main/java/fr/free/nrw/commons/MvpView.java b/app/src/main/java/fr/free/nrw/commons/MvpView.java new file mode 100644 index 000000000..7485b2aaf --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/MvpView.java @@ -0,0 +1,8 @@ +package fr.free.nrw.commons; + +/** + * Base interface for all the views + */ +public interface MvpView { + void showMessage(String message); +} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/Campaign.java b/app/src/main/java/fr/free/nrw/commons/campaigns/Campaign.java new file mode 100644 index 000000000..2bd4893b8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/Campaign.java @@ -0,0 +1,55 @@ +package fr.free.nrw.commons.campaigns; + +import com.google.gson.annotations.SerializedName; + +/** + * A data class to hold a campaign + */ +public class Campaign { + + @SerializedName("title") private String title; + @SerializedName("description") private String description; + @SerializedName("startDate") private String startDate; + @SerializedName("endDate") private String endDate; + @SerializedName("link") private String link; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getStartDate() { + return startDate; + } + + public void setStartDate(String startDate) { + this.startDate = startDate; + } + + public String getEndDate() { + return endDate; + } + + public void setEndDate(String endDate) { + this.endDate = endDate; + } + + public String getLink() { + return link; + } + + public void setLink(String link) { + this.link = link; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.java new file mode 100644 index 000000000..a715aaf63 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.java @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.campaigns; + +import com.google.gson.annotations.SerializedName; + +/** + * A data class to hold the campaign configs + */ +class CampaignConfig { + + @SerializedName("showOnlyLiveCampaigns") private boolean showOnlyLiveCampaigns; + @SerializedName("sortBy") private String sortBy; +} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.java new file mode 100644 index 000000000..dd0bd51ce --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.java @@ -0,0 +1,24 @@ +package fr.free.nrw.commons.campaigns; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +/** + * Data class to hold the response from the campaigns api + */ +public class CampaignResponseDTO { + + @SerializedName("config") + private CampaignConfig campaignConfig; + + @SerializedName("campaigns") + private List campaigns; + + public CampaignConfig getCampaignConfig() { + return campaignConfig; + } + + public List getCampaigns() { + return campaigns; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java new file mode 100644 index 000000000..dec62cc1b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java @@ -0,0 +1,110 @@ +package fr.free.nrw.commons.campaigns; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.contributions.MainActivity; +import fr.free.nrw.commons.utils.SwipableCardView; +import fr.free.nrw.commons.utils.ViewUtil; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * A view which represents a single campaign + */ +public class CampaignView extends SwipableCardView { + Campaign campaign = null; + private ViewHolder viewHolder; + + public CampaignView(@NonNull Context context) { + super(context); + init(); + } + + public CampaignView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(); + } + + public CampaignView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public void setCampaign(Campaign campaign) { + this.campaign = campaign; + if (campaign != null) { + this.setVisibility(View.VISIBLE); + viewHolder.init(); + } else { + this.setVisibility(View.GONE); + } + } + + @Override public boolean onSwipe(View view) { + view.setVisibility(View.GONE); + ((MainActivity) getContext()).prefs.edit() + .putBoolean("displayCampaignsCardView", false) + .apply(); + ViewUtil.showLongToast(getContext(), + getResources().getString(R.string.nearby_campaign_dismiss_message)); + return true; + } + + private void init() { + View rootView = inflate(getContext(), R.layout.layout_campagin, this); + viewHolder = new ViewHolder(rootView); + setOnClickListener(view -> { + if (campaign != null) { + showCampaignInBrowser(campaign.getLink()); + } + }); + } + + /** + * open the url associated with the campaign in the system's default browser + */ + private void showCampaignInBrowser(String link) { + Intent view = new Intent(); + view.setAction(Intent.ACTION_VIEW); + view.setData(Uri.parse(link)); + getContext().startActivity(view); + } + + public class ViewHolder { + + @BindView(R.id.tv_title) TextView tvTitle; + @BindView(R.id.tv_description) TextView tvDescription; + @BindView(R.id.tv_dates) TextView tvDates; + + public ViewHolder(View itemView) { + ButterKnife.bind(this, itemView); + } + + public void init() { + if (campaign != null) { + tvTitle.setText(campaign.getTitle()); + tvDescription.setText(campaign.getDescription()); + SimpleDateFormat inputDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + SimpleDateFormat outputDateFormat = new SimpleDateFormat("dd MMM"); + try { + Date startDate = inputDateFormat.parse(campaign.getStartDate()); + Date endDate = inputDateFormat.parse(campaign.getEndDate()); + tvDates.setText(String.format("%1s - %2s", outputDateFormat.format(startDate), + outputDateFormat.format(endDate))); + } catch (ParseException e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java new file mode 100644 index 000000000..98ef7e6de --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java @@ -0,0 +1,99 @@ +package fr.free.nrw.commons.campaigns; + +import android.util.Log; +import fr.free.nrw.commons.BasePresenter; +import fr.free.nrw.commons.MvpView; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import io.reactivex.Single; +import io.reactivex.SingleObserver; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * The presenter for the campaigns view, fetches the campaigns from the api and informs the view on + * success and error + */ +public class CampaignsPresenter implements BasePresenter { + private final String TAG = "#CampaignsPresenter#"; + private ICampaignsView view; + private MediaWikiApi mediaWikiApi; + private Disposable disposable; + private Campaign campaign; + + @Override public void onAttachView(MvpView view) { + this.view = (ICampaignsView) view; + this.mediaWikiApi = ((ICampaignsView) view).getMediaWikiApi(); + } + + @Override public void onDetachView() { + this.view = null; + disposable.dispose(); + } + + /** + * make the api call to fetch the campaigns + */ + public void getCampaigns() { + if (view != null && mediaWikiApi != null) { + //If we already have a campaign, lets not make another call + if (this.campaign != null) { + view.showCampaigns(campaign); + return; + } + Single campaigns = mediaWikiApi.getCampaigns(); + campaigns.observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribeWith(new SingleObserver() { + + @Override public void onSubscribe(Disposable d) { + disposable = d; + } + + @Override public void onSuccess(CampaignResponseDTO campaignResponseDTO) { + List campaigns = campaignResponseDTO.getCampaigns(); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + if (campaigns == null || campaigns.isEmpty()) { + Log.e(TAG, "The campaigns list is empty"); + view.showCampaigns(null); + } + Collections.sort(campaigns, (campaign, t1) -> { + Date date1, date2; + try { + date1 = dateFormat.parse(campaign.getStartDate()); + date2 = dateFormat.parse(t1.getStartDate()); + } catch (ParseException e) { + e.printStackTrace(); + return -1; + } + return date1.compareTo(date2); + }); + Date campaignEndDate = null; + try { + campaignEndDate = dateFormat.parse(campaigns.get(0).getEndDate()); + } catch (ParseException e) { + e.printStackTrace(); + } + if (campaignEndDate == null) { + view.showCampaigns(null); + } else if (campaignEndDate.compareTo(new Date()) > 0) { + campaign = campaigns.get(0); + view.showCampaigns(campaign); + } else { + Log.e(TAG, "The campaigns has already finished"); + view.showCampaigns(null); + } + } + + @Override public void onError(Throwable e) { + Log.e(TAG, "could not fetch campaigns: " + e.getMessage()); + } + }); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java b/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java new file mode 100644 index 000000000..8610728b3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.campaigns; + +import fr.free.nrw.commons.MvpView; +import fr.free.nrw.commons.mwapi.MediaWikiApi; + +/** + * Interface which defines the view contracts of the campaign view + */ +public interface ICampaignsView extends MvpView { + MediaWikiApi getMediaWikiApi(); + + void showCampaigns(Campaign campaign); +} 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 index c8f140575..8b43c9f90 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -14,6 +14,7 @@ import android.os.Bundle; import android.os.IBinder; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; @@ -22,6 +23,8 @@ import android.support.v4.content.Loader; import android.support.v4.app.LoaderManager; import android.support.v4.widget.CursorAdapter; import android.support.v7.app.AlertDialog; +import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -30,6 +33,14 @@ import android.widget.AdapterView; import android.widget.CheckBox; import android.widget.CompoundButton; +import android.widget.Toast; +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.campaigns.Campaign; +import fr.free.nrw.commons.campaigns.CampaignResponseDTO; +import fr.free.nrw.commons.campaigns.CampaignView; +import fr.free.nrw.commons.campaigns.CampaignsPresenter; +import fr.free.nrw.commons.campaigns.ICampaignsView; import java.util.ArrayList; import java.util.concurrent.CountDownLatch; @@ -60,6 +71,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import org.acra.util.ToastSender; import timber.log.Timber; import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; @@ -76,7 +88,7 @@ public class ContributionsFragment MediaDetailPagerFragment.MediaDetailProvider, FragmentManager.OnBackStackChangedListener, ContributionsListFragment.SourceRefresher, - LocationUpdateListener + LocationUpdateListener,ICampaignsView { @Inject @Named("default_preferences") @@ -112,6 +124,10 @@ public class ContributionsFragment private boolean isFragmentAttachedBefore = false; private View checkBoxView; private CheckBox checkBox; + private CampaignsPresenter presenter; + + + @BindView(R.id.campaigns_view) CampaignView campaignView; /** * Since we will need to use parent activity on onAuthCookieAcquired, we have to wait @@ -142,6 +158,10 @@ public class ContributionsFragment @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_contributions, container, false); + ButterKnife.bind(this, view); + presenter = new CampaignsPresenter(); + presenter.onAttachView(this); + campaignView.setVisibility(View.GONE); nearbyNoificationCardView = view.findViewById(R.id.card_view_nearby); checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null); checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again); @@ -173,6 +193,27 @@ public class ContributionsFragment setUploadCount(); } + getChildFragmentManager().registerFragmentLifecycleCallbacks( + new FragmentManager.FragmentLifecycleCallbacks() { + @Override public void onFragmentResumed(FragmentManager fm, Fragment f) { + super.onFragmentResumed(fm, f); + //If media detail pager fragment is visible, hide the campaigns view [might not be the best way to do, this but yeah, this proves to work for now] + Log.e("#CF#", "onFragmentResumed" + f.getClass().getName()); + if (f instanceof MediaDetailPagerFragment) { + campaignView.setVisibility(View.GONE); + } + } + + @Override public void onFragmentDetached(FragmentManager fm, Fragment f) { + super.onFragmentDetached(fm, f); + Log.e("#CF#", "onFragmentDetached" + f.getClass().getName()); + //If media detail pager fragment is detached, ContributionsList fragment is gonna be visible, [becomes tightly coupled though] + if (f instanceof MediaDetailPagerFragment) { + fetchCampaigns(); + } + } + }, true); + return view; } @@ -537,7 +578,7 @@ public class ContributionsFragment nearbyNoificationCardView.setVisibility(View.GONE); } - + fetchCampaigns(); } /** @@ -694,5 +735,38 @@ public class ContributionsFragment // Update closest nearby card view if location changed more than 500 meters updateClosestNearbyCardViewInfo(); } + + @Override public void onViewCreated(@NonNull View view, + @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + } + + /** + * ask the presenter to fetch the campaigns only if user has not manually disabled it + */ + private void fetchCampaigns() { + if (prefs.getBoolean("displayCampaignsCardView", true)) { + presenter.getCampaigns(); + } + } + + @Override public void showMessage(String message) { + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + } + + @Override public MediaWikiApi getMediaWikiApi() { + return mediaWikiApi; + } + + @Override public void showCampaigns(Campaign campaign) { + if (campaign != null) { + campaignView.setCampaign(campaign); + } + } + + @Override public void onDestroyView() { + super.onDestroyView(); + presenter.onDetachView(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 2e1cbb1ac..8c4c39639 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -11,6 +11,7 @@ import android.text.TextUtils; import com.google.gson.Gson; +import fr.free.nrw.commons.campaigns.CampaignResponseDTO; import org.apache.http.HttpResponse; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.scheme.PlainSocketFactory; @@ -77,6 +78,8 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { private SharedPreferences categoryPreferences; private Gson gson; private final OkHttpClient okHttpClient; + private final String WIKIMEDIA_CAMPAIGNS_BASE_URL = + "https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json"; public ApacheHttpClientMediaWikiApi(Context context, String apiURL, @@ -1054,4 +1057,18 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { } } + @Override public Single getCampaigns() { + return Single.fromCallable(() -> { + Request request = new Request.Builder().url(WIKIMEDIA_CAMPAIGNS_BASE_URL).build(); + Response response = okHttpClient.newCall(request).execute(); + if (response != null && response.body() != null && response.isSuccessful()) { + String json = response.body().string(); + if (json == null) { + return null; + } + return gson.fromJson(json, CampaignResponseDTO.class); + } + return null; + }); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index d7bf65802..46d71dc26 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -4,6 +4,7 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import fr.free.nrw.commons.campaigns.CampaignResponseDTO; import java.io.IOException; import java.io.InputStream; import java.util.List; @@ -105,6 +106,8 @@ public interface MediaWikiApi { void logout(); + Single getCampaigns(); + interface ProgressListener { void onProgress(long transferred, long total); } 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 index 61798a95a..c9f0e0ff2 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNoificationCardView.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyNoificationCardView.java @@ -6,12 +6,8 @@ import android.content.res.Resources; import android.os.Handler; 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.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.Button; @@ -20,19 +16,17 @@ 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.SwipableCardView; 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{ +public class NearbyNoificationCardView extends SwipableCardView { - private static final float MINIMUM_THRESHOLD_FOR_SWIPE = 100; private Context context; private Button permissionRequestButton; @@ -99,41 +93,15 @@ public class NearbyNoificationCardView extends CardView{ private void setActionListeners() { this.setOnClickListener(view -> ((MainActivity)context).viewPager.setCurrentItem(1)); - - this.setOnTouchListener( - (v, event) -> { - boolean isSwipe = false; - float deltaX=0.0f; - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - x1 = event.getX(); - break; - case MotionEvent.ACTION_UP: - x2 = event.getX(); - deltaX = x2 - x1; - if (deltaX < 0) { - //Right to left swipe - isSwipe = true; - } else if (deltaX > 0) { - //Left to right swipe - isSwipe = true; - } - break; - } - if (isSwipe && (pixelToDp(Math.abs(deltaX)) > MINIMUM_THRESHOLD_FOR_SWIPE)) { - v.setVisibility(GONE); - // Save shared preference for nearby card view accordingly - ((MainActivity) context).prefs.edit() - .putBoolean("displayNearbyCardView", false).apply(); - ViewUtil.showLongToast(context, getResources().getString(R.string.nearby_notification_dismiss_message)); - return true; - } - return false; - }); } - private float pixelToDp(float pixels) { - return (pixels / Resources.getSystem().getDisplayMetrics().density); + @Override public boolean onSwipe(View view) { + view.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; } /** diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java new file mode 100644 index 000000000..a65033d15 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java @@ -0,0 +1,72 @@ +package fr.free.nrw.commons.utils; + +import android.content.Context; +import android.content.res.Resources; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.CardView; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; + +/** + * A card view which informs onSwipe events to its child + */ +public abstract class SwipableCardView extends CardView { + float x1, x2; + private static final float MINIMUM_THRESHOLD_FOR_SWIPE = 100; + + public SwipableCardView(@NonNull Context context) { + super(context); + interceptOnTouchListener(); + } + + public SwipableCardView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + interceptOnTouchListener(); + } + + public SwipableCardView(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + interceptOnTouchListener(); + } + + private void interceptOnTouchListener() { + this.setOnTouchListener((v, event) -> { + boolean isSwipe = false; + float deltaX = 0.0f; + Log.e("#SwipableCardView#", event.getAction() + ""); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + x1 = event.getX(); + break; + case MotionEvent.ACTION_UP: + x2 = event.getX(); + deltaX = x2 - x1; + if (deltaX < 0) { + //Right to left swipe + isSwipe = true; + } else if (deltaX > 0) { + //Left to right swipe + isSwipe = true; + } + break; + } + if (isSwipe && (pixelToDp(Math.abs(deltaX)) > MINIMUM_THRESHOLD_FOR_SWIPE)) { + return onSwipe(v); + } + return false; + }); + } + + /** + * abstract function which informs swipe events to those who have inherited from it + */ + public abstract boolean onSwipe(View view); + + private float pixelToDp(float pixels) { + return (pixels / Resources.getSystem().getDisplayMetrics().density); + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_campaign.png b/app/src/main/res/drawable-hdpi/ic_campaign.png new file mode 100755 index 0000000000000000000000000000000000000000..315ec45d3debf032b26a9d3079e8d93e3cc14696 GIT binary patch literal 807 zcmV+?1K9kDP)-gW$C2Ed+TpTq; zKRl^!)C^PbhT2gRyaMizn&1k+c~SlB2Vl3TZq@`aN3W<}dH~=vb)q_HB+>~N*u#U2(XcVP$ANa{Ck(bK>d<@{o zD3w6~c8EOGGYv3ZUrPbMMyWgjV5H`elgj|y6X9_Hn?@=0k@TU~QI5T3088|YFjxlg zT$IAKvW>K>RkD)jCIDO)|MC`Ku6mJ{24*U6r0X1D3rBgxJOKQvLHx_n0Jey<>i4MPE( z8OvDLt;Pdb=w0XQ9_84k#PoM4fOR8_mIhf}ceNwI-Uh`ox&SyO(r_bSu?A6;?Jco? ztYA9e;Zimxeu^Tra&Ot(^A6yhNW=Glmm@FrZ08Yk03XE)UIcs-X_yGOGjdSZwjMEC z(zsZ`gMiVIhIxSNa&GrimA4>A#AinumH;k`GMpu!rm>7m0ZStd<8yzb5i;^=8Oyj6 zFh0`odF}*pCs5=YUj}?0X?PXzTV(ORd|rq;z}eQ(TN91-ww|-R zZz%wlIw~s8*5*oUu8pH|4EKpNJ~2G1f{lHkkauhnRmlj~nXIgdt}`O4gWA@ylS7>D lbcfp6I{q3n!>|UU008^E{!MYjLN@>a002ovPDHLkV1l)zdDj2{ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_campaign.png b/app/src/main/res/drawable-mdpi/ic_campaign.png new file mode 100755 index 0000000000000000000000000000000000000000..b60884dd61bcb441f337802227db1e2f0eea9c85 GIT binary patch literal 542 zcmV+(0^$9MP)5)cv@MX?CS$@kLom?<jLT^7}05J;Aq;#^r!VlKFh| z0Pssv9|2qu=`;ZIos-Nb`P*D{_E1vJu>drPY+PU?81L+q-`;qz(FKv6=K#~(8wI%6 zG{7^ld%gxRz<2I)y3^d_I|BiH9ec)0wn($Z^Ree#qOz?D+M==xV{bXb$2yp7y>+H& gXV+4I6~aga0Br8YdJxYUO#lD@07*qoM6N<$g3tB_SpWb4 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_campaign.png b/app/src/main/res/drawable-xhdpi/ic_campaign.png new file mode 100755 index 0000000000000000000000000000000000000000..8b93f79779b831b4acdc976571d06dff60665376 GIT binary patch literal 1051 zcmV+$1mydPP)|bV4?eRM1GsaY{ zaSN_%RBN0;*p{j-_9Lv78C6ScL)80>q`a|+%EZRR?V^0KD%Yt@tV7)2jII1I3k^J` zGO-;=PE%eOi8q1IRVEH4Ne5Hs-D@USS-6*EH!BZJLo);IpfWHj4VX7E-As}1xcd#G zs3f+;v@#`~m{XM%a}r<s+h?* zfEVc@tPl6FLD3n}1dJ|UOpR;Ggci02Td^<4uos)K7$e6ARfW(8;~zk>DduZ=<1}tp z2v1;klrN6N+?49*pJTUZ&X+H)#vCONOu^#}G>sf=jk#C8cn-6%%EVagZ*7IGh-6#jEV!oFzp1|y$7>$&+pB(Ilc|yK84|9)Vc!SbrjXy&Asbh0YYdp%} z8P6s?I&GNE<%?;O0vf~llrp~@td47H`QszZLyBMliaSLiJcRjJX|OM@r4_;56!uhj z!N!uf_Ej2;M{7JRHcZMNBs)9W=6D~|#CS@F3veB+2xi~~qJCz@geis$JVD)eGHm|p2|GOh+@lP6}S0oTdOf-(3C*Z(Xn4=m09xW3As zu$q%LT-|Krk`VXv literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_campaign.png b/app/src/main/res/drawable-xxhdpi/ic_campaign.png new file mode 100755 index 0000000000000000000000000000000000000000..069ad8e1e86588718b83601b012da31d767c88a3 GIT binary patch literal 1590 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 jkw_#Gi9{liSgZU2fD00000NkvXXu0mjf?QH-L literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/fragment_contributions.xml b/app/src/main/res/layout/fragment_contributions.xml index dd1959178..5fc1a74dc 100644 --- a/app/src/main/res/layout/fragment_contributions.xml +++ b/app/src/main/res/layout/fragment_contributions.xml @@ -12,11 +12,21 @@ app:cardBackgroundColor="?attr/mainCardBackground" /> + + + android:id="@+id/root_frame" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginTop="2dp" + android:background="#000" + > \ No newline at end of file diff --git a/app/src/main/res/layout/layout_campagin.xml b/app/src/main/res/layout/layout_campagin.xml new file mode 100644 index 000000000..47824f5a3 --- /dev/null +++ b/app/src/main/res/layout/layout_campagin.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1631087a6..915c48729 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -433,5 +433,9 @@ Upload your first media by touching the camera or gallery icon above. Never ask this again Display location permission Ask for location permission when needed for nearby notification card view feature. + Ends on: + Display campaigns + Tap here to see the ongoing campaigns + You won\'t see the campaigns anymore. However, you can re-enable this notification in Settings if you wish. diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index e272e02af..39d0f3872 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -47,6 +47,11 @@ android:title="@string/display_location_permission_title" android:defaultValue="true" android:summary="@string/display_location_permission_explanation" /> +