convert DisplayUtils to kt

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2024-03-21 15:19:55 +01:00 committed by Andy Scherzinger
parent bb6af562cc
commit cc19157867
8 changed files with 547 additions and 582 deletions

View File

@ -110,7 +110,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
@Suppress("NestedBlockDepth", "ComplexMethod", "LongMethod") @Suppress("NestedBlockDepth", "ComplexMethod", "LongMethod")
override fun onBind(message: ChatMessage) { override fun onBind(message: ChatMessage) {
super.onBind(message) super.onBind(message)
image.minimumHeight = DisplayUtils.convertDpToPixel(MIN_IMAGE_HEIGHT, context).toInt() image.minimumHeight = DisplayUtils.convertDpToPixel(MIN_IMAGE_HEIGHT, context!!).toInt()
time.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) time.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)

View File

@ -90,7 +90,7 @@ class SystemMessageViewHolder(itemView: View) : MessageHolders.IncomingTextMessa
} else { } else {
individualMap["name"] individualMap["name"]
} }
messageString = DisplayUtils.searchAndColor(messageString, searchText, mentionColor) messageString = DisplayUtils.searchAndColor(messageString, searchText!!, mentionColor)
} }
} }
} }

View File

@ -2142,7 +2142,7 @@ class ChatActivity :
true true
) )
if (DisplayUtils.isDarkModeOn(supportActionBar?.themedContext)) { if (DisplayUtils.isDarkModeOn(supportActionBar?.themedContext!!)) {
url = "$url/dark" url = "$url/dark"
} }

View File

