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:
parent
0def7e7230
commit
22834431ca
30 changed files with 624 additions and 510 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
135
app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.kt
Normal file
135
app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
74
app/src/main/java/com/keylesspalace/tusky/util/PairedList.kt
Normal file
74
app/src/main/java/com/keylesspalace/tusky/util/PairedList.kt
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.kt
Normal file
67
app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
102
app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.kt
Normal file
102
app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.kt
Normal 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())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue