mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-30 22:34:02 +01:00 
			
		
		
		
	Convert media package to kotlin (#6369)
	
		
			
	
		
	
	
		
	
		
			Some checks are pending
		
		
	
	
		
			
				
	
				Android CI / Run tests and generate APK (push) Waiting to run
				
			
		
		
	
	
				
					
				
			
		
			Some checks are pending
		
		
	
	Android CI / Run tests and generate APK (push) Waiting to run
				
			* Convert Caption to kotlin * Convert CaptionListViewAdapter to kotlin * Convert CaptionListViewAdapter to kotlin * Removed unused class * Converted MwParseResult / MwParseResponse to kotlin * Convert CustomOkHttpNetworkFetcher to kotlin * Break up MediaDetailPagerFragment to make it easier to convert to kotlin * Convert MediaDetailProvider to kotlin * Convert the MediaDetailAdapter to kotlin * Convert MediaDetailPagerFragment to kotlin
This commit is contained in:
		
							parent
							
								
									79f52db929
								
							
						
					
					
						commit
						8fc7e1039b
					
				
					 33 changed files with 1030 additions and 1184 deletions
				
			
		|  | @ -1,43 +0,0 @@ | |||
| package fr.free.nrw.commons.media; | ||||
| 
 | ||||
| import com.google.gson.annotations.SerializedName; | ||||
| 
 | ||||
| /** | ||||
|  * Model class for parsing Captions when fetching captions using filename in MediaClient | ||||
|  */ | ||||
| public class Caption { | ||||
| 
 | ||||
|     /** | ||||
|      * users language in which caption is written | ||||
|      */ | ||||
|     @SerializedName("language") | ||||
|     private String language; | ||||
|     @SerializedName("value") | ||||
|     private String value; | ||||
| 
 | ||||
|     /** | ||||
|      * No args constructor for use in serialization | ||||
|      */ | ||||
|     public Caption() { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param value | ||||
|      * @param language | ||||
|      */ | ||||
|     public Caption(String language, String value) { | ||||
|         super(); | ||||
|         this.language = language; | ||||
|         this.value = value; | ||||
|     } | ||||
| 
 | ||||
|     @SerializedName("language") | ||||
|     public String getLanguage() { | ||||
|         return language; | ||||
|     } | ||||
| 
 | ||||
|     @SerializedName("value") | ||||
|     public String  getValue() { | ||||
|         return value; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										19
									
								
								app/src/main/java/fr/free/nrw/commons/media/Caption.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/src/main/java/fr/free/nrw/commons/media/Caption.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| package fr.free.nrw.commons.media | ||||
| 
 | ||||
| import com.google.gson.annotations.SerializedName | ||||
| 
 | ||||
| /** | ||||
|  * Model class for parsing Captions when fetching captions using filename in MediaClient | ||||
|  */ | ||||
| class Caption() { | ||||
|     @SerializedName("language") | ||||
|     var language: String? = null | ||||
| 
 | ||||
|     @SerializedName("value") | ||||
|     var value: String? = null | ||||
| 
 | ||||
|     constructor(language: String?, value: String?) : this() { | ||||
|         this.language = language | ||||
|         this.value = value | ||||
|     } | ||||
| } | ||||
|  | @ -1,67 +0,0 @@ | |||
| package fr.free.nrw.commons.media; | ||||
| 
 | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.BaseAdapter; | ||||
| import android.widget.TextView; | ||||
| import fr.free.nrw.commons.R; | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * Adapter for Caption Listview | ||||
|  */ | ||||
| public class CaptionListViewAdapter extends BaseAdapter { | ||||
| 
 | ||||
|   List<Caption> captions; | ||||
| 
 | ||||
|   public CaptionListViewAdapter(final List<Caption> captions) { | ||||
|     this.captions = captions; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * @return size of captions list | ||||
|    */ | ||||
|   @Override | ||||
|   public int getCount() { | ||||
|     return captions.size(); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * @return Object at position i | ||||
|    */ | ||||
|   @Override | ||||
|   public Object getItem(final int i) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * @return id for current item | ||||
|    */ | ||||
|   @Override | ||||
|   public long getItemId(final int i) { | ||||
|     return 0; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * inflate the view and bind data with UI | ||||
|    */ | ||||
|   @Override | ||||
|   public View getView(final int i, final View view, final ViewGroup viewGroup) { | ||||
|     final TextView captionLanguageTextView; | ||||
|     final TextView captionTextView; | ||||
|     final View captionLayout = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.caption_item, null); | ||||
|     captionLanguageTextView = captionLayout.findViewById(R.id.caption_language_textview); | ||||
|     captionTextView = captionLayout.findViewById(R.id.caption_text); | ||||
|     if (captions.size() == 1 && captions.get(0).getValue().equals("No Caption")) { | ||||
|       captionLanguageTextView.setText(captions.get(i).getLanguage()); | ||||
|       captionTextView.setText(captions.get(i).getValue()); | ||||
|     } else { | ||||
|       captionLanguageTextView.setText(captions.get(i).getLanguage() + ":"); | ||||
|       captionTextView.setText(captions.get(i).getValue()); | ||||
|     } | ||||
| 
 | ||||
|     return captionLayout; | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,34 @@ | |||
| package fr.free.nrw.commons.media | ||||
| 
 | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.BaseAdapter | ||||
| import android.widget.TextView | ||||
| import fr.free.nrw.commons.R | ||||
| 
 | ||||
| /** | ||||
|  * Adapter for Caption Listview | ||||
|  */ | ||||
| class CaptionListViewAdapter(var captions: List<Caption>) : BaseAdapter() { | ||||
|     override fun getCount(): Int = captions.size | ||||
| 
 | ||||
|     override fun getItem(i: Int): Any? = null | ||||
| 
 | ||||
|     override fun getItemId(i: Int): Long = 0 | ||||
| 
 | ||||
|     override fun getView(i: Int, view: View, viewGroup: ViewGroup): View { | ||||
|         val captionLayout = LayoutInflater.from(viewGroup.context).inflate(R.layout.caption_item, null) | ||||
|         val captionLanguageTextView = captionLayout.findViewById<TextView>(R.id.caption_language_textview) | ||||
|         val captionTextView = captionLayout.findViewById<TextView>(R.id.caption_text) | ||||
|         if (captions.size == 1 && captions[0].value == "No Caption") { | ||||
|             captionLanguageTextView.text = captions[i].language | ||||
|             captionTextView.text = captions[i].value | ||||
|         } else { | ||||
|             captionLanguageTextView.text = captions[i].language + ":" | ||||
|             captionTextView.text = captions[i].value | ||||
|         } | ||||
| 
 | ||||
|         return captionLayout | ||||
|     } | ||||
| } | ||||
|  | @ -1,76 +0,0 @@ | |||
| package fr.free.nrw.commons.media; | ||||
| 
 | ||||
| import com.google.gson.annotations.SerializedName; | ||||
| 
 | ||||
| import java.util.Map; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Represents the Wikibase item associated with a Wikimedia Commons file. | ||||
|  * For instance the Wikibase item M63996 represents the Commons file "Paul Cézanne - The Pigeon Tower at Bellevue - 1936.19 - Cleveland Museum of Art.jpg" | ||||
|  */ | ||||
| public class CommonsWikibaseItem { | ||||
| 
 | ||||
|     @SerializedName("type") | ||||
|     private String type; | ||||
|     @SerializedName("id") | ||||
|     private String id; | ||||
|     @SerializedName("labels") | ||||
|     private Map<String, Caption> labels; | ||||
|     @SerializedName("statements") | ||||
|     private Object statements = null; | ||||
| 
 | ||||
|     /** | ||||
|      * No args constructor for use in serialization | ||||
|      */ | ||||
|     public CommonsWikibaseItem() { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param id | ||||
|      * @param statements | ||||
|      * @param labels | ||||
|      * @param type | ||||
|      */ | ||||
|     public CommonsWikibaseItem(String type, String id, Map<String, Caption> labels, Object statements) { | ||||
|         super(); | ||||
|         this.type = type; | ||||
|         this.id = id; | ||||
|         this.labels = labels; | ||||
|         this.statements = statements; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Ex: "mediainfo | ||||
|      */ | ||||
|     @SerializedName("type") | ||||
|     public String getType() { | ||||
|         return type; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @return Wikibase Id | ||||
|      */ | ||||
|     @SerializedName("id") | ||||
|     public String getId() { | ||||
|         return id; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @return value of captions | ||||
|      */ | ||||
|     @SerializedName("labels") | ||||
|     public Map<String, Caption> getLabels() { | ||||
|         return labels; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Contains the Depicts item | ||||
|      */ | ||||
|     @SerializedName("statements") | ||||
|     public Object getStatements() { | ||||
|         return statements; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,236 +0,0 @@ | |||
| package fr.free.nrw.commons.media; | ||||
| 
 | ||||
| import android.net.Uri; | ||||
| import android.os.Looper; | ||||
| import android.os.SystemClock; | ||||
| import androidx.annotation.Nullable; | ||||
| import com.facebook.imagepipeline.common.BytesRange; | ||||
| import com.facebook.imagepipeline.image.EncodedImage; | ||||
| import com.facebook.imagepipeline.producers.BaseNetworkFetcher; | ||||
| import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks; | ||||
| import com.facebook.imagepipeline.producers.Consumer; | ||||
| import com.facebook.imagepipeline.producers.FetchState; | ||||
| import com.facebook.imagepipeline.producers.NetworkFetcher; | ||||
| import com.facebook.imagepipeline.producers.ProducerContext; | ||||
| import fr.free.nrw.commons.CommonsApplication; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import java.io.IOException; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| import java.util.concurrent.Executor; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import javax.inject.Singleton; | ||||
| import okhttp3.CacheControl; | ||||
| import okhttp3.Call; | ||||
| import okhttp3.OkHttpClient; | ||||
| import okhttp3.Request; | ||||
| import okhttp3.Response; | ||||
| import okhttp3.ResponseBody; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| // Custom implementation of Fresco's Network fetcher to skip downloading of images when limited connection mode is enabled | ||||
| // https://github.com/facebook/fresco/blob/master/imagepipeline-backends/imagepipeline-okhttp3/src/main/java/com/facebook/imagepipeline/backends/okhttp3/OkHttpNetworkFetcher.java | ||||
| @Singleton | ||||
| public class CustomOkHttpNetworkFetcher | ||||
|     extends BaseNetworkFetcher<CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState> { | ||||
| 
 | ||||
|     private static final String QUEUE_TIME = "queue_time"; | ||||
|     private static final String FETCH_TIME = "fetch_time"; | ||||
|     private static final String TOTAL_TIME = "total_time"; | ||||
|     private static final String IMAGE_SIZE = "image_size"; | ||||
|     private final Call.Factory mCallFactory; | ||||
|     private final @Nullable | ||||
|     CacheControl mCacheControl; | ||||
|     private final Executor mCancellationExecutor; | ||||
|     private final JsonKvStore defaultKvStore; | ||||
| 
 | ||||
|     /** | ||||
|      * @param okHttpClient client to use | ||||
|      */ | ||||
|     @Inject | ||||
|     public CustomOkHttpNetworkFetcher(final OkHttpClient okHttpClient, | ||||
|         @Named("default_preferences") final JsonKvStore defaultKvStore) { | ||||
|         this(okHttpClient, okHttpClient.dispatcher().executorService(), defaultKvStore); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param callFactory          custom {@link Call.Factory} for fetching image from the network | ||||
|      * @param cancellationExecutor executor on which fetching cancellation is performed if | ||||
|      *                             cancellation is requested from the UI Thread | ||||
|      */ | ||||
|     public CustomOkHttpNetworkFetcher(final Call.Factory callFactory, | ||||
|         final Executor cancellationExecutor, | ||||
|         final JsonKvStore defaultKvStore) { | ||||
|         this(callFactory, cancellationExecutor, defaultKvStore, true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param callFactory          custom {@link Call.Factory} for fetching image from the network | ||||
|      * @param cancellationExecutor executor on which fetching cancellation is performed if | ||||
|      *                             cancellation is requested from the UI Thread | ||||
|      * @param disableOkHttpCache   true if network requests should not be cached by OkHttp | ||||
|      */ | ||||
|     public CustomOkHttpNetworkFetcher( | ||||
|         final Call.Factory callFactory, final Executor cancellationExecutor, | ||||
|         final JsonKvStore defaultKvStore, | ||||
|         final boolean disableOkHttpCache) { | ||||
|         this.defaultKvStore = defaultKvStore; | ||||
|         mCallFactory = callFactory; | ||||
|         mCancellationExecutor = cancellationExecutor; | ||||
|         mCacheControl = disableOkHttpCache ? new CacheControl.Builder().noStore().build() : null; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public OkHttpNetworkFetchState createFetchState( | ||||
|         final Consumer<EncodedImage> consumer, final ProducerContext context) { | ||||
|         return new OkHttpNetworkFetchState(consumer, context); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void fetch( | ||||
|         final OkHttpNetworkFetchState fetchState, final NetworkFetcher.Callback callback) { | ||||
|         fetchState.submitTime = SystemClock.elapsedRealtime(); | ||||
|         final Uri uri = fetchState.getUri(); | ||||
| 
 | ||||
|         try { | ||||
|             if (defaultKvStore | ||||
|                 .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) { | ||||
|                 Timber.d("Skipping loading of image as limited connection mode is enabled"); | ||||
|                 callback.onFailure( | ||||
|                     new Exception("Failing image request as limited connection mode is enabled")); | ||||
|                 return; | ||||
|             } | ||||
|             final Request.Builder requestBuilder = new Request.Builder().url(uri.toString()).get(); | ||||
| 
 | ||||
|             if (mCacheControl != null) { | ||||
|                 requestBuilder.cacheControl(mCacheControl); | ||||
|             } | ||||
| 
 | ||||
|             final BytesRange bytesRange = fetchState.getContext().getImageRequest().getBytesRange(); | ||||
|             if (bytesRange != null) { | ||||
|                 requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue()); | ||||
|             } | ||||
| 
 | ||||
|             fetchWithRequest(fetchState, callback, requestBuilder.build()); | ||||
|         } catch (final Exception e) { | ||||
|             // handle error while creating the request | ||||
|             callback.onFailure(e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onFetchCompletion(final OkHttpNetworkFetchState fetchState, final int byteSize) { | ||||
|         fetchState.fetchCompleteTime = SystemClock.elapsedRealtime(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Map<String, String> getExtraMap(final OkHttpNetworkFetchState fetchState, | ||||
|         final int byteSize) { | ||||
|         final Map<String, String> extraMap = new HashMap<>(4); | ||||
|         extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime)); | ||||
|         extraMap | ||||
|             .put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime)); | ||||
|         extraMap | ||||
|             .put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime)); | ||||
|         extraMap.put(IMAGE_SIZE, Integer.toString(byteSize)); | ||||
|         return extraMap; | ||||
|     } | ||||
| 
 | ||||
|     protected void fetchWithRequest( | ||||
|         final OkHttpNetworkFetchState fetchState, | ||||
|         final NetworkFetcher.Callback callback, | ||||
|         final Request request) { | ||||
|         final Call call = mCallFactory.newCall(request); | ||||
| 
 | ||||
|         fetchState | ||||
|             .getContext() | ||||
|             .addCallbacks( | ||||
|                 new BaseProducerContextCallbacks() { | ||||
|                     @Override | ||||
|                     public void onCancellationRequested() { | ||||
|                         onFetchCancellationRequested(call); | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|         call.enqueue( | ||||
|             new okhttp3.Callback() { | ||||
|                 @Override | ||||
|                 public void onResponse(final Call call, final Response response) { | ||||
|                     onFetchResponse(fetchState, call, response, callback); | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onFailure(final Call call, final IOException e) { | ||||
|                     handleException(call, e, callback); | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     private void onFetchCancellationRequested(final Call call) { | ||||
|         if (Looper.myLooper() != Looper.getMainLooper()) { | ||||
|             call.cancel(); | ||||
|         } else { | ||||
|             mCancellationExecutor.execute(call::cancel); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void onFetchResponse(final OkHttpNetworkFetchState fetchState, final Call call, | ||||
|         final Response response, | ||||
|         final NetworkFetcher.Callback callback) { | ||||
|         fetchState.responseTime = SystemClock.elapsedRealtime(); | ||||
|         try (final ResponseBody body = response.body()) { | ||||
|             if (!response.isSuccessful()) { | ||||
|                 handleException( | ||||
|                     call, new IOException("Unexpected HTTP code " + response), | ||||
|                     callback); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             final BytesRange responseRange = | ||||
|                 BytesRange.fromContentRangeHeader(response.header("Content-Range")); | ||||
|             if (responseRange != null | ||||
|                 && !(responseRange.from == 0 | ||||
|                 && responseRange.to == BytesRange.TO_END_OF_CONTENT)) { | ||||
|                 // Only treat as a partial image if the range is not all of the content | ||||
|                 fetchState.setResponseBytesRange(responseRange); | ||||
|                 fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT); | ||||
|             } | ||||
| 
 | ||||
|             long contentLength = body.contentLength(); | ||||
|             if (contentLength < 0) { | ||||
|                 contentLength = 0; | ||||
|             } | ||||
|             callback.onResponse(body.byteStream(), (int) contentLength); | ||||
|         } catch (final Exception e) { | ||||
|             handleException(call, e, callback); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles exceptions. | ||||
|      * | ||||
|      * <p>OkHttp notifies callers of cancellations via an IOException. If IOException is caught | ||||
|      * after request cancellation, then the exception is interpreted as successful cancellation and | ||||
|      * onCancellation is called. Otherwise onFailure is called. | ||||
|      */ | ||||
|     private void handleException(final Call call, final Exception e, final Callback callback) { | ||||
|         if (call.isCanceled()) { | ||||
|             callback.onCancellation(); | ||||
|         } else { | ||||
|             callback.onFailure(e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class OkHttpNetworkFetchState extends FetchState { | ||||
| 
 | ||||
|         public long submitTime; | ||||
|         public long responseTime; | ||||
|         public long fetchCompleteTime; | ||||
| 
 | ||||
|         public OkHttpNetworkFetchState( | ||||
|             final Consumer<EncodedImage> consumer, final ProducerContext producerContext) { | ||||
|             super(consumer, producerContext); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,199 @@ | |||
| package fr.free.nrw.commons.media | ||||
| 
 | ||||
| import android.os.Looper | ||||
| import android.os.SystemClock | ||||
| import com.facebook.imagepipeline.common.BytesRange | ||||
| import com.facebook.imagepipeline.image.EncodedImage | ||||
| import com.facebook.imagepipeline.producers.BaseNetworkFetcher | ||||
| import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks | ||||
| import com.facebook.imagepipeline.producers.Consumer | ||||
| import com.facebook.imagepipeline.producers.FetchState | ||||
| import com.facebook.imagepipeline.producers.NetworkFetcher | ||||
| import com.facebook.imagepipeline.producers.ProducerContext | ||||
| import fr.free.nrw.commons.CommonsApplication | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore | ||||
| import okhttp3.CacheControl | ||||
| import okhttp3.Call | ||||
| import okhttp3.Callback | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.Request | ||||
| import okhttp3.Response | ||||
| import timber.log.Timber | ||||
| import java.io.IOException | ||||
| import java.util.concurrent.Executor | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Named | ||||
| import javax.inject.Singleton | ||||
| 
 | ||||
| // Custom implementation of Fresco's Network fetcher to skip downloading of images when limited connection mode is enabled | ||||
| // https://github.com/facebook/fresco/blob/master/imagepipeline-backends/imagepipeline-okhttp3/src/main/java/com/facebook/imagepipeline/backends/okhttp3/OkHttpNetworkFetcher.java | ||||
| @Singleton | ||||
| class CustomOkHttpNetworkFetcher | ||||
| @JvmOverloads constructor( | ||||
|     private val mCallFactory: Call.Factory, | ||||
|     private val mCancellationExecutor: Executor, | ||||
|     private val defaultKvStore: JsonKvStore, | ||||
|     disableOkHttpCache: Boolean = true | ||||
| ) : BaseNetworkFetcher<OkHttpNetworkFetchState>() { | ||||
| 
 | ||||
|     private val mCacheControl = | ||||
|         if (disableOkHttpCache) CacheControl.Builder().noStore().build() else null | ||||
|     private val isLimitedConnectionMode: Boolean | ||||
|         get() = defaultKvStore.getBoolean( | ||||
|             CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, | ||||
|             false | ||||
|         ) | ||||
| 
 | ||||
|     /** | ||||
|      * @param okHttpClient client to use | ||||
|      */ | ||||
|     @Inject | ||||
|     constructor( | ||||
|         okHttpClient: OkHttpClient, | ||||
|         @Named("default_preferences") defaultKvStore: JsonKvStore | ||||
|     ) : this(okHttpClient, okHttpClient.dispatcher.executorService, defaultKvStore) | ||||
| 
 | ||||
|     /** | ||||
|      * @param mCallFactory          custom [Call.Factory] for fetching image from the network | ||||
|      * @param mCancellationExecutor executor on which fetching cancellation is performed if | ||||
|      * cancellation is requested from the UI Thread | ||||
|      * @param disableOkHttpCache   true if network requests should not be cached by OkHttp | ||||
|      */ | ||||
|     override fun createFetchState(consumer: Consumer<EncodedImage>, context: ProducerContext) = | ||||
|         OkHttpNetworkFetchState(consumer, context) | ||||
| 
 | ||||
|     override fun fetch( | ||||
|         fetchState: OkHttpNetworkFetchState, callback: NetworkFetcher.Callback | ||||
|     ) { | ||||
|         fetchState.submitTime = SystemClock.elapsedRealtime() | ||||
| 
 | ||||
|         try { | ||||
|             if (isLimitedConnectionMode) { | ||||
|                 Timber.d("Skipping loading of image as limited connection mode is enabled") | ||||
|                 callback.onFailure(Exception("Failing image request as limited connection mode is enabled")) | ||||
|                 return | ||||
|             } | ||||
| 
 | ||||
|             val requestBuilder = Request.Builder().url(fetchState.uri.toString()).get() | ||||
| 
 | ||||
|             if (mCacheControl != null) { | ||||
|                 requestBuilder.cacheControl(mCacheControl) | ||||
|             } | ||||
| 
 | ||||
|             val bytesRange = fetchState.context.imageRequest.bytesRange | ||||
|             if (bytesRange != null) { | ||||
|                 requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue()) | ||||
|             } | ||||
| 
 | ||||
|             fetchWithRequest(fetchState, callback, requestBuilder.build()) | ||||
|         } catch (e: Exception) { | ||||
|             // handle error while creating the request | ||||
|             callback.onFailure(e) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onFetchCompletion(fetchState: OkHttpNetworkFetchState, byteSize: Int) { | ||||
|         fetchState.fetchCompleteTime = SystemClock.elapsedRealtime() | ||||
|     } | ||||
| 
 | ||||
|     override fun getExtraMap(fetchState: OkHttpNetworkFetchState, byteSize: Int) = | ||||
|         fetchState.toExtraMap(byteSize) | ||||
| 
 | ||||
|     private fun fetchWithRequest( | ||||
|         fetchState: OkHttpNetworkFetchState, callback: NetworkFetcher.Callback, request: Request | ||||
|     ) { | ||||
|         val call = mCallFactory.newCall(request) | ||||
| 
 | ||||
|         fetchState.context.addCallbacks(object : BaseProducerContextCallbacks() { | ||||
|             override fun onCancellationRequested() { | ||||
|                 onFetchCancellationRequested(call) | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         call.enqueue(object : Callback { | ||||
|             override fun onResponse(call: Call, response: Response) = | ||||
|                 onFetchResponse(fetchState, call, response, callback) | ||||
| 
 | ||||
|             override fun onFailure(call: Call, e: IOException) = | ||||
|                 handleException(call, e, callback) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private fun onFetchCancellationRequested(call: Call) { | ||||
|         if (Looper.myLooper() != Looper.getMainLooper()) { | ||||
|             call.cancel() | ||||
|         } else { | ||||
|             mCancellationExecutor.execute { call.cancel() } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun onFetchResponse( | ||||
|         fetchState: OkHttpNetworkFetchState, | ||||
|         call: Call, | ||||
|         response: Response, | ||||
|         callback: NetworkFetcher.Callback | ||||
|     ) { | ||||
|         fetchState.responseTime = SystemClock.elapsedRealtime() | ||||
|         try { | ||||
|             response.body.use { body -> | ||||
|                 if (!response.isSuccessful) { | ||||
|                     handleException(call, IOException("Unexpected HTTP code $response"), callback) | ||||
|                     return | ||||
|                 } | ||||
|                 val responseRange = | ||||
|                     BytesRange.fromContentRangeHeader(response.header("Content-Range")) | ||||
|                 if (responseRange != null && !(responseRange.from == 0 && responseRange.to == BytesRange.TO_END_OF_CONTENT)) { | ||||
|                     // Only treat as a partial image if the range is not all of the content | ||||
|                     fetchState.responseBytesRange = responseRange | ||||
|                     fetchState.onNewResultStatusFlags = Consumer.IS_PARTIAL_RESULT | ||||
|                 } | ||||
| 
 | ||||
|                 var contentLength = body!!.contentLength() | ||||
|                 if (contentLength < 0) { | ||||
|                     contentLength = 0 | ||||
|                 } | ||||
|                 callback.onResponse(body.byteStream(), contentLength.toInt()) | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             handleException(call, e, callback) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles exceptions. | ||||
|      * | ||||
|      * OkHttp notifies callers of cancellations via an IOException. If IOException is caught | ||||
|      * after request cancellation, then the exception is interpreted as successful cancellation and | ||||
|      * onCancellation is called. Otherwise onFailure is called. | ||||
|      */ | ||||
|     private fun handleException(call: Call, e: Exception, callback: NetworkFetcher.Callback) { | ||||
|         if (call.isCanceled()) { | ||||
|             callback.onCancellation() | ||||
|         } else { | ||||
|             callback.onFailure(e) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class OkHttpNetworkFetchState( | ||||
|     consumer: Consumer<EncodedImage>?, producerContext: ProducerContext? | ||||
| ) : FetchState(consumer, producerContext) { | ||||
|     var submitTime: Long = 0 | ||||
|     var responseTime: Long = 0 | ||||
|     var fetchCompleteTime: Long = 0 | ||||
| 
 | ||||
|     fun toExtraMap(byteSize: Int) = buildMap { | ||||
|         put(QUEUE_TIME, (responseTime - submitTime).toString()) | ||||
|         put(FETCH_TIME, (fetchCompleteTime - responseTime).toString()) | ||||
|         put(TOTAL_TIME, (fetchCompleteTime - submitTime).toString()) | ||||
|         put(IMAGE_SIZE, byteSize.toString()) | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private const val QUEUE_TIME = "queue_time" | ||||
|         private const val FETCH_TIME = "fetch_time" | ||||
|         private const val TOTAL_TIME = "total_time" | ||||
|         private const val IMAGE_SIZE = "image_size" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -0,0 +1,76 @@ | |||
| package fr.free.nrw.commons.media | ||||
| 
 | ||||
| import android.view.ViewGroup | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.FragmentManager | ||||
| import androidx.fragment.app.FragmentStatePagerAdapter | ||||
| import fr.free.nrw.commons.media.MediaDetailFragment.Companion.forMedia | ||||
| import timber.log.Timber | ||||
| 
 | ||||
| // FragmentStatePagerAdapter allows user to swipe across collection of images (no. of images undetermined) | ||||
| class MediaDetailAdapter( | ||||
|     val mediaDetailPagerFragment: MediaDetailPagerFragment, | ||||
|     fm: FragmentManager | ||||
| ) : FragmentStatePagerAdapter(fm) { | ||||
|     /** | ||||
|      * Keeps track of the current displayed fragment. | ||||
|      */ | ||||
|     private var currentFragment: Fragment? = null | ||||
| 
 | ||||
|     override fun getItem(i: Int): Fragment { | ||||
|         if (i == 0) { | ||||
|             // See bug https://code.google.com/p/android/issues/detail?id=27526 | ||||
|             if (mediaDetailPagerFragment.activity == null) { | ||||
|                 Timber.d("Skipping getItem. Returning as activity is destroyed!") | ||||
|                 return Fragment() | ||||
|             } | ||||
|             mediaDetailPagerFragment.binding!!.mediaDetailsPager.postDelayed( | ||||
|                 { mediaDetailPagerFragment.requireActivity().invalidateOptionsMenu() }, 5 | ||||
|             ) | ||||
|         } | ||||
|         return if (mediaDetailPagerFragment.isFromFeaturedRootFragment) { | ||||
|             forMedia( | ||||
|                 mediaDetailPagerFragment.position + i, | ||||
|                 mediaDetailPagerFragment.editable, mediaDetailPagerFragment.isFeaturedImage, | ||||
|                 mediaDetailPagerFragment.isWikipediaButtonDisplayed | ||||
|             ) | ||||
|         } else { | ||||
|             forMedia( | ||||
|                 i, mediaDetailPagerFragment.editable, | ||||
|                 mediaDetailPagerFragment.isFeaturedImage, | ||||
|                 mediaDetailPagerFragment.isWikipediaButtonDisplayed | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun getCount(): Int { | ||||
|         if (mediaDetailPagerFragment.activity == null) { | ||||
|             Timber.d("Skipping getCount. Returning as activity is destroyed!") | ||||
|             return 0 | ||||
|         } | ||||
|         return mediaDetailPagerFragment.mediaDetailProvider!!.getTotalMediaCount() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * If current fragment is of type MediaDetailFragment, return it, otherwise return null. | ||||
|      * | ||||
|      * @return MediaDetailFragment | ||||
|      */ | ||||
|     val currentMediaDetailFragment: MediaDetailFragment? | ||||
|         get() = currentFragment as? MediaDetailFragment | ||||
| 
 | ||||
|     /** | ||||
|      * Called to inform the adapter of which item is currently considered to be the "primary", that | ||||
|      * is the one show to the user as the current page. | ||||
|      */ | ||||
|     override fun setPrimaryItem( | ||||
|         container: ViewGroup, position: Int, | ||||
|         obj: Any | ||||
|     ) { | ||||
|         // Update the current fragment if changed | ||||
|         if (currentFragment !== obj) { | ||||
|             currentFragment = (obj as Fragment) | ||||
|         } | ||||
|         super.setPrimaryItem(container, position, obj) | ||||
|     } | ||||
| } | ||||
|  | @ -77,7 +77,6 @@ import fr.free.nrw.commons.CommonsApplication.Companion.instance | |||
| import fr.free.nrw.commons.Media | ||||
| import fr.free.nrw.commons.MediaDataExtractor | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.utils.UnderlineUtils | ||||
| import fr.free.nrw.commons.actions.ThanksClient | ||||
| import fr.free.nrw.commons.auth.SessionManager | ||||
| import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException | ||||
|  | @ -102,7 +101,6 @@ import fr.free.nrw.commons.kvstore.JsonKvStore | |||
| import fr.free.nrw.commons.language.AppLanguageLookUpTable | ||||
| import fr.free.nrw.commons.location.LocationServiceManager | ||||
| import fr.free.nrw.commons.locationpicker.LocationPicker | ||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider | ||||
| import fr.free.nrw.commons.profile.ProfileActivity | ||||
| import fr.free.nrw.commons.review.ReviewHelper | ||||
| import fr.free.nrw.commons.settings.Prefs | ||||
|  |  | |||
|  | @ -1,678 +0,0 @@ | |||
| package fr.free.nrw.commons.media; | ||||
| 
 | ||||
| import static fr.free.nrw.commons.utils.UrlUtilsKt.handleWebUrl; | ||||
| 
 | ||||
| import android.os.Handler; | ||||
| import android.os.Looper; | ||||
| import android.widget.ProgressBar; | ||||
| import android.content.ActivityNotFoundException; | ||||
| import android.content.Intent; | ||||
| import android.graphics.Bitmap; | ||||
| import android.graphics.BitmapFactory; | ||||
| import android.net.Uri; | ||||
| import android.os.Bundle; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuInflater; | ||||
| import android.view.MenuItem; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.Toast; | ||||
| import androidx.appcompat.app.ActionBar; | ||||
| import androidx.appcompat.app.AlertDialog; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.core.content.ContextCompat; | ||||
| import androidx.fragment.app.Fragment; | ||||
| import androidx.fragment.app.FragmentManager; | ||||
| import androidx.fragment.app.FragmentStatePagerAdapter; | ||||
| import androidx.viewpager.widget.ViewPager; | ||||
| import com.google.android.material.snackbar.Snackbar; | ||||
| import fr.free.nrw.commons.CommonsApplication; | ||||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.utils.ClipboardUtils; | ||||
| import fr.free.nrw.commons.auth.SessionManager; | ||||
| import fr.free.nrw.commons.bookmarks.models.Bookmark; | ||||
| import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider; | ||||
| import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao; | ||||
| import fr.free.nrw.commons.contributions.Contribution; | ||||
| import fr.free.nrw.commons.contributions.MainActivity; | ||||
| import fr.free.nrw.commons.databinding.FragmentMediaDetailPagerBinding; | ||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; | ||||
| import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; | ||||
| import fr.free.nrw.commons.profile.ProfileActivity; | ||||
| import fr.free.nrw.commons.utils.DownloadUtils; | ||||
| import fr.free.nrw.commons.utils.ImageUtils; | ||||
| import fr.free.nrw.commons.utils.NetworkUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
| import io.reactivex.Observable; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import java.net.URL; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Objects; | ||||
| import java.util.concurrent.Callable; | ||||
| import javax.inject.Inject; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener, MediaDetailFragment.Callback { | ||||
| 
 | ||||
|     @Inject BookmarkPicturesDao bookmarkDao; | ||||
| 
 | ||||
|     @Inject | ||||
|     protected OkHttpJsonApiClient okHttpJsonApiClient; | ||||
| 
 | ||||
|     @Inject | ||||
|     protected SessionManager sessionManager; | ||||
| 
 | ||||
|     private static CompositeDisposable compositeDisposable = new CompositeDisposable(); | ||||
| 
 | ||||
|     private FragmentMediaDetailPagerBinding binding; | ||||
| 
 | ||||
|     private boolean editable; | ||||
|     private boolean isFeaturedImage; | ||||
|     private boolean isWikipediaButtonDisplayed; | ||||
|     MediaDetailAdapter adapter; | ||||
|     private Bookmark bookmark; | ||||
|     private MediaDetailProvider provider; | ||||
|     private boolean isFromFeaturedRootFragment; | ||||
|     private int position; | ||||
| 
 | ||||
|     /** | ||||
|      * ProgressBar used to indicate the loading status of media items. | ||||
|      */ | ||||
|     private ProgressBar imageProgressBar; | ||||
| 
 | ||||
|     private ArrayList<Integer> removedItems=new ArrayList<Integer>(); | ||||
| 
 | ||||
|     public void clearRemoved(){ | ||||
|         removedItems.clear(); | ||||
|     } | ||||
|     public ArrayList<Integer> getRemovedItems() { | ||||
|         return removedItems; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Use this factory method to create a new instance of this fragment using the provided | ||||
|      * parameters. | ||||
|      * | ||||
|      * This method will create a new instance of MediaDetailPagerFragment and the arguments will be | ||||
|      * saved to a bundle which will be later available in the {@link #onCreate(Bundle)} | ||||
|      * @param editable | ||||
|      * @param isFeaturedImage | ||||
|      * @return | ||||
|      */ | ||||
|     public static MediaDetailPagerFragment newInstance(boolean editable, boolean isFeaturedImage) { | ||||
|         MediaDetailPagerFragment mediaDetailPagerFragment = new MediaDetailPagerFragment(); | ||||
|         Bundle args = new Bundle(); | ||||
|         args.putBoolean("is_editable", editable); | ||||
|         args.putBoolean("is_featured_image", isFeaturedImage); | ||||
|         mediaDetailPagerFragment.setArguments(args); | ||||
|         return mediaDetailPagerFragment; | ||||
|     } | ||||
| 
 | ||||
|     public MediaDetailPagerFragment() { | ||||
|         // Required empty public constructor | ||||
|     }; | ||||
| 
 | ||||
| 
 | ||||
|     @Override | ||||
|     public View onCreateView(LayoutInflater inflater, | ||||
|                              ViewGroup container, | ||||
|                              Bundle savedInstanceState) { | ||||
|         binding = FragmentMediaDetailPagerBinding.inflate(inflater, container, false); | ||||
|         binding.mediaDetailsPager.addOnPageChangeListener(this); | ||||
|         // Initialize the ProgressBar by finding it in the layout | ||||
|         imageProgressBar = binding.getRoot().findViewById(R.id.itemProgressBar); | ||||
|         adapter = new MediaDetailAdapter(getChildFragmentManager()); | ||||
| 
 | ||||
|         // ActionBar is now supported in both activities - if this crashes something is quite wrong | ||||
|         final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); | ||||
|         if (actionBar != null) { | ||||
|             actionBar.setDisplayHomeAsUpEnabled(true); | ||||
|         } | ||||
|         else { | ||||
|             throw new AssertionError("Action bar should not be null!"); | ||||
|         } | ||||
| 
 | ||||
|         // If fragment is associated with ProfileActivity, then hide the tabLayout | ||||
|         if (getActivity() instanceof ProfileActivity) { | ||||
|             ((ProfileActivity)getActivity()).setTabLayoutVisibility(false); | ||||
|         } | ||||
| 
 | ||||
|         // Else if fragment is associated with MainActivity then hide that tab layout | ||||
|         else if (getActivity() instanceof MainActivity) { | ||||
|             ((MainActivity)getActivity()).hideTabs(); | ||||
|         } | ||||
| 
 | ||||
|         binding.mediaDetailsPager.setAdapter(adapter); | ||||
| 
 | ||||
|         if (savedInstanceState != null) { | ||||
|             final int pageNumber = savedInstanceState.getInt("current-page"); | ||||
|             binding.mediaDetailsPager.setCurrentItem(pageNumber, false); | ||||
|             getActivity().invalidateOptionsMenu(); | ||||
|         } | ||||
|         adapter.notifyDataSetChanged(); | ||||
| 
 | ||||
|         return binding.getRoot(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onSaveInstanceState(Bundle outState) { | ||||
|         super.onSaveInstanceState(outState); | ||||
|         outState.putInt("current-page", binding.mediaDetailsPager.getCurrentItem()); | ||||
|         outState.putBoolean("editable", editable); | ||||
|         outState.putBoolean("isFeaturedImage", isFeaturedImage); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         if (savedInstanceState != null) { | ||||
|             editable = savedInstanceState.getBoolean("editable", false); | ||||
|             isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage", false); | ||||
| 
 | ||||
|         } | ||||
|         setHasOptionsMenu(true); | ||||
|         initProvider(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * initialise the provider, based on from where the fragment was started, as in from an activity | ||||
|      * or a fragment | ||||
|      */ | ||||
|     private void initProvider() { | ||||
|         if (getParentFragment() instanceof MediaDetailProvider) { | ||||
|             provider = (MediaDetailProvider) getParentFragment(); | ||||
|         } else if (getActivity() instanceof MediaDetailProvider) { | ||||
|             provider = (MediaDetailProvider) getActivity(); | ||||
|         } else { | ||||
|             throw new ClassCastException("Parent must implement MediaDetailProvider"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public MediaDetailProvider getMediaDetailProvider() { | ||||
|         return provider; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onOptionsItemSelected(MenuItem item) { | ||||
|         if (getActivity() == null) { | ||||
|             Timber.d("Returning as activity is destroyed!"); | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         Media m = provider.getMediaAtPosition(binding.mediaDetailsPager.getCurrentItem()); | ||||
|         MediaDetailFragment mediaDetailFragment = this.adapter.getCurrentMediaDetailFragment(); | ||||
|         switch (item.getItemId()) { | ||||
|             case R.id.menu_bookmark_current_image: | ||||
|                 boolean bookmarkExists = bookmarkDao.updateBookmark(bookmark); | ||||
|                 Snackbar snackbar = bookmarkExists ? Snackbar.make(getView(), R.string.add_bookmark, Snackbar.LENGTH_LONG) : Snackbar.make(getView(), R.string.remove_bookmark, Snackbar.LENGTH_LONG); | ||||
|                 snackbar.show(); | ||||
|                 updateBookmarkState(item); | ||||
|                 return true; | ||||
|             case R.id.menu_copy_link: | ||||
|                 String uri = m.getPageTitle().getCanonicalUri(); | ||||
|                 ClipboardUtils.copy("shareLink", uri, requireContext()); | ||||
|                 Timber.d("Copied share link to clipboard: %s", uri); | ||||
|                 Toast.makeText(requireContext(), getString(R.string.menu_link_copied), | ||||
|                     Toast.LENGTH_SHORT).show(); | ||||
|                 return true; | ||||
|             case R.id.menu_share_current_image: | ||||
|                 Intent shareIntent = new Intent(Intent.ACTION_SEND); | ||||
|                 shareIntent.setType("text/plain"); | ||||
|                 shareIntent.putExtra(Intent.EXTRA_TEXT, m.getDisplayTitle() + " \n" + m.getPageTitle().getCanonicalUri()); | ||||
|                 startActivity(Intent.createChooser(shareIntent, "Share image via...")); | ||||
| 
 | ||||
|                 //Add media detail to backstack when the share button is clicked | ||||
|                 //So that when the share is cancelled or completed the media detail page is on top | ||||
|                 // of back stack fixing:https://github.com/commons-app/apps-android-commons/issues/2296 | ||||
|                 FragmentManager supportFragmentManager = getActivity().getSupportFragmentManager(); | ||||
|                 if (supportFragmentManager.getBackStackEntryCount() < 2) { | ||||
|                     supportFragmentManager | ||||
|                         .beginTransaction() | ||||
|                         .addToBackStack(MediaDetailPagerFragment.class.getName()) | ||||
|                         .commit(); | ||||
|                     supportFragmentManager.executePendingTransactions(); | ||||
|                 } | ||||
|                 return true; | ||||
|             case R.id.menu_browser_current_image: | ||||
|                 // View in browser | ||||
|                 handleWebUrl(requireContext(), Uri.parse(m.getPageTitle().getMobileUri())); | ||||
|                 return true; | ||||
|             case R.id.menu_download_current_image: | ||||
|                 // Download | ||||
|                 if (!NetworkUtils.isInternetConnectionEstablished(getActivity())) { | ||||
|                     ViewUtil.showShortSnackbar(getView(), R.string.no_internet); | ||||
|                     return false; | ||||
|                 } | ||||
|                 DownloadUtils.downloadMedia(getActivity(), m); | ||||
|                 return true; | ||||
|             case R.id.menu_set_as_wallpaper: | ||||
|                 // Set wallpaper | ||||
|                 setWallpaper(m); | ||||
|                 return true; | ||||
|             case R.id.menu_set_as_avatar: | ||||
|                 // Set avatar | ||||
|                 setAvatar(m); | ||||
|                 return true; | ||||
|             case R.id.menu_view_user_page: | ||||
|                 if (m != null && m.getUser() != null) { | ||||
|                     ProfileActivity.startYourself(getActivity(), m.getUser(), | ||||
|                         !Objects.equals(sessionManager.getUserName(), m.getUser())); | ||||
|                 } | ||||
|                 return true; | ||||
|             case R.id.menu_view_report: | ||||
|                 showReportDialog(m); | ||||
|             case R.id.menu_view_set_white_background: | ||||
|                 if (mediaDetailFragment != null) { | ||||
|                     mediaDetailFragment.onImageBackgroundChanged(ContextCompat.getColor(getContext(), R.color.white)); | ||||
|                 } | ||||
|                 return true; | ||||
|             case R.id.menu_view_set_black_background: | ||||
|                 if (mediaDetailFragment != null) { | ||||
|                     mediaDetailFragment.onImageBackgroundChanged(ContextCompat.getColor(getContext(), R.color.black)); | ||||
|                 } | ||||
|                 return true; | ||||
|             default: | ||||
|                 return super.onOptionsItemSelected(item); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void showReportDialog(final Media media) { | ||||
|         if (media == null) { | ||||
|             return; | ||||
|         } | ||||
|         final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); | ||||
|         final String[] values = requireContext().getResources() | ||||
|             .getStringArray(R.array.report_violation_options); | ||||
|         builder.setTitle(R.string.report_violation); | ||||
|         builder.setItems(R.array.report_violation_options, (dialog, which) -> { | ||||
|             sendReportEmail(media, values[which]); | ||||
|         }); | ||||
|         builder.setNegativeButton(R.string.cancel, (dialog, which) -> {}); | ||||
|         builder.setCancelable(false); | ||||
|         builder.show(); | ||||
|     } | ||||
| 
 | ||||
|     private void sendReportEmail(final Media media, final String type) { | ||||
|         final String technicalInfo = getTechInfo(media, type); | ||||
| 
 | ||||
|         final Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO); | ||||
|         feedbackIntent.setType("message/rfc822"); | ||||
|         feedbackIntent.setData(Uri.parse("mailto:")); | ||||
|         feedbackIntent.putExtra(Intent.EXTRA_EMAIL, | ||||
|             new String[]{CommonsApplication.REPORT_EMAIL}); | ||||
|         feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, | ||||
|             CommonsApplication.REPORT_EMAIL_SUBJECT); | ||||
|         feedbackIntent.putExtra(Intent.EXTRA_TEXT, technicalInfo); | ||||
|         try { | ||||
|             startActivity(feedbackIntent); | ||||
|         } catch (final ActivityNotFoundException e) { | ||||
|             Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private String getTechInfo(final Media media, final String type) { | ||||
|         final StringBuilder builder = new StringBuilder(); | ||||
| 
 | ||||
|         builder.append("Report type: ") | ||||
|             .append(type) | ||||
|             .append("\n\n"); | ||||
| 
 | ||||
|         builder.append("Image that you want to report: ") | ||||
|             .append(media.getImageUrl()) | ||||
|             .append("\n\n"); | ||||
| 
 | ||||
|         builder.append("User that you want to report: ") | ||||
|             .append(media.getUser()) | ||||
|             .append("\n\n"); | ||||
| 
 | ||||
|         if (sessionManager.getUserName() != null) { | ||||
|             builder.append("Your username: ") | ||||
|                 .append(sessionManager.getUserName()) | ||||
|                 .append("\n\n"); | ||||
|         } | ||||
| 
 | ||||
|         builder.append("Violation reason: ") | ||||
|             .append("\n"); | ||||
| 
 | ||||
|         builder.append("----------------------------------------------") | ||||
|             .append("\n") | ||||
|             .append("(please write reason here)") | ||||
|             .append("\n") | ||||
|             .append("----------------------------------------------") | ||||
|             .append("\n\n") | ||||
|             .append("Thank you for your report! Our team will investigate as soon as possible.") | ||||
|             .append("\n") | ||||
|             .append("Please note that images also have a `Nominate for deletion` button."); | ||||
| 
 | ||||
|         return builder.toString(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the media as the device's wallpaper if the imageUrl is not null | ||||
|      * Fails silently if setting the wallpaper fails | ||||
|      * @param media | ||||
|      */ | ||||
|     private void setWallpaper(Media media) { | ||||
|         if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) { | ||||
|             Timber.d("Media URL not present"); | ||||
|             return; | ||||
|         } | ||||
|         ImageUtils.setWallpaperFromImageUrl(getActivity(), Uri.parse(media.getImageUrl())); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the media as user's leaderboard avatar | ||||
|      * @param media | ||||
|      */ | ||||
|     private void setAvatar(Media media) { | ||||
|         if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) { | ||||
|             Timber.d("Media URL not present"); | ||||
|             return; | ||||
|         } | ||||
|         ImageUtils.setAvatarFromImageUrl(getActivity(), media.getImageUrl(), | ||||
|             Objects.requireNonNull(sessionManager.getCurrentAccount()).name, | ||||
|             okHttpJsonApiClient, compositeDisposable); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | ||||
|         if (!editable) { // Disable menu options for editable views | ||||
|             menu.clear(); // see http://stackoverflow.com/a/8495697/17865 | ||||
|             inflater.inflate(R.menu.fragment_image_detail, menu); | ||||
|             if (binding.mediaDetailsPager != null) { | ||||
|                 MediaDetailProvider provider = getMediaDetailProvider(); | ||||
|                 if(provider == null) { | ||||
|                     return; | ||||
|                 } | ||||
|                 final int position; | ||||
|                 if (isFromFeaturedRootFragment) { | ||||
|                     position = this.position; | ||||
|                 } else { | ||||
|                     position = binding.mediaDetailsPager.getCurrentItem(); | ||||
|                 } | ||||
| 
 | ||||
|                 Media m = provider.getMediaAtPosition(position); | ||||
|                 if (m != null) { | ||||
|                     // Enable default set of actions, then re-enable different set of actions only if it is a failed contrib | ||||
|                     menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true); | ||||
|                     menu.findItem(R.id.menu_copy_link).setEnabled(true).setVisible(true); | ||||
|                     menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true); | ||||
|                     menu.findItem(R.id.menu_download_current_image).setEnabled(true).setVisible(true); | ||||
|                     menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true).setVisible(true); | ||||
|                     menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(true).setVisible(true); | ||||
|                     if (m.getUser() != null) { | ||||
|                         menu.findItem(R.id.menu_view_user_page).setEnabled(true).setVisible(true); | ||||
|                     } | ||||
| 
 | ||||
|                     try { | ||||
|                         URL mediaUrl = new URL(m.getImageUrl()); | ||||
|                         this.handleBackgroundColorMenuItems( | ||||
|                             () -> BitmapFactory.decodeStream(mediaUrl.openConnection().getInputStream()), | ||||
|                             menu | ||||
|                         ); | ||||
|                     } catch (Exception e) { | ||||
|                         Timber.e("Cant detect media transparency"); | ||||
|                     } | ||||
| 
 | ||||
|                     // Initialize bookmark object | ||||
|                     bookmark = new Bookmark( | ||||
|                             m.getFilename(), | ||||
|                             m.getAuthorOrUser(), | ||||
|                             BookmarkPicturesContentProvider.uriForName(m.getFilename()) | ||||
|                     ); | ||||
|                     updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image)); | ||||
|                     final Integer contributionState = provider.getContributionStateAt(position); | ||||
|                     if (contributionState != null) { | ||||
|                         switch (contributionState) { | ||||
|                             case Contribution.STATE_FAILED: | ||||
|                             case Contribution.STATE_IN_PROGRESS: | ||||
|                             case Contribution.STATE_QUEUED: | ||||
|                                 menu.findItem(R.id.menu_browser_current_image).setEnabled(false) | ||||
|                                         .setVisible(false); | ||||
|                                 menu.findItem(R.id.menu_copy_link).setEnabled(false) | ||||
|                                     .setVisible(false); | ||||
|                                 menu.findItem(R.id.menu_share_current_image).setEnabled(false) | ||||
|                                         .setVisible(false); | ||||
|                                 menu.findItem(R.id.menu_download_current_image).setEnabled(false) | ||||
|                                         .setVisible(false); | ||||
|                                 menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false) | ||||
|                                         .setVisible(false); | ||||
|                                 menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false) | ||||
|                                         .setVisible(false); | ||||
|                                 break; | ||||
|                             case Contribution.STATE_COMPLETED: | ||||
|                                 // Default set of menu items works fine. Treat same as regular media object | ||||
|                                 break; | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     menu.findItem(R.id.menu_browser_current_image).setEnabled(false) | ||||
|                             .setVisible(false); | ||||
|                     menu.findItem(R.id.menu_copy_link).setEnabled(false) | ||||
|                         .setVisible(false); | ||||
|                     menu.findItem(R.id.menu_share_current_image).setEnabled(false) | ||||
|                             .setVisible(false); | ||||
|                     menu.findItem(R.id.menu_download_current_image).setEnabled(false) | ||||
|                             .setVisible(false); | ||||
|                     menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false) | ||||
|                             .setVisible(false); | ||||
|                     menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false) | ||||
|                             .setVisible(false); | ||||
|                 } | ||||
| 
 | ||||
|                 if (!sessionManager.isUserLoggedIn()) { | ||||
|                     menu.findItem(R.id.menu_set_as_avatar).setVisible(false); | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Decide wether or not we should display the background color menu items | ||||
|      * We display them if the image is transparent | ||||
|      * @param getBitmap | ||||
|      * @param menu | ||||
|      */ | ||||
|     private void handleBackgroundColorMenuItems(Callable<Bitmap> getBitmap, Menu menu) { | ||||
|         Observable.fromCallable( | ||||
|                 getBitmap | ||||
|             ).subscribeOn(Schedulers.newThread()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(image -> { | ||||
|                 if (image.hasAlpha()) { | ||||
|                     menu.findItem(R.id.menu_view_set_white_background).setVisible(true).setEnabled(true); | ||||
|                     menu.findItem(R.id.menu_view_set_black_background).setVisible(true).setEnabled(true); | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     private void updateBookmarkState(MenuItem item) { | ||||
|         boolean isBookmarked = bookmarkDao.findBookmark(bookmark); | ||||
|         if(isBookmarked) { | ||||
|             if(removedItems.contains(binding.mediaDetailsPager.getCurrentItem())) { | ||||
|                 removedItems.remove(new Integer(binding.mediaDetailsPager.getCurrentItem())); | ||||
|             } | ||||
|         } | ||||
|         else { | ||||
|             if(!removedItems.contains(binding.mediaDetailsPager.getCurrentItem())) { | ||||
|                 removedItems.add(binding.mediaDetailsPager.getCurrentItem()); | ||||
|             } | ||||
|         } | ||||
|         int icon = isBookmarked ? R.drawable.menu_ic_round_star_filled_24px : R.drawable.menu_ic_round_star_border_24px; | ||||
|         item.setIcon(icon); | ||||
|     } | ||||
| 
 | ||||
|     public void showImage(int i, boolean isWikipediaButtonDisplayed) { | ||||
|         this.isWikipediaButtonDisplayed = isWikipediaButtonDisplayed; | ||||
|         setViewPagerCurrentItem(i); | ||||
|     } | ||||
| 
 | ||||
|     public void showImage(int i) { | ||||
|         setViewPagerCurrentItem(i); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This function waits for the item to load then sets the item to current item | ||||
|      * @param position current item that to be shown | ||||
|      */ | ||||
|     private void setViewPagerCurrentItem(int position) { | ||||
| 
 | ||||
|         final Handler handler = new Handler(Looper.getMainLooper()); | ||||
|         final Runnable runnable = new Runnable() { | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 // Show the ProgressBar while waiting for the item to load | ||||
|                 imageProgressBar.setVisibility(View.VISIBLE); | ||||
|                 // Check if the adapter has enough items loaded | ||||
|                 if(adapter.getCount() > position){ | ||||
|                     // Set the current item in the ViewPager | ||||
|                     binding.mediaDetailsPager.setCurrentItem(position, false); | ||||
|                     // Hide the ProgressBar once the item is loaded | ||||
|                     imageProgressBar.setVisibility(View.GONE); | ||||
|                 } else { | ||||
|                     // If the item is not ready yet, post the Runnable again | ||||
|                     handler.post(this); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|         // Start the Runnable | ||||
|         handler.post(runnable); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The method notify the viewpager that number of items have changed. | ||||
|      */ | ||||
|     public void notifyDataSetChanged(){ | ||||
|         if (null != adapter) { | ||||
|             adapter.notifyDataSetChanged(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onPageScrolled(int i, float v, int i2) { | ||||
|         if(getActivity() == null) { | ||||
|             Timber.d("Returning as activity is destroyed!"); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         getActivity().invalidateOptionsMenu(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onPageSelected(int i) { | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onPageScrollStateChanged(int i) { | ||||
|     } | ||||
| 
 | ||||
|     public void onDataSetChanged() { | ||||
|         if (null != adapter) { | ||||
|             adapter.notifyDataSetChanged(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Called after the media is nominated for deletion | ||||
|      * | ||||
|      * @param index item position that has been nominated | ||||
|      */ | ||||
|     @Override | ||||
|     public void nominatingForDeletion(int index) { | ||||
|       provider.refreshNominatedMedia(index); | ||||
|     } | ||||
| 
 | ||||
|     public interface MediaDetailProvider { | ||||
|         Media getMediaAtPosition(int i); | ||||
| 
 | ||||
|         int getTotalMediaCount(); | ||||
| 
 | ||||
|         Integer getContributionStateAt(int position); | ||||
| 
 | ||||
|         // Reload media detail fragment once media is nominated | ||||
|         void refreshNominatedMedia(int index); | ||||
|     } | ||||
| 
 | ||||
|     //FragmentStatePagerAdapter allows user to swipe across collection of images (no. of images undetermined) | ||||
|     private class MediaDetailAdapter extends FragmentStatePagerAdapter { | ||||
| 
 | ||||
|         /** | ||||
|          * Keeps track of the current displayed fragment. | ||||
|          */ | ||||
|         private Fragment mCurrentFragment; | ||||
| 
 | ||||
|         public MediaDetailAdapter(FragmentManager fm) { | ||||
|             super(fm); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public Fragment getItem(int i) { | ||||
|             if (i == 0) { | ||||
|                 // See bug https://code.google.com/p/android/issues/detail?id=27526 | ||||
|                 if(getActivity() == null) { | ||||
|                     Timber.d("Skipping getItem. Returning as activity is destroyed!"); | ||||
|                     return null; | ||||
|                 } | ||||
|                 binding.mediaDetailsPager.postDelayed(() -> getActivity().invalidateOptionsMenu(), 5); | ||||
|             } | ||||
|             if (isFromFeaturedRootFragment) { | ||||
|                 return MediaDetailFragment.forMedia(position+i, editable, isFeaturedImage, isWikipediaButtonDisplayed); | ||||
|             } else { | ||||
|                 return MediaDetailFragment.forMedia(i, editable, isFeaturedImage, isWikipediaButtonDisplayed); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public int getCount() { | ||||
|             if (getActivity() == null) { | ||||
|                 Timber.d("Skipping getCount. Returning as activity is destroyed!"); | ||||
|                 return 0; | ||||
|             } | ||||
|             return provider.getTotalMediaCount(); | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Get the currently displayed fragment. | ||||
|          * @return | ||||
|          */ | ||||
|         public Fragment getCurrentFragment() { | ||||
|             return mCurrentFragment; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * If current fragment is of type MediaDetailFragment, return it, otherwise return null. | ||||
|          * @return MediaDetailFragment | ||||
|          */ | ||||
|         public MediaDetailFragment getCurrentMediaDetailFragment() { | ||||
|             if (mCurrentFragment instanceof MediaDetailFragment) { | ||||
|                 return (MediaDetailFragment) mCurrentFragment; | ||||
|             } | ||||
| 
 | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Called to inform the adapter of which item is currently considered to be the "primary", | ||||
|          * that is the one show to the user as the current page. | ||||
|          * @param container | ||||
|          * @param position | ||||
|          * @param object | ||||
|          */ | ||||
|         @Override | ||||
|         public void setPrimaryItem(@NonNull final ViewGroup container, final int position, | ||||
|             @NonNull final Object object) { | ||||
|             // Update the current fragment if changed | ||||
|             if(getCurrentFragment() != object) { | ||||
|                 mCurrentFragment = ((Fragment)object); | ||||
|             } | ||||
|             super.setPrimaryItem(container, position, object); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,622 @@ | |||
| package fr.free.nrw.commons.media | ||||
| 
 | ||||
| import android.content.ActivityNotFoundException | ||||
| import android.content.DialogInterface | ||||
| import android.content.Intent | ||||
| import android.graphics.Bitmap | ||||
| import android.graphics.BitmapFactory | ||||
| import android.net.Uri | ||||
| import android.os.Bundle | ||||
| import android.os.Handler | ||||
| import android.os.Looper | ||||
| import android.view.LayoutInflater | ||||
| import android.view.Menu | ||||
| import android.view.MenuInflater | ||||
| import android.view.MenuItem | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.ProgressBar | ||||
| import android.widget.Toast | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.viewpager.widget.ViewPager.OnPageChangeListener | ||||
| import com.google.android.material.snackbar.Snackbar | ||||
| import fr.free.nrw.commons.CommonsApplication | ||||
| import fr.free.nrw.commons.Media | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.auth.SessionManager | ||||
| import fr.free.nrw.commons.bookmarks.models.Bookmark | ||||
| import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider | ||||
| import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao | ||||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| import fr.free.nrw.commons.contributions.MainActivity | ||||
| import fr.free.nrw.commons.databinding.FragmentMediaDetailPagerBinding | ||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment | ||||
| import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient | ||||
| import fr.free.nrw.commons.profile.ProfileActivity | ||||
| import fr.free.nrw.commons.profile.ProfileActivity.Companion.startYourself | ||||
| import fr.free.nrw.commons.utils.ClipboardUtils.copy | ||||
| import fr.free.nrw.commons.utils.DownloadUtils.downloadMedia | ||||
| import fr.free.nrw.commons.utils.ImageUtils.setAvatarFromImageUrl | ||||
| import fr.free.nrw.commons.utils.ImageUtils.setWallpaperFromImageUrl | ||||
| import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished | ||||
| import fr.free.nrw.commons.utils.ViewUtil.showShortSnackbar | ||||
| import fr.free.nrw.commons.utils.handleWebUrl | ||||
| import io.reactivex.Observable | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers | ||||
| import io.reactivex.disposables.CompositeDisposable | ||||
| import io.reactivex.functions.Consumer | ||||
| import io.reactivex.schedulers.Schedulers | ||||
| import timber.log.Timber | ||||
| import java.net.URL | ||||
| import java.util.concurrent.Callable | ||||
| import javax.inject.Inject | ||||
| import androidx.core.net.toUri | ||||
| 
 | ||||
| class MediaDetailPagerFragment : CommonsDaggerSupportFragment(), OnPageChangeListener, | ||||
|     MediaDetailFragment.Callback { | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var bookmarkDao: BookmarkPicturesDao? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var okHttpJsonApiClient: OkHttpJsonApiClient? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var sessionManager: SessionManager? = null | ||||
| 
 | ||||
|     var binding: FragmentMediaDetailPagerBinding? = null | ||||
|     var editable: Boolean = false | ||||
|     var isFeaturedImage: Boolean = false | ||||
|     var isWikipediaButtonDisplayed: Boolean = false | ||||
|     var adapter: MediaDetailAdapter? = null | ||||
|     var bookmark: Bookmark? = null | ||||
|     var mediaDetailProvider: MediaDetailProvider? = null | ||||
|     var isFromFeaturedRootFragment: Boolean = false | ||||
|     var position: Int = 0 | ||||
| 
 | ||||
|     /** | ||||
|      * ProgressBar used to indicate the loading status of media items. | ||||
|      */ | ||||
|     var imageProgressBar: ProgressBar? = null | ||||
| 
 | ||||
|     var removedItems: ArrayList<Int> = ArrayList() | ||||
| 
 | ||||
|     fun clearRemoved() = removedItems.clear() | ||||
| 
 | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, | ||||
|         container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View? { | ||||
|         binding = FragmentMediaDetailPagerBinding.inflate(inflater, container, false) | ||||
|         binding!!.mediaDetailsPager.addOnPageChangeListener(this) | ||||
|         // Initialize the ProgressBar by finding it in the layout | ||||
|         imageProgressBar = binding!!.root.findViewById(R.id.itemProgressBar) | ||||
|         adapter = MediaDetailAdapter(this, childFragmentManager) | ||||
| 
 | ||||
|         // ActionBar is now supported in both activities - if this crashes something is quite wrong | ||||
|         val actionBar = (activity as AppCompatActivity).supportActionBar | ||||
|         if (actionBar != null) { | ||||
|             actionBar.setDisplayHomeAsUpEnabled(true) | ||||
|         } else { | ||||
|             throw AssertionError("Action bar should not be null!") | ||||
|         } | ||||
| 
 | ||||
|         // If fragment is associated with ProfileActivity, then hide the tabLayout | ||||
|         if (activity is ProfileActivity) { | ||||
|             (activity as ProfileActivity).setTabLayoutVisibility(false) | ||||
|         } else if (activity is MainActivity) { | ||||
|             (activity as MainActivity).hideTabs() | ||||
|         } | ||||
| 
 | ||||
|         binding!!.mediaDetailsPager.adapter = adapter | ||||
| 
 | ||||
|         if (savedInstanceState != null) { | ||||
|             val pageNumber = savedInstanceState.getInt("current-page") | ||||
|             binding!!.mediaDetailsPager.setCurrentItem(pageNumber, false) | ||||
|             requireActivity().invalidateOptionsMenu() | ||||
|         } | ||||
|         adapter!!.notifyDataSetChanged() | ||||
| 
 | ||||
|         return binding!!.root | ||||
|     } | ||||
| 
 | ||||
|     override fun onSaveInstanceState(outState: Bundle) { | ||||
|         super.onSaveInstanceState(outState) | ||||
|         outState.putInt("current-page", binding!!.mediaDetailsPager.currentItem) | ||||
|         outState.putBoolean("editable", editable) | ||||
|         outState.putBoolean("isFeaturedImage", isFeaturedImage) | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         if (savedInstanceState != null) { | ||||
|             editable = savedInstanceState.getBoolean("editable", false) | ||||
|             isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage", false) | ||||
|         } | ||||
|         setHasOptionsMenu(true) | ||||
|         initProvider() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * initialise the provider, based on from where the fragment was started, as in from an activity | ||||
|      * or a fragment | ||||
|      */ | ||||
|     private fun initProvider() { | ||||
|         if (parentFragment is MediaDetailProvider) { | ||||
|             mediaDetailProvider = parentFragment as MediaDetailProvider | ||||
|         } else if (activity is MediaDetailProvider) { | ||||
|             mediaDetailProvider = activity as MediaDetailProvider? | ||||
|         } else { | ||||
|             throw ClassCastException("Parent must implement MediaDetailProvider") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         if (activity == null) { | ||||
|             Timber.d("Returning as activity is destroyed!") | ||||
|             return true | ||||
|         } | ||||
| 
 | ||||
|         val m = mediaDetailProvider!!.getMediaAtPosition(binding!!.mediaDetailsPager.currentItem) | ||||
|         val mediaDetailFragment = adapter!!.currentMediaDetailFragment | ||||
|         when (item.itemId) { | ||||
|             R.id.menu_bookmark_current_image -> { | ||||
|                 val bookmarkExists = bookmarkDao!!.updateBookmark(bookmark) | ||||
|                 val snackbar = if (bookmarkExists) Snackbar.make( | ||||
|                     requireView(), | ||||
|                     R.string.add_bookmark, | ||||
|                     Snackbar.LENGTH_LONG | ||||
|                 ) else Snackbar.make( | ||||
|                     requireView(), R.string.remove_bookmark, Snackbar.LENGTH_LONG | ||||
|                 ) | ||||
|                 snackbar.show() | ||||
|                 updateBookmarkState(item) | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             R.id.menu_copy_link -> { | ||||
|                 val uri = m!!.pageTitle.canonicalUri | ||||
|                 copy("shareLink", uri, requireContext()) | ||||
|                 Timber.d("Copied share link to clipboard: %s", uri) | ||||
|                 Toast.makeText( | ||||
|                     requireContext(), getString(R.string.menu_link_copied), | ||||
|                     Toast.LENGTH_SHORT | ||||
|                 ).show() | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             R.id.menu_share_current_image -> { | ||||
|                 val shareIntent = Intent(Intent.ACTION_SEND) | ||||
|                 shareIntent.setType("text/plain") | ||||
|                 shareIntent.putExtra( | ||||
|                     Intent.EXTRA_TEXT, """${m!!.displayTitle}  | ||||
| ${m.pageTitle.canonicalUri}""" | ||||
|                 ) | ||||
|                 startActivity(Intent.createChooser(shareIntent, "Share image via...")) | ||||
| 
 | ||||
|                 //Add media detail to backstack when the share button is clicked | ||||
|                 //So that when the share is cancelled or completed the media detail page is on top | ||||
|                 // of back stack fixing:https://github.com/commons-app/apps-android-commons/issues/2296 | ||||
|                 val supportFragmentManager = requireActivity().supportFragmentManager | ||||
|                 if (supportFragmentManager.backStackEntryCount < 2) { | ||||
|                     supportFragmentManager | ||||
|                         .beginTransaction() | ||||
|                         .addToBackStack(MediaDetailPagerFragment::class.java.name) | ||||
|                         .commit() | ||||
|                     supportFragmentManager.executePendingTransactions() | ||||
|                 } | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             R.id.menu_browser_current_image -> { | ||||
|                 // View in browser | ||||
|                 handleWebUrl(requireContext(), m!!.pageTitle.mobileUri.toUri()) | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             R.id.menu_download_current_image -> { | ||||
|                 // Download | ||||
|                 if (!isInternetConnectionEstablished(activity)) { | ||||
|                     showShortSnackbar(requireView(), R.string.no_internet) | ||||
|                     return false | ||||
|                 } | ||||
|                 downloadMedia(activity, m!!) | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             R.id.menu_set_as_wallpaper -> { | ||||
|                 // Set wallpaper | ||||
|                 setWallpaper(m!!) | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             R.id.menu_set_as_avatar -> { | ||||
|                 // Set avatar | ||||
|                 setAvatar(m!!) | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             R.id.menu_view_user_page -> { | ||||
|                 if (m?.user != null) { | ||||
|                     startYourself( | ||||
|                         requireActivity(), m.user!!, | ||||
|                         sessionManager!!.userName != m.user | ||||
|                     ) | ||||
|                 } | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             R.id.menu_view_report -> { | ||||
|                 showReportDialog(m) | ||||
|                 mediaDetailFragment?.onImageBackgroundChanged( | ||||
|                     ContextCompat.getColor( | ||||
|                         requireContext(), | ||||
|                         R.color.white | ||||
|                     ) | ||||
|                 ) | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             R.id.menu_view_set_white_background -> { | ||||
|                 mediaDetailFragment?.onImageBackgroundChanged( | ||||
|                     ContextCompat.getColor( | ||||
|                         requireContext(), | ||||
|                         R.color.white | ||||
|                     ) | ||||
|                 ) | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             R.id.menu_view_set_black_background -> { | ||||
|                 mediaDetailFragment?.onImageBackgroundChanged( | ||||
|                     ContextCompat.getColor( | ||||
|                         requireContext(), | ||||
|                         R.color.black | ||||
|                     ) | ||||
|                 ) | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             else -> return super.onOptionsItemSelected(item) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun showReportDialog(media: Media?) { | ||||
|         if (media == null) { | ||||
|             return | ||||
|         } | ||||
|         val builder = AlertDialog.Builder(requireActivity()) | ||||
|         val values = requireContext().resources | ||||
|             .getStringArray(R.array.report_violation_options) | ||||
|         builder.setTitle(R.string.report_violation) | ||||
|         builder.setItems( | ||||
|             R.array.report_violation_options | ||||
|         ) { dialog: DialogInterface?, which: Int -> | ||||
|             sendReportEmail(media, values[which]) | ||||
|         } | ||||
|         builder.setNegativeButton( | ||||
|             R.string.cancel | ||||
|         ) { dialog: DialogInterface?, which: Int -> } | ||||
|         builder.setCancelable(false) | ||||
|         builder.show() | ||||
|     } | ||||
| 
 | ||||
|     private fun sendReportEmail(media: Media, type: String) { | ||||
|         val technicalInfo = getTechInfo(media, type) | ||||
| 
 | ||||
|         val feedbackIntent = Intent(Intent.ACTION_SENDTO) | ||||
|         feedbackIntent.setType("message/rfc822") | ||||
|         feedbackIntent.setData(Uri.parse("mailto:")) | ||||
|         feedbackIntent.putExtra( | ||||
|             Intent.EXTRA_EMAIL, | ||||
|             arrayOf(CommonsApplication.REPORT_EMAIL) | ||||
|         ) | ||||
|         feedbackIntent.putExtra( | ||||
|             Intent.EXTRA_SUBJECT, | ||||
|             CommonsApplication.REPORT_EMAIL_SUBJECT | ||||
|         ) | ||||
|         feedbackIntent.putExtra(Intent.EXTRA_TEXT, technicalInfo) | ||||
|         try { | ||||
|             startActivity(feedbackIntent) | ||||
|         } catch (e: ActivityNotFoundException) { | ||||
|             Toast.makeText(activity, R.string.no_email_client, Toast.LENGTH_SHORT).show() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun getTechInfo(media: Media, type: String): String { | ||||
|         val builder = StringBuilder() | ||||
| 
 | ||||
|         builder.append("Report type: ") | ||||
|             .append(type) | ||||
|             .append("\n\n") | ||||
| 
 | ||||
|         builder.append("Image that you want to report: ") | ||||
|             .append(media.imageUrl) | ||||
|             .append("\n\n") | ||||
| 
 | ||||
|         builder.append("User that you want to report: ") | ||||
|             .append(media.user) | ||||
|             .append("\n\n") | ||||
| 
 | ||||
|         if (sessionManager!!.userName != null) { | ||||
|             builder.append("Your username: ") | ||||
|                 .append(sessionManager!!.userName) | ||||
|                 .append("\n\n") | ||||
|         } | ||||
| 
 | ||||
|         builder.append("Violation reason: ") | ||||
|             .append("\n") | ||||
| 
 | ||||
|         builder.append("----------------------------------------------") | ||||
|             .append("\n") | ||||
|             .append("(please write reason here)") | ||||
|             .append("\n") | ||||
|             .append("----------------------------------------------") | ||||
|             .append("\n\n") | ||||
|             .append("Thank you for your report! Our team will investigate as soon as possible.") | ||||
|             .append("\n") | ||||
|             .append("Please note that images also have a `Nominate for deletion` button.") | ||||
| 
 | ||||
|         return builder.toString() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the media as the device's wallpaper if the imageUrl is not null | ||||
|      * Fails silently if setting the wallpaper fails | ||||
|      * @param media | ||||
|      */ | ||||
|     private fun setWallpaper(media: Media) { | ||||
|         if (media.imageUrl == null || media.imageUrl!!.isEmpty()) { | ||||
|             Timber.d("Media URL not present") | ||||
|             return | ||||
|         } | ||||
|         setWallpaperFromImageUrl(requireActivity(), media.imageUrl!!.toUri()) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the media as user's leaderboard avatar | ||||
|      * @param media | ||||
|      */ | ||||
|     private fun setAvatar(media: Media) { | ||||
|         if (media.imageUrl == null || media.imageUrl!!.isEmpty()) { | ||||
|             Timber.d("Media URL not present") | ||||
|             return | ||||
|         } | ||||
|         setAvatarFromImageUrl( | ||||
|             requireActivity(), media.imageUrl!!, | ||||
|             sessionManager!!.currentAccount!!.name, | ||||
|             okHttpJsonApiClient!!, Companion.compositeDisposable | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { | ||||
|         if (!editable) { // Disable menu options for editable views | ||||
|             menu.clear() // see http://stackoverflow.com/a/8495697/17865 | ||||
|             inflater.inflate(R.menu.fragment_image_detail, menu) | ||||
|             if (binding!!.mediaDetailsPager != null) { | ||||
|                 val provider = mediaDetailProvider ?: return | ||||
|                 val position = if (isFromFeaturedRootFragment) { | ||||
|                     position | ||||
|                 } else { | ||||
|                     binding!!.mediaDetailsPager.currentItem | ||||
|                 } | ||||
| 
 | ||||
|                 val m = provider.getMediaAtPosition(position) | ||||
|                 if (m != null) { | ||||
|                     // Enable default set of actions, then re-enable different set of actions only if it is a failed contrib | ||||
|                     menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true) | ||||
|                     menu.findItem(R.id.menu_copy_link).setEnabled(true).setVisible(true) | ||||
|                     menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true) | ||||
|                     menu.findItem(R.id.menu_download_current_image).setEnabled(true) | ||||
|                         .setVisible(true) | ||||
|                     menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true) | ||||
|                         .setVisible(true) | ||||
|                     menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(true).setVisible(true) | ||||
|                     if (m.user != null) { | ||||
|                         menu.findItem(R.id.menu_view_user_page).setEnabled(true).setVisible(true) | ||||
|                     } | ||||
| 
 | ||||
|                     try { | ||||
|                         val mediaUrl = URL(m.imageUrl) | ||||
|                         handleBackgroundColorMenuItems({ | ||||
|                             BitmapFactory.decodeStream( | ||||
|                                 mediaUrl.openConnection().getInputStream() | ||||
|                             ) | ||||
|                         }, menu) | ||||
|                     } catch (e: Exception) { | ||||
|                         Timber.e("Cant detect media transparency") | ||||
|                     } | ||||
| 
 | ||||
|                     // Initialize bookmark object | ||||
|                     bookmark = Bookmark( | ||||
|                         m.filename, | ||||
|                         m.getAuthorOrUser(), | ||||
|                         BookmarkPicturesContentProvider.uriForName(m.filename) | ||||
|                     ) | ||||
|                     updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image)) | ||||
|                     val contributionState = provider.getContributionStateAt(position) | ||||
|                     if (contributionState != null) { | ||||
|                         when (contributionState) { | ||||
|                             Contribution.STATE_FAILED, Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED -> { | ||||
|                                 menu.findItem(R.id.menu_browser_current_image).setEnabled(false) | ||||
|                                     .setVisible(false) | ||||
|                                 menu.findItem(R.id.menu_copy_link).setEnabled(false) | ||||
|                                     .setVisible(false) | ||||
|                                 menu.findItem(R.id.menu_share_current_image).setEnabled(false) | ||||
|                                     .setVisible(false) | ||||
|                                 menu.findItem(R.id.menu_download_current_image).setEnabled(false) | ||||
|                                     .setVisible(false) | ||||
|                                 menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false) | ||||
|                                     .setVisible(false) | ||||
|                                 menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false) | ||||
|                                     .setVisible(false) | ||||
|                             } | ||||
| 
 | ||||
|                             Contribution.STATE_COMPLETED -> {} | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     menu.findItem(R.id.menu_browser_current_image).setEnabled(false) | ||||
|                         .setVisible(false) | ||||
|                     menu.findItem(R.id.menu_copy_link).setEnabled(false) | ||||
|                         .setVisible(false) | ||||
|                     menu.findItem(R.id.menu_share_current_image).setEnabled(false) | ||||
|                         .setVisible(false) | ||||
|                     menu.findItem(R.id.menu_download_current_image).setEnabled(false) | ||||
|                         .setVisible(false) | ||||
|                     menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false) | ||||
|                         .setVisible(false) | ||||
|                     menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false) | ||||
|                         .setVisible(false) | ||||
|                 } | ||||
| 
 | ||||
|                 if (!sessionManager!!.isUserLoggedIn) { | ||||
|                     menu.findItem(R.id.menu_set_as_avatar).setVisible(false) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Decide wether or not we should display the background color menu items | ||||
|      * We display them if the image is transparent | ||||
|      * @param getBitmap | ||||
|      * @param menu | ||||
|      */ | ||||
|     private fun handleBackgroundColorMenuItems(getBitmap: Callable<Bitmap>, menu: Menu) { | ||||
|         Observable.fromCallable( | ||||
|             getBitmap | ||||
|         ).subscribeOn(Schedulers.newThread()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(Consumer { image: Bitmap -> | ||||
|                 if (image.hasAlpha()) { | ||||
|                     menu.findItem(R.id.menu_view_set_white_background).setVisible(true) | ||||
|                         .setEnabled(true) | ||||
|                     menu.findItem(R.id.menu_view_set_black_background).setVisible(true) | ||||
|                         .setEnabled(true) | ||||
|                 } | ||||
|             }) | ||||
|     } | ||||
| 
 | ||||
|     private fun updateBookmarkState(item: MenuItem) { | ||||
|         val isBookmarked = bookmarkDao!!.findBookmark(bookmark) | ||||
|         if (isBookmarked) { | ||||
|             if (removedItems.contains(binding!!.mediaDetailsPager.currentItem)) { | ||||
|                 removedItems.remove(binding!!.mediaDetailsPager.currentItem) | ||||
|             } | ||||
|         } else { | ||||
|             if (!removedItems.contains(binding!!.mediaDetailsPager.currentItem)) { | ||||
|                 removedItems.add(binding!!.mediaDetailsPager.currentItem) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         item.setIcon(if (isBookmarked) { | ||||
|             R.drawable.menu_ic_round_star_filled_24px | ||||
|         } else { | ||||
|             R.drawable.menu_ic_round_star_border_24px | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     fun showImage(i: Int, isWikipediaButtonDisplayed: Boolean) { | ||||
|         this.isWikipediaButtonDisplayed = isWikipediaButtonDisplayed | ||||
|         setViewPagerCurrentItem(i) | ||||
|     } | ||||
| 
 | ||||
|     fun showImage(i: Int) { | ||||
|         setViewPagerCurrentItem(i) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This function waits for the item to load then sets the item to current item | ||||
|      * @param position current item that to be shown | ||||
|      */ | ||||
|     private fun setViewPagerCurrentItem(position: Int) { | ||||
|         val handler = Handler(Looper.getMainLooper()) | ||||
|         val runnable: Runnable = object : Runnable { | ||||
|             override fun run() { | ||||
|                 // Show the ProgressBar while waiting for the item to load | ||||
|                 imageProgressBar!!.visibility = View.VISIBLE | ||||
|                 // Check if the adapter has enough items loaded | ||||
|                 if (adapter!!.count > position) { | ||||
|                     // Set the current item in the ViewPager | ||||
|                     binding!!.mediaDetailsPager.setCurrentItem(position, false) | ||||
|                     // Hide the ProgressBar once the item is loaded | ||||
|                     imageProgressBar!!.visibility = View.GONE | ||||
|                 } else { | ||||
|                     // If the item is not ready yet, post the Runnable again | ||||
|                     handler.post(this) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         // Start the Runnable | ||||
|         handler.post(runnable) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The method notify the viewpager that number of items have changed. | ||||
|      */ | ||||
|     fun notifyDataSetChanged() { | ||||
|         if (null != adapter) { | ||||
|             adapter!!.notifyDataSetChanged() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onPageScrolled(i: Int, v: Float, i2: Int) { | ||||
|         if (activity == null) { | ||||
|             Timber.d("Returning as activity is destroyed!") | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         requireActivity().invalidateOptionsMenu() | ||||
|     } | ||||
| 
 | ||||
|     override fun onPageSelected(i: Int) { | ||||
|     } | ||||
| 
 | ||||
|     override fun onPageScrollStateChanged(i: Int) { | ||||
|     } | ||||
| 
 | ||||
|     fun onDataSetChanged() { | ||||
|         if (null != adapter) { | ||||
|             adapter!!.notifyDataSetChanged() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Called after the media is nominated for deletion | ||||
|      * | ||||
|      * @param index item position that has been nominated | ||||
|      */ | ||||
|     override fun nominatingForDeletion(index: Int) { | ||||
|         mediaDetailProvider!!.refreshNominatedMedia(index) | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private val compositeDisposable = CompositeDisposable() | ||||
| 
 | ||||
|         /** | ||||
|          * Use this factory method to create a new instance of this fragment using the provided | ||||
|          * parameters. | ||||
|          * | ||||
|          * This method will create a new instance of MediaDetailPagerFragment and the arguments will be | ||||
|          * saved to a bundle which will be later available in the [.onCreate] | ||||
|          * @param editable | ||||
|          * @param isFeaturedImage | ||||
|          * @return | ||||
|          */ | ||||
|         @JvmStatic | ||||
|         fun newInstance(editable: Boolean, isFeaturedImage: Boolean): MediaDetailPagerFragment { | ||||
|             val mediaDetailPagerFragment = MediaDetailPagerFragment() | ||||
|             val args = Bundle() | ||||
|             args.putBoolean("is_editable", editable) | ||||
|             args.putBoolean("is_featured_image", isFeaturedImage) | ||||
|             mediaDetailPagerFragment.arguments = args | ||||
|             return mediaDetailPagerFragment | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,14 @@ | |||
| package fr.free.nrw.commons.media | ||||
| 
 | ||||
| import fr.free.nrw.commons.Media | ||||
| 
 | ||||
| interface MediaDetailProvider { | ||||
|     fun getMediaAtPosition(i: Int): Media? | ||||
| 
 | ||||
|     fun getTotalMediaCount(): Int | ||||
| 
 | ||||
|     fun getContributionStateAt(position: Int): Int? | ||||
| 
 | ||||
|     // Reload media detail fragment once media is nominated | ||||
|     fun refreshNominatedMedia(index: Int) | ||||
| } | ||||
|  | @ -1,25 +0,0 @@ | |||
| package fr.free.nrw.commons.media; | ||||
| 
 | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.VisibleForTesting; | ||||
| 
 | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwResponse; | ||||
| 
 | ||||
| public class MwParseResponse extends MwResponse { | ||||
|     @Nullable | ||||
|     private MwParseResult parse; | ||||
| 
 | ||||
|     @Nullable | ||||
|     public MwParseResult parse() { | ||||
|         return parse; | ||||
|     } | ||||
| 
 | ||||
|     public boolean success() { | ||||
|         return parse != null; | ||||
|     } | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     protected void setParse(@Nullable MwParseResult parse) { | ||||
|         this.parse = parse; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,17 @@ | |||
| package fr.free.nrw.commons.media | ||||
| 
 | ||||
| import androidx.annotation.VisibleForTesting | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwResponse | ||||
| 
 | ||||
| class MwParseResponse : MwResponse() { | ||||
|     private var parse: MwParseResult? = null | ||||
| 
 | ||||
|     fun parse(): MwParseResult? = parse | ||||
| 
 | ||||
|     fun success(): Boolean = parse != null | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     protected fun setParse(parse: MwParseResult?) { | ||||
|         this.parse = parse | ||||
|     } | ||||
| } | ||||
|  | @ -1,18 +0,0 @@ | |||
| package fr.free.nrw.commons.media; | ||||
| 
 | ||||
| import com.google.gson.annotations.SerializedName; | ||||
| 
 | ||||
| public class MwParseResult { | ||||
|     @SuppressWarnings("unused") private int pageid; | ||||
|     @SuppressWarnings("unused") private int index; | ||||
|     private MwParseText text; | ||||
| 
 | ||||
|     public String text() { | ||||
|         return text.text; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public class MwParseText{ | ||||
|         @SerializedName("*") private String text; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										18
									
								
								app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| package fr.free.nrw.commons.media | ||||
| 
 | ||||
| import com.google.gson.annotations.SerializedName | ||||
| 
 | ||||
| class MwParseResult { | ||||
|     private val pageid = 0 | ||||
|     private val index = 0 | ||||
|     private val text: MwParseText? = null | ||||
| 
 | ||||
|     fun text(): String? { | ||||
|         return text?.text | ||||
|     } | ||||
| 
 | ||||
|     inner class MwParseText { | ||||
|         @SerializedName("*") | ||||
|         internal val text: String? = null | ||||
|     } | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Paul Hawke
						Paul Hawke