1package eu.siacs.conversations.utils;
2
3import android.content.Context;
4import android.graphics.drawable.Drawable;
5import android.content.res.ColorStateList;
6import android.text.SpannableStringBuilder;
7import android.text.format.DateFormat;
8import android.text.format.DateUtils;
9import android.util.Pair;
10import android.widget.TextView;
11import androidx.annotation.ColorInt;
12import androidx.core.content.res.ResourcesCompat;
13import androidx.annotation.ColorRes;
14import androidx.annotation.StringRes;
15import androidx.core.content.ContextCompat;
16
17import com.google.common.collect.Lists;
18import com.google.android.material.color.MaterialColors;
19import com.google.common.base.Strings;
20import com.google.common.primitives.Ints;
21
22import java.math.BigInteger;
23import java.nio.charset.StandardCharsets;
24import java.security.MessageDigest;
25import java.util.Arrays;
26import java.util.Calendar;
27import java.util.Date;
28import java.util.List;
29import java.util.Locale;
30
31import de.gultsch.common.Linkify;
32import com.google.android.material.color.MaterialColors;
33import com.google.common.base.Strings;
34import eu.siacs.conversations.Config;
35import eu.siacs.conversations.R;
36import eu.siacs.conversations.crypto.axolotl.AxolotlService;
37import eu.siacs.conversations.entities.Account;
38import eu.siacs.conversations.entities.Contact;
39import eu.siacs.conversations.entities.Conversation;
40import eu.siacs.conversations.entities.Conversational;
41import eu.siacs.conversations.entities.Message;
42import eu.siacs.conversations.entities.MucOptions;
43import eu.siacs.conversations.entities.Presence;
44import eu.siacs.conversations.entities.Reaction;
45import eu.siacs.conversations.entities.RtpSessionStatus;
46import eu.siacs.conversations.entities.Transferable;
47import eu.siacs.conversations.services.XmppConnectionService;
48import eu.siacs.conversations.ui.util.QuoteHelper;
49import eu.siacs.conversations.worker.ExportBackupWorker;
50import eu.siacs.conversations.xmpp.Jid;
51import java.util.Arrays;
52import java.util.Calendar;
53import java.util.Date;
54import java.util.List;
55import java.util.Locale;
56
57public class UIHelper {
58
59 private static final List<String> LOCATION_QUESTIONS =
60 Arrays.asList(
61 "where are you", // en
62 "where are you now", // en
63 "where are you right now", // en
64 "whats your 20", // en
65 "what is your 20", // en
66 "what's your 20", // en
67 "whats your twenty", // en
68 "what is your twenty", // en
69 "what's your twenty", // en
70 "wo bist du", // de
71 "wo bist du jetzt", // de
72 "wo bist du gerade", // de
73 "wo seid ihr", // de
74 "wo seid ihr jetzt", // de
75 "wo seid ihr gerade", // de
76 "dónde estás", // es
77 "donde estas" // es
78 );
79
80 private static final List<Character> PUNCTIONATION =
81 Arrays.asList('.', ',', '?', '!', ';', ':');
82
83 private static final int SHORT_DATE_FLAGS =
84 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL;
85 private static final int FULL_DATE_FLAGS =
86 DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE;
87
88 public static String readableTimeDifference(Context context, long time) {
89 return readableTimeDifference(context, time, false);
90 }
91
92 public static String readableTimeDifferenceFull(Context context, long time) {
93 return readableTimeDifference(context, time, true);
94 }
95
96 private static String readableTimeDifference(Context context, long time, boolean fullDate) {
97 if (time == 0) {
98 return context.getString(R.string.just_now);
99 }
100 Date date = new Date(time);
101 long difference = (System.currentTimeMillis() - time) / 1000;
102 if (difference < 60) {
103 return context.getString(R.string.just_now);
104 } else if (difference < 60 * 2) {
105 return context.getString(R.string.minute_ago);
106 } else if (difference < 60 * 15) {
107 return context.getString(R.string.minutes_ago, Math.round(difference / 60.0));
108 } else if (today(date)) {
109 java.text.DateFormat df = DateFormat.getTimeFormat(context);
110 return df.format(date);
111 } else {
112 if (fullDate) {
113 return DateUtils.formatDateTime(context, date.getTime(), FULL_DATE_FLAGS);
114 } else {
115 return DateUtils.formatDateTime(context, date.getTime(), SHORT_DATE_FLAGS);
116 }
117 }
118 }
119
120 private static boolean today(Date date) {
121 return sameDay(date, new Date(System.currentTimeMillis()));
122 }
123
124 public static boolean today(long date) {
125 return sameDay(date, System.currentTimeMillis());
126 }
127
128 public static boolean yesterday(long date) {
129 Calendar cal1 = Calendar.getInstance();
130 Calendar cal2 = Calendar.getInstance();
131 cal1.add(Calendar.DAY_OF_YEAR, -1);
132 cal2.setTime(new Date(date));
133 return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
134 && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
135 }
136
137 public static boolean sameDay(long a, long b) {
138 return sameDay(new Date(a), new Date(b));
139 }
140
141 private static boolean sameDay(Date a, Date b) {
142 Calendar cal1 = Calendar.getInstance();
143 Calendar cal2 = Calendar.getInstance();
144 cal1.setTime(a);
145 cal2.setTime(b);
146 return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
147 && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
148 }
149
150 public static String lastseen(Context context, boolean active, long time) {
151 long difference = (System.currentTimeMillis() - time) / 1000;
152 if (active) {
153 return context.getString(R.string.online_right_now);
154 } else if (difference < 60) {
155 return context.getString(R.string.last_seen_now);
156 } else if (difference < 60 * 2) {
157 return context.getString(R.string.last_seen_min);
158 } else if (difference < 60 * 60) {
159 return context.getString(R.string.last_seen_mins, Math.round(difference / 60.0));
160 } else if (difference < 60 * 60 * 2) {
161 return context.getString(R.string.last_seen_hour);
162 } else if (difference < 60 * 60 * 24) {
163 return context.getString(
164 R.string.last_seen_hours, Math.round(difference / (60.0 * 60.0)));
165 } else if (difference < 60 * 60 * 48) {
166 return context.getString(R.string.last_seen_day);
167 } else {
168 return context.getString(
169 R.string.last_seen_days, Math.round(difference / (60.0 * 60.0 * 24.0)));
170 }
171 }
172
173 public static int identiconHash(String name) {
174 try {
175 MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
176 byte[] digest = sha1.digest(name.getBytes(StandardCharsets.UTF_8));
177 return Ints.fromByteArray(digest);
178 } catch (Exception e) {
179 return 0;
180 }
181 }
182
183 public static int getColorForName(final String name) {
184 return XEP0392Helper.rgbFromNick(name);
185 }
186
187 public static Pair<CharSequence, Boolean> getMessagePreview(final XmppConnectionService context, final Message message) {
188 return getMessagePreview(context, message, 0);
189 }
190
191 public static Pair<CharSequence, Boolean> getMessagePreview(
192 final XmppConnectionService context, final Message message, @ColorInt int textColor) {
193 final Transferable d = message.getTransferable();
194 final boolean moderated = message.getModerated() != null;
195 final boolean muted = message.getStatus() == Message.STATUS_RECEIVED && message.getConversation().getMode() == Conversation.MODE_MULTI && context.isMucUserMuted(new MucOptions.User(null, message.getConversation().getJid(), message.getOccupantId(), null, null));
196 if (muted) {
197 return new Pair<>("Muted", false);
198 } else if (d != null && !moderated) {
199 switch (d.getStatus()) {
200 case Transferable.STATUS_CHECKING:
201 return new Pair<>(
202 context.getString(
203 R.string.checking_x,
204 getFileDescriptionString(context, message)),
205 true);
206 case Transferable.STATUS_DOWNLOADING:
207 return new Pair<>(
208 context.getString(
209 R.string.receiving_x_file,
210 getFileDescriptionString(context, message),
211 d.getProgress()),
212 true);
213 case Transferable.STATUS_OFFER:
214 case Transferable.STATUS_OFFER_CHECK_FILESIZE:
215 return new Pair<>(
216 context.getString(
217 R.string.x_file_offered_for_download,
218 getFileDescriptionString(context, message)),
219 true);
220 case Transferable.STATUS_FAILED:
221 return new Pair<>(context.getString(R.string.file_transmission_failed), true);
222 case Transferable.STATUS_CANCELLED:
223 return new Pair<>(
224 context.getString(R.string.file_transmission_cancelled), true);
225 case Transferable.STATUS_UPLOADING:
226 if (message.getStatus() == Message.STATUS_OFFERED) {
227 return new Pair<>(
228 context.getString(
229 R.string.offering_x_file,
230 getFileDescriptionString(context, message)),
231 true);
232 } else {
233 return new Pair<>(
234 context.getString(
235 R.string.sending_x_file,
236 getFileDescriptionString(context, message)),
237 true);
238 }
239 default:
240 return new Pair<>("", false);
241 }
242 } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
243 return new Pair<>(context.getString(R.string.pgp_message), true);
244 } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
245 return new Pair<>(context.getString(R.string.decryption_failed), true);
246 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
247 return new Pair<>(context.getString(R.string.not_encrypted_for_this_device), true);
248 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
249 return new Pair<>(context.getString(R.string.omemo_decryption_failed), true);
250 } else if (message.isFileOrImage() && !moderated) {
251 return new Pair<>(getFileDescriptionString(context, message), true);
252 } else if (message.getType() == Message.TYPE_RTP_SESSION) {
253 RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody());
254 final boolean received = message.getStatus() == Message.STATUS_RECEIVED;
255 if (!rtpSessionStatus.successful && received) {
256 return new Pair<>(context.getString(R.string.missed_call), true);
257 } else {
258 return new Pair<>(
259 context.getString(
260 received ? R.string.incoming_call : R.string.outgoing_call),
261 true);
262 }
263 } else {
264 final String body = MessageUtils.filterLtrRtl(message.getBody());
265 if (body.startsWith(Message.ME_COMMAND)) {
266 return new Pair<>(
267 body.replaceAll(
268 "^" + Message.ME_COMMAND,
269 UIHelper.getMessageDisplayName(message) + " "),
270 false);
271 } else if (message.isGeoUri()) {
272 return new Pair<>(context.getString(R.string.location), true);
273 } else if (!moderated && (message.treatAsDownloadable()
274 || MessageUtils.unInitiatedButKnownSize(message))) {
275 return new Pair<>(
276 context.getString(
277 R.string.x_file_offered_for_download,
278 getFileDescriptionString(context, message)),
279 true);
280 } else {
281 Drawable fallbackImg = ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_photo_24dp, null);
282 fallbackImg.setBounds(0, 0, fallbackImg.getIntrinsicWidth(), fallbackImg.getIntrinsicHeight());
283 SpannableStringBuilder styledBody = message.getSpannableBody(null, fallbackImg);
284 final var processMarkup = styledBody.getSpans(0, body.length(), Message.PlainTextSpan.class).length > 0;
285 if (textColor != 0 && processMarkup) {
286 StylingHelper.format(styledBody, 0, styledBody.length() - 1, textColor, false);
287 }
288 Linkify.addLinks(styledBody, message.getConversation().getAccount(), message.getConversation().getJid());
289
290 for (final android.text.style.QuoteSpan quote : Lists.reverse(Lists.newArrayList(styledBody.getSpans(0, styledBody.length(), android.text.style.QuoteSpan.class)))) {
291 int start = styledBody.getSpanStart(quote);
292 int end = styledBody.getSpanEnd(quote);
293 if (start < 0 || end < 0 || (start == 0 && end == styledBody.length())) continue;
294 styledBody.delete(start, end);
295 styledBody.removeSpan(quote);
296 }
297 if (!processMarkup) return new Pair<>(styledBody, false);
298
299 SpannableStringBuilder builder = new SpannableStringBuilder();
300 for (CharSequence l : CharSequenceUtils.split(styledBody, '\n')) {
301 if (l.length() > 0) {
302 if (l.toString().equals("```")) {
303 continue;
304 }
305 char first = l.charAt(0);
306 if ((!QuoteHelper.isPositionQuoteStart(l, 0))) {
307 CharSequence line = CharSequenceUtils.trim(l);
308 if (line.length() == 0) {
309 continue;
310 }
311 char last = line.charAt(line.length() - 1);
312 if (builder.length() != 0) {
313 builder.append(' ');
314 }
315 builder.append(line);
316 if (!PUNCTIONATION.contains(last)) {
317 break;
318 }
319 }
320 }
321 }
322 if (builder.length() == 0) {
323 builder.append(body.trim());
324 }
325 return new Pair<>(builder, false);
326 }
327 }
328 }
329
330 public static boolean isLastLineQuote(String body) {
331 if (body.endsWith("\n")) {
332 return false;
333 }
334 String[] lines = body.split("\n");
335 if (lines.length == 0) {
336 return false;
337 }
338 String line = lines[lines.length - 1];
339 if (line.isEmpty()) {
340 return false;
341 }
342 char first = line.charAt(0);
343 return first == '>' && isPositionFollowedByQuoteableCharacter(line, 0) || first == '\u00bb';
344 }
345
346 public static CharSequence shorten(CharSequence input) {
347 return input.length() > 256 ? StylingHelper.subSequence(input, 0, 256) : input;
348 }
349
350 public static boolean isPositionPrecededByBodyStart(CharSequence body, int pos) {
351 // true if not a single linebreak before current position
352 for (int i = pos - 1; i >= 0; i--) {
353 if (body.charAt(i) != ' ') {
354 return false;
355 }
356 }
357 return true;
358 }
359
360 public static boolean isPositionPrecededByLineStart(CharSequence body, int pos) {
361 if (isPositionPrecededByBodyStart(body, pos)) {
362 return true;
363 }
364 return body.charAt(pos - 1) == '\n';
365 }
366
367 public static boolean isPositionFollowedByQuoteableCharacter(CharSequence body, int pos) {
368 return !isPositionFollowedByNumber(body, pos)
369 && !isPositionFollowedByEmoticon(body, pos)
370 && !isPositionFollowedByEquals(body, pos);
371 }
372
373 private static boolean isPositionFollowedByNumber(CharSequence body, int pos) {
374 boolean previousWasNumber = false;
375 for (int i = pos + 1; i < body.length(); i++) {
376 char c = body.charAt(i);
377 if (Character.isDigit(body.charAt(i))) {
378 previousWasNumber = true;
379 } else if (previousWasNumber && (c == '.' || c == ',')) {
380 previousWasNumber = false;
381 } else {
382 return (Character.isWhitespace(c) || c == '%' || c == '+') && previousWasNumber;
383 }
384 }
385 return previousWasNumber;
386 }
387
388 private static boolean isPositionFollowedByEquals(CharSequence body, int pos) {
389 return body.length() > pos + 1 && body.charAt(pos + 1) == '=';
390 }
391
392 private static boolean isPositionFollowedByEmoticon(CharSequence body, int pos) {
393 if (body.length() <= pos + 1) {
394 return false;
395 } else {
396 final char first = body.charAt(pos + 1);
397 return first == ';'
398 || first == ':'
399 || first == '.' // do not quote >.< (but >>.<)
400 || closingBeforeWhitespace(body, pos + 1);
401 }
402 }
403
404 private static boolean closingBeforeWhitespace(CharSequence body, int pos) {
405 for (int i = pos; i < body.length(); ++i) {
406 final char c = body.charAt(i);
407 if (Character.isWhitespace(c)) {
408 return false;
409 } else if (QuoteHelper.isPositionQuoteCharacter(body, pos)
410 || QuoteHelper.isPositionQuoteEndCharacter(body, pos)) {
411 return body.length() == i + 1 || Character.isWhitespace(body.charAt(i + 1));
412 }
413 }
414 return false;
415 }
416
417 public static String getDisplayName(MucOptions.User user) {
418 Contact contact = user.getContact();
419 if (contact != null) {
420 return contact.getDisplayName();
421 } else {
422 final String name = user.getNick();
423 if (name != null) {
424 return name;
425 }
426 final Jid realJid = user.getRealJid();
427 if (realJid != null) {
428 return JidHelper.localPartOrFallback(realJid);
429 }
430 return null;
431 }
432 }
433
434 public static String concatNames(List<MucOptions.User> users) {
435 return concatNames(users, users.size());
436 }
437
438 public static String concatNames(List<MucOptions.User> users, int max) {
439 StringBuilder builder = new StringBuilder();
440 final boolean shortNames = users.size() >= 3;
441 for (int i = 0; i < Math.min(users.size(), max); ++i) {
442 if (builder.length() != 0) {
443 builder.append(", ");
444 }
445 final String name = UIHelper.getDisplayName(users.get(i));
446 if (name != null) {
447 builder.append(shortNames ? name.split("\\s+")[0] : name);
448 }
449 }
450 return builder.toString();
451 }
452
453 public static String getFileDescriptionString(final Context context, final Message message) {
454 final String mime = message.getMimeType();
455 if (Strings.isNullOrEmpty(mime)) {
456 return context.getString(R.string.file);
457 } else if (MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime)) {
458 return context.getString(R.string.multimedia_file);
459 } else if (mime.equals("audio/x-m4b")) {
460 return context.getString(R.string.audiobook);
461 } else if (mime.startsWith("audio/")) {
462 return context.getString(R.string.audio);
463 } else if (mime.startsWith("video/")) {
464 return context.getString(R.string.video);
465 } else if (mime.equals("image/gif")) {
466 return context.getString(R.string.gif);
467 } else if (mime.equals("image/svg+xml")) {
468 return context.getString(R.string.vector_graphic);
469 } else if (mime.startsWith("image/") || message.getType() == Message.TYPE_IMAGE) {
470 return context.getString(R.string.image);
471 } else if (mime.contains("pdf")) {
472 return context.getString(R.string.pdf_document);
473 } else if (MimeUtils.WORD_DOCUMENT_MIMES.contains(mime)) {
474 return context.getString(R.string.word_document);
475 } else if (mime.equals("application/vnd.android.package-archive")) {
476 return context.getString(R.string.apk);
477 } else if (mime.equals(ExportBackupWorker.MIME_TYPE)) {
478 return context.getString(R.string.conversations_backup);
479 } else if (mime.contains("vcard")) {
480 return context.getString(R.string.vcard);
481 } else if (mime.equals("text/x-vcalendar") || mime.equals("text/calendar")) {
482 return context.getString(R.string.event);
483 } else if (mime.equals("application/epub+zip")
484 || mime.equals("application/vnd.amazon.mobi8-ebook")) {
485 return context.getString(R.string.ebook);
486 } else if (mime.equals("application/gpx+xml")) {
487 return context.getString(R.string.gpx_track);
488 } else if (mime.equals("application/webxdc+zip")) {
489 final var name = message.getFileParams().getName();
490 if (name != null && name.length() < 20) {
491 return name;
492 } else {
493 return "Widget";
494 }
495 } else if (mime.equals("text/plain")) {
496 return context.getString(R.string.plain_text_document);
497 } else {
498 return mime;
499 }
500 }
501
502 public static String getMessageDisplayName(final Message message) {
503 if (message.getModerated() != null) return "moderated";
504
505 final Conversational conversation = message.getConversation();
506 if (message.getStatus() == Message.STATUS_RECEIVED) {
507 final Contact contact = message.getContact();
508 if (conversation.getMode() == Conversation.MODE_MULTI) {
509 if (contact != null) {
510 return contact.getDisplayName();
511 } else {
512 if (conversation instanceof Conversation) {
513 final MucOptions.User user = ((Conversation) conversation).getMucOptions().findUserByFullJid(message.getCounterpart());
514 if (user != null) {
515 final String dname = getDisplayName(user);
516 if (dname != null) return dname;
517 }
518 }
519 return getDisplayedMucCounterpart(message.getCounterpart());
520 }
521 } else {
522 return contact != null ? contact.getDisplayName() : "";
523 }
524 } else {
525 if (conversation instanceof Conversation
526 && conversation.getMode() == Conversation.MODE_MULTI) {
527 return ((Conversation) conversation).getMucOptions().getSelf().getNick();
528 } else {
529 final Account account = conversation.getAccount();
530 final Jid jid = account.getJid();
531 final String displayName = account.getDisplayName();
532 if (Strings.isNullOrEmpty(displayName)) {
533 return jid.getLocal() != null ? jid.getLocal() : jid.getDomain().toString();
534 } else {
535 return displayName;
536 }
537 }
538 }
539 }
540
541 public static String getDisplayName(final Conversational conversation, final Reaction reaction) {
542 if (conversation.getMode() == Conversation.MODE_MULTI) {
543 if (conversation instanceof Conversation) {
544 MucOptions.User user = ((Conversation) conversation).getMucOptions().findUserByFullJid(reaction.trueJid);
545 if (user == null) user = ((Conversation) conversation).getMucOptions().findUserByOccupantId(reaction.occupantId, reaction.from);
546 if (user != null) {
547 final String dname = getDisplayName(user);
548 if (dname != null) return dname;
549 }
550 }
551 return getDisplayedMucCounterpart(reaction.from);
552 }
553
554 return conversation == null ? reaction.from.asBareJid().toString() : conversation.getAccount().getRoster().getContact(reaction.from).getDisplayName();
555 }
556
557 public static String getMessageHint(final Context context, final Conversation conversation) {
558 return switch (conversation.getNextEncryption()) {
559 case Message.ENCRYPTION_NONE -> {
560 if (Config.multipleEncryptionChoices()) {
561 yield context.getString(R.string.send_message);
562 } else {
563 yield context.getString(R.string.send_message_to_x, conversation.getName());
564 }
565 }
566 case Message.ENCRYPTION_AXOLOTL -> {
567 final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
568 if (axolotlService != null && axolotlService.trustedSessionVerified(conversation)) {
569 yield context.getString(R.string.send_omemo_x509_message);
570 } else {
571 yield context.getString(R.string.send_encrypted_message);
572 }
573 }
574 default -> context.getString(R.string.send_encrypted_message);
575 };
576 }
577
578 public static String getDisplayedMucCounterpart(final Jid counterpart) {
579 if (counterpart == null) {
580 return "";
581 } else if (!counterpart.isBareJid()) {
582 return counterpart.getResource().trim();
583 } else {
584 return counterpart.toString().trim();
585 }
586 }
587
588 public static boolean receivedLocationQuestion(final Message message) {
589 if (message == null
590 || message.getStatus() != Message.STATUS_RECEIVED
591 || message.getType() != Message.TYPE_TEXT) {
592 return false;
593 }
594 final String body =
595 Strings.nullToEmpty(message.getBody())
596 .trim()
597 .toLowerCase(Locale.getDefault())
598 .replace("?", "")
599 .replace("¿", "");
600 return LOCATION_QUESTIONS.contains(body);
601 }
602
603 public static void setStatus(final TextView textView, Presence.Status status) {
604 final @StringRes int text;
605 final @ColorRes int color =
606 switch (status) {
607 case CHAT -> {
608 text = R.string.presence_chat;
609 yield R.color.green_800;
610 }
611 case ONLINE -> {
612 text = R.string.presence_online;
613 yield R.color.green_800;
614 }
615 case AWAY -> {
616 text = R.string.presence_away;
617 yield R.color.amber_800;
618 }
619 case XA -> {
620 text = R.string.presence_xa;
621 yield R.color.orange_800;
622 }
623 case DND -> {
624 text = R.string.presence_dnd;
625 yield R.color.red_800;
626 }
627 default -> throw new IllegalStateException();
628 };
629 textView.setText(text);
630 textView.setBackgroundTintList(
631 ColorStateList.valueOf(
632 MaterialColors.harmonizeWithPrimary(
633 textView.getContext(),
634 ContextCompat.getColor(textView.getContext(), color))));
635 }
636
637 public static String filesizeToString(long size) {
638 if (size > (1.5 * 1024 * 1024)) {
639 return Math.round(size * 1f / (1024 * 1024)) + " MiB";
640 } else if (size >= 1024) {
641 return Math.round(size * 1f / 1024) + " KiB";
642 } else {
643 return size + " B";
644 }
645 }
646}