Convert util/{HttpHeaderLink,PairedList,TimestampUtils,ThemeUtils} to Kotlin (#3046)

* Fix off-by-one error in HttpHeaderLink

Link headers with multiple URLs with multiple parameters were being parsed
incorrectly.

Detected by adding unit tests ahead of converting to Kotlin.

* Convert util/HttpHeaderLink from Java to Kotlin

* Convert util/ThemeUtils from Java to Kotlin

* Convert util/TimestampUtils from Java to Kotlin

* Add tests for PairedList

* Convert util/PairedList from Java to Kotlin

* Implement feedback from PR

* Relicense as GPL
This commit is contained in:
Nik Clayton 2022-12-31 13:01:35 +01:00 committed by GitHub
commit 22834431ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 624 additions and 510 deletions

View file

@ -1,162 +0,0 @@
/* Written in 2017 by Andrew Dawson
*
* To the extent possible under law, the author(s) have dedicated all copyright and related and
* neighboring rights to this software to the public domain worldwide. This software is distributed
* without any warranty.
*
* You should have received a copy of the CC0 Public Domain Dedication along with this software.
* If not, see <http://creativecommons.org/publicdomain/zero/1.0/>. */
package com.keylesspalace.tusky.util;
import android.net.Uri;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* Represents one link and its parameters from the link header of an HTTP message.
*
* @see <a href="https://tools.ietf.org/html/rfc5988">RFC5988</a>
*/
public class HttpHeaderLink {
private static class Parameter {
public String name;
public String value;
}
private List<Parameter> parameters;
public Uri uri;
private HttpHeaderLink(String uri) {
this.uri = Uri.parse(uri);
this.parameters = new ArrayList<>();
}
private static int findAny(String s, int fromIndex, char[] set) {
for (int i = fromIndex; i < s.length(); i++) {
char c = s.charAt(i);
for (char member : set) {
if (c == member) {
return i;
}
}
}
return -1;
}
private static int findEndOfQuotedString(String line, int start) {
for (int i = start; i < line.length(); i++) {
char c = line.charAt(i);
if (c == '\\') {
i += 1;
} else if (c == '"') {
return i;
}
}
return -1;
}
private static class ValueResult {
String value;
int end;
ValueResult() {
end = -1;
}
void setValue(String value) {
value = value.trim();
if (!value.isEmpty()) {
this.value = value;
}
}
}
private static ValueResult parseValue(String line, int start) {
ValueResult result = new ValueResult();
int foundIndex = findAny(line, start, new char[] {';', ',', '"'});
if (foundIndex == -1) {
result.setValue(line.substring(start));
return result;
}
char c = line.charAt(foundIndex);
if (c == ';' || c == ',') {
result.end = foundIndex;
result.setValue(line.substring(start, foundIndex));
return result;
} else {
int quoteEnd = findEndOfQuotedString(line, foundIndex + 1);
if (quoteEnd == -1) {
quoteEnd = line.length();
}
result.end = quoteEnd;
result.setValue(line.substring(foundIndex + 1, quoteEnd));
return result;
}
}
private static int parseParameters(String line, int start, HttpHeaderLink link) {
for (int i = start; i < line.length(); i++) {
int foundIndex = findAny(line, i, new char[] {'=', ','});
if (foundIndex == -1) {
return -1;
} else if (line.charAt(foundIndex) == ',') {
return foundIndex;
}
Parameter parameter = new Parameter();
parameter.name = line.substring(line.indexOf(';', i) + 1, foundIndex).trim();
link.parameters.add(parameter);
ValueResult result = parseValue(line, foundIndex);
parameter.value = result.value;
if (result.end == -1) {
return -1;
} else {
i = result.end;
}
}
return -1;
}
/**
* @param line the entire link header, not including the initial "Link:"
* @return all links found in the header
*/
public static List<HttpHeaderLink> parse(@Nullable String line) {
List<HttpHeaderLink> linkList = new ArrayList<>();
if (line != null) {
for (int i = 0; i < line.length(); i++) {
int uriEnd = line.indexOf('>', i);
String uri = line.substring(line.indexOf('<', i) + 1, uriEnd);
HttpHeaderLink link = new HttpHeaderLink(uri);
linkList.add(link);
int parseEnd = parseParameters(line, uriEnd, link);
if (parseEnd == -1) {
break;
} else {
i = parseEnd;
}
}
}
return linkList;
}
/**
* @param links intended to be those returned by parse()
* @param relationType of the parameter "rel", commonly "next" or "prev"
* @return the link matching the given relation type
*/
@Nullable
public static HttpHeaderLink findByRelationType(List<HttpHeaderLink> links,
String relationType) {
for (HttpHeaderLink link : links) {
for (Parameter parameter : link.parameters) {
if (parameter.name.equals("rel") && parameter.value.equals(relationType)) {
return link;
}
}
}
return null;
}
}

View file

@ -0,0 +1,135 @@
/* Copyright 2022 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>.
*/
package com.keylesspalace.tusky.util
import android.net.Uri
import androidx.annotation.VisibleForTesting
import androidx.core.net.toUri
/**
* Represents one link and its parameters from the link header of an HTTP message.
*
* @see [RFC5988](https://tools.ietf.org/html/rfc5988)
*/
class HttpHeaderLink @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) constructor(
uri: String
) {
data class Parameter(val name: String, val value: String?)
private val parameters: MutableList<Parameter> = ArrayList()
val uri: Uri = uri.toUri()
private data class ValueResult(val value: String, val end: Int = -1)
companion object {
private fun findEndOfQuotedString(line: String, start: Int): Int {
var i = start
while (i < line.length) {
val c = line[i]
if (c == '\\') {
i += 1
} else if (c == '"') {
return i
}
i++
}
return -1
}
private fun parseValue(line: String, start: Int): ValueResult {
val foundIndex = line.indexOfAny(charArrayOf(';', ',', '"'), start, false)
if (foundIndex == -1) {
return ValueResult(line.substring(start).trim())
}
val c = line[foundIndex]
return if (c == ';' || c == ',') {
ValueResult(line.substring(start, foundIndex).trim(), foundIndex)
} else {
var quoteEnd = findEndOfQuotedString(line, foundIndex + 1)
if (quoteEnd == -1) {
quoteEnd = line.length
}
ValueResult(line.substring(foundIndex + 1, quoteEnd).trim(), quoteEnd)
}
}
private fun parseParameters(line: String, start: Int, link: HttpHeaderLink): Int {
var i = start
while (i < line.length) {
val foundIndex = line.indexOfAny(charArrayOf('=', ','), i, false)
if (foundIndex == -1) {
return -1
} else if (line[foundIndex] == ',') {
return foundIndex
}
val name = line.substring(line.indexOf(';', i) + 1, foundIndex).trim()
val result = parseValue(line, foundIndex)
val value = result.value
val parameter = Parameter(name, value)
link.parameters.add(parameter)
i = if (result.end == -1) {
return -1
} else {
result.end
}
}
return -1
}
/**
* @param line the entire link header, not including the initial "Link:"
* @return all links found in the header
*/
fun parse(line: String?): List<HttpHeaderLink> {
val links: MutableList<HttpHeaderLink> = mutableListOf()
line ?: return links
var i = 0
while (i < line.length) {
val uriEnd = line.indexOf('>', i)
val uri = line.substring(line.indexOf('<', i) + 1, uriEnd)
val link = HttpHeaderLink(uri)
links.add(link)
val parseEnd = parseParameters(line, uriEnd, link)
i = if (parseEnd == -1) {
break
} else {
parseEnd
}
i++
}
return links
}
/**
* @param links intended to be those returned by parse()
* @param relationType of the parameter "rel", commonly "next" or "prev"
* @return the link matching the given relation type
*/
fun findByRelationType(
links: List<HttpHeaderLink>,
relationType: String
): HttpHeaderLink? {
return links.find { link ->
link.parameters.any { parameter ->
parameter.name == "rel" && parameter.value == relationType
}
}
}
}
}

