mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-11-01 07:13:56 +01:00
Merge branch 'main' into bookmark
This commit is contained in:
commit
4daa3d6094
31 changed files with 1334 additions and 1092 deletions
|
|
@ -62,7 +62,7 @@ class SessionManager @Inject constructor(
|
||||||
fun forceLogin(context: Context?) =
|
fun forceLogin(context: Context?) =
|
||||||
context?.let { LoginActivity.startYourself(it) }
|
context?.let { LoginActivity.startYourself(it) }
|
||||||
|
|
||||||
fun getPreference(key: String?): Boolean =
|
fun getPreference(key: String): Boolean =
|
||||||
defaultKvStore.getBoolean(key)
|
defaultKvStore.getBoolean(key)
|
||||||
|
|
||||||
fun logout(): Completable = Completable.fromObservable(
|
fun logout(): Completable = Completable.fromObservable(
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
121
app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.kt
Normal file
121
app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.kt
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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?)
|
||||||
|
}
|
||||||
|
|
@ -1,215 +0,0 @@
|
||||||
package fr.free.nrw.commons.kvstore;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class BasicKvStore implements KeyValueStore {
|
|
||||||
private static final String KEY_VERSION = "__version__";
|
|
||||||
/*
|
|
||||||
This class only performs puts, sets and clears.
|
|
||||||
A commit returns a boolean indicating whether it has succeeded, we are not throwing an exception as it will
|
|
||||||
require the dev to handle it in every usage - instead we will pass on this boolean so it can be evaluated if needed.
|
|
||||||
*/
|
|
||||||
private final SharedPreferences _store;
|
|
||||||
|
|
||||||
public BasicKvStore(Context context, String storeName) {
|
|
||||||
_store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If you don't want onVersionUpdate to be called on a fresh creation, the first version supplied for the kvstore should be set to 0.
|
|
||||||
*/
|
|
||||||
public BasicKvStore(Context context, String storeName, int version) {
|
|
||||||
this(context,storeName,version,false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BasicKvStore(Context context, String storeName, int version, boolean clearAllOnUpgrade) {
|
|
||||||
_store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE);
|
|
||||||
int oldVersion = getInt(KEY_VERSION);
|
|
||||||
|
|
||||||
if (version > oldVersion) {
|
|
||||||
Timber.i("version updated from %s to %s, with clearFlag %b", oldVersion, version, clearAllOnUpgrade);
|
|
||||||
onVersionUpdate(oldVersion, version, clearAllOnUpgrade);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (version < oldVersion) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"kvstore downgrade not allowed, old version:" + oldVersion + ", new version: " +
|
|
||||||
version);
|
|
||||||
}
|
|
||||||
//Keep this statement at the end so that clearing of store does not cause version also to get removed.
|
|
||||||
putIntInternal(KEY_VERSION, version);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onVersionUpdate(int oldVersion, int version, boolean clearAllFlag) {
|
|
||||||
if(clearAllFlag) {
|
|
||||||
clearAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getKeySet() {
|
|
||||||
Map<String, ?> allContents = new HashMap<>(_store.getAll());
|
|
||||||
allContents.remove(KEY_VERSION);
|
|
||||||
return allContents.keySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public Map<String, ?> getAll() {
|
|
||||||
Map<String, ?> allContents = _store.getAll();
|
|
||||||
if (allContents == null || allContents.size() == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
allContents.remove(KEY_VERSION);
|
|
||||||
return new HashMap<>(allContents);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getString(String key) {
|
|
||||||
return getString(key, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getBoolean(String key) {
|
|
||||||
return getBoolean(key, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getLong(String key) {
|
|
||||||
return getLong(key, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getInt(String key) {
|
|
||||||
return getInt(key, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getString(String key, String defaultValue) {
|
|
||||||
return _store.getString(key, defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getBoolean(String key, boolean defaultValue) {
|
|
||||||
return _store.getBoolean(key, defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getLong(String key, long defaultValue) {
|
|
||||||
return _store.getLong(key, defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getInt(String key, int defaultValue) {
|
|
||||||
return _store.getInt(key, defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void putAllStrings(Map<String, String> keyValuePairs) {
|
|
||||||
SharedPreferences.Editor editor = _store.edit();
|
|
||||||
for (Map.Entry<String, String> keyValuePair : keyValuePairs.entrySet()) {
|
|
||||||
putString(editor, keyValuePair.getKey(), keyValuePair.getValue(), false);
|
|
||||||
}
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void putString(String key, String value) {
|
|
||||||
SharedPreferences.Editor editor = _store.edit();
|
|
||||||
putString(editor, key, value, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void putString(SharedPreferences.Editor editor, String key, String value,
|
|
||||||
boolean commit) {
|
|
||||||
assertKeyNotReserved(key);
|
|
||||||
editor.putString(key, value);
|
|
||||||
if(commit) {
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void putBoolean(String key, boolean value) {
|
|
||||||
assertKeyNotReserved(key);
|
|
||||||
SharedPreferences.Editor editor = _store.edit();
|
|
||||||
editor.putBoolean(key, value);
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void putLong(String key, long value) {
|
|
||||||
assertKeyNotReserved(key);
|
|
||||||
SharedPreferences.Editor editor = _store.edit();
|
|
||||||
editor.putLong(key, value);
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void putInt(String key, int value) {
|
|
||||||
assertKeyNotReserved(key);
|
|
||||||
putIntInternal(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean contains(String key) {
|
|
||||||
return _store.contains(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void remove(String key) {
|
|
||||||
SharedPreferences.Editor editor = _store.edit();
|
|
||||||
editor.remove(key);
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearAll() {
|
|
||||||
int version = getInt(KEY_VERSION);
|
|
||||||
SharedPreferences.Editor editor = _store.edit();
|
|
||||||
editor.clear();
|
|
||||||
editor.apply();
|
|
||||||
putIntInternal(KEY_VERSION, version);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearAllWithVersion() {
|
|
||||||
SharedPreferences.Editor editor = _store.edit();
|
|
||||||
editor.clear();
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void putIntInternal(String key, int value) {
|
|
||||||
SharedPreferences.Editor editor = _store.edit();
|
|
||||||
editor.putInt(key, value);
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertKeyNotReserved(String key) {
|
|
||||||
if (key.equals(KEY_VERSION)) {
|
|
||||||
throw new IllegalArgumentException(key + "is a reserved key");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void registerChangeListener(SharedPreferences.OnSharedPreferenceChangeListener l) {
|
|
||||||
_store.registerOnSharedPreferenceChangeListener(l);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unregisterChangeListener(SharedPreferences.OnSharedPreferenceChangeListener l) {
|
|
||||||
_store.unregisterOnSharedPreferenceChangeListener(l);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getStringSet(String key){
|
|
||||||
return _store.getStringSet(key, new HashSet<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void putStringSet(String key,Set<String> value){
|
|
||||||
_store.edit().putStringSet(key,value).apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
152
app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.kt
Normal file
152
app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.kt
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
package fr.free.nrw.commons.kvstore
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
open class BasicKvStore : KeyValueStore {
|
||||||
|
/*
|
||||||
|
This class only performs puts, sets and clears.
|
||||||
|
A commit returns a boolean indicating whether it has succeeded, we are not throwing an exception as it will
|
||||||
|
require the dev to handle it in every usage - instead we will pass on this boolean so it can be evaluated if needed.
|
||||||
|
*/
|
||||||
|
private val _store: SharedPreferences
|
||||||
|
|
||||||
|
constructor(context: Context, storeName: String?) {
|
||||||
|
_store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If you don't want onVersionUpdate to be called on a fresh creation, the first version supplied for the kvstore should be set to 0.
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
storeName: String?,
|
||||||
|
version: Int,
|
||||||
|
clearAllOnUpgrade: Boolean = false
|
||||||
|
) {
|
||||||
|
_store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE)
|
||||||
|
val oldVersion = _store.getInt(KEY_VERSION, 0)
|
||||||
|
|
||||||
|
require(version >= oldVersion) {
|
||||||
|
"kvstore downgrade not allowed, old version:" + oldVersion + ", new version: " +
|
||||||
|
version
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version > oldVersion) {
|
||||||
|
Timber.i(
|
||||||
|
"version updated from %s to %s, with clearFlag %b",
|
||||||
|
oldVersion,
|
||||||
|
version,
|
||||||
|
clearAllOnUpgrade
|
||||||
|
)
|
||||||
|
onVersionUpdate(oldVersion, version, clearAllOnUpgrade)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Keep this statement at the end so that clearing of store does not cause version also to get removed.
|
||||||
|
_store.edit { putInt(KEY_VERSION, version) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val all: Map<String, *>?
|
||||||
|
get() {
|
||||||
|
val allContents = _store.all
|
||||||
|
if (allContents == null || allContents.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
allContents.remove(KEY_VERSION)
|
||||||
|
return HashMap(allContents)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(key: String): String? =
|
||||||
|
getString(key, null)
|
||||||
|
|
||||||
|
override fun getBoolean(key: String): Boolean =
|
||||||
|
getBoolean(key, false)
|
||||||
|
|
||||||
|
override fun getLong(key: String): Long =
|
||||||
|
getLong(key, 0)
|
||||||
|
|
||||||
|
override fun getInt(key: String): Int =
|
||||||
|
getInt(key, 0)
|
||||||
|
|
||||||
|
fun getStringSet(key: String?): MutableSet<String> =
|
||||||
|
_store.getStringSet(key, HashSet())!!
|
||||||
|
|
||||||
|
override fun getString(key: String, defaultValue: String?): String? =
|
||||||
|
_store.getString(key, defaultValue)
|
||||||
|
|
||||||
|
override fun getBoolean(key: String, defaultValue: Boolean): Boolean =
|
||||||
|
_store.getBoolean(key, defaultValue)
|
||||||
|
|
||||||
|
override fun getLong(key: String, defaultValue: Long): Long =
|
||||||
|
_store.getLong(key, defaultValue)
|
||||||
|
|
||||||
|
override fun getInt(key: String, defaultValue: Int): Int =
|
||||||
|
_store.getInt(key, defaultValue)
|
||||||
|
|
||||||
|
fun putAllStrings(kvData: Map<String, String>) = assertKeyNotReserved(kvData.keys) {
|
||||||
|
for ((key, value) in kvData) {
|
||||||
|
putString(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putString(key: String, value: String) = assertKeyNotReserved(key) {
|
||||||
|
putString(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putBoolean(key: String, value: Boolean) = assertKeyNotReserved(key) {
|
||||||
|
putBoolean(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putLong(key: String, value: Long) = assertKeyNotReserved(key) {
|
||||||
|
putLong(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putInt(key: String, value: Int) = assertKeyNotReserved(key) {
|
||||||
|
putInt(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putStringSet(key: String?, value: Set<String?>?) =
|
||||||
|
_store.edit{ putStringSet(key, value) }
|
||||||
|
|
||||||
|
override fun remove(key: String) = assertKeyNotReserved(key) {
|
||||||
|
remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contains(key: String): Boolean {
|
||||||
|
if (key == KEY_VERSION) return false
|
||||||
|
return _store.contains(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearAll() {
|
||||||
|
val version = _store.getInt(KEY_VERSION, 0)
|
||||||
|
_store.edit {
|
||||||
|
clear()
|
||||||
|
putInt(KEY_VERSION, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onVersionUpdate(oldVersion: Int, version: Int, clearAllFlag: Boolean) {
|
||||||
|
if (clearAllFlag) {
|
||||||
|
clearAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun assertKeyNotReserved(key: Set<String>, block: SharedPreferences.Editor.() -> Unit) {
|
||||||
|
key.forEach { require(it != KEY_VERSION) { "$it is a reserved key" } }
|
||||||
|
_store.edit { block(this) }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun assertKeyNotReserved(key: String, block: SharedPreferences.Editor.() -> Unit) {
|
||||||
|
require(key != KEY_VERSION) { "$key is a reserved key" }
|
||||||
|
_store.edit { block(this) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@VisibleForTesting
|
||||||
|
const val KEY_VERSION: String = "__version__"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
package fr.free.nrw.commons.kvstore;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.JsonSyntaxException;
|
|
||||||
|
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class JsonKvStore extends BasicKvStore {
|
|
||||||
private final Gson gson;
|
|
||||||
|
|
||||||
public JsonKvStore(Context context, String storeName, Gson gson) {
|
|
||||||
super(context, storeName);
|
|
||||||
this.gson = gson;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonKvStore(Context context, String storeName, int version, Gson gson) {
|
|
||||||
super(context, storeName, version);
|
|
||||||
this.gson = gson;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonKvStore(Context context, String storeName, int version, boolean clearAllOnUpgrade, Gson gson) {
|
|
||||||
super(context, storeName, version, clearAllOnUpgrade);
|
|
||||||
this.gson = gson;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> void putAllJsons(Map<String, T> jsonMap) {
|
|
||||||
Map<String, String> stringsMap = new HashMap<>(jsonMap.size());
|
|
||||||
for (Map.Entry<String, T> keyValuePair : jsonMap.entrySet()) {
|
|
||||||
String jsonString = gson.toJson(keyValuePair.getValue());
|
|
||||||
stringsMap.put(keyValuePair.getKey(), jsonString);
|
|
||||||
}
|
|
||||||
putAllStrings(stringsMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> void putJson(String key, T object) {
|
|
||||||
putString(key, gson.toJson(object));
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> void putJsonWithTypeInfo(String key, T object, Type type) {
|
|
||||||
putString(key, gson.toJson(object, type));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public <T> T getJson(String key, Class<T> clazz) {
|
|
||||||
String jsonString = getString(key);
|
|
||||||
try {
|
|
||||||
return gson.fromJson(jsonString, clazz);
|
|
||||||
} catch (JsonSyntaxException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public <T> T getJson(String key, Type type) {
|
|
||||||
String jsonString = getString(key);
|
|
||||||
try {
|
|
||||||
return gson.fromJson(jsonString, type);
|
|
||||||
} catch (JsonSyntaxException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
52
app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.kt
Normal file
52
app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.kt
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
package fr.free.nrw.commons.kvstore
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonSyntaxException
|
||||||
|
|
||||||
|
class JsonKvStore : BasicKvStore {
|
||||||
|
val gson: Gson
|
||||||
|
|
||||||
|
constructor(context: Context, storeName: String?, gson: Gson) : super(context, storeName) {
|
||||||
|
this.gson = gson
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context, storeName: String?, version: Int, gson: Gson) : super(
|
||||||
|
context, storeName, version
|
||||||
|
) {
|
||||||
|
this.gson = gson
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
storeName: String?,
|
||||||
|
version: Int,
|
||||||
|
clearAllOnUpgrade: Boolean,
|
||||||
|
gson: Gson
|
||||||
|
) : super(context, storeName, version, clearAllOnUpgrade) {
|
||||||
|
this.gson = gson
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> putJson(key: String, value: T) = assertKeyNotReserved(key) {
|
||||||
|
putString(key, gson.toJson(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
message = "Migrate to newer Kotlin syntax",
|
||||||
|
replaceWith = ReplaceWith("getJson<T>(key)")
|
||||||
|
)
|
||||||
|
fun <T> getJson(key: String, clazz: Class<T>?): T? = try {
|
||||||
|
gson.fromJson(getString(key), clazz)
|
||||||
|
} catch (e: JsonSyntaxException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Later, when the calls are coming from Kotlin, this will allow us to
|
||||||
|
// drop the "clazz" parameter, and just pick up the type at the call site.
|
||||||
|
// The deprecation warning should help migration!
|
||||||
|
inline fun <reified T> getJson(key: String): T? = try {
|
||||||
|
gson.fromJson(getString(key), T::class.java)
|
||||||
|
} catch (e: JsonSyntaxException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
package fr.free.nrw.commons.kvstore;
|
|
||||||
|
|
||||||
public interface KeyValueStore {
|
|
||||||
String getString(String key);
|
|
||||||
|
|
||||||
boolean getBoolean(String key);
|
|
||||||
|
|
||||||
long getLong(String key);
|
|
||||||
|
|
||||||
int getInt(String key);
|
|
||||||
|
|
||||||
String getString(String key, String defaultValue);
|
|
||||||
|
|
||||||
boolean getBoolean(String key, boolean defaultValue);
|
|
||||||
|
|
||||||
long getLong(String key, long defaultValue);
|
|
||||||
|
|
||||||
int getInt(String key, int defaultValue);
|
|
||||||
|
|
||||||
void putString(String key, String value);
|
|
||||||
|
|
||||||
void putBoolean(String key, boolean value);
|
|
||||||
|
|
||||||
void putLong(String key, long value);
|
|
||||||
|
|
||||||
void putInt(String key, int value);
|
|
||||||
|
|
||||||
boolean contains(String key);
|
|
||||||
|
|
||||||
void remove(String key);
|
|
||||||
|
|
||||||
void clearAll();
|
|
||||||
|
|
||||||
void clearAllWithVersion();
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package fr.free.nrw.commons.kvstore
|
||||||
|
|
||||||
|
interface KeyValueStore {
|
||||||
|
fun getString(key: String): String?
|
||||||
|
|
||||||
|
fun getBoolean(key: String): Boolean
|
||||||
|
|
||||||
|
fun getLong(key: String): Long
|
||||||
|
|
||||||
|
fun getInt(key: String): Int
|
||||||
|
|
||||||
|
fun getString(key: String, defaultValue: String?): String?
|
||||||
|
|
||||||
|
fun getBoolean(key: String, defaultValue: Boolean): Boolean
|
||||||
|
|
||||||
|
fun getLong(key: String, defaultValue: Long): Long
|
||||||
|
|
||||||
|
fun getInt(key: String, defaultValue: Int): Int
|
||||||
|
|
||||||
|
fun putString(key: String, value: String)
|
||||||
|
|
||||||
|
fun putBoolean(key: String, value: Boolean)
|
||||||
|
|
||||||
|
fun putLong(key: String, value: Long)
|
||||||
|
|
||||||
|
fun putInt(key: String, value: Int)
|
||||||
|
|
||||||
|
fun contains(key: String): Boolean
|
||||||
|
|
||||||
|
fun remove(key: String)
|
||||||
|
|
||||||
|
fun clearAll()
|
||||||
|
}
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
package fr.free.nrw.commons.logging;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
|
||||||
import fr.free.nrw.commons.utils.DeviceInfoUtil;
|
|
||||||
import org.acra.data.CrashReportData;
|
|
||||||
import org.acra.sender.ReportSenderException;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class responsible for sending logs to developers
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
public class CommonsLogSender extends LogsSender {
|
|
||||||
private static final String LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com";
|
|
||||||
private static final String LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Android App (%s) Logs";
|
|
||||||
private static final String BETA_LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Beta Android App (%s) Logs";
|
|
||||||
|
|
||||||
private SessionManager sessionManager;
|
|
||||||
private Context context;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public CommonsLogSender(SessionManager sessionManager,
|
|
||||||
Context context) {
|
|
||||||
super(sessionManager);
|
|
||||||
|
|
||||||
this.sessionManager = sessionManager;
|
|
||||||
this.context = context;
|
|
||||||
boolean isBeta = ConfigUtils.isBetaFlavour();
|
|
||||||
this.logFileName = isBeta ? "CommonsBetaAppLogs.zip" : "CommonsAppLogs.zip";
|
|
||||||
String emailSubjectFormat = isBeta ? BETA_LOGS_PRIVATE_EMAIL_SUBJECT : LOGS_PRIVATE_EMAIL_SUBJECT;
|
|
||||||
this.emailSubject = String.format(emailSubjectFormat, sessionManager.getUserName());
|
|
||||||
this.emailBody = getExtraInfo();
|
|
||||||
this.mailTo = LOGS_PRIVATE_EMAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attach any extra meta information about user or device that might help in debugging
|
|
||||||
* @return String with extra meta information useful for debugging
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String getExtraInfo() {
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
|
|
||||||
// Getting API Level
|
|
||||||
builder.append("API level: ")
|
|
||||||
.append(DeviceInfoUtil.getAPILevel())
|
|
||||||
.append("\n");
|
|
||||||
|
|
||||||
// Getting Android Version
|
|
||||||
builder.append("Android version: ")
|
|
||||||
.append(DeviceInfoUtil.getAndroidVersion())
|
|
||||||
.append("\n");
|
|
||||||
|
|
||||||
// Getting Device Manufacturer
|
|
||||||
builder.append("Device manufacturer: ")
|
|
||||||
.append(DeviceInfoUtil.getDeviceManufacturer())
|
|
||||||
.append("\n");
|
|
||||||
|
|
||||||
// Getting Device Model
|
|
||||||
builder.append("Device model: ")
|
|
||||||
.append(DeviceInfoUtil.getDeviceModel())
|
|
||||||
.append("\n");
|
|
||||||
|
|
||||||
// Getting Device Name
|
|
||||||
builder.append("Device: ")
|
|
||||||
.append(DeviceInfoUtil.getDevice())
|
|
||||||
.append("\n");
|
|
||||||
|
|
||||||
// Getting Network Type
|
|
||||||
builder.append("Network type: ")
|
|
||||||
.append(DeviceInfoUtil.getConnectionType(context))
|
|
||||||
.append("\n");
|
|
||||||
|
|
||||||
// Getting App Version
|
|
||||||
builder.append("App version name: ")
|
|
||||||
.append(ConfigUtils.getVersionNameWithSha(context))
|
|
||||||
.append("\n");
|
|
||||||
|
|
||||||
// Getting Username
|
|
||||||
builder.append("User name: ")
|
|
||||||
.append(sessionManager.getUserName())
|
|
||||||
.append("\n");
|
|
||||||
|
|
||||||
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean requiresForeground() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void send(@NotNull Context context, @NotNull CrashReportData crashReportData,
|
|
||||||
@NotNull Bundle bundle) throws ReportSenderException {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
package fr.free.nrw.commons.logging
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
|
import fr.free.nrw.commons.utils.ConfigUtils
|
||||||
|
import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha
|
||||||
|
import fr.free.nrw.commons.utils.DeviceInfoUtil
|
||||||
|
import org.acra.data.CrashReportData
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class responsible for sending logs to developers
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class CommonsLogSender @Inject constructor(
|
||||||
|
private val sessionManager: SessionManager,
|
||||||
|
private val context: Context
|
||||||
|
) : LogsSender(sessionManager) {
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com"
|
||||||
|
private const val LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Android App (%s) Logs"
|
||||||
|
private const val BETA_LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Beta Android App (%s) Logs"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val isBeta = ConfigUtils.isBetaFlavour
|
||||||
|
logFileName = if (isBeta) "CommonsBetaAppLogs.zip" else "CommonsAppLogs.zip"
|
||||||
|
val emailSubjectFormat = if (isBeta)
|
||||||
|
BETA_LOGS_PRIVATE_EMAIL_SUBJECT
|
||||||
|
else
|
||||||
|
LOGS_PRIVATE_EMAIL_SUBJECT
|
||||||
|
emailSubject = emailSubjectFormat.format(sessionManager.userName)
|
||||||
|
emailBody = getExtraInfo()
|
||||||
|
mailTo = LOGS_PRIVATE_EMAIL
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach any extra meta information about the user or device that might help in debugging.
|
||||||
|
* @return String with extra meta information useful for debugging.
|
||||||
|
*/
|
||||||
|
public override fun getExtraInfo(): String {
|
||||||
|
return buildString {
|
||||||
|
// Getting API Level
|
||||||
|
append("API level: ")
|
||||||
|
.append(DeviceInfoUtil.getAPILevel())
|
||||||
|
.append("\n")
|
||||||
|
|
||||||
|
// Getting Android Version
|
||||||
|
append("Android version: ")
|
||||||
|
.append(DeviceInfoUtil.getAndroidVersion())
|
||||||
|
.append("\n")
|
||||||
|
|
||||||
|
// Getting Device Manufacturer
|
||||||
|
append("Device manufacturer: ")
|
||||||
|
.append(DeviceInfoUtil.getDeviceManufacturer())
|
||||||
|
.append("\n")
|
||||||
|
|
||||||
|
// Getting Device Model
|
||||||
|
append("Device model: ")
|
||||||
|
.append(DeviceInfoUtil.getDeviceModel())
|
||||||
|
.append("\n")
|
||||||
|
|
||||||
|
// Getting Device Name
|
||||||
|
append("Device: ")
|
||||||
|
.append(DeviceInfoUtil.getDevice())
|
||||||
|
.append("\n")
|
||||||
|
|
||||||
|
// Getting Network Type
|
||||||
|
append("Network type: ")
|
||||||
|
.append(DeviceInfoUtil.getConnectionType(context))
|
||||||
|
.append("\n")
|
||||||
|
|
||||||
|
// Getting App Version
|
||||||
|
append("App version name: ")
|
||||||
|
.append(context.getVersionNameWithSha())
|
||||||
|
.append("\n")
|
||||||
|
|
||||||
|
// Getting Username
|
||||||
|
append("User name: ")
|
||||||
|
.append(sessionManager.userName)
|
||||||
|
.append("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the log sending process requires the app to be in the foreground.
|
||||||
|
* @return False as it does not require foreground execution.
|
||||||
|
*/
|
||||||
|
override fun requiresForeground(): Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends logs to developers. Implementation can be extended.
|
||||||
|
*/
|
||||||
|
override fun send(
|
||||||
|
context: Context,
|
||||||
|
errorContent: CrashReportData,
|
||||||
|
extras: Bundle) {
|
||||||
|
// Add logic here if needed.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
package fr.free.nrw.commons.logging;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
|
|
||||||
import ch.qos.logback.classic.LoggerContext;
|
|
||||||
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
|
|
||||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
|
||||||
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy;
|
|
||||||
import ch.qos.logback.core.rolling.RollingFileAppender;
|
|
||||||
import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extends Timber's debug tree to write logs to a file
|
|
||||||
*/
|
|
||||||
public class FileLoggingTree extends Timber.DebugTree implements LogLevelSettableTree {
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
|
|
||||||
private int logLevel;
|
|
||||||
private final String logFileName;
|
|
||||||
private int fileSize;
|
|
||||||
private FixedWindowRollingPolicy rollingPolicy;
|
|
||||||
private final Executor executor;
|
|
||||||
|
|
||||||
public FileLoggingTree(int logLevel,
|
|
||||||
String logFileName,
|
|
||||||
String logDirectory,
|
|
||||||
int fileSizeInKb,
|
|
||||||
Executor executor) {
|
|
||||||
this.logLevel = logLevel;
|
|
||||||
this.logFileName = logFileName;
|
|
||||||
this.fileSize = fileSizeInKb;
|
|
||||||
configureLogger(logDirectory);
|
|
||||||
this.executor = executor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Can be overridden to change file's log level
|
|
||||||
* @param logLevel
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void setLogLevel(int logLevel) {
|
|
||||||
this.logLevel = logLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check and log any message
|
|
||||||
* @param priority
|
|
||||||
* @param tag
|
|
||||||
* @param message
|
|
||||||
* @param t
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void log(final int priority, final String tag, @NonNull final String message, Throwable t) {
|
|
||||||
executor.execute(() -> logMessage(priority, tag, message));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log any message based on the priority
|
|
||||||
* @param priority
|
|
||||||
* @param tag
|
|
||||||
* @param message
|
|
||||||
*/
|
|
||||||
private void logMessage(int priority, String tag, String message) {
|
|
||||||
String messageWithTag = String.format("[%s] : %s", tag, message);
|
|
||||||
switch (priority) {
|
|
||||||
case Log.VERBOSE:
|
|
||||||
logger.trace(messageWithTag);
|
|
||||||
break;
|
|
||||||
case Log.DEBUG:
|
|
||||||
logger.debug(messageWithTag);
|
|
||||||
break;
|
|
||||||
case Log.INFO:
|
|
||||||
logger.info(messageWithTag);
|
|
||||||
break;
|
|
||||||
case Log.WARN:
|
|
||||||
logger.warn(messageWithTag);
|
|
||||||
break;
|
|
||||||
case Log.ERROR:
|
|
||||||
logger.error(messageWithTag);
|
|
||||||
break;
|
|
||||||
case Log.ASSERT:
|
|
||||||
logger.error(messageWithTag);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a particular log line should be logged in the file or not
|
|
||||||
* @param priority
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected boolean isLoggable(int priority) {
|
|
||||||
return priority >= logLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures the logger with a file size rolling policy (SizeBasedTriggeringPolicy)
|
|
||||||
* https://github.com/tony19/logback-android/wiki
|
|
||||||
* @param logDir
|
|
||||||
*/
|
|
||||||
private void configureLogger(String logDir) {
|
|
||||||
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
|
|
||||||
loggerContext.reset();
|
|
||||||
|
|
||||||
RollingFileAppender<ILoggingEvent> rollingFileAppender = new RollingFileAppender<>();
|
|
||||||
rollingFileAppender.setContext(loggerContext);
|
|
||||||
rollingFileAppender.setFile(logDir + "/" + logFileName + ".0.log");
|
|
||||||
|
|
||||||
rollingPolicy = new FixedWindowRollingPolicy();
|
|
||||||
rollingPolicy.setContext(loggerContext);
|
|
||||||
rollingPolicy.setMinIndex(1);
|
|
||||||
rollingPolicy.setMaxIndex(4);
|
|
||||||
rollingPolicy.setParent(rollingFileAppender);
|
|
||||||
rollingPolicy.setFileNamePattern(logDir + "/" + logFileName + ".%i.log");
|
|
||||||
rollingPolicy.start();
|
|
||||||
|
|
||||||
SizeBasedTriggeringPolicy<ILoggingEvent> triggeringPolicy = new SizeBasedTriggeringPolicy<>();
|
|
||||||
triggeringPolicy.setContext(loggerContext);
|
|
||||||
triggeringPolicy.setMaxFileSize(String.format(Locale.ENGLISH, "%dKB", fileSize));
|
|
||||||
triggeringPolicy.start();
|
|
||||||
|
|
||||||
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
|
|
||||||
encoder.setContext(loggerContext);
|
|
||||||
encoder.setPattern("%-27(%date{ISO8601}) [%-5level] [%thread] %msg%n");
|
|
||||||
encoder.start();
|
|
||||||
|
|
||||||
rollingFileAppender.setEncoder(encoder);
|
|
||||||
rollingFileAppender.setRollingPolicy(rollingPolicy);
|
|
||||||
rollingFileAppender.setTriggeringPolicy(triggeringPolicy);
|
|
||||||
rollingFileAppender.start();
|
|
||||||
ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger)
|
|
||||||
LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
|
|
||||||
logger.addAppender(rollingFileAppender);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
133
app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt
Normal file
133
app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
package fr.free.nrw.commons.logging
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.LoggerContext
|
||||||
|
import ch.qos.logback.classic.encoder.PatternLayoutEncoder
|
||||||
|
import ch.qos.logback.classic.spi.ILoggingEvent
|
||||||
|
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy
|
||||||
|
import ch.qos.logback.core.rolling.RollingFileAppender
|
||||||
|
import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends Timber's debug tree to write logs to a file.
|
||||||
|
*/
|
||||||
|
class FileLoggingTree(
|
||||||
|
private var logLevel: Int,
|
||||||
|
private val logFileName: String,
|
||||||
|
logDirectory: String,
|
||||||
|
private val fileSizeInKb: Int,
|
||||||
|
private val executor: Executor
|
||||||
|
) : Timber.DebugTree(), LogLevelSettableTree {
|
||||||
|
|
||||||
|
private val logger: Logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)
|
||||||
|
private lateinit var rollingPolicy: FixedWindowRollingPolicy
|
||||||
|
|
||||||
|
init {
|
||||||
|
configureLogger(logDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can be overridden to change the file's log level.
|
||||||
|
* @param logLevel The new log level.
|
||||||
|
*/
|
||||||
|
override fun setLogLevel(logLevel: Int) {
|
||||||
|
this.logLevel = logLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks and logs any message.
|
||||||
|
* @param priority The priority of the log message.
|
||||||
|
* @param tag The tag associated with the log message.
|
||||||
|
* @param message The log message.
|
||||||
|
* @param t An optional throwable.
|
||||||
|
*/
|
||||||
|
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||||
|
executor.execute {
|
||||||
|
logMessage(priority, tag.orEmpty(), message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a message based on the priority.
|
||||||
|
* @param priority The priority of the log message.
|
||||||
|
* @param tag The tag associated with the log message.
|
||||||
|
* @param message The log message.
|
||||||
|
*/
|
||||||
|
private fun logMessage(priority: Int, tag: String, message: String) {
|
||||||
|
val messageWithTag = "[$tag] : $message"
|
||||||
|
when (priority) {
|
||||||
|
Log.VERBOSE -> logger.trace(messageWithTag)
|
||||||
|
Log.DEBUG -> logger.debug(messageWithTag)
|
||||||
|
Log.INFO -> logger.info(messageWithTag)
|
||||||
|
Log.WARN -> logger.warn(messageWithTag)
|
||||||
|
Log.ERROR, Log.ASSERT -> logger.error(messageWithTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a particular log line should be logged in the file or not.
|
||||||
|
* @param priority The priority of the log message.
|
||||||
|
* @return True if the log message should be logged, false otherwise.
|
||||||
|
*/
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun isLoggable(priority: Int): Boolean {
|
||||||
|
return priority >= logLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the logger with a file size rolling policy (SizeBasedTriggeringPolicy).
|
||||||
|
* https://github.com/tony19/logback-android/wiki
|
||||||
|
* @param logDir The directory where logs should be stored.
|
||||||
|
*/
|
||||||
|
private fun configureLogger(logDir: String) {
|
||||||
|
val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext
|
||||||
|
loggerContext.reset()
|
||||||
|
|
||||||
|
val rollingFileAppender = RollingFileAppender<ILoggingEvent>().apply {
|
||||||
|
context = loggerContext
|
||||||
|
file = "$logDir/$logFileName.0.log"
|
||||||
|
}
|
||||||
|
|
||||||
|
rollingPolicy = FixedWindowRollingPolicy().apply {
|
||||||
|
context = loggerContext
|
||||||
|
minIndex = 1
|
||||||
|
maxIndex = 4
|
||||||
|
setParent(rollingFileAppender)
|
||||||
|
fileNamePattern = "$logDir/$logFileName.%i.log"
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
val triggeringPolicy = SizeBasedTriggeringPolicy<ILoggingEvent>().apply {
|
||||||
|
context = loggerContext
|
||||||
|
maxFileSize = "$fileSizeInKb"
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
val encoder = PatternLayoutEncoder().apply {
|
||||||
|
context = loggerContext
|
||||||
|
pattern = "%-27(%date{ISO8601}) [%-5level] [%thread] %msg%n"
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
rollingFileAppender.apply {
|
||||||
|
this.encoder = encoder
|
||||||
|
rollingPolicy = rollingPolicy
|
||||||
|
this.triggeringPolicy = triggeringPolicy
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
val rootLogger = LoggerFactory.getLogger(
|
||||||
|
Logger.ROOT_LOGGER_NAME
|
||||||
|
) as ch.qos.logback.classic.Logger
|
||||||
|
rootLogger.addAppender(rollingFileAppender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package fr.free.nrw.commons.logging;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Can be implemented to set the log level for file tree
|
|
||||||
*/
|
|
||||||
public interface LogLevelSettableTree {
|
|
||||||
void setLogLevel(int logLevel);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package fr.free.nrw.commons.logging
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can be implemented to set the log level for file tree
|
||||||
|
*/
|
||||||
|
interface LogLevelSettableTree {
|
||||||
|
fun setLogLevel(logLevel: Int)
|
||||||
|
}
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
package fr.free.nrw.commons.logging;
|
|
||||||
|
|
||||||
import android.os.Environment;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.upload.FileUtils;
|
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the log directory
|
|
||||||
*/
|
|
||||||
public final class LogUtils {
|
|
||||||
private LogUtils() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the directory for saving logs on the device
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String getLogDirectory() {
|
|
||||||
String dirPath;
|
|
||||||
if (ConfigUtils.isBetaFlavour()) {
|
|
||||||
dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/beta";
|
|
||||||
} else {
|
|
||||||
dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/prod";
|
|
||||||
}
|
|
||||||
|
|
||||||
FileUtils.recursivelyCreateDirs(dirPath);
|
|
||||||
return dirPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the directory for saving logs on the device
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String getLogZipDirectory() {
|
|
||||||
String dirPath;
|
|
||||||
if (ConfigUtils.isBetaFlavour()) {
|
|
||||||
dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/beta/zip";
|
|
||||||
} else {
|
|
||||||
dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/prod/zip";
|
|
||||||
}
|
|
||||||
|
|
||||||
FileUtils.recursivelyCreateDirs(dirPath);
|
|
||||||
return dirPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
57
app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt
Normal file
57
app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
package fr.free.nrw.commons.logging
|
||||||
|
|
||||||
|
import android.os.Environment
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.upload.FileUtils
|
||||||
|
import fr.free.nrw.commons.utils.ConfigUtils
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the log directory
|
||||||
|
*/
|
||||||
|
object LogUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the directory for saving logs on the device.
|
||||||
|
*
|
||||||
|
* @return The path to the log directory.
|
||||||
|
*/
|
||||||
|
fun getLogDirectory(): String {
|
||||||
|
val dirPath = if (ConfigUtils.isBetaFlavour) {
|
||||||
|
"${Environment
|
||||||
|
.getExternalStoragePublicDirectory(
|
||||||
|
Environment.DIRECTORY_DOWNLOADS
|
||||||
|
)}/logs/beta"
|
||||||
|
} else {
|
||||||
|
"${Environment
|
||||||
|
.getExternalStoragePublicDirectory(
|
||||||
|
Environment.DIRECTORY_DOWNLOADS
|
||||||
|
)}/logs/prod"
|
||||||
|
}
|
||||||
|
|
||||||
|
FileUtils.recursivelyCreateDirs(dirPath)
|
||||||
|
return dirPath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the directory for saving zipped logs on the device.
|
||||||
|
*
|
||||||
|
* @return The path to the zipped log directory.
|
||||||
|
*/
|
||||||
|
fun getLogZipDirectory(): String {
|
||||||
|
val dirPath = if (ConfigUtils.isBetaFlavour) {
|
||||||
|
"${Environment
|
||||||
|
.getExternalStoragePublicDirectory(
|
||||||
|
Environment.DIRECTORY_DOWNLOADS
|
||||||
|
)}/logs/beta/zip"
|
||||||
|
} else {
|
||||||
|
"${Environment
|
||||||
|
.getExternalStoragePublicDirectory(
|
||||||
|
Environment.DIRECTORY_DOWNLOADS
|
||||||
|
)}/logs/prod/zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
FileUtils.recursivelyCreateDirs(dirPath)
|
||||||
|
return dirPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
package fr.free.nrw.commons.logging;
|
|
||||||
|
|
||||||
import static org.acra.ACRA.getErrorReporter;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.content.FileProvider;
|
|
||||||
|
|
||||||
import org.acra.data.CrashReportData;
|
|
||||||
import org.acra.sender.ReportSender;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipOutputStream;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract class that implements Acra's log sender
|
|
||||||
*/
|
|
||||||
public abstract class LogsSender implements ReportSender {
|
|
||||||
|
|
||||||
String mailTo;
|
|
||||||
String logFileName;
|
|
||||||
String emailSubject;
|
|
||||||
String emailBody;
|
|
||||||
|
|
||||||
private final SessionManager sessionManager;
|
|
||||||
|
|
||||||
LogsSender(SessionManager sessionManager) {
|
|
||||||
this.sessionManager = sessionManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overrides send method of ACRA's ReportSender to send logs
|
|
||||||
*
|
|
||||||
* @param context
|
|
||||||
* @param report
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void send(@NonNull final Context context, @Nullable CrashReportData report) {
|
|
||||||
sendLogs(context, report);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets zipped log files and sends it via email. Can be modified to change the send log mechanism
|
|
||||||
*
|
|
||||||
* @param context
|
|
||||||
* @param report
|
|
||||||
*/
|
|
||||||
private void sendLogs(Context context, CrashReportData report) {
|
|
||||||
final Uri logFileUri = getZippedLogFileUri(context, report);
|
|
||||||
if (logFileUri != null) {
|
|
||||||
sendEmail(context, logFileUri);
|
|
||||||
} else {
|
|
||||||
getErrorReporter().handleSilentException(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/***
|
|
||||||
* Provides any extra information that you want to send. The return value will be
|
|
||||||
* delivered inside the report verbatim
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
protected abstract String getExtraInfo();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires an intent to send email with logs
|
|
||||||
*
|
|
||||||
* @param context
|
|
||||||
* @param logFileUri
|
|
||||||
*/
|
|
||||||
private void sendEmail(Context context, Uri logFileUri) {
|
|
||||||
String subject = emailSubject;
|
|
||||||
String body = emailBody;
|
|
||||||
|
|
||||||
Intent emailIntent = new Intent(Intent.ACTION_SEND);
|
|
||||||
emailIntent.setType("message/rfc822");
|
|
||||||
emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{mailTo});
|
|
||||||
emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
|
||||||
emailIntent.putExtra(Intent.EXTRA_TEXT, body);
|
|
||||||
emailIntent.putExtra(Intent.EXTRA_STREAM, logFileUri);
|
|
||||||
emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
context.startActivity(Intent.createChooser(emailIntent, context.getString(R.string.share_logs_using)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the URI for the zipped log file
|
|
||||||
*
|
|
||||||
* @param report
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private Uri getZippedLogFileUri(Context context, CrashReportData report) {
|
|
||||||
try {
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
if (report != null) {
|
|
||||||
attachCrashInfo(report, builder);
|
|
||||||
}
|
|
||||||
attachUserInfo(builder);
|
|
||||||
attachExtraInfo(builder);
|
|
||||||
byte[] metaData = builder.toString().getBytes(Charset.forName("UTF-8"));
|
|
||||||
File zipFile = new File(LogUtils.getLogZipDirectory(), logFileName);
|
|
||||||
writeLogToZipFile(metaData, zipFile);
|
|
||||||
return FileProvider
|
|
||||||
.getUriForFile(context,
|
|
||||||
context.getApplicationContext().getPackageName() + ".provider", zipFile);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.w(e, "Error in generating log file");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if there are any pending crash reports and attaches them to the logs
|
|
||||||
*
|
|
||||||
* @param report
|
|
||||||
* @param builder
|
|
||||||
*/
|
|
||||||
private void attachCrashInfo(CrashReportData report, StringBuilder builder) {
|
|
||||||
if (report == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
builder.append(report);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attaches username to the the meta_data file
|
|
||||||
*
|
|
||||||
* @param builder
|
|
||||||
*/
|
|
||||||
private void attachUserInfo(StringBuilder builder) {
|
|
||||||
builder.append("MediaWiki Username = ").append(sessionManager.getUserName()).append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets any extra meta information to be attached with the log files
|
|
||||||
*
|
|
||||||
* @param builder
|
|
||||||
*/
|
|
||||||
private void attachExtraInfo(StringBuilder builder) {
|
|
||||||
String infoToBeAttached = getExtraInfo();
|
|
||||||
builder.append(infoToBeAttached);
|
|
||||||
builder.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zips the logs and meta information
|
|
||||||
*
|
|
||||||
* @param metaData
|
|
||||||
* @param zipFile
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
private void writeLogToZipFile(byte[] metaData, File zipFile) throws IOException {
|
|
||||||
FileOutputStream fos = new FileOutputStream(zipFile);
|
|
||||||
BufferedOutputStream bos = new BufferedOutputStream(fos);
|
|
||||||
ZipOutputStream zos = new ZipOutputStream(bos);
|
|
||||||
File logDir = new File(LogUtils.getLogDirectory());
|
|
||||||
|
|
||||||
if (!logDir.exists() || logDir.listFiles().length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
for (File file : logDir.listFiles()) {
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
FileInputStream fis = new FileInputStream(file);
|
|
||||||
BufferedInputStream bis = new BufferedInputStream(fis);
|
|
||||||
zos.putNextEntry(new ZipEntry(file.getName()));
|
|
||||||
int length;
|
|
||||||
while ((length = bis.read(buffer)) > 0) {
|
|
||||||
zos.write(buffer, 0, length);
|
|
||||||
}
|
|
||||||
zos.closeEntry();
|
|
||||||
bis.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
//attach metadata as a separate file
|
|
||||||
zos.putNextEntry(new ZipEntry("meta_data.txt"));
|
|
||||||
zos.write(metaData);
|
|
||||||
zos.closeEntry();
|
|
||||||
|
|
||||||
zos.flush();
|
|
||||||
zos.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
193
app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt
Normal file
193
app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
package fr.free.nrw.commons.logging
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
|
||||||
|
import org.acra.data.CrashReportData
|
||||||
|
import org.acra.sender.ReportSender
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
|
import org.acra.ACRA.errorReporter
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class that implements Acra's log sender.
|
||||||
|
*/
|
||||||
|
abstract class LogsSender(
|
||||||
|
private val sessionManager: SessionManager
|
||||||
|
): ReportSender {
|
||||||
|
|
||||||
|
var mailTo: String? = null
|
||||||
|
var logFileName: String? = null
|
||||||
|
var emailSubject: String? = null
|
||||||
|
var emailBody: String? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the send method of ACRA's ReportSender to send logs.
|
||||||
|
*
|
||||||
|
* @param context The context in which to send the logs.
|
||||||
|
* @param report The crash report data, if any.
|
||||||
|
*/
|
||||||
|
fun sendWithNullable(context: Context, report: CrashReportData?) {
|
||||||
|
if (report == null) {
|
||||||
|
errorReporter.handleSilentException(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
send(context, report)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun send(context: Context, report: CrashReportData) {
|
||||||
|
sendLogs(context, report)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets zipped log files and sends them via email. Can be modified to change the send
|
||||||
|
* log mechanism.
|
||||||
|
*
|
||||||
|
* @param context The context in which to send the logs.
|
||||||
|
* @param report The crash report data, if any.
|
||||||
|
*/
|
||||||
|
private fun sendLogs(context: Context, report: CrashReportData?) {
|
||||||
|
val logFileUri = getZippedLogFileUri(context, report)
|
||||||
|
if (logFileUri != null) {
|
||||||
|
sendEmail(context, logFileUri)
|
||||||
|
} else {
|
||||||
|
errorReporter.handleSilentException(null)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides any extra information that you want to send. The return value will be
|
||||||
|
* delivered inside the report verbatim.
|
||||||
|
*
|
||||||
|
* @return A string containing the extra information.
|
||||||
|
*/
|
||||||
|
protected abstract fun getExtraInfo(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires an intent to send an email with logs.
|
||||||
|
*
|
||||||
|
* @param context The context in which to send the email.
|
||||||
|
* @param logFileUri The URI of the zipped log file.
|
||||||
|
*/
|
||||||
|
private fun sendEmail(context: Context, logFileUri: Uri) {
|
||||||
|
val emailIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "message/rfc822"
|
||||||
|
putExtra(Intent.EXTRA_EMAIL, arrayOf(mailTo))
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, emailSubject)
|
||||||
|
putExtra(Intent.EXTRA_TEXT, emailBody)
|
||||||
|
putExtra(Intent.EXTRA_STREAM, logFileUri)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
context.startActivity(Intent.createChooser(emailIntent, context.getString(R.string.share_logs_using)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the URI for the zipped log file.
|
||||||
|
*
|
||||||
|
* @param context The context for file URI generation.
|
||||||
|
* @param report The crash report data, if any.
|
||||||
|
* @return The URI of the zipped log file or null if an error occurs.
|
||||||
|
*/
|
||||||
|
private fun getZippedLogFileUri(context: Context, report: CrashReportData?): Uri? {
|
||||||
|
return try {
|
||||||
|
val builder = StringBuilder().apply {
|
||||||
|
report?.let { attachCrashInfo(it, this) }
|
||||||
|
attachUserInfo(this)
|
||||||
|
attachExtraInfo(this)
|
||||||
|
}
|
||||||
|
val metaData = builder.toString().toByteArray(Charsets.UTF_8)
|
||||||
|
val zipFile = File(LogUtils.getLogZipDirectory(), logFileName ?: "logs.zip")
|
||||||
|
writeLogToZipFile(metaData, zipFile)
|
||||||
|
FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.applicationContext.packageName}.provider",
|
||||||
|
zipFile
|
||||||
|
)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.w(e, "Error in generating log file")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if there are any pending crash reports and attaches them to the logs.
|
||||||
|
*
|
||||||
|
* @param report The crash report data, if any.
|
||||||
|
* @param builder The string builder to append crash info.
|
||||||
|
*/
|
||||||
|
private fun attachCrashInfo(report: CrashReportData?, builder: StringBuilder) {
|
||||||
|
if(report != null) {
|
||||||
|
builder.append(report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the username to the metadata file.
|
||||||
|
*
|
||||||
|
* @param builder The string builder to append user info.
|
||||||
|
*/
|
||||||
|
private fun attachUserInfo(builder: StringBuilder) {
|
||||||
|
builder.append("MediaWiki Username = ").append(sessionManager.userName).append("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets any extra metadata information to be attached with the log files.
|
||||||
|
*
|
||||||
|
* @param builder The string builder to append extra info.
|
||||||
|
*/
|
||||||
|
private fun attachExtraInfo(builder: StringBuilder) {
|
||||||
|
builder.append(getExtraInfo()).append("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zips the logs and metadata information.
|
||||||
|
*
|
||||||
|
* @param metaData The metadata to be added to the zip file.
|
||||||
|
* @param zipFile The zip file to write to.
|
||||||
|
* @throws IOException If an I/O error occurs.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun writeLogToZipFile(metaData: ByteArray, zipFile: File) {
|
||||||
|
val logDir = File(LogUtils.getLogDirectory())
|
||||||
|
if (!logDir.exists() || logDir.listFiles().isNullOrEmpty()) return
|
||||||
|
|
||||||
|
ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { zos ->
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
logDir.listFiles()?.forEach { file ->
|
||||||
|
if (file.isDirectory) return@forEach
|
||||||
|
FileInputStream(file).use { fis ->
|
||||||
|
BufferedInputStream(fis).use { bis ->
|
||||||
|
zos.putNextEntry(ZipEntry(file.name))
|
||||||
|
var length: Int
|
||||||
|
while (bis.read(buffer).also { length = it } > 0) {
|
||||||
|
zos.write(buffer, 0, length)
|
||||||
|
}
|
||||||
|
zos.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach metadata as a separate file.
|
||||||
|
zos.putNextEntry(ZipEntry("meta_data.txt"))
|
||||||
|
zos.write(metaData)
|
||||||
|
zos.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package fr.free.nrw.commons.settings
|
package fr.free.nrw.commons.settings
|
||||||
|
|
||||||
import android.Manifest.permission
|
import android.Manifest.permission
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
|
|
@ -527,7 +528,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
PermissionUtils.PERMISSIONS_STORAGE
|
PermissionUtils.PERMISSIONS_STORAGE
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
commonsLogSender.send(requireActivity(), null)
|
commonsLogSender.sendWithNullable(requireActivity(), null)
|
||||||
} else {
|
} else {
|
||||||
requestExternalStoragePermissions()
|
requestExternalStoragePermissions()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class SystemThemeUtils @Inject constructor(
|
||||||
// Returns true if the device is in night mode or false otherwise
|
// Returns true if the device is in night mode or false otherwise
|
||||||
fun isDeviceInNightMode(): Boolean {
|
fun isDeviceInNightMode(): Boolean {
|
||||||
return getSystemDefaultThemeBool(
|
return getSystemDefaultThemeBool(
|
||||||
applicationKvStore.getString(Prefs.KEY_THEME_VALUE, getSystemDefaultTheme())
|
applicationKvStore.getString(Prefs.KEY_THEME_VALUE, getSystemDefaultTheme())!!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
* Jduranboger
|
* Jduranboger
|
||||||
* Jelou
|
* Jelou
|
||||||
* Johnny243
|
* Johnny243
|
||||||
|
* Josuert
|
||||||
* Juanman
|
* Juanman
|
||||||
* Keneth Urrutia
|
* Keneth Urrutia
|
||||||
* Ktranz
|
* Ktranz
|
||||||
|
|
@ -166,6 +167,7 @@
|
||||||
<string name="categories_search_text_hint">Buscar categorías</string>
|
<string name="categories_search_text_hint">Buscar categorías</string>
|
||||||
<string name="depicts_search_text_hint">Buscar elementos que tu archivo multimedia representa (montaña, Taj Mahal, etc.)</string>
|
<string name="depicts_search_text_hint">Buscar elementos que tu archivo multimedia representa (montaña, Taj Mahal, etc.)</string>
|
||||||
<string name="menu_save_categories">Guardar</string>
|
<string name="menu_save_categories">Guardar</string>
|
||||||
|
<string name="menu_overflow_desc">Menú de desbordamiento</string>
|
||||||
<string name="refresh_button">Actualizar</string>
|
<string name="refresh_button">Actualizar</string>
|
||||||
<string name="display_list_button">Lista</string>
|
<string name="display_list_button">Lista</string>
|
||||||
<string name="contributions_subtitle_zero">(No hay subidas aún)</string>
|
<string name="contributions_subtitle_zero">(No hay subidas aún)</string>
|
||||||
|
|
@ -522,6 +524,7 @@
|
||||||
<string name="no_notification">No tienes notificaciones sin leer</string>
|
<string name="no_notification">No tienes notificaciones sin leer</string>
|
||||||
<string name="no_read_notification">No tienes ninguna notificación leída</string>
|
<string name="no_read_notification">No tienes ninguna notificación leída</string>
|
||||||
<string name="share_logs_using">Compartir registros usando</string>
|
<string name="share_logs_using">Compartir registros usando</string>
|
||||||
|
<string name="check_your_email_inbox">Revisa tu bandeja de entrada</string>
|
||||||
<string name="menu_option_read">Ver leídas</string>
|
<string name="menu_option_read">Ver leídas</string>
|
||||||
<string name="menu_option_unread">Ver no leidas</string>
|
<string name="menu_option_unread">Ver no leidas</string>
|
||||||
<string name="error_occurred_in_picking_images">Ocurrió un error mientras se elegían imagenes</string>
|
<string name="error_occurred_in_picking_images">Ocurrió un error mientras se elegían imagenes</string>
|
||||||
|
|
@ -819,8 +822,26 @@
|
||||||
<string name="please_enter_some_comments">Por favor, escriba algunos comentarios.</string>
|
<string name="please_enter_some_comments">Por favor, escriba algunos comentarios.</string>
|
||||||
<string name="talk">Discusión</string>
|
<string name="talk">Discusión</string>
|
||||||
<string name="write_something_about_the_item">Escriba algo sobre el elemento \'%1$s\'. Será visible públicamente.</string>
|
<string name="write_something_about_the_item">Escriba algo sobre el elemento \'%1$s\'. Será visible públicamente.</string>
|
||||||
|
<string name="does_not_exist_anymore_no_picture_can_ever_be_taken_of_it">\' %1$s \' ya no existe, nunca se podrá tomar ninguna fotografía de él.</string>
|
||||||
|
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\' %1$s \' está en un lugar diferente. Especifique el lugar correcto a continuación y, si es posible, escriba la latitud y longitud correctas.</string>
|
||||||
|
<string name="other_problem_or_information_please_explain_below">Otro problema o información (por favor explique a continuación).</string>
|
||||||
|
<string name="feedback_destination_note">Sus comentarios se publicarán en la siguiente página wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile_app/Feedback</a></string>
|
||||||
|
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">¿Estás seguro de que deseas cancelar todas las subidas?</string>
|
||||||
<string name="cancelling_all_the_uploads">Cancelando todas las subidas...</string>
|
<string name="cancelling_all_the_uploads">Cancelando todas las subidas...</string>
|
||||||
<string name="uploads">Subidas</string>
|
<string name="uploads">Subidas</string>
|
||||||
<string name="pending">Pendiente</string>
|
<string name="pending">Pendiente</string>
|
||||||
<string name="failed">Falló</string>
|
<string name="failed">Falló</string>
|
||||||
|
<string name="could_not_load_place_data">No se pudieron cargar los datos del lugar</string>
|
||||||
|
<string name="custom_selector_delete_folder">Eliminar carpeta</string>
|
||||||
|
<string name="custom_selector_confirm_deletion_title">Confirmar eliminación</string>
|
||||||
|
<string name="custom_selector_confirm_deletion_message">¿Está seguro de que deseas eliminar la carpeta %1$s que contiene %2$d elementos?</string>
|
||||||
|
<string name="custom_selector_delete">Eliminar</string>
|
||||||
|
<string name="custom_selector_cancel">Cancelar</string>
|
||||||
|
<string name="custom_selector_folder_deleted_success">La carpeta %1$s se eliminó correctamente</string>
|
||||||
|
<string name="custom_selector_folder_deleted_failure">No se pudo eliminar la carpeta %1$s</string>
|
||||||
|
<string name="custom_selector_error_trashing_folder_contents">Error al eliminar el contenido de la carpeta: %1$s</string>
|
||||||
|
<string name="custom_selector_folder_not_found_error">No se pudo recuperar la ruta de la carpeta para el ID del bucket: %1$d</string>
|
||||||
|
<string name="red_pin">Este lugar aún no tiene foto, ¡ve y toma una!</string>
|
||||||
|
<string name="green_pin">Este lugar ya tiene una foto.</string>
|
||||||
|
<string name="grey_pin">Ahora comprobando si este lugar tiene una foto.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -542,9 +542,9 @@
|
||||||
<string name="review_is_uploaded_by">%1$s הועלה על ידי: %2$s</string>
|
<string name="review_is_uploaded_by">%1$s הועלה על ידי: %2$s</string>
|
||||||
<string name="default_description_language">שפת התיאור כבררת מחדל</string>
|
<string name="default_description_language">שפת התיאור כבררת מחדל</string>
|
||||||
<string name="delete_helper_show_deletion_title">העמדה למחיקה</string>
|
<string name="delete_helper_show_deletion_title">העמדה למחיקה</string>
|
||||||
<string name="delete_helper_show_deletion_title_success">הצלחה</string>
|
<string name="delete_helper_show_deletion_title_success">זה עבד</string>
|
||||||
<string name="delete_helper_show_deletion_message_if">הקובץ %1$s הועמד למחיקה.</string>
|
<string name="delete_helper_show_deletion_message_if">הקובץ %1$s הועמד למחיקה.</string>
|
||||||
<string name="delete_helper_show_deletion_title_failed">כשלון</string>
|
<string name="delete_helper_show_deletion_title_failed">זה לא עבד</string>
|
||||||
<string name="delete_helper_show_deletion_message_else">לא ניתן לבקש מחיקה</string>
|
<string name="delete_helper_show_deletion_message_else">לא ניתן לבקש מחיקה</string>
|
||||||
<string name="delete_helper_ask_spam_selfie">תמונה עצמית (סלפי) שלא משמשת בשום ערך</string>
|
<string name="delete_helper_ask_spam_selfie">תמונה עצמית (סלפי) שלא משמשת בשום ערך</string>
|
||||||
<string name="delete_helper_ask_spam_blurry">תמונה מטושטשת לגמרי</string>
|
<string name="delete_helper_ask_spam_blurry">תמונה מטושטשת לגמרי</string>
|
||||||
|
|
@ -825,7 +825,7 @@
|
||||||
<string name="custom_selector_delete_folder">מחיקת תיקייה</string>
|
<string name="custom_selector_delete_folder">מחיקת תיקייה</string>
|
||||||
<string name="custom_selector_confirm_deletion_title">אישור מחיקה</string>
|
<string name="custom_selector_confirm_deletion_title">אישור מחיקה</string>
|
||||||
<string name="custom_selector_confirm_deletion_message">למחוק את התיקייה %1$s על כל %2$d פריטיה?</string>
|
<string name="custom_selector_confirm_deletion_message">למחוק את התיקייה %1$s על כל %2$d פריטיה?</string>
|
||||||
<string name="custom_selector_delete">מחיקה</string>
|
<string name="custom_selector_delete">למחוק</string>
|
||||||
<string name="custom_selector_cancel">ביטול</string>
|
<string name="custom_selector_cancel">ביטול</string>
|
||||||
<string name="custom_selector_folder_deleted_success">התיקייה %1$s נמחקה</string>
|
<string name="custom_selector_folder_deleted_success">התיקייה %1$s נמחקה</string>
|
||||||
<string name="custom_selector_folder_deleted_failure">מחיקת התיקייה %1$s נכשלה</string>
|
<string name="custom_selector_folder_deleted_failure">מחיקת התיקייה %1$s נכשלה</string>
|
||||||
|
|
|
||||||
|
|
@ -204,4 +204,5 @@
|
||||||
<string name="explore_map_details">{{Identical|Detail}}</string>
|
<string name="explore_map_details">{{Identical|Detail}}</string>
|
||||||
<string name="set_up_avatar_toast_string">\"Set as avatar\" should be translated the same as {{msg-wm|Commons-android-strings-menu set avatar}}.</string>
|
<string name="set_up_avatar_toast_string">\"Set as avatar\" should be translated the same as {{msg-wm|Commons-android-strings-menu set avatar}}.</string>
|
||||||
<string name="multiple_files_depiction">{{Doc-commons-app-depicts}}</string>
|
<string name="multiple_files_depiction">{{Doc-commons-app-depicts}}</string>
|
||||||
|
<string name="custom_selector_delete">An answer to the question in {{msg-wm|Commons-android-strings-custom selector confirm deletion message}}.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -20,25 +20,24 @@ import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class CampaignsPresenterTest {
|
class CampaignsPresenterTest {
|
||||||
@Mock
|
@Mock
|
||||||
lateinit var okHttpJsonApiClient: OkHttpJsonApiClient
|
private lateinit var okHttpJsonApiClient: OkHttpJsonApiClient
|
||||||
|
|
||||||
lateinit var campaignsPresenter: CampaignsPresenter
|
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal lateinit var view: ICampaignsView
|
private lateinit var view: ICampaignsView
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal lateinit var campaignResponseDTO: CampaignResponseDTO
|
private lateinit var campaignResponseDTO: CampaignResponseDTO
|
||||||
lateinit var campaignsSingle: Single<CampaignResponseDTO>
|
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
lateinit var campaign: Campaign
|
private lateinit var campaign: Campaign
|
||||||
|
|
||||||
lateinit var testScheduler: TestScheduler
|
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var disposable: Disposable
|
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
|
* initial setup, test environment
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
package fr.free.nrw.commons.kvstore
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import com.nhaarman.mockitokotlin2.atLeast
|
||||||
|
import com.nhaarman.mockitokotlin2.verify
|
||||||
|
import com.nhaarman.mockitokotlin2.whenever
|
||||||
|
import fr.free.nrw.commons.kvstore.BasicKvStore.Companion.KEY_VERSION
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.ArgumentMatchers.anyInt
|
||||||
|
import org.mockito.ArgumentMatchers.anyString
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
|
||||||
|
class BasicKvStoreTest {
|
||||||
|
private val context = mock<Context>()
|
||||||
|
private val prefs = mock<SharedPreferences>()
|
||||||
|
private val editor = mock<SharedPreferences.Editor>()
|
||||||
|
private lateinit var store: BasicKvStore
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
whenever(context.getSharedPreferences(anyString(), anyInt())).thenReturn(prefs)
|
||||||
|
whenever(prefs.edit()).thenReturn(editor)
|
||||||
|
store = BasicKvStore(context, "name")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun versionUpdate() {
|
||||||
|
whenever(prefs.getInt(KEY_VERSION, 0)).thenReturn(99)
|
||||||
|
BasicKvStore(context, "name", 100, true)
|
||||||
|
|
||||||
|
// It should clear itself and automatically put the new version number
|
||||||
|
verify(prefs, atLeast(2)).edit()
|
||||||
|
verify(editor).clear()
|
||||||
|
verify(editor).putInt(KEY_VERSION, 100)
|
||||||
|
verify(editor, atLeast(2)).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException::class)
|
||||||
|
fun versionDowngradeNotAllowed() {
|
||||||
|
whenever(prefs.getInt(KEY_VERSION, 0)).thenReturn(100)
|
||||||
|
BasicKvStore(context, "name", 99, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun versionRedactedFromGetAll() {
|
||||||
|
val all = mutableMapOf("key" to "value", KEY_VERSION to 100)
|
||||||
|
whenever(prefs.all).thenReturn(all)
|
||||||
|
|
||||||
|
val result = store.all
|
||||||
|
Assert.assertEquals(mapOf("key" to "value"), result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getAllHandlesNull() {
|
||||||
|
whenever(prefs.all).thenReturn(null)
|
||||||
|
Assert.assertNull(store.all)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getAllHandlesEmpty() {
|
||||||
|
whenever(prefs.all).thenReturn(emptyMap())
|
||||||
|
Assert.assertNull(store.all)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getString() {
|
||||||
|
whenever(prefs.getString("key", null)).thenReturn("value")
|
||||||
|
Assert.assertEquals("value", store.getString("key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getBoolean() {
|
||||||
|
whenever(prefs.getBoolean("key", false)).thenReturn(true)
|
||||||
|
Assert.assertTrue(store.getBoolean("key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getLong() {
|
||||||
|
whenever(prefs.getLong("key", 0L)).thenReturn(100)
|
||||||
|
Assert.assertEquals(100L, store.getLong("key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getInt() {
|
||||||
|
whenever(prefs.getInt("key", 0)).thenReturn(100)
|
||||||
|
Assert.assertEquals(100, store.getInt("key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getStringWithDefault() {
|
||||||
|
whenever(prefs.getString("key", "junk")).thenReturn("value")
|
||||||
|
Assert.assertEquals("value", store.getString("key", "junk"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getBooleanWithDefault() {
|
||||||
|
whenever(prefs.getBoolean("key", true)).thenReturn(true)
|
||||||
|
Assert.assertTrue(store.getBoolean("key", true))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getLongWithDefault() {
|
||||||
|
whenever(prefs.getLong("key", 22L)).thenReturn(100)
|
||||||
|
Assert.assertEquals(100L, store.getLong("key", 22L))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getIntWithDefault() {
|
||||||
|
whenever(prefs.getInt("key", 22)).thenReturn(100)
|
||||||
|
Assert.assertEquals(100, store.getInt("key", 22))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun putAllStrings() {
|
||||||
|
store.putAllStrings(
|
||||||
|
mapOf(
|
||||||
|
"one" to "fish",
|
||||||
|
"two" to "fish",
|
||||||
|
"red" to "fish",
|
||||||
|
"blue" to "fish"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(prefs).edit()
|
||||||
|
verify(editor).putString("one", "fish")
|
||||||
|
verify(editor).putString("two", "fish")
|
||||||
|
verify(editor).putString("red", "fish")
|
||||||
|
verify(editor).putString("blue", "fish")
|
||||||
|
verify(editor).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException::class)
|
||||||
|
fun putAllStringsWithReservedKey() {
|
||||||
|
store.putAllStrings(
|
||||||
|
mapOf(
|
||||||
|
"this" to "that",
|
||||||
|
KEY_VERSION to "something"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun putString() {
|
||||||
|
store.putString("this" , "that")
|
||||||
|
|
||||||
|
verify(prefs).edit()
|
||||||
|
verify(editor).putString("this", "that")
|
||||||
|
verify(editor).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun putBoolean() {
|
||||||
|
store.putBoolean("this" , true)
|
||||||
|
|
||||||
|
verify(prefs).edit()
|
||||||
|
verify(editor).putBoolean("this", true)
|
||||||
|
verify(editor).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun putLong() {
|
||||||
|
store.putLong("this" , 123L)
|
||||||
|
|
||||||
|
verify(prefs).edit()
|
||||||
|
verify(editor).putLong("this", 123L)
|
||||||
|
verify(editor).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun putInt() {
|
||||||
|
store.putInt("this" , 16)
|
||||||
|
|
||||||
|
verify(prefs).edit()
|
||||||
|
verify(editor).putInt("this", 16)
|
||||||
|
verify(editor).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException::class)
|
||||||
|
fun putStringWithReservedKey() {
|
||||||
|
store.putString(KEY_VERSION, "that")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException::class)
|
||||||
|
fun putBooleanWithReservedKey() {
|
||||||
|
store.putBoolean(KEY_VERSION, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException::class)
|
||||||
|
fun putLongWithReservedKey() {
|
||||||
|
store.putLong(KEY_VERSION, 33L)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException::class)
|
||||||
|
fun putIntWithReservedKey() {
|
||||||
|
store.putInt(KEY_VERSION, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testContains() {
|
||||||
|
whenever(prefs.contains("key")).thenReturn(true)
|
||||||
|
Assert.assertTrue(store.contains("key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun containsRedactsVersion() {
|
||||||
|
whenever(prefs.contains(KEY_VERSION)).thenReturn(true)
|
||||||
|
Assert.assertFalse(store.contains(KEY_VERSION))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun remove() {
|
||||||
|
store.remove("key")
|
||||||
|
|
||||||
|
verify(prefs).edit()
|
||||||
|
verify(editor).remove("key")
|
||||||
|
verify(editor).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException::class)
|
||||||
|
fun removeWithReservedKey() {
|
||||||
|
store.remove(KEY_VERSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clearAllPreservesVersion() {
|
||||||
|
whenever(prefs.getInt(KEY_VERSION, 0)).thenReturn(99)
|
||||||
|
|
||||||
|
store.clearAll()
|
||||||
|
|
||||||
|
verify(prefs).edit()
|
||||||
|
verify(editor).clear()
|
||||||
|
verify(editor).putInt(KEY_VERSION, 99)
|
||||||
|
verify(editor).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
package fr.free.nrw.commons.kvstore
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.nhaarman.mockitokotlin2.atLeast
|
||||||
|
import com.nhaarman.mockitokotlin2.verify
|
||||||
|
import com.nhaarman.mockitokotlin2.whenever
|
||||||
|
import fr.free.nrw.commons.kvstore.BasicKvStore.Companion.KEY_VERSION
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.ArgumentMatchers.anyInt
|
||||||
|
import org.mockito.ArgumentMatchers.anyString
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
|
||||||
|
class JsonKvStoreTest {
|
||||||
|
private val context = mock<Context>()
|
||||||
|
private val prefs = mock<SharedPreferences>()
|
||||||
|
private val editor = mock<SharedPreferences.Editor>()
|
||||||
|
|
||||||
|
private val gson = Gson()
|
||||||
|
private val testData = Person(16, "Bob", true, Pet("Poodle", 2))
|
||||||
|
private val expected = gson.toJson(testData)
|
||||||
|
|
||||||
|
private lateinit var store: JsonKvStore
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
whenever(context.getSharedPreferences(anyString(), anyInt())).thenReturn(prefs)
|
||||||
|
whenever(prefs.edit()).thenReturn(editor)
|
||||||
|
store = JsonKvStore(context, "name", gson)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun putJson() {
|
||||||
|
store.putJson("person", testData)
|
||||||
|
|
||||||
|
verify(prefs).edit()
|
||||||
|
verify(editor).putString("person", expected)
|
||||||
|
verify(editor).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException::class)
|
||||||
|
fun putJsonWithReservedKey() {
|
||||||
|
store.putJson(KEY_VERSION, testData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getJson() {
|
||||||
|
whenever(prefs.getString("key", null)).thenReturn(expected)
|
||||||
|
|
||||||
|
val result = store.getJson("key", Person::class.java)
|
||||||
|
|
||||||
|
Assert.assertEquals(testData, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getJsonInTheFuture() {
|
||||||
|
whenever(prefs.getString("key", null)).thenReturn(expected)
|
||||||
|
|
||||||
|
val resultOne: Person? = store.getJson("key")
|
||||||
|
Assert.assertEquals(testData, resultOne)
|
||||||
|
|
||||||
|
val resultTwo = store.getJson<Person?>("key")
|
||||||
|
Assert.assertEquals(testData, resultTwo)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getJsonHandlesMalformedJson() {
|
||||||
|
whenever(prefs.getString("key", null)).thenReturn("junk")
|
||||||
|
|
||||||
|
val result = store.getJson("key", Person::class.java)
|
||||||
|
|
||||||
|
Assert.assertNull(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Person(
|
||||||
|
val age: Int, val name: String, val hasPets: Boolean, val pet: Pet?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Pet(
|
||||||
|
val breed: String, val age: Int
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue