123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- /*
- * Nextcloud Talk application
- *
- * @author Mario Danic
- * 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.annotation.SuppressLint;
- 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.Animatable;
- import android.graphics.drawable.BitmapDrawable;
- import android.graphics.drawable.Drawable;
- import android.graphics.drawable.VectorDrawable;
- 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.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.Log;
- import android.util.TypedValue;
- import android.view.View;
- import android.view.ViewGroup;
- import android.view.Window;
- import android.view.WindowManager;
- 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.NonNull;
- import androidx.annotation.Nullable;
- import androidx.annotation.XmlRes;
- import androidx.appcompat.app.AppCompatDelegate;
- import androidx.appcompat.widget.AppCompatDrawableManager;
- import androidx.appcompat.widget.SearchView;
- import androidx.core.content.ContextCompat;
- import androidx.core.content.res.ResourcesCompat;
- import androidx.core.graphics.ColorUtils;
- import androidx.core.graphics.drawable.DrawableCompat;
- import androidx.emoji.text.EmojiCompat;
- import androidx.viewpager.widget.ViewPager;
- import com.facebook.common.executors.UiThreadImmediateExecutorService;
- import com.facebook.common.references.CloseableReference;
- import com.facebook.datasource.DataSource;
- import com.facebook.drawee.backends.pipeline.Fresco;
- import com.facebook.drawee.controller.ControllerListener;
- import com.facebook.drawee.interfaces.DraweeController;
- import com.facebook.drawee.view.SimpleDraweeView;
- import com.facebook.imagepipeline.common.RotationOptions;
- import com.facebook.imagepipeline.core.ImagePipeline;
- import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
- import com.facebook.imagepipeline.image.CloseableImage;
- import com.facebook.imagepipeline.image.ImageInfo;
- import com.facebook.imagepipeline.postprocessors.RoundAsCirclePostprocessor;
- import com.facebook.imagepipeline.postprocessors.RoundPostprocessor;
- import com.facebook.imagepipeline.request.ImageRequest;
- import com.facebook.imagepipeline.request.ImageRequestBuilder;
- import com.facebook.widget.text.span.BetterImageSpan;
- import com.google.android.material.chip.ChipDrawable;
- import com.nextcloud.talk.R;
- import com.nextcloud.talk.application.NextcloudTalkApplication;
- import com.nextcloud.talk.events.UserMentionClickEvent;
- import com.nextcloud.talk.models.database.UserEntity;
- import com.nextcloud.talk.utils.preferences.AppPreferences;
- import com.nextcloud.talk.utils.text.Spans;
- import org.greenrobot.eventbus.EventBus;
- import java.lang.reflect.Constructor;
- import java.lang.reflect.InvocationTargetException;
- import java.lang.reflect.Method;
- import java.util.HashMap;
- import java.util.Map;
- 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.XmlRes;
- import androidx.appcompat.widget.AppCompatDrawableManager;
- import androidx.core.content.ContextCompat;
- import androidx.core.graphics.drawable.DrawableCompat;
- import androidx.emoji.text.EmojiCompat;
- public class DisplayUtils {
- private static final String TAG = "DisplayUtils";
- 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://";
- 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());
- }
- private static void updateViewSize(@Nullable ImageInfo imageInfo, SimpleDraweeView draweeView) {
- if (imageInfo != null) {
- int maxSize = draweeView.getContext().getResources().getDimensionPixelSize(R.dimen.maximum_file_preview_size);
- draweeView.getLayoutParams().width = imageInfo.getWidth() > maxSize ? maxSize : imageInfo.getWidth();
- draweeView.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
- draweeView.setAspectRatio((float) imageInfo.getWidth() / imageInfo.getHeight());
- draweeView.requestLayout();
- }
- }
- public static Drawable getRoundedDrawable(Drawable drawable) {
- Bitmap bitmap = getBitmap(drawable);
- new RoundAsCirclePostprocessor(true).process(bitmap);
- return new BitmapDrawable(bitmap);
- }
- public static Bitmap getRoundedBitmapFromVectorDrawableResource(Resources resources, int resource) {
- VectorDrawable vectorDrawable = (VectorDrawable) resources.getDrawable(resource);
- Bitmap bitmap = getBitmap(vectorDrawable);
- new RoundPostprocessor(true).process(bitmap);
- return bitmap;
- }
- public static Drawable getRoundedBitmapDrawableFromVectorDrawableResource(Resources resources, int resource) {
- return new BitmapDrawable(getRoundedBitmapFromVectorDrawableResource(resources, resource));
- }
- private 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 ImageRequest getImageRequestForUrl(String url, @Nullable UserEntity userEntity) {
- Map<String, String> headers = new HashMap<>();
- if (userEntity != null && url.startsWith(userEntity.getBaseUrl()) && (url.contains("index.php/core/preview?fileId=") || url.contains("/avatar/"))) {
- headers.put("Authorization", ApiUtils.getCredentials(userEntity.getUsername(), userEntity.getToken()));
- }
- return ImageRequestBuilder.newBuilderWithSource(Uri.parse(url))
- .setProgressiveRenderingEnabled(true)
- .setRotationOptions(RotationOptions.autoRotate())
- .disableDiskCache()
- .setHeaders(headers)
- .build();
- }
- public static ControllerListener getImageControllerListener(SimpleDraweeView draweeView) {
- return new ControllerListener() {
- @Override
- public void onSubmit(String id, Object callerContext) {
- }
- @Override
- public void onFinalImageSet(String id, @androidx.annotation.Nullable Object imageInfo, @androidx.annotation.Nullable Animatable animatable) {
- updateViewSize((ImageInfo) imageInfo, draweeView);
- }
- @Override
- public void onIntermediateImageSet(String id, @androidx.annotation.Nullable Object imageInfo) {
- updateViewSize((ImageInfo) imageInfo, draweeView);
- }
- @Override
- public void onIntermediateImageFailed(String id, Throwable throwable) {
- }
- @Override
- public void onFailure(String id, Throwable throwable) {
- }
- @Override
- public void onRelease(String id) {
- }
- };
- }
- public static float convertDpToPixel(float dp, Context context) {
- return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
- context.getResources().getDisplayMetrics()) + 0.5f);
- }
- // Solution inspired by https://stackoverflow.com/questions/34936590/why-isnt-my-vector-drawable-scaling-as-expected
- public static void useCompatVectorIfNeeded() {
- if (Build.VERSION.SDK_INT < 23) {
- try {
- @SuppressLint("RestrictedApi") AppCompatDrawableManager drawableManager = AppCompatDrawableManager.get();
- Class<?> inflateDelegateClass = Class.forName("android.support.v7.widget.AppCompatDrawableManager$InflateDelegate");
- Class<?> vdcInflateDelegateClass = Class.forName("android.support.v7.widget.AppCompatDrawableManager$VdcInflateDelegate");
- Constructor<?> constructor = vdcInflateDelegateClass.getDeclaredConstructor();
- constructor.setAccessible(true);
- Object vdcInflateDelegate = constructor.newInstance();
- Class<?> args[] = {String.class, inflateDelegateClass};
- Method addDelegate = AppCompatDrawableManager.class.getDeclaredMethod("addDelegate", args);
- addDelegate.setAccessible(true);
- addDelegate.invoke(drawableManager, "vector", vdcInflateDelegate);
- } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException |
- InvocationTargetException | IllegalAccessException e) {
- Log.e(TAG, "Failed to use reflection to enable proper vector scaling");
- }
- }
- }
- public static Drawable getTintedDrawable(Resources res, @DrawableRes int drawableResId, @ColorRes int colorResId) {
- Drawable drawable = res.getDrawable(drawableResId);
- int color = res.getColor(colorResId);
- drawable.setTint(color);
- return drawable;
- }
- public static Drawable getDrawableForMentionChipSpan(Context context, String id, CharSequence label,
- UserEntity conversationUser, String type,
- @XmlRes int chipResource,
- @Nullable EditText emojiEditText) {
- ChipDrawable chip = ChipDrawable.createFromResource(context, chipResource);
- chip.setText(EmojiCompat.get().process(label));
- chip.setEllipsize(TextUtils.TruncateAt.MIDDLE);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- Configuration config = context.getResources().getConfiguration();
- chip.setLayoutDirection(config.getLayoutDirection());
- }
- int drawable;
- boolean isCall = "call".equals(type) || "calls".equals(type);
- if (!isCall) {
- 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 (!isCall) {
- String url = ApiUtils.getUrlForAvatarWithName(conversationUser.getBaseUrl(), id, R.dimen.avatar_size_big);
- if ("guests".equals(type) || "guest".equals(type)) {
- url = ApiUtils.getUrlForAvatarWithNameForGuests(conversationUser.getBaseUrl(), String.valueOf(label), R.dimen.avatar_size_big);
- }
- ImageRequest imageRequest = getImageRequestForUrl(url, null);
- ImagePipeline imagePipeline = Fresco.getImagePipeline();
- DataSource<CloseableReference<CloseableImage>> dataSource = imagePipeline.fetchDecodedImage(imageRequest, context);
- dataSource.subscribe(
- new BaseBitmapDataSubscriber() {
- @Override
- protected void onNewResultImpl(Bitmap bitmap) {
- if (bitmap != null) {
- chip.setChipIcon(getRoundedDrawable(new BitmapDrawable(bitmap)));
- // A hack to refresh the chip icon
- if (emojiEditText != null) {
- emojiEditText.post(() -> emojiEditText.setTextKeepState(emojiEditText.getText(), TextView.BufferType.SPANNABLE));
- }
- }
- }
- @Override
- protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
- }
- },
- UiThreadImmediateExecutorService.getInstance());
- }
- return chip;
- }
- public static Spannable searchAndReplaceWithMentionSpan(Context context, Spannable text,
- String id, String label, String type,
- UserEntity conversationUser,
- @XmlRes int chipXmlRes) {
- Spannable spannableString = new SpannableString(text);
- String stringText = text.toString();
- Matcher m = Pattern.compile("@" + label,
- 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 = -1;
- Spans.MentionChipSpan mentionChipSpan;
- while (m.find()) {
- int start = stringText.indexOf(m.group(), lastStartIndex);
- int end = start + m.group().length();
- lastStartIndex = end;
- mentionChipSpan = new Spans.MentionChipSpan(DisplayUtils.getDrawableForMentionChipSpan(context,
- id, label, conversationUser, type, chipXmlRes, null),
- BetterImageSpan.ALIGN_CENTER, id,
- label);
- spannableString.setSpan(mentionChipSpan, 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) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- 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);
- } else if (isLightTheme) {
- window.setStatusBarColor(Color.BLACK);
- }
- }
- }
- /**
- * Tests if light color is set
- *
- * @param color the color
- * @return true if primaryColor is lighter than MAX_LIGHTNESS
- */
- public static boolean lightTheme(int color) {
- float[] hsl = colorToHSL(color);
- 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);
- }
- /**
- * Theme search view
- *
- * @param searchView searchView to be changed
- * @param context the app's context
- */
- public static void themeSearchView(SearchView searchView, Context context) {
- // hacky as no default way is provided
- SearchView.SearchAutoComplete editText = searchView.findViewById(R.id.search_src_text);
- editText.setTextSize(16);
- editText.setHintTextColor(context.getResources().getColor(R.color.fontSecondaryAppbar));
- }
- /**
- * 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(UserEntity user, SimpleDraweeView avatarImageView, boolean deleteCache) {
- String avatarId;
- if (!TextUtils.isEmpty(user.getUserId())) {
- avatarId = user.getUserId();
- } else {
- avatarId = user.getUsername();
- }
- String avatarString = ApiUtils.getUrlForAvatarWithName(user.getBaseUrl(), avatarId, R.dimen.avatar_size_big);
- // clear cache
- if (deleteCache) {
- Uri avatarUri = Uri.parse(avatarString);
- ImagePipeline imagePipeline = Fresco.getImagePipeline();
- imagePipeline.evictFromMemoryCache(avatarUri);
- imagePipeline.evictFromDiskCache(avatarUri);
- imagePipeline.evictFromCache(avatarUri);
- }
- DraweeController draweeController = Fresco.newDraweeControllerBuilder()
- .setOldController(avatarImageView.getController())
- .setAutoPlayAnimations(true)
- .setImageRequest(DisplayUtils.getImageRequestForUrl(avatarString, null))
- .build();
- avatarImageView.setController(draweeController);
- }
- }
|