@ -70,6 +70,7 @@ fun ImageView.loadConversationAvatar(
) )
} }
@Suppress("ReturnCount")
fun ImageView.loadConversationAvatar( fun ImageView.loadConversationAvatar(
user: User, user: User,
conversation: ConversationModel, conversation: ConversationModel,

View File

@ -281,7 +281,7 @@ class LocationPickerActivity :
locationOverlay.setPersonHotspot(PERSON_HOT_SPOT_X, PERSON_HOT_SPOT_Y) locationOverlay.setPersonHotspot(PERSON_HOT_SPOT_X, PERSON_HOT_SPOT_Y)
locationOverlay.setPersonIcon( locationOverlay.setPersonIcon(
DisplayUtils.getBitmap( DisplayUtils.getBitmap(
ResourcesCompat.getDrawable(resources!!, R.drawable.current_location_circle, null) ResourcesCompat.getDrawable(resources!!, R.drawable.current_location_circle, null)!!
) )
) )
binding.map.overlays.add(locationOverlay) binding.map.overlays.add(locationOverlay)

View File

@ -1,576 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Andy Scherzinger
* @author Tim Krüger
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.utils;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.util.TypedValue;
import android.view.View;
import android.view.Window;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import com.google.android.material.chip.ChipDrawable;
import com.nextcloud.talk.R;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.data.user.model.User;
import com.nextcloud.talk.events.UserMentionClickEvent;
import com.nextcloud.talk.extensions.ImageViewExtensionsKt;
import com.nextcloud.talk.ui.theme.ViewThemeUtils;
import com.nextcloud.talk.utils.text.Spans;
import org.greenrobot.eventbus.EventBus;
import java.text.DateFormat;
import java.util.Date;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.XmlRes;
import androidx.core.content.ContextCompat;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.graphics.ColorUtils;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.emoji2.text.EmojiCompat;
import coil.Coil;
import coil.request.ImageRequest;
import coil.target.Target;
import coil.transform.CircleCropTransformation;
import third.parties.fresco.BetterImageSpan;
import static com.nextcloud.talk.utils.FileSortOrder.SORT_A_TO_Z_ID;
import static com.nextcloud.talk.utils.FileSortOrder.SORT_BIG_TO_SMALL_ID;
import static com.nextcloud.talk.utils.FileSortOrder.SORT_NEW_TO_OLD_ID;
import static com.nextcloud.talk.utils.FileSortOrder.SORT_OLD_TO_NEW_ID;
import static com.nextcloud.talk.utils.FileSortOrder.SORT_SMALL_TO_BIG_ID;
import static com.nextcloud.talk.utils.FileSortOrder.SORT_Z_TO_A_ID;
public class DisplayUtils {
private static final String TAG = DisplayUtils.class.getSimpleName();
private static final int INDEX_LUMINATION = 2;
private static final double MAX_LIGHTNESS = 0.92;
private static final String TWITTER_HANDLE_PREFIX = "@";
private static final String HTTP_PROTOCOL = "http://";
private static final String HTTPS_PROTOCOL = "https://";
private static final int DATE_TIME_PARTS_SIZE = 2;
public static Boolean isDarkModeOn(Context context) {
int currentNightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
}
public static void setClickableString(String string, String url, TextView textView) {
SpannableString spannableString = new SpannableString(string);
spannableString.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
NextcloudTalkApplication
.Companion
.getSharedApplication()
.getApplicationContext()
.startActivity(browserIntent);
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
}
}, 0, string.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
textView.setText(spannableString);
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
public static Bitmap getBitmap(Drawable drawable) {
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(),
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
public static float convertDpToPixel(float dp, Context context) {
return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
context.getResources().getDisplayMetrics()) + 0.5f);
}
public static float convertPixelToDp(float px, Context context) {
return px / context.getResources().getDisplayMetrics().density;
}
public static Drawable getTintedDrawable(Resources res, @DrawableRes int drawableResId, @ColorRes int colorResId) {
Drawable drawable = ResourcesCompat.getDrawable(res, drawableResId, null);
int color = res.getColor(colorResId);
if (drawable != null) {
drawable.setTint(color);
}
return drawable;
}
public static Drawable getDrawableForMentionChipSpan(Context context,
String id,
String roomToken,
CharSequence label,
User conversationUser,
String type,
@XmlRes int chipResource,
@Nullable EditText emojiEditText,
ViewThemeUtils viewThemeUtils,
Boolean isFederated) {
ChipDrawable chip = ChipDrawable.createFromResource(context, chipResource);
chip.setText(EmojiCompat.get().process(label));
chip.setEllipsize(TextUtils.TruncateAt.MIDDLE);
if (chipResource == R.xml.chip_you) {
viewThemeUtils.material.colorChipDrawable(context, chip);
}
Configuration config = context.getResources().getConfiguration();
chip.setLayoutDirection(config.getLayoutDirection());
int drawable;
boolean isCallOrGroup =
"call".equals(type) || "calls".equals(type) || "groups".equals(type) || "user-group".equals(type);
if (!isCallOrGroup) {
if (chipResource == R.xml.chip_you) {
drawable = R.drawable.mention_chip;
} else {
drawable = R.drawable.accent_circle;
}
chip.setChipIconResource(drawable);
} else {
chip.setChipIconResource(R.drawable.ic_circular_group);
}
chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight());
if (!isCallOrGroup) {
String url = ApiUtils.getUrlForAvatar(conversationUser.getBaseUrl(), id, false);
if ("guests".equals(type) || "guest".equals(type)) {
url = ApiUtils.getUrlForGuestAvatar(
conversationUser.getBaseUrl(),
String.valueOf(label), true);
}
if (isFederated) {
int darkTheme = (DisplayUtils.isDarkModeOn(context))? 1 : 0;
url = ApiUtils.getUrlForFederatedAvatar(Objects.requireNonNull(conversationUser.getBaseUrl()),
roomToken, id,
darkTheme, false);
}
ImageRequest imageRequest = new ImageRequest.Builder(context)
.data(url)
.crossfade(true)
.transformations(new CircleCropTransformation())
.target(new Target() {
@Override
public void onStart(@Nullable Drawable drawable) {
}
@Override
public void onError(@Nullable Drawable drawable) {
chip.setChipIcon(drawable);
}
@Override
public void onSuccess(@NonNull Drawable drawable) {
chip.setChipIcon(drawable);
// A hack to refresh the chip icon
if (emojiEditText != null) {
emojiEditText.post(() -> emojiEditText.setTextKeepState(
emojiEditText.getText(),
TextView.BufferType.SPANNABLE));
}
}
})
.build();
Coil.imageLoader(context).enqueue(imageRequest);
}
return chip;
}
public static Spannable searchAndReplaceWithMentionSpan(String key, Context context, Spanned text,
String id, String roomToken,
String label, String type,
User conversationUser,
@XmlRes int chipXmlRes,
ViewThemeUtils viewThemeUtils,
Boolean isFederated) {
Spannable spannableString = new SpannableString(text);
String stringText = text.toString();
String keyWithBrackets = "{" + key + "}";
Matcher m = Pattern.compile(keyWithBrackets, Pattern.CASE_INSENSITIVE | Pattern.LITERAL | Pattern.MULTILINE)
.matcher(spannableString);
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
EventBus.getDefault().post(new UserMentionClickEvent(id));
}
};
int lastStartIndex = 0;
Spans.MentionChipSpan mentionChipSpan;
while (m.find()) {
int start = stringText.indexOf(m.group(), lastStartIndex);
int end = start + m.group().length();
lastStartIndex = end;
Drawable drawableForChip = DisplayUtils.getDrawableForMentionChipSpan(context,
id,
roomToken,
label,
conversationUser,
type,
chipXmlRes,
null,
viewThemeUtils,
isFederated);
mentionChipSpan = new Spans.MentionChipSpan(drawableForChip,
BetterImageSpan.ALIGN_CENTER,
id,
label);
spannableString.setSpan(mentionChipSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
if (chipXmlRes == R.xml.chip_you) {
spannableString.setSpan(
viewThemeUtils.talk.themeForegroundColorSpan(context),
start,
end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if ("user".equals(type) && !conversationUser.getUserId().equals(id)) {
spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
return spannableString;
}
public static Spannable searchAndColor(Spannable text, String searchText, @ColorInt int color) {
Spannable spannableString = new SpannableString(text);
String stringText = text.toString();
if (TextUtils.isEmpty(text) || TextUtils.isEmpty(searchText)) {
return spannableString;
}
Matcher m = Pattern.compile(searchText,
Pattern.CASE_INSENSITIVE | Pattern.LITERAL | Pattern.MULTILINE)
.matcher(spannableString);
int textSize = NextcloudTalkApplication.Companion.getSharedApplication().getResources().getDimensionPixelSize(R.dimen
.chat_text_size);
int lastStartIndex = -1;
while (m.find()) {
int start = stringText.indexOf(m.group(), lastStartIndex);
int end = start + m.group().length();
lastStartIndex = end;
spannableString.setSpan(new ForegroundColorSpan(color), start, end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spannableString.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spannableString.setSpan(new AbsoluteSizeSpan(textSize), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return spannableString;
}
public static Drawable getMessageSelector(@ColorInt int normalColor,
@ColorInt int selectedColor,
@ColorInt int pressedColor,
@DrawableRes int shape) {
Drawable vectorDrawable = ContextCompat.getDrawable(NextcloudTalkApplication.Companion.getSharedApplication()
.getApplicationContext(),
shape);
Drawable drawable = DrawableCompat.wrap(vectorDrawable).mutate();
DrawableCompat.setTintList(
drawable,
new ColorStateList(
new int[][]{
new int[]{android.R.attr.state_selected},
new int[]{android.R.attr.state_pressed},
new int[]{-android.R.attr.state_pressed, -android.R.attr.state_selected}
},
new int[]{selectedColor, pressedColor, normalColor}
));
return drawable;
}
/**
* Sets the color of the status bar to {@code color}.
*
* @param activity activity
* @param color the color
*/
public static void applyColorToStatusBar(Activity activity, @ColorInt int color) {
Window window = activity.getWindow();
boolean isLightTheme = lightTheme(color);
if (window != null) {
View decor = window.getDecorView();
if (isLightTheme) {
int systemUiFlagLightStatusBar;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
systemUiFlagLightStatusBar = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR |
View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
} else {
systemUiFlagLightStatusBar = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
}
decor.setSystemUiVisibility(systemUiFlagLightStatusBar);
} else {
decor.setSystemUiVisibility(0);
}
window.setStatusBarColor(color);
}
}
/**
* Tests if light color is set
*
* @param color the color
* @return true if primaryColor is lighter than MAX_LIGHTNESS
*/
@SuppressWarnings("CLI_CONSTANT_LIST_INDEX")
public static boolean lightTheme(int color) {
float[] hsl = colorToHSL(color);
// spotbugs dislikes fixed index access
// which is enforced by having such an
// array from Android-API itself
return hsl[INDEX_LUMINATION] >= MAX_LIGHTNESS;
}
private static float[] colorToHSL(int color) {
float[] hsl = new float[3];
ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl);
return hsl;
}
public static void applyColorToNavigationBar(Window window, @ColorInt int color) {
window.setNavigationBarColor(color);
}
/**
* beautifies a given URL by removing any http/https protocol prefix.
*
* @param url to be beautified url
* @return beautified url
*/
public static String beautifyURL(@Nullable String url) {
if (TextUtils.isEmpty(url)) {
return "";
}
if (url.length() >= 7 && HTTP_PROTOCOL.equalsIgnoreCase(url.substring(0, 7))) {
return url.substring(HTTP_PROTOCOL.length()).trim();
}
if (url.length() >= 8 && HTTPS_PROTOCOL.equalsIgnoreCase(url.substring(0, 8))) {
return url.substring(HTTPS_PROTOCOL.length()).trim();
}
return url.trim();
}
/**
* beautifies a given twitter handle by prefixing it with an @ in case it is missing.
*
* @param handle to be beautified twitter handle
* @return beautified twitter handle
*/
public static String beautifyTwitterHandle(@Nullable String handle) {
if (handle != null) {
String trimmedHandle = handle.trim();
if (TextUtils.isEmpty(trimmedHandle)) {
return "";
}
if (trimmedHandle.startsWith(TWITTER_HANDLE_PREFIX)) {
return trimmedHandle;
} else {
return TWITTER_HANDLE_PREFIX + trimmedHandle;
}
} else {
return "";
}
}
public static void loadAvatarImage(User user, ImageView avatarImageView, boolean deleteCache) {
if (user != null && avatarImageView != null) {
String avatarId;
if (!TextUtils.isEmpty(user.getUserId())) {
avatarId = user.getUserId();
} else {
avatarId = user.getUsername();
}
if (avatarId != null) {
ImageViewExtensionsKt.loadUserAvatar(avatarImageView, user, avatarId, true, deleteCache);
}
}
}
public static @StringRes
int getSortOrderStringId(FileSortOrder sortOrder) {
switch (sortOrder.getName()) {
case SORT_Z_TO_A_ID:
return R.string.menu_item_sort_by_name_z_a;
case SORT_NEW_TO_OLD_ID:
return R.string.menu_item_sort_by_date_newest_first;
case SORT_OLD_TO_NEW_ID:
return R.string.menu_item_sort_by_date_oldest_first;
case SORT_BIG_TO_SMALL_ID:
return R.string.menu_item_sort_by_size_biggest_first;
case SORT_SMALL_TO_BIG_ID:
return R.string.menu_item_sort_by_size_smallest_first;
case SORT_A_TO_Z_ID:
default:
return R.string.menu_item_sort_by_name_a_z;
}
}
/**
* calculates the relative time string based on the given modification timestamp.
*
* @param context the app's context
* @param modificationTimestamp the UNIX timestamp of the file modification time in milliseconds.
* @return a relative time string
*/
public static CharSequence getRelativeTimestamp(Context context, long modificationTimestamp, boolean showFuture) {
return getRelativeDateTimeString(context,
modificationTimestamp,
DateUtils.SECOND_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
0,
showFuture);
}
public static CharSequence getRelativeDateTimeString(Context c,
long time,
long minResolution,
long transitionResolution,
int flags,
boolean showFuture) {
CharSequence dateString = "";
// in Future
if (!showFuture && time > System.currentTimeMillis()) {
return DisplayUtils.unixTimeToHumanReadable(time);
}
// < 60 seconds -> seconds ago
long diff = System.currentTimeMillis() - time;
if (diff > 0 && diff < 60 * 1000 && minResolution == DateUtils.SECOND_IN_MILLIS) {
return c.getString(R.string.secondsAgo);
} else {
dateString = DateUtils.getRelativeDateTimeString(c, time, minResolution, transitionResolution, flags);
}
String[] parts = dateString.toString().split(",");
if (parts.length == DATE_TIME_PARTS_SIZE) {
if (parts[1].contains(":") && !parts[0].contains(":")) {
return parts[0];
} else if (parts[0].contains(":") && !parts[1].contains(":")) {
return parts[1];
}
}
// dateString contains unexpected format. fallback: use relative date time string from android api as is.
return dateString.toString();
}
/**
* Converts Unix time to human readable format
*
* @param milliseconds that have passed since 01/01/1970
* @return The human readable time for the users locale
*/
public static String unixTimeToHumanReadable(long milliseconds) {
Date date = new Date(milliseconds);
DateFormat df = DateFormat.getDateTimeInstance();
return df.format(date);
}
public static String ellipsize(String text, int maxLength) {
if (text.length() > maxLength) {
return text.substring(0, maxLength - 1) + "";
}
return text;
}
}

View File

@ -0,0 +1,540 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Andy Scherzinger
* @author Tim Krüger
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.utils
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.text.Spannable
import android.text.SpannableString
import android.text.Spanned
import android.text.TextPaint
import android.text.TextUtils
import android.text.format.DateUtils
import android.text.method.LinkMovementMethod
import android.text.style.AbsoluteSizeSpan
import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.util.TypedValue
import android.view.View
import android.view.Window
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.annotation.XmlRes
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.DrawableCompat
import androidx.emoji2.text.EmojiCompat
import coil.Coil.imageLoader
import coil.request.ImageRequest
import coil.target.Target
import coil.transform.CircleCropTransformation
import com.google.android.material.chip.ChipDrawable
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.events.UserMentionClickEvent
import com.nextcloud.talk.extensions.loadUserAvatar
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils.getUrlForAvatar
import com.nextcloud.talk.utils.ApiUtils.getUrlForFederatedAvatar
import com.nextcloud.talk.utils.ApiUtils.getUrlForGuestAvatar
import com.nextcloud.talk.utils.text.Spans.MentionChipSpan
import org.greenrobot.eventbus.EventBus
import third.parties.fresco.BetterImageSpan
import java.text.DateFormat
import java.util.Date
import java.util.regex.Pattern
object DisplayUtils {
private val TAG = DisplayUtils::class.java.getSimpleName()
private const val INDEX_LUMINATION = 2
private const val MAX_LIGHTNESS = 0.92
private const val TWITTER_HANDLE_PREFIX = "@"
private const val HTTP_PROTOCOL = "http://"
private const val HTTPS_PROTOCOL = "https://"
private const val DATE_TIME_PARTS_SIZE = 2
fun isDarkModeOn(context: Context): Boolean {
val currentNightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return currentNightMode == Configuration.UI_MODE_NIGHT_YES
}
fun setClickableString(string: String, url: String?, textView: TextView) {
val spannableString = SpannableString(string)
spannableString.setSpan(
object : ClickableSpan() {
override fun onClick(widget: View) {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
sharedApplication!!.applicationContext.startActivity(browserIntent)
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = false
}
},
0,
string.length,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
textView.text = spannableString
textView.movementMethod = LinkMovementMethod.getInstance()
}
fun getBitmap(drawable: Drawable): Bitmap {
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
@JvmStatic
fun convertDpToPixel(dp: Float, context: Context): Float {
return Math.round(
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp,
context.resources.displayMetrics
) + 0.5f
).toFloat()
}
fun convertPixelToDp(px: Float, context: Context): Float {
return px / context.resources.displayMetrics.density
}
fun getTintedDrawable(res: Resources, @DrawableRes drawableResId: Int, @ColorRes colorResId: Int): Drawable? {
val drawable = ResourcesCompat.getDrawable(res, drawableResId, null)
val color = res.getColor(colorResId)
drawable?.setTint(color)
return drawable
}
@JvmStatic
fun getDrawableForMentionChipSpan(
context: Context,
id: String?,
roomToken: String?,
label: CharSequence,
conversationUser: User,
type: String,
@XmlRes chipResource: Int,
emojiEditText: EditText?,
viewThemeUtils: ViewThemeUtils,
isFederated: Boolean
): Drawable {
val chip = ChipDrawable.createFromResource(context, chipResource)
chip.text = EmojiCompat.get().process(label)
chip.ellipsize = TextUtils.TruncateAt.MIDDLE
if (chipResource == R.xml.chip_you) {
viewThemeUtils.material.colorChipDrawable(context, chip)
}
val config = context.resources.configuration
chip.setLayoutDirection(config.layoutDirection)
val drawable: Int
val isCallOrGroup = "call" == type || "calls" == type || "groups" == type || "user-group" == type
if (!isCallOrGroup) {
drawable = if (chipResource == R.xml.chip_you) {
R.drawable.mention_chip
} else {
R.drawable.accent_circle
}
chip.setChipIconResource(drawable)
} else {
chip.setChipIconResource(R.drawable.ic_circular_group)
}
chip.setBounds(0, 0, chip.intrinsicWidth, chip.intrinsicHeight)
if (!isCallOrGroup) {
var url = getUrlForAvatar(conversationUser.baseUrl, id, false)
if ("guests" == type || "guest" == type) {
url = getUrlForGuestAvatar(
conversationUser.baseUrl, label.toString(), true
)
}
if (isFederated) {
val darkTheme = if (isDarkModeOn(context)) 1 else 0
url = getUrlForFederatedAvatar(
conversationUser.baseUrl!!,
roomToken!!, id!!,
darkTheme, false
)
}
val imageRequest: ImageRequest = ImageRequest.Builder(context)
.data(url)
.crossfade(true)
.transformations(CircleCropTransformation())
.target(object : Target {
override fun onStart(placeholder: Drawable?) {}
override fun onError(error: Drawable?) {
chip.chipIcon = error
}
override fun onSuccess(result: Drawable) {
chip.chipIcon = result
// A hack to refresh the chip icon
emojiEditText?.post {
emojiEditText.setTextKeepState(
emojiEditText.getText(),
TextView.BufferType.SPANNABLE
)
}
}
})
.build()
imageLoader(context).enqueue(imageRequest)
}
return chip
}
fun searchAndReplaceWithMentionSpan(
key: String,
context: Context,
text: Spanned,
id: String,
roomToken: String?,
label: String,
type: String,
conversationUser: User,
@XmlRes chipXmlRes: Int,
viewThemeUtils: ViewThemeUtils,
isFederated: Boolean
): Spannable {
val spannableString: Spannable = SpannableString(text)
val stringText = text.toString()
val keyWithBrackets = "{$key}"
val m = Pattern.compile(keyWithBrackets, Pattern.CASE_INSENSITIVE or Pattern.LITERAL or Pattern.MULTILINE)
.matcher(spannableString)
val clickableSpan: ClickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
EventBus.getDefault().post(UserMentionClickEvent(id))
}
}
var lastStartIndex = 0
var mentionChipSpan: MentionChipSpan
while (m.find()) {
val start = stringText.indexOf(m.group(), lastStartIndex)
val end = start + m.group().length
lastStartIndex = end
val drawableForChip = getDrawableForMentionChipSpan(
context,
id,
roomToken,
label,
conversationUser,
type,
chipXmlRes,
null,
viewThemeUtils,
isFederated
)
mentionChipSpan = MentionChipSpan(
drawableForChip,
BetterImageSpan.ALIGN_CENTER,
id,
label
)
spannableString.setSpan(mentionChipSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
if (chipXmlRes == R.xml.chip_you) {
spannableString.setSpan(
viewThemeUtils.talk.themeForegroundColorSpan(context),
start,
end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
if ("user" == type && conversationUser.userId != id) {
spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
}
}
return spannableString
}
fun searchAndColor(text: Spannable, searchText: String, @ColorInt color: Int): Spannable {
val spannableString: Spannable = SpannableString(text)
val stringText = text.toString()
if (TextUtils.isEmpty(text) || TextUtils.isEmpty(searchText)) {
return spannableString
}
val m = Pattern.compile(
searchText,
Pattern.CASE_INSENSITIVE or Pattern.LITERAL or Pattern.MULTILINE
)
.matcher(spannableString)
val textSize = sharedApplication!!.resources.getDimensionPixelSize(R.dimen.chat_text_size)
var lastStartIndex = -1
while (m.find()) {
val start = stringText.indexOf(m.group(), lastStartIndex)
val end = start + m.group().length
lastStartIndex = end
spannableString.setSpan(
ForegroundColorSpan(color),
start,
end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
spannableString.setSpan(StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannableString.setSpan(AbsoluteSizeSpan(textSize), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
return spannableString
}
fun getMessageSelector(
@ColorInt normalColor: Int,
@ColorInt selectedColor: Int,
@ColorInt pressedColor: Int,
@DrawableRes shape: Int
): Drawable {
val vectorDrawable = ContextCompat.getDrawable(
sharedApplication!!.applicationContext,
shape
)
val drawable = DrawableCompat.wrap(vectorDrawable!!).mutate()
DrawableCompat.setTintList(
drawable,
ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_selected),
intArrayOf(android.R.attr.state_pressed),
intArrayOf(-android.R.attr.state_pressed, -android.R.attr.state_selected)
),
intArrayOf(selectedColor, pressedColor, normalColor)
)
)
return drawable
}
/**
* Sets the color of the status bar to `color`.
*
* @param activity activity
* @param color the color
*/
fun applyColorToStatusBar(activity: Activity, @ColorInt color: Int) {
val window = activity.window
val isLightTheme = lightTheme(color)
if (window != null) {
val decor = window.decorView
if (isLightTheme) {
val systemUiFlagLightStatusBar: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or
View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
} else {
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
decor.systemUiVisibility = systemUiFlagLightStatusBar
} else {
decor.systemUiVisibility = 0
}
window.statusBarColor = color
}
}
/**
* Tests if light color is set
*
* @param color the color
* @return true if primaryColor is lighter than MAX_LIGHTNESS
*/
fun lightTheme(color: Int): Boolean {
val hsl = colorToHSL(color)
// spotbugs dislikes fixed index access
// which is enforced by having such an
// array from Android-API itself
return hsl[INDEX_LUMINATION] >= MAX_LIGHTNESS
}
private fun colorToHSL(color: Int): FloatArray {
val hsl = FloatArray(3)
ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl)
return hsl
}
fun applyColorToNavigationBar(window: Window, @ColorInt color: Int) {
window.navigationBarColor = color
}
/**
* beautifies a given URL by removing any http/https protocol prefix.
*
* @param url to be beautified url
* @return beautified url
*/
@Suppress("ReturnCount")
fun beautifyURL(url: String?): String {
if (TextUtils.isEmpty(url)) {
return ""
}
if (url!!.length >= 7 && HTTP_PROTOCOL.equals(url.substring(0, 7), ignoreCase = true)) {
return url.substring(HTTP_PROTOCOL.length).trim { it <= ' ' }
}
return if (url.length >= 8 && HTTPS_PROTOCOL.equals(url.substring(0, 8), ignoreCase = true)) {
url.substring(HTTPS_PROTOCOL.length).trim { it <= ' ' }
} else {
url.trim { it <= ' ' }
}
}
/**
* beautifies a given twitter handle by prefixing it with an @ in case it is missing.
*
* @param handle to be beautified twitter handle
* @return beautified twitter handle
*/
fun beautifyTwitterHandle(handle: String?): String {
return if (handle != null) {
val trimmedHandle = handle.trim { it <= ' ' }
if (TextUtils.isEmpty(trimmedHandle)) {
return ""
}
if (trimmedHandle.startsWith(TWITTER_HANDLE_PREFIX)) {
trimmedHandle
} else {
TWITTER_HANDLE_PREFIX + trimmedHandle
}
} else {
""
}
}
fun loadAvatarImage(user: User?, avatarImageView: ImageView?, deleteCache: Boolean) {
if (user != null && avatarImageView != null) {
val avatarId: String? = if (!TextUtils.isEmpty(user.userId)) {
user.userId
} else {
user.username
}
if (avatarId != null) {
avatarImageView.loadUserAvatar(user, avatarId, true, deleteCache)
}
}
}
@StringRes
fun getSortOrderStringId(sortOrder: FileSortOrder): Int {
return when (sortOrder.name) {
FileSortOrder.SORT_Z_TO_A_ID -> R.string.menu_item_sort_by_name_z_a
FileSortOrder.SORT_NEW_TO_OLD_ID -> R.string.menu_item_sort_by_date_newest_first
FileSortOrder.SORT_OLD_TO_NEW_ID -> R.string.menu_item_sort_by_date_oldest_first
FileSortOrder.SORT_BIG_TO_SMALL_ID -> R.string.menu_item_sort_by_size_biggest_first
FileSortOrder.SORT_SMALL_TO_BIG_ID -> R.string.menu_item_sort_by_size_smallest_first
FileSortOrder.SORT_A_TO_Z_ID -> R.string.menu_item_sort_by_name_a_z
else -> R.string.menu_item_sort_by_name_a_z
}
}
/**
* calculates the relative time string based on the given modification timestamp.
*
* @param context the app's context
* @param modificationTimestamp the UNIX timestamp of the file modification time in milliseconds.
* @return a relative time string
*/
fun getRelativeTimestamp(context: Context, modificationTimestamp: Long, showFuture: Boolean): CharSequence {
return getRelativeDateTimeString(
context,
modificationTimestamp,
DateUtils.SECOND_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
0,
showFuture
)
}
@Suppress("ReturnCount")
private fun getRelativeDateTimeString(
c: Context,
time: Long,
minResolution: Long,
transitionResolution: Long,
flags: Int,
showFuture: Boolean
): CharSequence {
val dateString: CharSequence
// in Future
if (!showFuture && time > System.currentTimeMillis()) {
return unixTimeToHumanReadable(time)
}
// < 60 seconds -> seconds ago
val diff = System.currentTimeMillis() - time
dateString = if (diff > 0 && diff < 60 * 1000 && minResolution == DateUtils.SECOND_IN_MILLIS) {
return c.getString(R.string.secondsAgo)
} else {
DateUtils.getRelativeDateTimeString(c, time, minResolution, transitionResolution, flags)
}
val parts = dateString.toString().split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (parts.size == DATE_TIME_PARTS_SIZE) {
if (parts[1].contains(":") && !parts[0].contains(":")) {
return parts[0]
} else if (parts[0].contains(":") && !parts[1].contains(":")) {
return parts[1]
}
}
// dateString contains unexpected format. fallback: use relative date time string from android api as is.
return dateString.toString()
}
/**
* Converts Unix time to human readable format
*
* @param milliseconds that have passed since 01/01/1970
* @return The human readable time for the users locale
*/
fun unixTimeToHumanReadable(milliseconds: Long): String {
val date = Date(milliseconds)
val df = DateFormat.getDateTimeInstance()
return df.format(date)
}
fun ellipsize(text: String, maxLength: Int): String {
return if (text.length > maxLength) {
text.substring(0, maxLength - 1) + ""
} else {
text
}
}
}

View File

@ -134,10 +134,10 @@ class MessageUtils(val context: Context) {
} }
messageStringInternal = DisplayUtils.searchAndReplaceWithMentionSpan( messageStringInternal = DisplayUtils.searchAndReplaceWithMentionSpan(
key, key!!,
themingContext, themingContext,
messageStringInternal, messageStringInternal,
id, id!!,
message.roomToken, message.roomToken,
individualHashMap["name"]!!, individualHashMap["name"]!!,
individualHashMap["type"]!!, individualHashMap["type"]!!,