View file

@ -16,7 +16,9 @@
package com.keylesspalace.tusky.util
import android.content.Context
import android.graphics.Color
import androidx.annotation.Px
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
@ -26,6 +28,6 @@ import com.mikepenz.iconics.utils.sizePx
fun makeIcon(context: Context, icon: GoogleMaterial.Icon, @Px iconSize: Int): IconicsDrawable {
return IconicsDrawable(context, icon).apply {
sizePx = iconSize
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
colorInt = MaterialColors.getColor(context, R.attr.iconColor, Color.BLACK)
}
}

View file

@ -19,6 +19,7 @@ package com.keylesspalace.tusky.util
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.text.SpannableStringBuilder
import android.text.Spanned
@ -33,6 +34,7 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.entity.Status.Mention
@ -251,9 +253,9 @@ private fun openLinkInBrowser(uri: Uri?, context: Context) {
* @param context context
*/
private fun openLinkInCustomTab(uri: Uri, context: Context) {
val toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface)
val navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor)
val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor)
val toolbarColor = MaterialColors.getColor(context, R.attr.colorSurface, Color.BLACK)
val navigationbarColor = MaterialColors.getColor(context, android.R.attr.navigationBarColor, Color.BLACK)
val navigationbarDividerColor = MaterialColors.getColor(context, R.attr.dividerColor, Color.BLACK)
val colorSchemeParams = CustomTabColorSchemeParams.Builder()
.setToolbarColor(toolbarColor)
.setNavigationBarColor(navigationbarColor)

