mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 14:53:59 +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
	
	 Neel Doshi
						Neel Doshi