Migrate campaigns package to kotlin (#5969)

* Convert ICampaignsView to kotlin along with simple fix

* Convert CampaignView to Kotlin

* Convert CampaignsPresenter to Kotlin

* Convert CampaignsPresenter to kotlin

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
This commit is contained in:
Paul Hawke 2024-11-28 22:44:26 -06:00 committed by GitHub
parent a6152f937e
commit 1afff73c24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 247 additions and 261 deletions

View file

@ -1,118 +0,0 @@
package fr.free.nrw.commons.campaigns;
import android.content.Context;
import android.net.Uri;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.campaigns.models.Campaign;
import fr.free.nrw.commons.databinding.LayoutCampaginBinding;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import fr.free.nrw.commons.utils.DateUtil;
import java.text.ParseException;
import java.util.Date;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.utils.SwipableCardView;
import fr.free.nrw.commons.utils.ViewUtil;
/**
* A view which represents a single campaign
*/
public class CampaignView extends SwipableCardView {
Campaign campaign;
private LayoutCampaginBinding binding;
private ViewHolder viewHolder;
public static final String CAMPAIGNS_DEFAULT_PREFERENCE = "displayCampaignsCardView";
public static final String WLM_CARD_PREFERENCE = "displayWLMCardView";
private String campaignPreference = CAMPAIGNS_DEFAULT_PREFERENCE;
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(final Campaign campaign) {
this.campaign = campaign;
if (campaign != null) {
if (campaign.isWLMCampaign()) {
campaignPreference = WLM_CARD_PREFERENCE;
}
setVisibility(View.VISIBLE);
viewHolder.init();
} else {
this.setVisibility(View.GONE);
}
}
@Override public boolean onSwipe(final View view) {
view.setVisibility(View.GONE);
((BaseActivity) getContext()).defaultKvStore
.putBoolean(CAMPAIGNS_DEFAULT_PREFERENCE, false);
ViewUtil.showLongToast(getContext(),
getResources().getString(R.string.nearby_campaign_dismiss_message));
return true;
}
private void init() {
binding = LayoutCampaginBinding.inflate(LayoutInflater.from(getContext()), this, true);
viewHolder = new ViewHolder();
setOnClickListener(view -> {
if (campaign != null) {
if (campaign.isWLMCampaign()) {
((MainActivity)(getContext())).showNearby();
} else {
Utils.handleWebUrl(getContext(), Uri.parse(campaign.getLink()));
}
}
});
}
public class ViewHolder {
public void init() {
if (campaign != null) {
binding.ivCampaign.setImageDrawable(
getResources().getDrawable(R.drawable.ic_campaign));
binding.tvTitle.setText(campaign.getTitle());
binding.tvDescription.setText(campaign.getDescription());
try {
if (campaign.isWLMCampaign()) {
binding.tvDates.setText(
String.format("%1s - %2s", campaign.getStartDate(),
campaign.getEndDate()));
} else {
final Date startDate = CommonsDateUtil.getIso8601DateFormatShort()
.parse(campaign.getStartDate());
final Date endDate = CommonsDateUtil.getIso8601DateFormatShort()
.parse(campaign.getEndDate());
binding.tvDates.setText(String.format("%1s - %2s", DateUtil.getExtraShortDateString(startDate),
DateUtil.getExtraShortDateString(endDate)));
}
} catch (final ParseException e) {
e.printStackTrace();
}
}
}
}
}

View file

@ -0,0 +1,121 @@
package fr.free.nrw.commons.campaigns
import android.content.Context
import android.net.Uri
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import androidx.core.content.ContextCompat
import fr.free.nrw.commons.R
import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.campaigns.models.Campaign
import fr.free.nrw.commons.contributions.MainActivity
import fr.free.nrw.commons.databinding.LayoutCampaginBinding
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.utils.CommonsDateUtil.getIso8601DateFormatShort
import fr.free.nrw.commons.utils.DateUtil.getExtraShortDateString
import fr.free.nrw.commons.utils.SwipableCardView
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
import timber.log.Timber
import java.text.ParseException
/**
* A view which represents a single campaign
*/
class CampaignView : SwipableCardView {
private var campaign: Campaign? = null
private var binding: LayoutCampaginBinding? = null
private var viewHolder: ViewHolder? = null
private var campaignPreference = CAMPAIGNS_DEFAULT_PREFERENCE
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init()
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context, attrs, defStyleAttr) {
init()
}
fun setCampaign(campaign: Campaign?) {
this.campaign = campaign
if (campaign != null) {
if (campaign.isWLMCampaign) {
campaignPreference = WLM_CARD_PREFERENCE
}
visibility = VISIBLE
viewHolder!!.init()
} else {
visibility = GONE
}
}
override fun onSwipe(view: View): Boolean {
view.visibility = GONE
(context as BaseActivity).defaultKvStore.putBoolean(CAMPAIGNS_DEFAULT_PREFERENCE, false)
showLongToast(
context,
resources.getString(R.string.nearby_campaign_dismiss_message)
)
return true
}
private fun init() {
binding = LayoutCampaginBinding.inflate(
LayoutInflater.from(context), this, true
)
viewHolder = ViewHolder()
setOnClickListener {
campaign?.let {
if (it.isWLMCampaign) {
((context) as MainActivity).showNearby()
} else {
Utils.handleWebUrl(context, Uri.parse(it.link))
}
}
}
}
inner class ViewHolder {
fun init() {
if (campaign != null) {
binding!!.ivCampaign.setImageDrawable(
ContextCompat.getDrawable(binding!!.root.context, R.drawable.ic_campaign)
)
binding!!.tvTitle.text = campaign!!.title
binding!!.tvDescription.text = campaign!!.description
try {
if (campaign!!.isWLMCampaign) {
binding!!.tvDates.text = String.format(
"%1s - %2s", campaign!!.startDate,
campaign!!.endDate
)
} else {
val startDate = getIso8601DateFormatShort().parse(
campaign?.startDate
)
val endDate = getIso8601DateFormatShort().parse(
campaign?.endDate
)
binding!!.tvDates.text = String.format(
"%1s - %2s", getExtraShortDateString(
startDate!!
), getExtraShortDateString(endDate!!)
)
}
} catch (e: ParseException) {
Timber.e(e)
}
}
}
}
companion object {
const val CAMPAIGNS_DEFAULT_PREFERENCE: String = "displayCampaignsCardView"
const val WLM_CARD_PREFERENCE: String = "displayWLMCardView"
}
}