View file

@ -1,94 +0,0 @@
package com.keylesspalace.tusky.util;
import androidx.annotation.Nullable;
import androidx.arch.core.util.Function;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
/**
* This list implementation can help to keep two lists in sync - like real models and view models.
* Every operation on the main list triggers update of the supplementary list (but not vice versa).
* This makes sure that the main list is always the source of truth.
* Main list is projected to the supplementary list by the passed mapper function.
* Paired list is newer actually exposed and clients are provided with {@code getPairedCopy()},
* {@code getPairedItem()} and {@code setPairedItem()}. This prevents modifications of the
* supplementary list size so lists are always have the same length.
* This implementation will not try to recover from exceptional cases so lists may be out of sync
* after the exception.
*
* It is most useful with immutable data because we cannot track changes inside stored objects.
* @param <T> type of elements in the main list
* @param <V> type of elements in supplementary list
*/
public final class PairedList<T, V> extends AbstractList<T> {
private final List<T> main = new ArrayList<>();
private final List<V> synced = new ArrayList<>();
private final Function<T, ? extends V> mapper;
/**
* Construct new paired list. Main and supplementary lists will be empty.
* @param mapper Function, which will be used to translate items from the main list to the
* supplementary one.
*/
public PairedList(Function<T, ? extends V> mapper) {
this.mapper = mapper;
}
public List<V> getPairedCopy() {
return new ArrayList<>(synced);
}
public V getPairedItem(int index) {
return synced.get(index);
}
@Nullable
public V getPairedItemOrNull(int index) {
if (index >= 0 && index < synced.size()) {
return synced.get(index);
} else {
return null;
}
}
public void setPairedItem(int index, V element) {
synced.set(index, element);
}
@Override
public T get(int index) {
return main.get(index);
}
@Override
public T set(int index, T element) {
synced.set(index, mapper.apply(element));
return main.set(index, element);
}
@Override
public boolean add(T t) {
synced.add(mapper.apply(t));
return main.add(t);
}
@Override
public void add(int index, T element) {
synced.add(index, mapper.apply(element));
main.add(index, element);
}
@Override
public T remove(int index) {
synced.remove(index);
return main.remove(index);
}
@Override
public int size() {
return main.size();
}
}

View file

@ -0,0 +1,74 @@
package com.keylesspalace.tusky.util
import androidx.arch.core.util.Function
/**
* This list implementation can help to keep two lists in sync - like real models and view models.
*
* Every operation on the main list triggers update of the supplementary list (but not vice versa).
*
* This makes sure that the main list is always the source of truth.
*
* Main list is projected to the supplementary list by the passed mapper function.
*
* Paired list is newer actually exposed and clients are provided with `getPairedCopy()`,
* `getPairedItem()` and `setPairedItem()`. This prevents modifications of the
* supplementary list size so lists are always have the same length.
*
* This implementation will not try to recover from exceptional cases so lists may be out of sync
* after the exception.
*
* It is most useful with immutable data because we cannot track changes inside stored objects.
*
* @param T type of elements in the main list
* @param V type of elements in supplementary list
* @param mapper Function, which will be used to translate items from the main list to the
* supplementary one.
* @constructor
*/
class PairedList<T, V> (private val mapper: Function<T, out V>) : AbstractMutableList<T>() {
private val main: MutableList<T> = ArrayList()
private val synced: MutableList<V> = ArrayList()
val pairedCopy: List<V>
get() = ArrayList(synced)
fun getPairedItem(index: Int): V {
return synced[index]
}
fun getPairedItemOrNull(index: Int): V? {
return synced.getOrNull(index)
}
fun setPairedItem(index: Int, element: V) {
synced[index] = element
}
override fun get(index: Int): T {
return main[index]
}
override fun set(index: Int, element: T): T {
synced[index] = mapper.apply(element)
return main.set(index, element)
}
override fun add(element: T): Boolean {
synced.add(mapper.apply(element))
return main.add(element)
}
override fun add(index: Int, element: T) {
synced.add(index, mapper.apply(element))
main.add(index, element)
}
override fun removeAt(index: Int): T {
synced.removeAt(index)
return main.removeAt(index)
}
override val size: Int
get() = main.size
}

