DisplayUtils.java 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Mario Danic
  5. * @author Andy Scherzinger
  6. * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  7. * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU General Public License as published by
  11. * the Free Software Foundation, either version 3 of the License, or
  12. * at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. */
  22. package com.nextcloud.talk.utils;
  23. import android.annotation.SuppressLint;
  24. import android.app.Activity;
  25. import android.content.Context;
  26. import android.content.Intent;
  27. import android.content.res.ColorStateList;
  28. import android.content.res.Configuration;
  29. import android.content.res.Resources;
  30. import android.graphics.Bitmap;
  31. import android.graphics.Canvas;
  32. import android.graphics.Color;
  33. import android.graphics.Typeface;
  34. import android.graphics.drawable.Animatable;
  35. import android.graphics.drawable.BitmapDrawable;
  36. import android.graphics.drawable.Drawable;
  37. import android.graphics.drawable.LayerDrawable;
  38. import android.graphics.drawable.VectorDrawable;
  39. import android.net.Uri;
  40. import android.os.Build;
  41. import android.text.Spannable;
  42. import android.text.SpannableString;
  43. import android.text.Spanned;
  44. import android.text.TextPaint;
  45. import android.text.TextUtils;
  46. import android.text.format.DateUtils;
  47. import android.text.method.LinkMovementMethod;
  48. import android.text.style.AbsoluteSizeSpan;
  49. import android.text.style.ClickableSpan;
  50. import android.text.style.ForegroundColorSpan;
  51. import android.text.style.StyleSpan;
  52. import android.util.Log;
  53. import android.util.TypedValue;
  54. import android.view.View;
  55. import android.view.ViewGroup;
  56. import android.view.Window;
  57. import android.widget.EditText;
  58. import android.widget.TextView;
  59. import com.facebook.common.executors.UiThreadImmediateExecutorService;
  60. import com.facebook.common.references.CloseableReference;
  61. import com.facebook.datasource.DataSource;
  62. import com.facebook.drawee.backends.pipeline.Fresco;
  63. import com.facebook.drawee.controller.ControllerListener;
  64. import com.facebook.drawee.interfaces.DraweeController;
  65. import com.facebook.drawee.view.SimpleDraweeView;
  66. import com.facebook.imagepipeline.common.RotationOptions;
  67. import com.facebook.imagepipeline.core.ImagePipeline;
  68. import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
  69. import com.facebook.imagepipeline.image.CloseableImage;
  70. import com.facebook.imagepipeline.image.ImageInfo;
  71. import com.facebook.imagepipeline.postprocessors.RoundAsCirclePostprocessor;
  72. import com.facebook.imagepipeline.postprocessors.RoundPostprocessor;
  73. import com.facebook.imagepipeline.request.ImageRequest;
  74. import com.facebook.imagepipeline.request.ImageRequestBuilder;
  75. import com.facebook.widget.text.span.BetterImageSpan;
  76. import com.google.android.material.chip.ChipDrawable;
  77. import com.nextcloud.talk.R;
  78. import com.nextcloud.talk.application.NextcloudTalkApplication;
  79. import com.nextcloud.talk.data.user.model.User;
  80. import com.nextcloud.talk.events.UserMentionClickEvent;
  81. import com.nextcloud.talk.ui.theme.ViewThemeUtils;
  82. import com.nextcloud.talk.utils.text.Spans;
  83. import org.greenrobot.eventbus.EventBus;
  84. import java.lang.reflect.Constructor;
  85. import java.lang.reflect.InvocationTargetException;
  86. import java.lang.reflect.Method;
  87. import java.text.DateFormat;
  88. import java.util.Date;
  89. import java.util.HashMap;
  90. import java.util.Map;
  91. import java.util.regex.Matcher;
  92. import java.util.regex.Pattern;
  93. import androidx.annotation.ColorInt;
  94. import androidx.annotation.ColorRes;
  95. import androidx.annotation.DrawableRes;
  96. import androidx.annotation.NonNull;
  97. import androidx.annotation.Nullable;
  98. import androidx.annotation.StringRes;
  99. import androidx.annotation.XmlRes;
  100. import androidx.appcompat.widget.AppCompatDrawableManager;
  101. import androidx.core.content.ContextCompat;
  102. import androidx.core.content.res.ResourcesCompat;
  103. import androidx.core.graphics.ColorUtils;
  104. import androidx.core.graphics.drawable.DrawableCompat;
  105. import androidx.emoji.text.EmojiCompat;
  106. import static com.nextcloud.talk.utils.FileSortOrder.sort_a_to_z_id;
  107. import static com.nextcloud.talk.utils.FileSortOrder.sort_big_to_small_id;
  108. import static com.nextcloud.talk.utils.FileSortOrder.sort_new_to_old_id;
  109. import static com.nextcloud.talk.utils.FileSortOrder.sort_old_to_new_id;
  110. import static com.nextcloud.talk.utils.FileSortOrder.sort_small_to_big_id;
  111. import static com.nextcloud.talk.utils.FileSortOrder.sort_z_to_a_id;
  112. public class DisplayUtils {
  113. private static final String TAG = "DisplayUtils";
  114. private static final int INDEX_LUMINATION = 2;
  115. private static final double MAX_LIGHTNESS = 0.92;
  116. private static final String TWITTER_HANDLE_PREFIX = "@";
  117. private static final String HTTP_PROTOCOL = "http://";
  118. private static final String HTTPS_PROTOCOL = "https://";
  119. private static final int DATE_TIME_PARTS_SIZE = 2;
  120. public static void setClickableString(String string, String url, TextView textView) {
  121. SpannableString spannableString = new SpannableString(string);
  122. spannableString.setSpan(new ClickableSpan() {
  123. @Override
  124. public void onClick(@NonNull View widget) {
  125. Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
  126. browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  127. NextcloudTalkApplication
  128. .Companion
  129. .getSharedApplication()
  130. .getApplicationContext()
  131. .startActivity(browserIntent);
  132. }
  133. @Override
  134. public void updateDrawState(@NonNull TextPaint ds) {
  135. super.updateDrawState(ds);
  136. ds.setUnderlineText(false);
  137. }
  138. }, 0, string.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
  139. textView.setText(spannableString);
  140. textView.setMovementMethod(LinkMovementMethod.getInstance());
  141. }
  142. private static void updateViewSize(@Nullable ImageInfo imageInfo, SimpleDraweeView draweeView) {
  143. if (imageInfo != null && draweeView.getId() != R.id.messageUserAvatar) {
  144. int maxSize = draweeView.getContext().getResources().getDimensionPixelSize(R.dimen.maximum_file_preview_size);
  145. draweeView.getLayoutParams().width = imageInfo.getWidth() > maxSize ? maxSize : imageInfo.getWidth();
  146. draweeView.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
  147. draweeView.setAspectRatio((float) imageInfo.getWidth() / imageInfo.getHeight());
  148. draweeView.requestLayout();
  149. }
  150. }
  151. public static Drawable getRoundedDrawable(Drawable drawable) {
  152. Bitmap bitmap = getBitmap(drawable);
  153. new RoundAsCirclePostprocessor(true).process(bitmap);
  154. return new BitmapDrawable(bitmap);
  155. }
  156. public static Bitmap getRoundedBitmapFromVectorDrawableResource(Resources resources, int resource) {
  157. VectorDrawable vectorDrawable = (VectorDrawable) ResourcesCompat.getDrawable(resources, resource, null);
  158. Bitmap bitmap = getBitmap(vectorDrawable);
  159. new RoundPostprocessor(true).process(bitmap);
  160. return bitmap;
  161. }
  162. public static Drawable getRoundedBitmapDrawableFromVectorDrawableResource(Resources resources, int resource) {
  163. return new BitmapDrawable(getRoundedBitmapFromVectorDrawableResource(resources, resource));
  164. }
  165. public static Bitmap getBitmap(Drawable drawable) {
  166. Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
  167. drawable.getIntrinsicHeight(),
  168. Bitmap.Config.ARGB_8888);
  169. Canvas canvas = new Canvas(bitmap);
  170. drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
  171. drawable.draw(canvas);
  172. return bitmap;
  173. }
  174. public static ImageRequest getImageRequestForUrl(String url) {
  175. return getImageRequestForUrl(url, (User) null);
  176. }
  177. public static ImageRequest getImageRequestForUrl(String url, @Nullable User user) {
  178. Map<String, String> headers = new HashMap<>();
  179. if (user != null &&
  180. url.startsWith(user.getBaseUrl()) &&
  181. (url.contains("index.php/core/preview?fileId=") || url.contains("/avatar/"))) {
  182. headers.put("Authorization", ApiUtils.getCredentials(user.getUsername(), user.getToken()));
  183. }
  184. return ImageRequestBuilder.newBuilderWithSource(Uri.parse(url))
  185. .setProgressiveRenderingEnabled(true)
  186. .setRotationOptions(RotationOptions.autoRotate())
  187. .disableDiskCache()
  188. .setHeaders(headers)
  189. .build();
  190. }
  191. public static ControllerListener getImageControllerListener(SimpleDraweeView draweeView) {
  192. return new ControllerListener() {
  193. @Override
  194. public void onSubmit(String id, Object callerContext) {
  195. }
  196. @Override
  197. public void onFinalImageSet(String id,
  198. @androidx.annotation.Nullable Object imageInfo,
  199. @androidx.annotation.Nullable Animatable animatable) {
  200. updateViewSize((ImageInfo) imageInfo, draweeView);
  201. }
  202. @Override
  203. public void onIntermediateImageSet(String id, @androidx.annotation.Nullable Object imageInfo) {
  204. updateViewSize((ImageInfo) imageInfo, draweeView);
  205. }
  206. @Override
  207. public void onIntermediateImageFailed(String id, Throwable throwable) {
  208. }
  209. @Override
  210. public void onFailure(String id, Throwable throwable) {
  211. }
  212. @Override
  213. public void onRelease(String id) {
  214. }
  215. };
  216. }
  217. public static float convertDpToPixel(float dp, Context context) {
  218. return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
  219. context.getResources().getDisplayMetrics()) + 0.5f);
  220. }
  221. public static float convertPixelToDp(float px, Context context) {
  222. return px / context.getResources().getDisplayMetrics().density;
  223. }
  224. // Solution inspired by https://stackoverflow.com/questions/34936590/why-isnt-my-vector-drawable-scaling-as-expected
  225. public static void useCompatVectorIfNeeded() {
  226. if (Build.VERSION.SDK_INT < 23) {
  227. try {
  228. @SuppressLint("RestrictedApi") AppCompatDrawableManager drawableManager = AppCompatDrawableManager.get();
  229. Class<?> inflateDelegateClass = Class.forName(
  230. "android.support.v7.widget.AppCompatDrawableManager$InflateDelegate");
  231. Class<?> vdcInflateDelegateClass = Class.forName(
  232. "android.support.v7.widget.AppCompatDrawableManager$VdcInflateDelegate");
  233. Constructor<?> constructor = vdcInflateDelegateClass.getDeclaredConstructor();
  234. constructor.setAccessible(true);
  235. Object vdcInflateDelegate = constructor.newInstance();
  236. Class<?> args[] = {String.class, inflateDelegateClass};
  237. Method addDelegate = AppCompatDrawableManager.class.getDeclaredMethod("addDelegate", args);
  238. addDelegate.setAccessible(true);
  239. addDelegate.invoke(drawableManager, "vector", vdcInflateDelegate);
  240. } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException |
  241. InvocationTargetException | IllegalAccessException e) {
  242. Log.e(TAG, "Failed to use reflection to enable proper vector scaling");
  243. }
  244. }
  245. }
  246. public static Drawable getTintedDrawable(Resources res, @DrawableRes int drawableResId, @ColorRes int colorResId) {
  247. Drawable drawable = ResourcesCompat.getDrawable(res, drawableResId, null);
  248. ;
  249. int color = res.getColor(colorResId);
  250. if (drawable != null) {
  251. drawable.setTint(color);
  252. }
  253. return drawable;
  254. }
  255. public static Drawable getDrawableForMentionChipSpan(Context context,
  256. String id,
  257. CharSequence label,
  258. User conversationUser,
  259. String type,
  260. @XmlRes int chipResource,
  261. @Nullable EditText emojiEditText,
  262. ViewThemeUtils viewThemeUtils) {
  263. ChipDrawable chip = ChipDrawable.createFromResource(context, chipResource);
  264. chip.setText(EmojiCompat.get().process(label));
  265. chip.setEllipsize(TextUtils.TruncateAt.MIDDLE);
  266. if (chipResource == R.xml.chip_you) {
  267. viewThemeUtils.material.colorChipDrawable(context, chip);
  268. }
  269. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  270. Configuration config = context.getResources().getConfiguration();
  271. chip.setLayoutDirection(config.getLayoutDirection());
  272. }
  273. int drawable;
  274. boolean isCall = "call".equals(type) || "calls".equals(type);
  275. if (!isCall) {
  276. if (chipResource == R.xml.chip_you) {
  277. drawable = R.drawable.mention_chip;
  278. } else {
  279. drawable = R.drawable.accent_circle;
  280. }
  281. chip.setChipIconResource(drawable);
  282. } else {
  283. chip.setChipIconResource(R.drawable.ic_circular_group);
  284. }
  285. chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight());
  286. if (!isCall) {
  287. String url = ApiUtils.getUrlForAvatar(conversationUser.getBaseUrl(), id, true);
  288. if ("guests".equals(type) || "guest".equals(type)) {
  289. url = ApiUtils.getUrlForGuestAvatar(
  290. conversationUser.getBaseUrl(),
  291. String.valueOf(label), true);
  292. }
  293. ImageRequest imageRequest = getImageRequestForUrl(url);
  294. ImagePipeline imagePipeline = Fresco.getImagePipeline();
  295. DataSource<CloseableReference<CloseableImage>> dataSource = imagePipeline.fetchDecodedImage(
  296. imageRequest,
  297. context);
  298. dataSource.subscribe(
  299. new BaseBitmapDataSubscriber() {
  300. @Override
  301. protected void onNewResultImpl(Bitmap bitmap) {
  302. if (bitmap != null) {
  303. chip.setChipIcon(getRoundedDrawable(new BitmapDrawable(bitmap)));
  304. // A hack to refresh the chip icon
  305. if (emojiEditText != null) {
  306. emojiEditText.post(() -> emojiEditText.setTextKeepState(
  307. emojiEditText.getText(),
  308. TextView.BufferType.SPANNABLE));
  309. }
  310. }
  311. }
  312. @Override
  313. protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
  314. }
  315. },
  316. UiThreadImmediateExecutorService.getInstance());
  317. }
  318. return chip;
  319. }
  320. public static Spannable searchAndReplaceWithMentionSpan(Context context, Spannable text,
  321. String id, String label, String type,
  322. User conversationUser,
  323. @XmlRes int chipXmlRes,
  324. ViewThemeUtils viewThemeUtils) {
  325. Spannable spannableString = new SpannableString(text);
  326. String stringText = text.toString();
  327. Matcher m = Pattern.compile("@" + label,
  328. Pattern.CASE_INSENSITIVE | Pattern.LITERAL | Pattern.MULTILINE)
  329. .matcher(spannableString);
  330. ClickableSpan clickableSpan = new ClickableSpan() {
  331. @Override
  332. public void onClick(@NonNull View widget) {
  333. EventBus.getDefault().post(new UserMentionClickEvent(id));
  334. }
  335. };
  336. int lastStartIndex = -1;
  337. Spans.MentionChipSpan mentionChipSpan;
  338. while (m.find()) {
  339. int start = stringText.indexOf(m.group(), lastStartIndex);
  340. int end = start + m.group().length();
  341. lastStartIndex = end;
  342. mentionChipSpan = new Spans.MentionChipSpan(DisplayUtils.getDrawableForMentionChipSpan(context,
  343. id,
  344. label,
  345. conversationUser,
  346. type,
  347. chipXmlRes,
  348. null,
  349. viewThemeUtils),
  350. BetterImageSpan.ALIGN_CENTER, id,
  351. label);
  352. spannableString.setSpan(mentionChipSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
  353. if (chipXmlRes == R.xml.chip_you) {
  354. spannableString.setSpan(
  355. new ForegroundColorSpan(viewThemeUtils.getScheme(context).getOnPrimary()),
  356. start,
  357. end,
  358. Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
  359. }
  360. if ("user".equals(type) && !conversationUser.getUserId().equals(id)) {
  361. spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
  362. }
  363. }
  364. return spannableString;
  365. }
  366. public static Spannable searchAndColor(Spannable text, String searchText, @ColorInt int color) {
  367. Spannable spannableString = new SpannableString(text);
  368. String stringText = text.toString();
  369. if (TextUtils.isEmpty(text) || TextUtils.isEmpty(searchText)) {
  370. return spannableString;
  371. }
  372. Matcher m = Pattern.compile(searchText,
  373. Pattern.CASE_INSENSITIVE | Pattern.LITERAL | Pattern.MULTILINE)
  374. .matcher(spannableString);
  375. int textSize = NextcloudTalkApplication.Companion.getSharedApplication().getResources().getDimensionPixelSize(R.dimen
  376. .chat_text_size);
  377. int lastStartIndex = -1;
  378. while (m.find()) {
  379. int start = stringText.indexOf(m.group(), lastStartIndex);
  380. int end = start + m.group().length();
  381. lastStartIndex = end;
  382. spannableString.setSpan(new ForegroundColorSpan(color), start, end,
  383. Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
  384. spannableString.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
  385. spannableString.setSpan(new AbsoluteSizeSpan(textSize), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
  386. }
  387. return spannableString;
  388. }
  389. public static Drawable getMessageSelector(@ColorInt int normalColor,
  390. @ColorInt int selectedColor,
  391. @ColorInt int pressedColor,
  392. @DrawableRes int shape) {
  393. Drawable vectorDrawable = ContextCompat.getDrawable(NextcloudTalkApplication.Companion.getSharedApplication()
  394. .getApplicationContext(),
  395. shape);
  396. Drawable drawable = DrawableCompat.wrap(vectorDrawable).mutate();
  397. DrawableCompat.setTintList(
  398. drawable,
  399. new ColorStateList(
  400. new int[][]{
  401. new int[]{android.R.attr.state_selected},
  402. new int[]{android.R.attr.state_pressed},
  403. new int[]{-android.R.attr.state_pressed, -android.R.attr.state_selected}
  404. },
  405. new int[]{selectedColor, pressedColor, normalColor}
  406. ));
  407. return drawable;
  408. }
  409. /**
  410. * Sets the color of the status bar to {@code color}.
  411. *
  412. * @param activity activity
  413. * @param color the color
  414. */
  415. public static void applyColorToStatusBar(Activity activity, @ColorInt int color) {
  416. Window window = activity.getWindow();
  417. boolean isLightTheme = lightTheme(color);
  418. if (window != null) {
  419. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  420. View decor = window.getDecorView();
  421. if (isLightTheme) {
  422. int systemUiFlagLightStatusBar;
  423. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  424. systemUiFlagLightStatusBar = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR |
  425. View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
  426. } else {
  427. systemUiFlagLightStatusBar = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
  428. }
  429. decor.setSystemUiVisibility(systemUiFlagLightStatusBar);
  430. } else {
  431. decor.setSystemUiVisibility(0);
  432. }
  433. window.setStatusBarColor(color);
  434. } else if (isLightTheme) {
  435. window.setStatusBarColor(Color.BLACK);
  436. }
  437. }
  438. }
  439. /**
  440. * Tests if light color is set
  441. *
  442. * @param color the color
  443. * @return true if primaryColor is lighter than MAX_LIGHTNESS
  444. */
  445. @SuppressWarnings("correctness")
  446. public static boolean lightTheme(int color) {
  447. float[] hsl = colorToHSL(color);
  448. // spotbugs dislikes fixed index access
  449. // which is enforced by having such an
  450. // array from Android-API itself
  451. return hsl[INDEX_LUMINATION] >= MAX_LIGHTNESS;
  452. }
  453. private static float[] colorToHSL(int color) {
  454. float[] hsl = new float[3];
  455. ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl);
  456. return hsl;
  457. }
  458. public static void applyColorToNavigationBar(Window window, @ColorInt int color) {
  459. window.setNavigationBarColor(color);
  460. }
  461. /**
  462. * beautifies a given URL by removing any http/https protocol prefix.
  463. *
  464. * @param url to be beautified url
  465. * @return beautified url
  466. */
  467. public static String beautifyURL(@Nullable String url) {
  468. if (TextUtils.isEmpty(url)) {
  469. return "";
  470. }
  471. if (url.length() >= 7 && HTTP_PROTOCOL.equalsIgnoreCase(url.substring(0, 7))) {
  472. return url.substring(HTTP_PROTOCOL.length()).trim();
  473. }
  474. if (url.length() >= 8 && HTTPS_PROTOCOL.equalsIgnoreCase(url.substring(0, 8))) {
  475. return url.substring(HTTPS_PROTOCOL.length()).trim();
  476. }
  477. return url.trim();
  478. }
  479. /**
  480. * beautifies a given twitter handle by prefixing it with an @ in case it is missing.
  481. *
  482. * @param handle to be beautified twitter handle
  483. * @return beautified twitter handle
  484. */
  485. public static String beautifyTwitterHandle(@Nullable String handle) {
  486. if (handle != null) {
  487. String trimmedHandle = handle.trim();
  488. if (TextUtils.isEmpty(trimmedHandle)) {
  489. return "";
  490. }
  491. if (trimmedHandle.startsWith(TWITTER_HANDLE_PREFIX)) {
  492. return trimmedHandle;
  493. } else {
  494. return TWITTER_HANDLE_PREFIX + trimmedHandle;
  495. }
  496. } else {
  497. return "";
  498. }
  499. }
  500. public static void loadAvatarImage(User user, SimpleDraweeView avatarImageView, boolean deleteCache) {
  501. String avatarId;
  502. if (!TextUtils.isEmpty(user.getUserId())) {
  503. avatarId = user.getUserId();
  504. } else {
  505. avatarId = user.getUsername();
  506. }
  507. String avatarString = ApiUtils.getUrlForAvatar(user.getBaseUrl(), avatarId, true);
  508. // clear cache
  509. if (deleteCache) {
  510. Uri avatarUri = Uri.parse(avatarString);
  511. ImagePipeline imagePipeline = Fresco.getImagePipeline();
  512. imagePipeline.evictFromMemoryCache(avatarUri);
  513. imagePipeline.evictFromDiskCache(avatarUri);
  514. imagePipeline.evictFromCache(avatarUri);
  515. }
  516. DraweeController draweeController = Fresco.newDraweeControllerBuilder()
  517. .setOldController(avatarImageView.getController())
  518. .setAutoPlayAnimations(true)
  519. .setImageRequest(DisplayUtils.getImageRequestForUrl(avatarString))
  520. .build();
  521. avatarImageView.setController(draweeController);
  522. }
  523. public static void loadAvatarPlaceholder(final SimpleDraweeView targetView) {
  524. final Context context = targetView.getContext();
  525. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  526. Drawable[] layers = new Drawable[2];
  527. layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background);
  528. layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground);
  529. LayerDrawable layerDrawable = new LayerDrawable(layers);
  530. targetView.getHierarchy().setPlaceholderImage(
  531. DisplayUtils.getRoundedDrawable(layerDrawable));
  532. } else {
  533. targetView.getHierarchy().setPlaceholderImage(R.mipmap.ic_launcher);
  534. }
  535. }
  536. public static void loadImage(final SimpleDraweeView targetView, final ImageRequest imageRequest) {
  537. final DraweeController newController = Fresco.newDraweeControllerBuilder()
  538. .setOldController(targetView.getController())
  539. .setAutoPlayAnimations(true)
  540. .setImageRequest(imageRequest)
  541. .build();
  542. targetView.setController(newController);
  543. }
  544. public static @StringRes
  545. int getSortOrderStringId(FileSortOrder sortOrder) {
  546. switch (sortOrder.getName()) {
  547. case sort_z_to_a_id:
  548. return R.string.menu_item_sort_by_name_z_a;
  549. case sort_new_to_old_id:
  550. return R.string.menu_item_sort_by_date_newest_first;
  551. case sort_old_to_new_id:
  552. return R.string.menu_item_sort_by_date_oldest_first;
  553. case sort_big_to_small_id:
  554. return R.string.menu_item_sort_by_size_biggest_first;
  555. case sort_small_to_big_id:
  556. return R.string.menu_item_sort_by_size_smallest_first;
  557. case sort_a_to_z_id:
  558. default:
  559. return R.string.menu_item_sort_by_name_a_z;
  560. }
  561. }
  562. /**
  563. * calculates the relative time string based on the given modification timestamp.
  564. *
  565. * @param context the app's context
  566. * @param modificationTimestamp the UNIX timestamp of the file modification time in milliseconds.
  567. * @return a relative time string
  568. */
  569. public static CharSequence getRelativeTimestamp(Context context, long modificationTimestamp, boolean showFuture) {
  570. return getRelativeDateTimeString(context,
  571. modificationTimestamp,
  572. android.text.format.DateUtils.SECOND_IN_MILLIS,
  573. DateUtils.WEEK_IN_MILLIS,
  574. 0,
  575. showFuture);
  576. }
  577. public static CharSequence getRelativeDateTimeString(Context c,
  578. long time,
  579. long minResolution,
  580. long transitionResolution,
  581. int flags,
  582. boolean showFuture) {
  583. CharSequence dateString = "";
  584. // in Future
  585. if (!showFuture && time > System.currentTimeMillis()) {
  586. return DisplayUtils.unixTimeToHumanReadable(time);
  587. }
  588. // < 60 seconds -> seconds ago
  589. long diff = System.currentTimeMillis() - time;
  590. if (diff > 0 && diff < 60 * 1000 && minResolution == DateUtils.SECOND_IN_MILLIS) {
  591. return c.getString(R.string.secondsAgo);
  592. } else {
  593. dateString = DateUtils.getRelativeDateTimeString(c, time, minResolution, transitionResolution, flags);
  594. }
  595. String[] parts = dateString.toString().split(",");
  596. if (parts.length == DATE_TIME_PARTS_SIZE) {
  597. if (parts[1].contains(":") && !parts[0].contains(":")) {
  598. return parts[0];
  599. } else if (parts[0].contains(":") && !parts[1].contains(":")) {
  600. return parts[1];
  601. }
  602. }
  603. // dateString contains unexpected format. fallback: use relative date time string from android api as is.
  604. return dateString.toString();
  605. }
  606. /**
  607. * Converts Unix time to human readable format
  608. *
  609. * @param milliseconds that have passed since 01/01/1970
  610. * @return The human readable time for the users locale
  611. */
  612. public static String unixTimeToHumanReadable(long milliseconds) {
  613. Date date = new Date(milliseconds);
  614. DateFormat df = DateFormat.getDateTimeInstance();
  615. return df.format(date);
  616. }
  617. }