View file

@ -1,123 +0,0 @@
package fr.free.nrw.commons.campaigns;
import android.annotation.SuppressLint;
import fr.free.nrw.commons.campaigns.models.Campaign;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import java.text.ParseException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.Scheduler;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.disposables.Disposable;
import timber.log.Timber;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
/**
* The presenter for the campaigns view, fetches the campaigns from the api and informs the view on
* success and error
*/
@Singleton
public class CampaignsPresenter implements BasePresenter<ICampaignsView> {
private final OkHttpJsonApiClient okHttpJsonApiClient;
private final Scheduler mainThreadScheduler;
private final Scheduler ioScheduler;
private ICampaignsView view;
private Disposable disposable;
private Campaign campaign;
@Inject
public CampaignsPresenter(OkHttpJsonApiClient okHttpJsonApiClient, @Named(IO_THREAD)Scheduler ioScheduler, @Named(MAIN_THREAD)Scheduler mainThreadScheduler) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.mainThreadScheduler=mainThreadScheduler;
this.ioScheduler=ioScheduler;
}
@Override
public void onAttachView(ICampaignsView view) {
this.view = view;
}
@Override public void onDetachView() {
this.view = null;
if (disposable != null) {
disposable.dispose();
}
}
/**
* make the api call to fetch the campaigns
*/
@SuppressLint("CheckResult")
public void getCampaigns() {
if (view != null && okHttpJsonApiClient != null) {
//If we already have a campaign, lets not make another call
if (this.campaign != null) {
view.showCampaigns(campaign);
return;
}
Single<CampaignResponseDTO> campaigns = okHttpJsonApiClient.getCampaigns();
campaigns.observeOn(mainThreadScheduler)
.subscribeOn(ioScheduler)
.subscribeWith(new SingleObserver<CampaignResponseDTO>() {
@Override public void onSubscribe(Disposable d) {
disposable = d;
}
@Override public void onSuccess(CampaignResponseDTO campaignResponseDTO) {
List<Campaign> campaigns = campaignResponseDTO.getCampaigns();
if (campaigns == null || campaigns.isEmpty()) {
Timber.e("The campaigns list is empty");
view.showCampaigns(null);
return;
}
Collections.sort(campaigns, (campaign, t1) -> {
Date date1, date2;
try {
date1 = CommonsDateUtil.getIso8601DateFormatShort().parse(campaign.getStartDate());
date2 = CommonsDateUtil.getIso8601DateFormatShort().parse(t1.getStartDate());
} catch (ParseException e) {
e.printStackTrace();
return -1;
}
return date1.compareTo(date2);
});
Date campaignEndDate, campaignStartDate;
Date currentDate = new Date();
try {
for (Campaign aCampaign : campaigns) {
campaignEndDate = CommonsDateUtil.getIso8601DateFormatShort().parse(aCampaign.getEndDate());
campaignStartDate = CommonsDateUtil.getIso8601DateFormatShort().parse(aCampaign.getStartDate());
if (campaignEndDate.compareTo(currentDate) >= 0
&& campaignStartDate.compareTo(currentDate) <= 0) {
campaign = aCampaign;
break;
}
}
} catch (ParseException e) {
e.printStackTrace();
}
view.showCampaigns(campaign);
}
@Override public void onError(Throwable e) {
Timber.e(e, "could not fetch campaigns");
}
});
}
}
}

View file