View file

@ -16,6 +16,7 @@
package com.keylesspalace.tusky.util
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.text.InputFilter
import android.text.TextUtils
@ -24,6 +25,7 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import com.bumptech.glide.Glide
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Emoji
@ -85,7 +87,7 @@ class StatusViewHelper(private val itemView: View) {
return
}
val mediaPreviewUnloaded = ColorDrawable(ThemeUtils.getColor(context, R.attr.colorBackgroundAccent))
val mediaPreviewUnloaded = ColorDrawable(MaterialColors.getColor(context, R.attr.colorBackgroundAccent, Color.BLACK))
val n = min(attachments.size, Status.MAX_MEDIA_ATTACHMENTS)
@ -292,7 +294,7 @@ class StatusViewHelper(private val itemView: View) {
if (useAbsoluteTime) {
context.getString(R.string.poll_info_time_absolute, absoluteTimeFormatter.format(poll.expiresAt, false))
} else {
TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp)
formatPollDuration(context, poll.expiresAt!!.time, timestamp)
}
}

View file

@ -1,83 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;
/**
* Provides runtime compatibility to obtain theme information and re-theme views, especially where
* the ability to do so is not supported in resource files.
*/
public class ThemeUtils {
public static final String APP_THEME_DEFAULT = ThemeUtils.THEME_NIGHT;
private static final String THEME_NIGHT = "night";
private static final String THEME_DAY = "day";
private static final String THEME_BLACK = "black";
private static final String THEME_AUTO = "auto";
private static final String THEME_SYSTEM = "auto_system";
@ColorInt
public static int getColor(@NonNull Context context, @AttrRes int attribute) {
TypedValue value = new TypedValue();
if (context.getTheme().resolveAttribute(attribute, value, true)) {
return value.data;
} else {
return Color.BLACK;
}
}
public static int getDimension(@NonNull Context context, @AttrRes int attribute) {
TypedArray array = context.obtainStyledAttributes(new int[] { attribute });
int dimen = array.getDimensionPixelSize(0, -1);
array.recycle();
return dimen;
}
public static void setDrawableTint(Context context, Drawable drawable, @AttrRes int attribute) {
drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN);
}
public static void setAppNightMode(String flavor) {
switch (flavor) {
default:
case THEME_NIGHT:
case THEME_BLACK:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
break;
case THEME_DAY:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
break;
case THEME_AUTO:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_TIME);
break;
case THEME_SYSTEM:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
break;
}
}
}

View file

@ -0,0 +1,67 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
@file:JvmName("ThemeUtils")
package com.keylesspalace.tusky.util
import android.content.Context
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import androidx.annotation.AttrRes
import androidx.appcompat.app.AppCompatDelegate
import com.google.android.material.color.MaterialColors
/**
* Provides runtime compatibility to obtain theme information and re-theme views, especially where
* the ability to do so is not supported in resource files.
*/
private const val THEME_NIGHT = "night"
private const val THEME_DAY = "day"
private const val THEME_BLACK = "black"
private const val THEME_AUTO = "auto"
private const val THEME_SYSTEM = "auto_system"
const val APP_THEME_DEFAULT = THEME_NIGHT
fun getDimension(context: Context, @AttrRes attribute: Int): Int {
val array = context.obtainStyledAttributes(intArrayOf(attribute))
val dimen = array.getDimensionPixelSize(0, -1)
array.recycle()
return dimen
}
fun setDrawableTint(context: Context, drawable: Drawable, @AttrRes attribute: Int) {
drawable.setColorFilter(
MaterialColors.getColor(context, attribute, Color.BLACK),
PorterDuff.Mode.SRC_IN
)
}
fun setAppNightMode(flavor: String?) {
when (flavor) {
THEME_NIGHT, THEME_BLACK -> AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_YES
)
THEME_DAY -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
THEME_AUTO -> AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_AUTO_TIME
)
THEME_SYSTEM -> AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
)
else -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
}

View file

