DisplayUtils.java 28 KB

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