@ -0,0 +1,107 @@
package fr.free.nrw.commons.campaigns
import android.annotation.SuppressLint
import fr.free.nrw.commons.BasePresenter
import fr.free.nrw.commons.campaigns.models.Campaign
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD
import fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.utils.CommonsDateUtil.getIso8601DateFormatShort
import io.reactivex.Scheduler
import io.reactivex.disposables.Disposable
import timber.log.Timber
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Date
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
/**
* The presenter for the campaigns view, fetches the campaigns from the api and informs the view on
* success and error
*/
@Singleton
class CampaignsPresenter @Inject constructor(
private val okHttpJsonApiClient: OkHttpJsonApiClient?,
@param:Named(IO_THREAD) private val ioScheduler: Scheduler,
@param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler
) : BasePresenter<ICampaignsView?> {
private var view: ICampaignsView? = null
private var disposable: Disposable? = null
private var campaign: Campaign? = null
override fun onAttachView(view: ICampaignsView) {
this.view = view
}
override fun onDetachView() {
view = null
disposable?.dispose()
}
/**
* make the api call to fetch the campaigns
*/
@SuppressLint("CheckResult")
fun getCampaigns() {
if (view != null && okHttpJsonApiClient != null) {
//If we already have a campaign, lets not make another call
if (campaign != null) {
view!!.showCampaigns(campaign)
return
}
okHttpJsonApiClient.campaigns
.observeOn(mainThreadScheduler)
.subscribeOn(ioScheduler)
.doOnSubscribe { disposable = it }
.subscribe({ campaignResponseDTO ->
val campaigns = campaignResponseDTO.campaigns?.toMutableList()
if (campaigns.isNullOrEmpty()) {
Timber.e("The campaigns list is empty")
view!!.showCampaigns(null)
} else {
sortCampaignsByStartDate(campaigns)
campaign = findActiveCampaign(campaigns)
view!!.showCampaigns(campaign)
}
}, {
Timber.e(it, "could not fetch campaigns")
})
}
}
private fun sortCampaignsByStartDate(campaigns: MutableList<Campaign>) {
val dateFormat: SimpleDateFormat = getIso8601DateFormatShort()
campaigns.sortWith(Comparator { campaign: Campaign, other: Campaign ->
val date1: Date?
val date2: Date?
try {
date1 = campaign.startDate?.let { dateFormat.parse(it) }
date2 = other.startDate?.let { dateFormat.parse(it) }
} catch (e: ParseException) {
Timber.e(e)
return@Comparator -1
}
if (date1 != null && date2 != null) date1.compareTo(date2) else -1
})
}
private fun findActiveCampaign(campaigns: List<Campaign>) : Campaign? {
val dateFormat: SimpleDateFormat = getIso8601DateFormatShort()
val currentDate = Date()
return try {
campaigns.firstOrNull {
val campaignStartDate = it.startDate?.let { s -> dateFormat.parse(s) }
val campaignEndDate = it.endDate?.let { s -> dateFormat.parse(s) }
campaignStartDate != null && campaignEndDate != null &&
campaignEndDate >= currentDate && campaignStartDate <= currentDate
}
} catch (e: ParseException) {
Timber.e(e, "could not find active campaign")
null
}
}
}

View file

@ -1,11 +0,0 @@
package fr.free.nrw.commons.campaigns;
import fr.free.nrw.commons.MvpView;
import fr.free.nrw.commons.campaigns.models.Campaign;
/**
* Interface which defines the view contracts of the campaign view
*/
public interface ICampaignsView extends MvpView {
void showCampaigns(Campaign campaign);
}

View file

@ -0,0 +1,11 @@
package fr.free.nrw.commons.campaigns
import fr.free.nrw.commons.MvpView
import fr.free.nrw.commons.campaigns.models.Campaign
/**
* Interface which defines the view contracts of the campaign view
*/
interface ICampaignsView : MvpView {
fun showCampaigns(campaign: Campaign?)
}

View file

@ -20,25 +20,24 @@ import kotlin.collections.ArrayList
class CampaignsPresenterTest {
@Mock
lateinit var okHttpJsonApiClient: OkHttpJsonApiClient
lateinit var campaignsPresenter: CampaignsPresenter
private lateinit var okHttpJsonApiClient: OkHttpJsonApiClient
@Mock
internal lateinit var view: ICampaignsView
private lateinit var view: ICampaignsView
@Mock
internal lateinit var campaignResponseDTO: CampaignResponseDTO
lateinit var campaignsSingle: Single<CampaignResponseDTO>
private lateinit var campaignResponseDTO: CampaignResponseDTO
@Mock
lateinit var campaign: Campaign
lateinit var testScheduler: TestScheduler
private lateinit var campaign: Campaign
@Mock
private lateinit var disposable: Disposable
private lateinit var campaignsPresenter: CampaignsPresenter
private lateinit var campaignsSingle: Single<CampaignResponseDTO>
private lateinit var testScheduler: TestScheduler
/**
* initial setup, test environment
*/