@ -1,108 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util;
import android.content.Context;
import com.keylesspalace.tusky.R;
public class TimestampUtils {
private static final long SECOND_IN_MILLIS = 1000;
private static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60;
private static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60;
private static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24;
private static final long YEAR_IN_MILLIS = DAY_IN_MILLIS * 365;
/**
* This is a rough duplicate of {@link android.text.format.DateUtils#getRelativeTimeSpanString},
* but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough.
*/
public static String getRelativeTimeSpanString(Context context, long then, long now) {
long span = now - then;
boolean future = false;
if (Math.abs(span) < SECOND_IN_MILLIS) {
return context.getString(R.string.status_created_at_now);
}
else if (span < 0) {
future = true;
span = -span;
}
int format;
if (span < MINUTE_IN_MILLIS) {
span /= SECOND_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_seconds;
} else {
format = R.string.abbreviated_seconds_ago;
}
} else if (span < HOUR_IN_MILLIS) {
span /= MINUTE_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_minutes;
} else {
format = R.string.abbreviated_minutes_ago;
}
} else if (span < DAY_IN_MILLIS) {
span /= HOUR_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_hours;
} else {
format = R.string.abbreviated_hours_ago;
}
} else if (span < YEAR_IN_MILLIS) {
span /= DAY_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_days;
} else {
format = R.string.abbreviated_days_ago;
}
} else {
span /= YEAR_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_years;
} else {
format = R.string.abbreviated_years_ago;
}
}
return context.getString(format, span);
}
public static String formatPollDuration(Context context, long then, long now) {
long span = then - now;
if (span < 0) {
span = 0;
}
int format;
if (span < MINUTE_IN_MILLIS) {
span /= SECOND_IN_MILLIS;
format = R.plurals.poll_timespan_seconds;
} else if (span < HOUR_IN_MILLIS) {
span /= MINUTE_IN_MILLIS;
format = R.plurals.poll_timespan_minutes;
} else if (span < DAY_IN_MILLIS) {
span /= HOUR_IN_MILLIS;
format = R.plurals.poll_timespan_hours;
} else {
span /= DAY_IN_MILLIS;
format = R.plurals.poll_timespan_days;
}
return context.getResources().getQuantityString(format, (int) span, (int) span);
}
}

View file

@ -0,0 +1,102 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
@file:JvmName("TimestampUtils")
package com.keylesspalace.tusky.util
import android.content.Context
import com.keylesspalace.tusky.R
import kotlin.math.abs
private const val SECOND_IN_MILLIS: Long = 1000
private const val MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60
private const val HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60
private const val DAY_IN_MILLIS = HOUR_IN_MILLIS * 24
private const val YEAR_IN_MILLIS = DAY_IN_MILLIS * 365
/**
* This is a rough duplicate of [android.text.format.DateUtils.getRelativeTimeSpanString],
* but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough.
*/
fun getRelativeTimeSpanString(context: Context, then: Long, now: Long): String {
var span = now - then
var future = false
if (abs(span) < SECOND_IN_MILLIS) {
return context.getString(R.string.status_created_at_now)
} else if (span < 0) {
future = true
span = -span
}
val format: Int
if (span < MINUTE_IN_MILLIS) {
span /= SECOND_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_seconds
} else {
R.string.abbreviated_seconds_ago
}
} else if (span < HOUR_IN_MILLIS) {
span /= MINUTE_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_minutes
} else {
R.string.abbreviated_minutes_ago
}
} else if (span < DAY_IN_MILLIS) {
span /= HOUR_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_hours
} else {
R.string.abbreviated_hours_ago
}
} else if (span < YEAR_IN_MILLIS) {
span /= DAY_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_days
} else {
R.string.abbreviated_days_ago
}
} else {
span /= YEAR_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_years
} else {
R.string.abbreviated_years_ago
}
}
return context.getString(format, span)
}
fun formatPollDuration(context: Context, then: Long, now: Long): String {
var span = then - now
if (span < 0) {
span = 0
}
val format: Int
if (span < MINUTE_IN_MILLIS) {
span /= SECOND_IN_MILLIS
format = R.plurals.poll_timespan_seconds
} else if (span < HOUR_IN_MILLIS) {
span /= MINUTE_IN_MILLIS
format = R.plurals.poll_timespan_minutes
} else if (span < DAY_IN_MILLIS) {
span /= HOUR_IN_MILLIS
format = R.plurals.poll_timespan_hours
} else {
span /= DAY_IN_MILLIS
format = R.plurals.poll_timespan_days
}
return context.resources.getQuantityString(format, span.toInt(), span.toInt())
}