From 015c5d5c63474860775ad78c1980c573dcf890d6 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Fri, 6 Dec 2024 21:20:06 -0600 Subject: [PATCH] Convert wikidata/mwapi to kotlin (part 3) (#6004) * Convert Edit to kotlin along with deleting unused class * Converted ExtMetadata to kotlin * Convert ImageInfo to kotlin * Removed unused class * Convert Notification to kotlin * Convert PageProperties to kotlin * Convert PageTitle to kotlin * Convert Namespace to kotlin --- .../commons/explore/media/MediaConverter.kt | 17 +- .../fr/free/nrw/commons/location/LatLng.kt | 7 + .../notification/NotificationClient.kt | 4 +- .../nrw/commons/wikidata/model/edit/Edit.java | 36 -- .../nrw/commons/wikidata/model/edit/Edit.kt | 25 ++ .../wikidata/model/edit/EditResult.java | 31 -- .../wikidata/model/gallery/ExtMetadata.java | 102 ------ .../wikidata/model/gallery/ExtMetadata.kt | 61 ++++ .../wikidata/model/gallery/ImageInfo.java | 121 ------- .../wikidata/model/gallery/ImageInfo.kt | 129 +++++++ .../wikidata/model/gallery/VideoInfo.java | 16 - .../model/notifications/Notification.java | 190 ---------- .../model/notifications/Notification.kt | 124 +++++++ .../wikidata/model/page/GeoMarshaller.java | 28 -- .../wikidata/model/page/GeoUnmarshaller.java | 39 -- .../page/{Namespace.java => Namespace.kt} | 76 ++-- .../wikidata/model/page/PageProperties.java | 156 -------- .../wikidata/model/page/PageProperties.kt | 156 ++++++++ .../wikidata/model/page/PageTitle.java | 339 ------------------ .../commons/wikidata/model/page/PageTitle.kt | 284 +++++++++++++++ .../explore/media/MediaConverterTest.kt | 20 +- .../notification/NotificationClientTest.kt | 4 +- 22 files changed, 832 insertions(+), 1133 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/edit/Edit.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/edit/Edit.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/edit/EditResult.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/VideoInfo.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/page/GeoMarshaller.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/page/GeoUnmarshaller.java rename app/src/main/java/fr/free/nrw/commons/wikidata/model/page/{Namespace.java => Namespace.kt} (56%) delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageProperties.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageProperties.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageTitle.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageTitle.kt diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt index a3103d41a..de084ba50 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt @@ -11,7 +11,6 @@ import fr.free.nrw.commons.wikidata.model.Entities import fr.free.nrw.commons.wikidata.model.gallery.ExtMetadata import fr.free.nrw.commons.wikidata.model.gallery.ImageInfo import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage -import org.apache.commons.lang3.StringUtils import java.text.ParseException import java.util.Date import javax.inject.Inject @@ -24,7 +23,7 @@ class MediaConverter entity: Entities.Entity, imageInfo: ImageInfo, ): Media { - val metadata = imageInfo.metadata + val metadata = imageInfo.getMetadata() requireNotNull(metadata) { "No metadata" } // Stores mapping of title attribute to hidden attribute of each category val myMap = mutableMapOf() @@ -32,8 +31,8 @@ class MediaConverter return Media( page.pageId().toString(), - imageInfo.thumbUrl.takeIf { it.isNotBlank() } ?: imageInfo.originalUrl, - imageInfo.originalUrl, + imageInfo.getThumbUrl().takeIf { it.isNotBlank() } ?: imageInfo.getOriginalUrl(), + imageInfo.getOriginalUrl(), page.title(), metadata.imageDescription(), safeParseDate(metadata.dateTime()), @@ -41,7 +40,7 @@ class MediaConverter metadata.prefixedLicenseUrl, getAuthor(metadata), getAuthor(metadata), - MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories), + MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories()), metadata.latLng, entity.labels().mapValues { it.value.value() }, entity.descriptions().mapValues { it.value.value() }, @@ -104,9 +103,5 @@ private val ExtMetadata.prefixedLicenseUrl: String } private val ExtMetadata.latLng: LatLng? - get() = - if (!StringUtils.isBlank(gpsLatitude) && !StringUtils.isBlank(gpsLongitude)) { - LatLng(gpsLatitude.toDouble(), gpsLongitude.toDouble(), 0.0f) - } else { - null - } + get() = LatLng.latLongOrNull(gpsLatitude(), gpsLongitude()) + diff --git a/app/src/main/java/fr/free/nrw/commons/location/LatLng.kt b/app/src/main/java/fr/free/nrw/commons/location/LatLng.kt index 4e21b93c2..7dd9a49ce 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LatLng.kt +++ b/app/src/main/java/fr/free/nrw/commons/location/LatLng.kt @@ -41,6 +41,13 @@ data class LatLng( * Accepts a non-null [Location] and converts it to a [LatLng]. */ companion object { + fun latLongOrNull(latitude: String?, longitude: String?): LatLng? = + if (!latitude.isNullOrBlank() && !longitude.isNullOrBlank()) { + LatLng(latitude.toDouble(), longitude.toDouble(), 0.0f) + } else { + null + } + /** * gets the latitude and longitude of a given non-null location * @param location the non-null location of the user diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.kt index a0bf1176a..b5c4f4a7a 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.kt @@ -64,8 +64,8 @@ class NotificationClient return Notification( notificationType = notificationType, notificationText = notificationText, - date = DateUtil.getMonthOnlyDateString(timestamp), - link = contents?.links?.primary?.url ?: "", + date = DateUtil.getMonthOnlyDateString(getTimestamp()), + link = contents?.links?.getPrimary()?.url ?: "", iconUrl = "", notificationId = id().toString(), ) diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/edit/Edit.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/edit/Edit.java deleted file mode 100644 index b79612ecc..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/edit/Edit.java +++ /dev/null @@ -1,36 +0,0 @@ -package fr.free.nrw.commons.wikidata.model.edit; - -import androidx.annotation.Nullable; -import fr.free.nrw.commons.wikidata.mwapi.MwPostResponse; - -public class Edit extends MwPostResponse { - @Nullable private Result edit; - - @Nullable public Result edit() { - return edit; - } - - public class Result { - @Nullable private String result; - @Nullable private String code; - @Nullable private String info; - @Nullable private String warning; - - public boolean editSucceeded() { - return "Success".equals(result); - } - - @Nullable public String code() { - return code; - } - - @Nullable public String info() { - return info; - } - - @Nullable public String warning() { - return warning; - } - - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/edit/Edit.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/edit/Edit.kt new file mode 100644 index 000000000..897b057bd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/edit/Edit.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.wikidata.model.edit + +import fr.free.nrw.commons.wikidata.mwapi.MwPostResponse + +class Edit : MwPostResponse() { + private val edit: Result? = null + + fun edit(): Result? = edit + + class Result { + private val result: String? = null + private val code: String? = null + private val info: String? = null + private val warning: String? = null + + fun editSucceeded(): Boolean = + "Success" == result + + fun code(): String? = code + + fun info(): String? = info + + fun warning(): String? = warning + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/edit/EditResult.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/edit/EditResult.java deleted file mode 100644 index 1733f374e..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/edit/EditResult.java +++ /dev/null @@ -1,31 +0,0 @@ -package fr.free.nrw.commons.wikidata.model.edit; - -import android.os.Parcel; -import android.os.Parcelable; -import fr.free.nrw.commons.wikidata.model.BaseModel; - -public abstract class EditResult extends BaseModel implements Parcelable { - private final String result; - - public EditResult(String result) { - this.result = result; - } - - protected EditResult(Parcel in) { - this.result = in.readString(); - } - - public String getResult() { - return result; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(result); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.java deleted file mode 100644 index 2bd63400f..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.java +++ /dev/null @@ -1,102 +0,0 @@ -package fr.free.nrw.commons.wikidata.model.gallery; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.gson.annotations.SerializedName; - -import org.apache.commons.lang3.StringUtils; - - -public class ExtMetadata { - @SerializedName("DateTime") @Nullable private Values dateTime; - @SerializedName("ObjectName") @Nullable private Values objectName; - @SerializedName("CommonsMetadataExtension") @Nullable private Values commonsMetadataExtension; - @SerializedName("Categories") @Nullable private Values categories; - @SerializedName("Assessments") @Nullable private Values assessments; - @SerializedName("GPSLatitude") @Nullable private Values gpsLatitude; - @SerializedName("GPSLongitude") @Nullable private Values gpsLongitude; - @SerializedName("ImageDescription") @Nullable private Values imageDescription; - @SerializedName("DateTimeOriginal") @Nullable private Values dateTimeOriginal; - @SerializedName("Artist") @Nullable private Values artist; - @SerializedName("Credit") @Nullable private Values credit; - @SerializedName("Permission") @Nullable private Values permission; - @SerializedName("AuthorCount") @Nullable private Values authorCount; - @SerializedName("LicenseShortName") @Nullable private Values licenseShortName; - @SerializedName("UsageTerms") @Nullable private Values usageTerms; - @SerializedName("LicenseUrl") @Nullable private Values licenseUrl; - @SerializedName("AttributionRequired") @Nullable private Values attributionRequired; - @SerializedName("Copyrighted") @Nullable private Values copyrighted; - @SerializedName("Restrictions") @Nullable private Values restrictions; - @SerializedName("License") @Nullable private Values license; - - @NonNull public String licenseShortName() { - return StringUtils.defaultString(licenseShortName == null ? null : licenseShortName.value()); - } - - @NonNull public String licenseUrl() { - return StringUtils.defaultString(licenseUrl == null ? null : licenseUrl.value()); - } - - @NonNull public String license() { - return StringUtils.defaultString(license == null ? null : license.value()); - } - - @NonNull public String imageDescription() { - return StringUtils.defaultString(imageDescription == null ? null : imageDescription.value()); - } - - @NonNull public String imageDescriptionSource() { - return StringUtils.defaultString(imageDescription == null ? null : imageDescription.source()); - } - - @NonNull public String objectName() { - return StringUtils.defaultString(objectName == null ? null : objectName.value()); - } - - @NonNull public String usageTerms() { - return StringUtils.defaultString(usageTerms == null ? null : usageTerms.value()); - } - - @NonNull public String dateTimeOriginal() { - return StringUtils.defaultString(dateTimeOriginal == null ? null : dateTimeOriginal.value()); - } - - @NonNull public String dateTime() { - return StringUtils.defaultString(dateTime == null ? null : dateTime.value()); - } - - @NonNull public String artist() { - return StringUtils.defaultString(artist == null ? null : artist.value()); - } - - @NonNull public String getCategories() { - return StringUtils.defaultString(categories == null ? null : categories.value()); - } - - @NonNull public String getGpsLatitude() { - return StringUtils.defaultString(gpsLatitude == null ? null : gpsLatitude.value()); - } - - @NonNull public String getGpsLongitude() { - return StringUtils.defaultString(gpsLongitude == null ? null : gpsLongitude.value()); - } - - @NonNull public String credit() { - return StringUtils.defaultString(credit == null ? null : credit.value()); - } - - public class Values { - @Nullable private String value; - @Nullable private String source; - @Nullable private String hidden; - - @Nullable public String value() { - return value; - } - - @Nullable public String source() { - return source; - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.kt new file mode 100644 index 000000000..63c018252 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.kt @@ -0,0 +1,61 @@ +package fr.free.nrw.commons.wikidata.model.gallery + +import com.google.gson.annotations.SerializedName +import org.apache.commons.lang3.StringUtils + +class ExtMetadata { + @SerializedName("DateTime") private val dateTime: Values? = null + @SerializedName("ObjectName") private val objectName: Values? = null + @SerializedName("CommonsMetadataExtension") private val commonsMetadataExtension: Values? = null + @SerializedName("Categories") private val categories: Values? = null + @SerializedName("Assessments") private val assessments: Values? = null + @SerializedName("GPSLatitude") private val gpsLatitude: Values? = null + @SerializedName("GPSLongitude") private val gpsLongitude: Values? = null + @SerializedName("ImageDescription") private val imageDescription: Values? = null + @SerializedName("DateTimeOriginal") private val dateTimeOriginal: Values? = null + @SerializedName("Artist") private val artist: Values? = null + @SerializedName("Credit") private val credit: Values? = null + @SerializedName("Permission") private val permission: Values? = null + @SerializedName("AuthorCount") private val authorCount: Values? = null + @SerializedName("LicenseShortName") private val licenseShortName: Values? = null + @SerializedName("UsageTerms") private val usageTerms: Values? = null + @SerializedName("LicenseUrl") private val licenseUrl: Values? = null + @SerializedName("AttributionRequired") private val attributionRequired: Values? = null + @SerializedName("Copyrighted") private val copyrighted: Values? = null + @SerializedName("Restrictions") private val restrictions: Values? = null + @SerializedName("License") private val license: Values? = null + + fun licenseShortName(): String = licenseShortName?.value ?: "" + + fun licenseUrl(): String = licenseUrl?.value ?: "" + + fun license(): String = license?.value ?: "" + + fun imageDescription(): String = imageDescription?.value ?: "" + + fun imageDescriptionSource(): String = imageDescription?.source ?: "" + + fun objectName(): String = objectName?.value ?: "" + + fun usageTerms(): String = usageTerms?.value ?: "" + + fun dateTimeOriginal(): String = dateTimeOriginal?.value ?: "" + + fun dateTime(): String = dateTime?.value ?: "" + + fun artist(): String = artist?.value ?: "" + + fun categories(): String = categories?.value ?: "" + + fun gpsLatitude(): String = gpsLatitude?.value ?: "" + + fun gpsLongitude(): String = gpsLongitude?.value ?: "" + + fun credit(): String = credit?.value ?: "" + + class Values { + val value: String? = null + val source: String? = null + val hidden: String? = null + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.java deleted file mode 100644 index 2e1349ae9..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.java +++ /dev/null @@ -1,121 +0,0 @@ -package fr.free.nrw.commons.wikidata.model.gallery; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.gson.annotations.SerializedName; - -import org.apache.commons.lang3.StringUtils; - -import java.io.Serializable; - -/** - * Gson POJO for a standard image info object as returned by the API ImageInfo module - */ - -public class ImageInfo implements Serializable { - private int size; - private int width; - private int height; - @Nullable private String source; - @SerializedName("thumburl") @Nullable private String thumbUrl; - @SerializedName("thumbwidth") private int thumbWidth; - @SerializedName("thumbheight") private int thumbHeight; - @SerializedName("url") @Nullable private String originalUrl; - @SerializedName("descriptionurl") @Nullable private String descriptionUrl; - @SerializedName("descriptionshorturl") @Nullable private String descriptionShortUrl; - @SerializedName("mime") @Nullable private String mimeType; - @SerializedName("extmetadata")@Nullable private ExtMetadata metadata; - @Nullable private String user; - @Nullable private String timestamp; - - /** - * Query width, default width parameter of the API query in pixels. - */ - final private static int QUERY_WIDTH = 640; - - /** - * Threshold height, the minimum height of the image in pixels. - */ - final private static int THRESHOLD_HEIGHT = 220; - - @NonNull - public String getSource() { - return StringUtils.defaultString(source); - } - - public void setSource(@Nullable String source) { - this.source = source; - } - - public int getSize() { - return size; - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - /** - * Get the thumbnail width. - * @return - */ - public int getThumbWidth() { return thumbWidth; } - - /** - * Get the thumbnail height. - * @return - */ - public int getThumbHeight() { return thumbHeight; } - - @NonNull public String getMimeType() { - return StringUtils.defaultString(mimeType, "*/*"); - } - - @NonNull public String getThumbUrl() { - updateThumbUrl(); - return StringUtils.defaultString(thumbUrl); - } - - @NonNull public String getOriginalUrl() { - return StringUtils.defaultString(originalUrl); - } - - @NonNull public String getUser() { - return StringUtils.defaultString(user); - } - - @NonNull public String getTimestamp() { - return StringUtils.defaultString(timestamp); - } - - @Nullable public ExtMetadata getMetadata() { - return metadata; - } - - /** - * Updates the ThumbUrl if image dimensions are not sufficient. - * Specifically, in panoramic images the height retrieved is less than required due to large width to height ratio, - * so we update the thumb url keeping a minimum height threshold. - */ - private void updateThumbUrl() { - // If thumbHeight retrieved from API is less than THRESHOLD_HEIGHT - if(getThumbHeight() < THRESHOLD_HEIGHT){ - // If thumbWidthRetrieved is same as queried width ( If not tells us that the image has no larger dimensions. ) - if(getThumbWidth() == QUERY_WIDTH){ - // Calculate new width depending on the aspect ratio. - final int finalWidth = (int)(THRESHOLD_HEIGHT * getThumbWidth() * 1.0 / getThumbHeight()); - thumbHeight = THRESHOLD_HEIGHT; - thumbWidth = finalWidth; - final String toReplace = "/" + QUERY_WIDTH + "px"; - final int position = thumbUrl.lastIndexOf(toReplace); - thumbUrl = (new StringBuilder(thumbUrl)).replace(position, position + toReplace.length(), "/" + thumbWidth + "px").toString(); - } - } - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.kt new file mode 100644 index 000000000..492e2e1f8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.kt @@ -0,0 +1,129 @@ +package fr.free.nrw.commons.wikidata.model.gallery + +import com.google.gson.annotations.SerializedName +import org.apache.commons.lang3.StringUtils +import java.io.Serializable + +/** + * Gson POJO for a standard image info object as returned by the API ImageInfo module + */ +open class ImageInfo : Serializable { + private val size = 0 + private val width = 0 + private val height = 0 + private var source: String? = null + + @SerializedName("thumburl") + private var thumbUrl: String? = null + + @SerializedName("thumbwidth") + private var thumbWidth = 0 + + @SerializedName("thumbheight") + private var thumbHeight = 0 + + @SerializedName("url") + private val originalUrl: String? = null + + @SerializedName("descriptionurl") + private val descriptionUrl: String? = null + + @SerializedName("descriptionshorturl") + private val descriptionShortUrl: String? = null + + @SerializedName("mime") + private val mimeType: String? = null + + @SerializedName("extmetadata") + private val metadata: ExtMetadata? = null + private val user: String? = null + private val timestamp: String? = null + + fun getSource(): String { + return source ?: "" + } + + fun setSource(source: String?) { + this.source = source + } + + fun getSize(): Int { + return size + } + + fun getWidth(): Int { + return width + } + + fun getHeight(): Int { + return height + } + + fun getThumbWidth(): Int { + return thumbWidth + } + + fun getThumbHeight(): Int { + return thumbHeight + } + + fun getMimeType(): String { + return mimeType ?: "*/*" + } + + fun getThumbUrl(): String { + updateThumbUrl() + return thumbUrl ?: "" + } + + fun getOriginalUrl(): String { + return originalUrl ?: "" + } + + fun getUser(): String { + return user ?: "" + } + + fun getTimestamp(): String { + return timestamp ?: "" + } + + fun getMetadata(): ExtMetadata? = metadata + + /** + * Updates the ThumbUrl if image dimensions are not sufficient. Specifically, in panoramic + * images the height retrieved is less than required due to large width to height ratio, so we + * update the thumb url keeping a minimum height threshold. + */ + private fun updateThumbUrl() { + // If thumbHeight retrieved from API is less than THRESHOLD_HEIGHT + if (getThumbHeight() < THRESHOLD_HEIGHT) { + // If thumbWidthRetrieved is same as queried width ( If not tells us that the image has no larger dimensions. ) + if (getThumbWidth() == QUERY_WIDTH) { + // Calculate new width depending on the aspect ratio. + val finalWidth = (THRESHOLD_HEIGHT * getThumbWidth() * 1.0 + / getThumbHeight()).toInt() + thumbHeight = THRESHOLD_HEIGHT + thumbWidth = finalWidth + val toReplace = "/" + QUERY_WIDTH + "px" + val position = thumbUrl!!.lastIndexOf(toReplace) + thumbUrl = (StringBuilder(thumbUrl ?: "")).replace( + position, + position + toReplace.length, "/" + thumbWidth + "px" + ).toString() + } + } + } + + companion object { + /** + * Query width, default width parameter of the API query in pixels. + */ + private const val QUERY_WIDTH = 640 + + /** + * Threshold height, the minimum height of the image in pixels. + */ + private const val THRESHOLD_HEIGHT = 220 + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/VideoInfo.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/VideoInfo.java deleted file mode 100644 index 32388d5cf..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/VideoInfo.java +++ /dev/null @@ -1,16 +0,0 @@ -package fr.free.nrw.commons.wikidata.model.gallery; - -import androidx.annotation.Nullable; - -import com.google.gson.annotations.SerializedName; - -import java.util.List; - -/** - * Gson POJO for a standard video info object as returned by the API VideoInfo module - */ -public class VideoInfo extends ImageInfo { - @Nullable private List codecs; - @SuppressWarnings("unused,NullableProblems") @Nullable private String name; - @SuppressWarnings("unused,NullableProblems") @Nullable @SerializedName("short_name") private String shortName; -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java deleted file mode 100644 index 929fe0d13..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java +++ /dev/null @@ -1,190 +0,0 @@ -package fr.free.nrw.commons.wikidata.model.notifications; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.annotations.SerializedName; - -import fr.free.nrw.commons.utils.DateUtil; -import org.apache.commons.lang3.StringUtils; -import fr.free.nrw.commons.wikidata.GsonUtil; - -import java.text.ParseException; -import java.util.Date; -import timber.log.Timber; - -public class Notification { - @Nullable private String wiki; - private long id; - @Nullable private String type; - @Nullable private String category; - - @Nullable private Title title; - @Nullable private Timestamp timestamp; - @SerializedName("*") @Nullable private Contents contents; - - @NonNull public String wiki() { - return StringUtils.defaultString(wiki); - } - - public long id() { - return id; - } - - public void setId(final long id) { - this.id = id; - } - - public long key() { - return id + wiki().hashCode(); - } - - @NonNull public String type() { - return StringUtils.defaultString(type); - } - - @Nullable public Title title() { - return title; - } - - @Nullable public Contents getContents() { - return contents; - } - - public void setContents(@Nullable final Contents contents) { - this.contents = contents; - } - - @NonNull public Date getTimestamp() { - return timestamp != null ? timestamp.date() : new Date(); - } - - public void setTimestamp(@Nullable final Timestamp timestamp) { - this.timestamp = timestamp; - } - - @NonNull String getUtcIso8601() { - return StringUtils.defaultString(timestamp != null ? timestamp.utciso8601 : null); - } - - public boolean isFromWikidata() { - return wiki().equals("wikidatawiki"); - } - - @Override public String toString() { - return Long.toString(id); - } - - public static class Title { - @Nullable private String full; - @Nullable private String text; - - @NonNull public String text() { - return StringUtils.defaultString(text); - } - - @NonNull public String full() { - return StringUtils.defaultString(full); - } - } - - public static class Timestamp { - @Nullable private String utciso8601; - - public void setUtciso8601(@Nullable final String utciso8601) { - this.utciso8601 = utciso8601; - } - - public Date date() { - try { - return DateUtil.iso8601DateParse(utciso8601); - } catch (ParseException e) { - Timber.e(e); - return new Date(); - } - } - } - - public static class Link { - @Nullable private String url; - @Nullable private String label; - @Nullable private String tooltip; - @Nullable private String description; - @Nullable private String icon; - - @NonNull public String getUrl() { - return StringUtils.defaultString(url); - } - - public void setUrl(@Nullable final String url) { - this.url = url; - } - - @NonNull public String getTooltip() { - return StringUtils.defaultString(tooltip); - } - - @NonNull public String getLabel() { - return StringUtils.defaultString(label); - } - - @NonNull public String getIcon() { - return StringUtils.defaultString(icon); - } - } - - public static class Links { - @Nullable private JsonElement primary; - private Link primaryLink; - - public void setPrimary(@Nullable final JsonElement primary) { - this.primary = primary; - } - - @Nullable public Link getPrimary() { - if (primary == null) { - return null; - } - if (primaryLink == null && primary instanceof JsonObject) { - primaryLink = GsonUtil.INSTANCE.getDefaultGson().fromJson(primary, Link.class); - } - return primaryLink; - } - - } - - public static class Contents { - @Nullable private String header; - @Nullable private String compactHeader; - @Nullable private String body; - @Nullable private String icon; - @Nullable private Links links; - - @NonNull public String getHeader() { - return StringUtils.defaultString(header); - } - - @NonNull public String getCompactHeader() { - return StringUtils.defaultString(compactHeader); - } - - public void setCompactHeader(@Nullable final String compactHeader) { - this.compactHeader = compactHeader; - } - - @NonNull public String getBody() { - return StringUtils.defaultString(body); - } - - @Nullable public Links getLinks() { - return links; - } - - public void setLinks(@Nullable final Links links) { - this.links = links; - } - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.kt new file mode 100644 index 000000000..dd73d9723 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.kt @@ -0,0 +1,124 @@ +package fr.free.nrw.commons.wikidata.model.notifications + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.annotations.SerializedName +import fr.free.nrw.commons.utils.DateUtil.iso8601DateParse +import fr.free.nrw.commons.wikidata.GsonUtil.defaultGson +import org.apache.commons.lang3.StringUtils +import timber.log.Timber +import java.text.ParseException +import java.util.Date + +class Notification { + private val wiki: String? = null + private var id: Long = 0 + private val type: String? = null + private val category: String? = null + + private val title: Title? = null + private var timestamp: Timestamp? = null + + @SerializedName("*") + var contents: Contents? = null + + fun wiki(): String = wiki ?: "" + + fun id(): Long = id + + fun setId(id: Long) { + this.id = id + } + + fun key(): Long = + id + wiki().hashCode() + + fun type(): String = + type ?: "" + + fun title(): Title? = title + + fun getTimestamp(): Date = + timestamp?.date() ?: Date() + + fun setTimestamp(timestamp: Timestamp?) { + this.timestamp = timestamp + } + + val utcIso8601: String + get() = timestamp?.utciso8601 ?: "" + + val isFromWikidata: Boolean + get() = wiki() == "wikidatawiki" + + override fun toString(): String = + id.toString() + + class Title { + private val full: String? = null + private val text: String? = null + + fun text(): String = text ?: "" + + fun full(): String = full ?: "" + } + + class Timestamp { + internal var utciso8601: String? = null + + fun setUtciso8601(utciso8601: String?) { + this.utciso8601 = utciso8601 + } + + fun date(): Date { + try { + return iso8601DateParse(utciso8601 ?: "") + } catch (e: ParseException) { + Timber.e(e) + return Date() + } + } + } + + class Link { + var url: String? = null + get() = field ?: "" + val label: String? = null + get() = field ?: "" + val tooltip: String? = null + get() = field ?: "" + private val description: String? = null + val icon: String? = null + get() = field ?: "" + } + + class Links { + private var primary: JsonElement? = null + private var primaryLink: Link? = null + + fun setPrimary(primary: JsonElement?) { + this.primary = primary + } + + fun getPrimary(): Link? { + if (primary == null) { + return null + } + if (primaryLink == null && primary is JsonObject) { + primaryLink = defaultGson.fromJson(primary, Link::class.java) + } + return primaryLink + } + } + + class Contents { + val header: String? = null + get() = field ?: "" + var compactHeader: String? = null + get() = field ?: "" + val body: String? = null + get() = field ?: "" + private val icon: String? = null + var links: Links? = null + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/GeoMarshaller.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/GeoMarshaller.java deleted file mode 100644 index 427833512..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/GeoMarshaller.java +++ /dev/null @@ -1,28 +0,0 @@ -package fr.free.nrw.commons.wikidata.model.page; - -import android.location.Location; - -import androidx.annotation.Nullable; - -import org.json.JSONException; -import org.json.JSONObject; - -public final class GeoMarshaller { - @Nullable - public static String marshal(@Nullable Location object) { - if (object == null) { - return null; - } - - JSONObject jsonObj = new JSONObject(); - try { - jsonObj.put(GeoUnmarshaller.LATITUDE, object.getLatitude()); - jsonObj.put(GeoUnmarshaller.LONGITUDE, object.getLongitude()); - } catch (JSONException e) { - throw new RuntimeException(e); - } - return jsonObj.toString(); - } - - private GeoMarshaller() { } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/GeoUnmarshaller.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/GeoUnmarshaller.java deleted file mode 100644 index aa5952964..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/GeoUnmarshaller.java +++ /dev/null @@ -1,39 +0,0 @@ -package fr.free.nrw.commons.wikidata.model.page; - -import android.location.Location; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.json.JSONException; -import org.json.JSONObject; - -public final class GeoUnmarshaller { - static final String LATITUDE = "latitude"; - static final String LONGITUDE = "longitude"; - - @Nullable - public static Location unmarshal(@Nullable String json) { - if (json == null) { - return null; - } - - JSONObject jsonObj; - try { - jsonObj = new JSONObject(json); - } catch (JSONException e) { - return null; - } - return unmarshal(jsonObj); - } - - @Nullable - public static Location unmarshal(@NonNull JSONObject jsonObj) { - Location ret = new Location((String) null); - ret.setLatitude(jsonObj.optDouble(LATITUDE)); - ret.setLongitude(jsonObj.optDouble(LONGITUDE)); - return ret; - } - - private GeoUnmarshaller() { } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/Namespace.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/Namespace.kt similarity index 56% rename from app/src/main/java/fr/free/nrw/commons/wikidata/model/page/Namespace.java rename to app/src/main/java/fr/free/nrw/commons/wikidata/model/page/Namespace.kt index 47aff28c2..a52fd0954 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/Namespace.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/Namespace.kt @@ -1,34 +1,28 @@ -package fr.free.nrw.commons.wikidata.model.page; +package fr.free.nrw.commons.wikidata.model.page -import androidx.annotation.NonNull; - -import fr.free.nrw.commons.wikidata.model.EnumCode; -import fr.free.nrw.commons.wikidata.model.EnumCodeMap; +import fr.free.nrw.commons.wikidata.model.EnumCode +import fr.free.nrw.commons.wikidata.model.EnumCodeMap /** An enumeration describing the different possible namespace codes. Do not attempt to use this - * class to preserve URL path information such as Talk: or User: or localization. - * @see Wikipedia:Namespace - * @see Extension default namespaces - * @see NSNumber+MWKTitleNamespace.h (iOS implementation) - * @see Manual:Namespace - * @see Namespaces reported by API + * class to preserve URL path information such as Talk: or User: or localization. + * + * @see [Wikipedia:Namespace](https://en.wikipedia.org/wiki/Wikipedia:Namespace) + * @see [Extension default namespaces](https://www.mediawiki.org/wiki/Extension_default_namespaces) + * @see [NSNumber+MWKTitleNamespace.h + * @see [Manual:Namespace](https://www.mediawiki.org/wiki/Manual:Namespace.Built-in_namespaces) + * @see [Namespaces reported by API](https://en.wikipedia.org/w/api.php?action=query&meta=siteinfo&siprop=namespaces|namespacealiases)](https://github.com/wikimedia/wikipedia-ios/blob/master/Wikipedia/Code/NSNumber+MWKTitleNamespace.h) */ -public enum Namespace implements EnumCode { +enum class Namespace(private val code: Int) : EnumCode { MEDIA(-2), - SPECIAL(-1) { - @Override - public boolean talk() { - return false; - } - }, - MAIN(0), // Main or Article + SPECIAL(-1) { override fun talk(): Boolean = false }, + MAIN(0), // Main or Article TALK(1), USER(2), USER_TALK(3), - PROJECT(4), // WP alias - PROJECT_TALK(5), // WT alias - FILE(6), // Image alias - FILE_TALK(7), // Image talk alias + PROJECT(4), // WP alias + PROJECT_TALK(5), // WT alias + FILE(6), // Image alias + FILE_TALK(7), // Image talk alias MEDIAWIKI(8), MEDIAWIKI_TALK(9), TEMPLATE(10), @@ -137,38 +131,20 @@ public enum Namespace implements EnumCode { GADGET_DEFINITION_TALK(2303), TOPIC(2600); - private static final int TALK_MASK = 0x1; - private static final EnumCodeMap MAP = new EnumCodeMap<>(Namespace.class); + override fun code(): Int = code - private final int code; + fun special(): Boolean = this === SPECIAL - @NonNull - public static Namespace of(int code) { - return MAP.get(code); - } + fun main(): Boolean = this === MAIN - @Override - public int code() { - return code; - } + fun file(): Boolean = this === FILE - public boolean special() { - return this == SPECIAL; - } + open fun talk(): Boolean = (code and TALK_MASK) == TALK_MASK - public boolean main() { - return this == MAIN; - } + companion object { + private const val TALK_MASK = 0x1 + private val MAP = EnumCodeMap(Namespace::class.java) - public boolean file() { - return this == FILE; - } - - public boolean talk() { - return (code & TALK_MASK) == TALK_MASK; - } - - Namespace(int code) { - this.code = code; + fun of(code: Int): Namespace = MAP[code] } } diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageProperties.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageProperties.java deleted file mode 100644 index 8b32252d8..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageProperties.java +++ /dev/null @@ -1,156 +0,0 @@ -package fr.free.nrw.commons.wikidata.model.page; - -import android.location.Location; -import android.os.Parcel; -import android.os.Parcelable; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Date; - -/** - * Immutable class that contains metadata associated with a PageTitle. - */ -public class PageProperties implements Parcelable { - private final int pageId; - @NonNull private final Namespace namespace; - private final long revisionId; - private final Date lastModified; - private final String displayTitleText; - private final String editProtectionStatus; - private final int languageCount; - private final boolean isMainPage; - private final boolean isDisambiguationPage; - /** Nullable URL with no scheme. For example, foo.bar.com/ instead of http://foo.bar.com/. */ - @Nullable private final String leadImageUrl; - @Nullable private final String leadImageName; - @Nullable private final String titlePronunciationUrl; - @Nullable private final Location geo; - @Nullable private final String wikiBaseItem; - @Nullable private final String descriptionSource; - - /** - * True if the user who first requested this page can edit this page - * FIXME: This is not a true page property, since it depends on current user. - */ - private final boolean canEdit; - - public int getPageId() { - return pageId; - } - - public boolean isMainPage() { - return isMainPage; - } - - public boolean isDisambiguationPage() { - return isDisambiguationPage; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel parcel, int flags) { - parcel.writeInt(pageId); - parcel.writeInt(namespace.code()); - parcel.writeLong(revisionId); - parcel.writeLong(lastModified.getTime()); - parcel.writeString(displayTitleText); - parcel.writeString(titlePronunciationUrl); - parcel.writeString(GeoMarshaller.marshal(geo)); - parcel.writeString(editProtectionStatus); - parcel.writeInt(languageCount); - parcel.writeInt(canEdit ? 1 : 0); - parcel.writeInt(isMainPage ? 1 : 0); - parcel.writeInt(isDisambiguationPage ? 1 : 0); - parcel.writeString(leadImageUrl); - parcel.writeString(leadImageName); - parcel.writeString(wikiBaseItem); - parcel.writeString(descriptionSource); - } - - private PageProperties(Parcel in) { - pageId = in.readInt(); - namespace = Namespace.of(in.readInt()); - revisionId = in.readLong(); - lastModified = new Date(in.readLong()); - displayTitleText = in.readString(); - titlePronunciationUrl = in.readString(); - geo = GeoUnmarshaller.unmarshal(in.readString()); - editProtectionStatus = in.readString(); - languageCount = in.readInt(); - canEdit = in.readInt() == 1; - isMainPage = in.readInt() == 1; - isDisambiguationPage = in.readInt() == 1; - leadImageUrl = in.readString(); - leadImageName = in.readString(); - wikiBaseItem = in.readString(); - descriptionSource = in.readString(); - } - - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { - @Override - public PageProperties createFromParcel(Parcel in) { - return new PageProperties(in); - } - - @Override - public PageProperties[] newArray(int size) { - return new PageProperties[size]; - } - }; - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - PageProperties that = (PageProperties) o; - - return pageId == that.pageId - && namespace == that.namespace - && revisionId == that.revisionId - && lastModified.equals(that.lastModified) - && displayTitleText.equals(that.displayTitleText) - && TextUtils.equals(titlePronunciationUrl, that.titlePronunciationUrl) - && (geo == that.geo || geo != null && geo.equals(that.geo)) - && languageCount == that.languageCount - && canEdit == that.canEdit - && isMainPage == that.isMainPage - && isDisambiguationPage == that.isDisambiguationPage - && TextUtils.equals(editProtectionStatus, that.editProtectionStatus) - && TextUtils.equals(leadImageUrl, that.leadImageUrl) - && TextUtils.equals(leadImageName, that.leadImageName) - && TextUtils.equals(wikiBaseItem, that.wikiBaseItem); - } - - @Override - public int hashCode() { - int result = lastModified.hashCode(); - result = 31 * result + displayTitleText.hashCode(); - result = 31 * result + (titlePronunciationUrl != null ? titlePronunciationUrl.hashCode() : 0); - result = 31 * result + (geo != null ? geo.hashCode() : 0); - result = 31 * result + (editProtectionStatus != null ? editProtectionStatus.hashCode() : 0); - result = 31 * result + languageCount; - result = 31 * result + (isMainPage ? 1 : 0); - result = 31 * result + (isDisambiguationPage ? 1 : 0); - result = 31 * result + (leadImageUrl != null ? leadImageUrl.hashCode() : 0); - result = 31 * result + (leadImageName != null ? leadImageName.hashCode() : 0); - result = 31 * result + (wikiBaseItem != null ? wikiBaseItem.hashCode() : 0); - result = 31 * result + (canEdit ? 1 : 0); - result = 31 * result + pageId; - result = 31 * result + namespace.code(); - result = 31 * result + (int) revisionId; - return result; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageProperties.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageProperties.kt new file mode 100644 index 000000000..ce8873e8c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageProperties.kt @@ -0,0 +1,156 @@ +package fr.free.nrw.commons.wikidata.model.page + +import android.location.Location +import android.os.Parcel +import android.os.Parcelable +import android.text.TextUtils +import org.json.JSONException +import org.json.JSONObject +import java.util.Date + +/** + * Immutable class that contains metadata associated with a PageTitle. + */ +class PageProperties private constructor(parcel: Parcel) : Parcelable { + val pageId: Int = parcel.readInt() + private val namespace = Namespace.of(parcel.readInt()) + private val revisionId = parcel.readLong() + private val lastModified = Date(parcel.readLong()) + private val displayTitleText = parcel.readString() + private val editProtectionStatus = parcel.readString() + private val languageCount = parcel.readInt() + val isMainPage: Boolean = parcel.readInt() == 1 + val isDisambiguationPage: Boolean = parcel.readInt() == 1 + + /** Nullable URL with no scheme. For example, foo.bar.com/ instead of http://foo.bar.com/. */ + private val leadImageUrl = parcel.readString() + private val leadImageName = parcel.readString() + private val titlePronunciationUrl = parcel.readString() + private val geo = unmarshal(parcel.readString()) + private val wikiBaseItem = parcel.readString() + private val descriptionSource = parcel.readString() + + /** + * True if the user who first requested this page can edit this page + * FIXME: This is not a true page property, since it depends on current user. + */ + private val canEdit = parcel.readInt() == 1 + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeInt(pageId) + parcel.writeInt(namespace.code()) + parcel.writeLong(revisionId) + parcel.writeLong(lastModified.time) + parcel.writeString(displayTitleText) + parcel.writeString(titlePronunciationUrl) + parcel.writeString(marshal(geo)) + parcel.writeString(editProtectionStatus) + parcel.writeInt(languageCount) + parcel.writeInt(if (canEdit) 1 else 0) + parcel.writeInt(if (isMainPage) 1 else 0) + parcel.writeInt(if (isDisambiguationPage) 1 else 0) + parcel.writeString(leadImageUrl) + parcel.writeString(leadImageName) + parcel.writeString(wikiBaseItem) + parcel.writeString(descriptionSource) + } + + override fun equals(o: Any?): Boolean { + if (this === o) { + return true + } + if (o == null || javaClass != o.javaClass) { + return false + } + + val that = o as PageProperties + + return pageId == that.pageId && + namespace === that.namespace && + revisionId == that.revisionId && + lastModified == that.lastModified && + displayTitleText == that.displayTitleText && + TextUtils.equals(titlePronunciationUrl, that.titlePronunciationUrl) && + (geo === that.geo || geo != null && geo == that.geo) && + languageCount == that.languageCount && + canEdit == that.canEdit && + isMainPage == that.isMainPage && + isDisambiguationPage == that.isDisambiguationPage && + TextUtils.equals(editProtectionStatus, that.editProtectionStatus) && + TextUtils.equals(leadImageUrl, that.leadImageUrl) && + TextUtils.equals(leadImageName, that.leadImageName) && + TextUtils.equals(wikiBaseItem, that.wikiBaseItem) + } + + override fun hashCode(): Int { + var result = lastModified.hashCode() + result = 31 * result + displayTitleText.hashCode() + result = 31 * result + (titlePronunciationUrl?.hashCode() ?: 0) + result = 31 * result + (geo?.hashCode() ?: 0) + result = 31 * result + (editProtectionStatus?.hashCode() ?: 0) + result = 31 * result + languageCount + result = 31 * result + (if (isMainPage) 1 else 0) + result = 31 * result + (if (isDisambiguationPage) 1 else 0) + result = 31 * result + (leadImageUrl?.hashCode() ?: 0) + result = 31 * result + (leadImageName?.hashCode() ?: 0) + result = 31 * result + (wikiBaseItem?.hashCode() ?: 0) + result = 31 * result + (if (canEdit) 1 else 0) + result = 31 * result + pageId + result = 31 * result + namespace.code() + result = 31 * result + revisionId.toInt() + return result + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): PageProperties { + return PageProperties(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} + +private const val LATITUDE: String = "latitude" +private const val LONGITUDE: String = "longitude" + +private fun marshal(location: Location?): String? { + if (location == null) { + return null + } + + val jsonObj = JSONObject().apply { + try { + put(LATITUDE, location.latitude) + put(LONGITUDE, location.longitude) + } catch (e: JSONException) { + throw RuntimeException(e) + } + } + + return jsonObj.toString() +} + +private fun unmarshal(json: String?): Location? { + if (json == null) { + return null + } + + return try { + val jsonObject = JSONObject(json) + Location(null as String?).apply { + latitude = jsonObject.optDouble(LATITUDE) + longitude = jsonObject.optDouble(LONGITUDE) + } + } catch (e: JSONException) { + null + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageTitle.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageTitle.java deleted file mode 100644 index f22ff7609..000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageTitle.java +++ /dev/null @@ -1,339 +0,0 @@ -package fr.free.nrw.commons.wikidata.model.page; - -import android.os.Parcel; -import android.os.Parcelable; -import android.text.TextUtils; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.gson.annotations.SerializedName; -import fr.free.nrw.commons.wikidata.model.WikiSite; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.text.Normalizer; -import java.util.Arrays; -import java.util.Locale; -import timber.log.Timber; - -/** - * Represents certain vital information about a page, including the title, namespace, - * and fragment (section anchor target). It can also contain a thumbnail URL for the - * page, and a short description retrieved from Wikidata. - * - * WARNING: This class is not immutable! Specifically, the thumbnail URL and the Wikidata - * description can be altered after construction. Therefore do NOT rely on all the fields - * of a PageTitle to remain constant for the lifetime of the object. - */ -public class PageTitle implements Parcelable { - - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { - @Override - public PageTitle createFromParcel(Parcel in) { - return new PageTitle(in); - } - - @Override - public PageTitle[] newArray(int size) { - return new PageTitle[size]; - } - }; - - /** - * The localised namespace of the page as a string, or null if the page is in mainspace. - * - * This field contains the prefix of the page's title, as opposed to the namespace ID used by - * MediaWiki. Therefore, mainspace pages always have a null namespace, as they have no prefix, - * and the namespace of a page will depend on the language of the wiki the user is currently - * looking at. - * - * Examples: - * * [[Manchester]] on enwiki will have a namespace of null - * * [[Deutschland]] on dewiki will have a namespace of null - * * [[User:Deskana]] on enwiki will have a namespace of "User" - * * [[Utilisateur:Deskana]] on frwiki will have a namespace of "Utilisateur", even if you got - * to the page by going to [[User:Deskana]] and having MediaWiki automatically redirect you. - */ - // TODO: remove. This legacy code is the localized namespace name (File, Special, Talk, etc) but - // isn't consistent across titles. e.g., articles with colons, such as RTÉ News: Six One, - // are broken. - @Nullable private final String namespace; - @NonNull private final String text; - @Nullable private final String fragment; - @Nullable private String thumbUrl; - @SerializedName("site") @NonNull private final WikiSite wiki; - @Nullable private String description; - @Nullable private final PageProperties properties; - // TODO: remove after the restbase endpoint supports ZH variants. - @Nullable private String convertedText; - - /** - * Creates a new PageTitle object. - * Use this if you want to pass in a fragment portion separately from the title. - * - * @param prefixedText title of the page with optional namespace prefix - * @param fragment optional fragment portion - * @param wiki the wiki site the page belongs to - * @return a new PageTitle object matching the given input parameters - */ - public static PageTitle withSeparateFragment(@NonNull String prefixedText, - @Nullable String fragment, @NonNull WikiSite wiki) { - if (TextUtils.isEmpty(fragment)) { - return new PageTitle(prefixedText, wiki, null, (PageProperties) null); - } else { - // TODO: this class needs some refactoring to allow passing in a fragment - // without having to do string manipulations. - return new PageTitle(prefixedText + "#" + fragment, wiki, null, (PageProperties) null); - } - } - - public PageTitle(@Nullable final String namespace, @NonNull String text, @Nullable String fragment, @Nullable String thumbUrl, @NonNull WikiSite wiki) { - this.namespace = namespace; - this.text = text; - this.fragment = fragment; - this.wiki = wiki; - this.thumbUrl = thumbUrl; - properties = null; - } - - public PageTitle(@Nullable String text, @NonNull WikiSite wiki, @Nullable String thumbUrl, @Nullable String description, @Nullable PageProperties properties) { - this(text, wiki, thumbUrl, properties); - this.description = description; - } - - public PageTitle(@Nullable String text, @NonNull WikiSite wiki, @Nullable String thumbUrl, @Nullable String description) { - this(text, wiki, thumbUrl); - this.description = description; - } - - public PageTitle(@Nullable String namespace, @NonNull String text, @NonNull WikiSite wiki) { - this(namespace, text, null, null, wiki); - } - - public PageTitle(@Nullable String text, @NonNull WikiSite wiki, @Nullable String thumbUrl) { - this(text, wiki, thumbUrl, (PageProperties) null); - } - - public PageTitle(@Nullable String text, @NonNull WikiSite wiki) { - this(text, wiki, null); - } - - private PageTitle(@Nullable String text, @NonNull WikiSite wiki, @Nullable String thumbUrl, - @Nullable PageProperties properties) { - if (text == null) { - text = ""; - } - // FIXME: Does not handle mainspace articles with a colon in the title well at all - String[] fragParts = text.split("#", -1); - text = fragParts[0]; - if (fragParts.length > 1) { - this.fragment = decodeURL(fragParts[1]).replace(" ", "_"); - } else { - this.fragment = null; - } - - String[] parts = text.split(":", -1); - if (parts.length > 1) { - String namespaceOrLanguage = parts[0]; - if (Arrays.asList(Locale.getISOLanguages()).contains(namespaceOrLanguage)) { - this.namespace = null; - this.wiki = new WikiSite(wiki.authority(), namespaceOrLanguage); - } else { - this.wiki = wiki; - this.namespace = namespaceOrLanguage; - } - this.text = TextUtils.join(":", Arrays.copyOfRange(parts, 1, parts.length)); - } else { - this.wiki = wiki; - this.namespace = null; - this.text = parts[0]; - } - - this.thumbUrl = thumbUrl; - this.properties = properties; - } - - /** - * Decodes a URL-encoded string into its UTF-8 equivalent. If the string cannot be decoded, the - * original string is returned. - * @param url The URL-encoded string that you wish to decode. - * @return The decoded string, or the input string if the decoding failed. - */ - @NonNull private String decodeURL(@NonNull String url) { - try { - return URLDecoder.decode(url, "UTF-8"); - } catch (IllegalArgumentException e) { - // Swallow IllegalArgumentException (can happen with malformed encoding), and just - // return the original string. - Timber.d("URL decoding failed. String was: %s", url); - return url; - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } - - @NonNull public WikiSite getWikiSite() { - return wiki; - } - - @NonNull public String getText() { - return text.replace(" ", "_"); - } - - @Nullable public String getFragment() { - return fragment; - } - - @Nullable public String getThumbUrl() { - return thumbUrl; - } - - public void setThumbUrl(@Nullable String thumbUrl) { - this.thumbUrl = thumbUrl; - } - - @Nullable public String getDescription() { - return description; - } - - public void setDescription(@Nullable String description) { - this.description = description; - } - - @NonNull - public String getConvertedText() { - return convertedText == null ? getPrefixedText() : convertedText; - } - - public void setConvertedText(@Nullable String convertedText) { - this.convertedText = convertedText; - } - - @NonNull public String getDisplayText() { - return getPrefixedText().replace("_", " "); - } - - @NonNull public String getDisplayTextWithoutNamespace() { - return text.replace("_", " "); - } - - public boolean hasProperties() { - return properties != null; - } - - @Nullable public PageProperties getProperties() { - return properties; - } - - public boolean isMainPage() { - return properties != null && properties.isMainPage(); - } - - public boolean isDisambiguationPage() { - return properties != null && properties.isDisambiguationPage(); - } - - public String getCanonicalUri() { - return getUriForDomain(getWikiSite().authority()); - } - - public String getMobileUri() { - return getUriForDomain(getWikiSite().mobileAuthority()); - } - - public String getUriForAction(String action) { - try { - return String.format( - "%1$s://%2$s/w/index.php?title=%3$s&action=%4$s", - getWikiSite().scheme(), - getWikiSite().authority(), - URLEncoder.encode(getPrefixedText(), "utf-8"), - action - ); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } - - public String getPrefixedText() { - - // TODO: find a better way to check if the namespace is a ISO Alpha2 Code (two digits country code) - return namespace == null ? getText() : addUnderscores(namespace) + ":" + getText(); - } - - private String addUnderscores(@NonNull String text) { - return text.replace(" ", "_"); - } - - @Override public void writeToParcel(Parcel parcel, int flags) { - parcel.writeString(namespace); - parcel.writeString(text); - parcel.writeString(fragment); - parcel.writeParcelable(wiki, flags); - parcel.writeParcelable(properties, flags); - parcel.writeString(thumbUrl); - parcel.writeString(description); - parcel.writeString(convertedText); - } - - @Override public boolean equals(Object o) { - if (!(o instanceof PageTitle)) { - return false; - } - - PageTitle other = (PageTitle)o; - // Not using namespace directly since that can be null - return normalizedEquals(other.getPrefixedText(), getPrefixedText()) && other.wiki.equals(wiki); - } - - // Compare two strings based on their normalized form, using the Unicode Normalization Form C. - // This should be used when comparing or verifying strings that will be exchanged between - // different platforms (iOS, desktop, etc) that may encode strings using inconsistent - // composition, especially for accents, diacritics, etc. - private boolean normalizedEquals(@Nullable String str1, @Nullable String str2) { - if (str1 == null || str2 == null) { - return (str1 == null && str2 == null); - } - return Normalizer.normalize(str1, Normalizer.Form.NFC) - .equals(Normalizer.normalize(str2, Normalizer.Form.NFC)); - } - - @Override public int hashCode() { - int result = getPrefixedText().hashCode(); - result = 31 * result + wiki.hashCode(); - return result; - } - - @Override public String toString() { - return getPrefixedText(); - } - - @Override public int describeContents() { - return 0; - } - - private String getUriForDomain(String domain) { - try { - return String.format( - "%1$s://%2$s/wiki/%3$s%4$s", - getWikiSite().scheme(), - domain, - URLEncoder.encode(getPrefixedText(), "utf-8"), - (this.fragment != null && this.fragment.length() > 0) ? ("#" + this.fragment) : "" - ); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } - - private PageTitle(Parcel in) { - namespace = in.readString(); - text = in.readString(); - fragment = in.readString(); - wiki = in.readParcelable(WikiSite.class.getClassLoader()); - properties = in.readParcelable(PageProperties.class.getClassLoader()); - thumbUrl = in.readString(); - description = in.readString(); - convertedText = in.readString(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageTitle.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageTitle.kt new file mode 100644 index 000000000..b039f55d6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/page/PageTitle.kt @@ -0,0 +1,284 @@ +package fr.free.nrw.commons.wikidata.model.page + +import android.os.Parcel +import android.os.Parcelable +import android.text.TextUtils +import com.google.gson.annotations.SerializedName +import fr.free.nrw.commons.wikidata.model.WikiSite +import timber.log.Timber +import java.io.UnsupportedEncodingException +import java.net.URLDecoder +import java.net.URLEncoder +import java.text.Normalizer +import java.util.Arrays +import java.util.Locale + +/** + * Represents certain vital information about a page, including the title, namespace, + * and fragment (section anchor target). It can also contain a thumbnail URL for the + * page, and a short description retrieved from Wikidata. + * + * WARNING: This class is not immutable! Specifically, the thumbnail URL and the Wikidata + * description can be altered after construction. Therefore do NOT rely on all the fields + * of a PageTitle to remain constant for the lifetime of the object. + */ +class PageTitle : Parcelable { + /** + * The localised namespace of the page as a string, or null if the page is in mainspace. + * + * This field contains the prefix of the page's title, as opposed to the namespace ID used by + * MediaWiki. Therefore, mainspace pages always have a null namespace, as they have no prefix, + * and the namespace of a page will depend on the language of the wiki the user is currently + * looking at. + * + * Examples: + * * [[Manchester]] on enwiki will have a namespace of null + * * [[Deutschland]] on dewiki will have a namespace of null + * * [[User:Deskana]] on enwiki will have a namespace of "User" + * * [[Utilisateur:Deskana]] on frwiki will have a namespace of "Utilisateur", even if you got + * to the page by going to [[User:Deskana]] and having MediaWiki automatically redirect you. + */ + // TODO: remove. This legacy code is the localized namespace name (File, Special, Talk, etc) but + // isn't consistent across titles. e.g., articles with colons, such as RTÉ News: Six One, + // are broken. + private val namespace: String? + private val text: String + val fragment: String? + var thumbUrl: String? + + @SerializedName("site") + val wikiSite: WikiSite + var description: String? = null + private val properties: PageProperties? + + // TODO: remove after the restbase endpoint supports ZH variants. + private var convertedText: String? = null + + constructor(namespace: String?, text: String, fragment: String?, thumbUrl: String?, wiki: WikiSite) { + this.namespace = namespace + this.text = text + this.fragment = fragment + this.thumbUrl = thumbUrl + wikiSite = wiki + properties = null + } + + constructor(text: String?, wiki: WikiSite, thumbUrl: String?, description: String?, properties: PageProperties?) : this(text, wiki, thumbUrl, properties) { + this.description = description + } + + constructor(text: String?, wiki: WikiSite, thumbUrl: String?, description: String?) : this(text, wiki, thumbUrl) { + this.description = description + } + + constructor(namespace: String?, text: String, wiki: WikiSite) : this(namespace, text, null, null, wiki) + + @JvmOverloads + constructor(text: String?, wiki: WikiSite, thumbUrl: String? = null) : this(text, wiki, thumbUrl, null as PageProperties?) + + private constructor(input: String?, wiki: WikiSite, thumbUrl: String?, properties: PageProperties?) { + var text = input ?: "" + // FIXME: Does not handle mainspace articles with a colon in the title well at all + val fragParts = text.split("#".toRegex()).toTypedArray() + text = fragParts[0] + fragment = if (fragParts.size > 1) { + decodeURL(fragParts[1]).replace(" ", "_") + } else { + null + } + + val parts = text.split(":".toRegex()).toTypedArray() + if (parts.size > 1) { + val namespaceOrLanguage = parts[0] + if (Arrays.asList(*Locale.getISOLanguages()).contains(namespaceOrLanguage)) { + namespace = null + wikiSite = WikiSite(wiki.authority(), namespaceOrLanguage) + } else { + wikiSite = wiki + namespace = namespaceOrLanguage + } + this.text = TextUtils.join(":", Arrays.copyOfRange(parts, 1, parts.size)) + } else { + wikiSite = wiki + namespace = null + this.text = parts[0] + } + + this.thumbUrl = thumbUrl + this.properties = properties + } + + /** + * Decodes a URL-encoded string into its UTF-8 equivalent. If the string cannot be decoded, the + * original string is returned. + * @param url The URL-encoded string that you wish to decode. + * @return The decoded string, or the input string if the decoding failed. + */ + private fun decodeURL(url: String): String { + try { + return URLDecoder.decode(url, "UTF-8") + } catch (e: IllegalArgumentException) { + // Swallow IllegalArgumentException (can happen with malformed encoding), and just + // return the original string. + Timber.d("URL decoding failed. String was: %s", url) + return url + } catch (e: UnsupportedEncodingException) { + throw RuntimeException(e) + } + } + + private fun getTextWithoutSpaces(): String = + text.replace(" ", "_") + + fun getConvertedText(): String = + if (convertedText == null) prefixedText else convertedText!! + + fun setConvertedText(convertedText: String?) { + this.convertedText = convertedText + } + + val displayText: String + get() = prefixedText.replace("_", " ") + + val displayTextWithoutNamespace: String + get() = text.replace("_", " ") + + fun hasProperties(): Boolean = + properties != null + + val isMainPage: Boolean + get() = properties != null && properties.isMainPage + + val isDisambiguationPage: Boolean + get() = properties != null && properties.isDisambiguationPage + + val canonicalUri: String + get() = getUriForDomain(wikiSite.authority()) + + val mobileUri: String + get() = getUriForDomain(wikiSite.mobileAuthority()) + + fun getUriForAction(action: String?): String { + try { + return String.format( + "%1\$s://%2\$s/w/index.php?title=%3\$s&action=%4\$s", + wikiSite.scheme(), + wikiSite.authority(), + URLEncoder.encode(prefixedText, "utf-8"), + action + ) + } catch (e: UnsupportedEncodingException) { + throw RuntimeException(e) + } + } + + // TODO: find a better way to check if the namespace is a ISO Alpha2 Code (two digits country code) + val prefixedText: String + get() = namespace?.let { addUnderscores(it) + ":" + getTextWithoutSpaces() } + ?: getTextWithoutSpaces() + + private fun addUnderscores(text: String): String = + text.replace(" ", "_") + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(namespace) + parcel.writeString(text) + parcel.writeString(fragment) + parcel.writeParcelable(wikiSite, flags) + parcel.writeParcelable(properties, flags) + parcel.writeString(thumbUrl) + parcel.writeString(description) + parcel.writeString(convertedText) + } + + override fun equals(o: Any?): Boolean { + if (o !is PageTitle) { + return false + } + + val other = o + // Not using namespace directly since that can be null + return normalizedEquals(other.prefixedText, prefixedText) && other.wikiSite.equals(wikiSite) + } + + // Compare two strings based on their normalized form, using the Unicode Normalization Form C. + // This should be used when comparing or verifying strings that will be exchanged between + // different platforms (iOS, desktop, etc) that may encode strings using inconsistent + // composition, especially for accents, diacritics, etc. + private fun normalizedEquals(str1: String?, str2: String?): Boolean { + if (str1 == null || str2 == null) { + return (str1 == null && str2 == null) + } + return (Normalizer.normalize(str1, Normalizer.Form.NFC) + == Normalizer.normalize(str2, Normalizer.Form.NFC)) + } + + override fun hashCode(): Int { + var result = prefixedText.hashCode() + result = 31 * result + wikiSite.hashCode() + return result + } + + override fun toString(): String = + prefixedText + + override fun describeContents(): Int = 0 + + private fun getUriForDomain(domain: String): String = try { + String.format( + "%1\$s://%2\$s/wiki/%3\$s%4\$s", + wikiSite.scheme(), + domain, + URLEncoder.encode(prefixedText, "utf-8"), + if ((fragment != null && fragment.length > 0)) ("#$fragment") else "" + ) + } catch (e: UnsupportedEncodingException) { + throw RuntimeException(e) + } + + private constructor(parcel: Parcel) { + namespace = parcel.readString() + text = parcel.readString()!! + fragment = parcel.readString() + wikiSite = parcel.readParcelable(WikiSite::class.java.classLoader)!! + properties = parcel.readParcelable(PageProperties::class.java.classLoader) + thumbUrl = parcel.readString() + description = parcel.readString() + convertedText = parcel.readString() + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): PageTitle { + return PageTitle(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + + /** + * Creates a new PageTitle object. + * Use this if you want to pass in a fragment portion separately from the title. + * + * @param prefixedText title of the page with optional namespace prefix + * @param fragment optional fragment portion + * @param wiki the wiki site the page belongs to + * @return a new PageTitle object matching the given input parameters + */ + fun withSeparateFragment( + prefixedText: String, + fragment: String?, wiki: WikiSite + ): PageTitle { + return if (TextUtils.isEmpty(fragment)) { + PageTitle(prefixedText, wiki, null, null as PageProperties?) + } else { + // TODO: this class needs some refactoring to allow passing in a fragment + // without having to do string manipulations. + PageTitle("$prefixedText#$fragment", wiki, null, null as PageProperties?) + } + } + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/media/MediaConverterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/media/MediaConverterTest.kt index e1e1b2ed7..8a3022a35 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/media/MediaConverterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/media/MediaConverterTest.kt @@ -42,22 +42,22 @@ class MediaConverterTest { @Test fun testConvertIfThumbUrlBlank() { - Mockito.`when`(imageInfo.metadata).thenReturn(metadata) - Mockito.`when`(imageInfo.thumbUrl).thenReturn("") - Mockito.`when`(imageInfo.originalUrl).thenReturn("originalUrl") - Mockito.`when`(imageInfo.metadata?.licenseUrl()).thenReturn("licenseUrl") - Mockito.`when`(imageInfo.metadata?.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss") + Mockito.`when`(imageInfo.getMetadata()).thenReturn(metadata) + Mockito.`when`(imageInfo.getThumbUrl()).thenReturn("") + Mockito.`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl") + Mockito.`when`(imageInfo.getMetadata()?.licenseUrl()).thenReturn("licenseUrl") + Mockito.`when`(imageInfo.getMetadata()?.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss") media = mediaConverter.convert(page, entity, imageInfo) assertEquals(media.thumbUrl, media.imageUrl, "originalUrl") } @Test fun testConvertIfThumbUrlNotBlank() { - Mockito.`when`(imageInfo.metadata).thenReturn(metadata) - Mockito.`when`(imageInfo.thumbUrl).thenReturn("thumbUrl") - Mockito.`when`(imageInfo.originalUrl).thenReturn("originalUrl") - Mockito.`when`(imageInfo.metadata?.licenseUrl()).thenReturn("licenseUrl") - Mockito.`when`(imageInfo.metadata?.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss") + Mockito.`when`(imageInfo.getMetadata()).thenReturn(metadata) + Mockito.`when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl") + Mockito.`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl") + Mockito.`when`(imageInfo.getMetadata()?.licenseUrl()).thenReturn("licenseUrl") + Mockito.`when`(imageInfo.getMetadata()?.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss") media = mediaConverter.convert(page, entity, imageInfo) assertEquals(media.thumbUrl, "thumbUrl") } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt index 7d7c668a8..2ce15aaa7 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt @@ -123,11 +123,11 @@ class NotificationClientTest { setTimestamp(Notification.Timestamp().apply { setUtciso8601(timestamp) }) contents = Notification.Contents().apply { - setCompactHeader(compactHeader) + this.compactHeader = compactHeader links = Notification.Links().apply { setPrimary(GsonUtil.defaultGson.toJsonTree( - Notification.Link().apply { setUrl(primaryUrl) } + Notification.Link().apply { url = primaryUrl } )) } }