Browse Source

convert DisplayUtils to kt

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 1 year ago
parent
commit
cc19157867

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt

@@ -110,7 +110,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
     @Suppress("NestedBlockDepth", "ComplexMethod", "LongMethod")
     override fun onBind(message: ChatMessage) {
         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)
 

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt

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

+ 1 - 1
app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

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

+ 1 - 0
app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt

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

+ 1 - 1
app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt

@@ -281,7 +281,7 @@ class LocationPickerActivity :
         locationOverlay.setPersonHotspot(PERSON_HOT_SPOT_X, PERSON_HOT_SPOT_Y)
         locationOverlay.setPersonIcon(
             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)

+ 0 - 576
app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java

@@ -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;
-    }
-}

+ 540 - 0
app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt

@@ -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
+        }
+    }
+}

+ 2 - 2
app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt

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