UIHelper.java

  1package eu.siacs.conversations.utils;
  2
  3import android.content.Context;
  4import android.text.format.DateFormat;
  5import android.text.format.DateUtils;
  6import android.util.Pair;
  7import android.widget.PopupMenu;
  8
  9import java.lang.reflect.Field;
 10import java.lang.reflect.Method;
 11import java.math.BigInteger;
 12import java.nio.ByteBuffer;
 13import java.security.MessageDigest;
 14import java.util.Arrays;
 15import java.util.Calendar;
 16import java.util.Date;
 17import java.util.List;
 18import java.util.Locale;
 19
 20import eu.siacs.conversations.Config;
 21import eu.siacs.conversations.R;
 22import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 23import eu.siacs.conversations.entities.Contact;
 24import eu.siacs.conversations.entities.Conversation;
 25import eu.siacs.conversations.entities.ListItem;
 26import eu.siacs.conversations.entities.Message;
 27import eu.siacs.conversations.entities.MucOptions;
 28import eu.siacs.conversations.entities.Presence;
 29import eu.siacs.conversations.entities.Transferable;
 30import eu.siacs.conversations.xmpp.jid.Jid;
 31
 32public class UIHelper {
 33
 34
 35	private static int COLORS[] = {
 36			0xFFE91E63, //pink 500
 37			0xFFAD1457, //pink 800
 38			0xFF9C27B0, //purple 500
 39			0xFF6A1B9A, //purple 800
 40			0xFF673AB7, //deep purple 500,
 41			0xFF4527A0, //deep purple 800,
 42			0xFF3F51B5, //indigo 500,
 43			0xFF283593, //indigo 800
 44			0xFF2196F3, //blue 500
 45			0xFF1565C0, //blue 800
 46			0xFF03A9F4, //light blue 500
 47			0xFF0277BD, //light blue 800
 48			0xFF00BCD4, //cyan 500
 49			0xFF00838F, //cyan 800
 50			0xFF009688, //teal 500,
 51			0xFF00695C, //teal 800,
 52			//0xFF558B2F, //light green 800
 53			0xFFC0CA33, //lime 600
 54			0xFF9E9D24, //lime 800
 55			0xFFEF6C00, //orange 800
 56			0xFFD84315, //deep orange 800,
 57			0xFF795548, //brown 500,
 58			//0xFF4E342E, //brown 800
 59			0xFF607D8B, //blue grey 500,
 60			0xFF37474F //blue grey 800
 61	};
 62
 63	private static final List<String> LOCATION_QUESTIONS = Arrays.asList(
 64			"where are you", //en
 65			"where are you now", //en
 66			"where are you right now", //en
 67			"whats your 20", //en
 68			"what is your 20", //en
 69			"what's your 20", //en
 70			"whats your twenty", //en
 71			"what is your twenty", //en
 72			"what's your twenty", //en
 73			"wo bist du", //de
 74			"wo bist du jetzt", //de
 75			"wo bist du gerade", //de
 76			"wo seid ihr", //de
 77			"wo seid ihr jetzt", //de
 78			"wo seid ihr gerade", //de
 79			"dónde estás", //es
 80			"donde estas" //es
 81		);
 82
 83	private static final List<Character> PUNCTIONATION = Arrays.asList('.',',','?','!',';',':');
 84
 85	private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE
 86		| DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL;
 87	private static final int FULL_DATE_FLAGS = DateUtils.FORMAT_SHOW_TIME
 88		| DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE;
 89
 90	public static String readableTimeDifference(Context context, long time) {
 91		return readableTimeDifference(context, time, false);
 92	}
 93
 94	public static String readableTimeDifferenceFull(Context context, long time) {
 95		return readableTimeDifference(context, time, true);
 96	}
 97
 98	private static String readableTimeDifference(Context context, long time,
 99			boolean fullDate) {
100		if (time == 0) {
101			return context.getString(R.string.just_now);
102		}
103		Date date = new Date(time);
104		long difference = (System.currentTimeMillis() - time) / 1000;
105		if (difference < 60) {
106			return context.getString(R.string.just_now);
107		} else if (difference < 60 * 2) {
108			return context.getString(R.string.minute_ago);
109		} else if (difference < 60 * 15) {
110			return context.getString(R.string.minutes_ago,Math.round(difference / 60.0));
111		} else if (today(date)) {
112			java.text.DateFormat df = DateFormat.getTimeFormat(context);
113			return df.format(date);
114		} else {
115			if (fullDate) {
116				return DateUtils.formatDateTime(context, date.getTime(),
117						FULL_DATE_FLAGS);
118			} else {
119				return DateUtils.formatDateTime(context, date.getTime(),
120						SHORT_DATE_FLAGS);
121			}
122		}
123	}
124
125	private static boolean today(Date date) {
126		return sameDay(date,new Date(System.currentTimeMillis()));
127	}
128
129	public static boolean today(long date) {
130		return sameDay(date,System.currentTimeMillis());
131	}
132
133	public static boolean yesterday(long date) {
134		Calendar cal1 = Calendar.getInstance();
135		Calendar cal2 = Calendar.getInstance();
136		cal1.add(Calendar.DAY_OF_YEAR,-1);
137		cal2.setTime(new Date(date));
138		return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
139				&& cal1.get(Calendar.DAY_OF_YEAR) == cal2
140				.get(Calendar.DAY_OF_YEAR);
141	}
142
143	public static boolean sameDay(long a, long b) {
144		return sameDay(new Date(a),new Date(b));
145	}
146
147	private static boolean sameDay(Date a, Date b) {
148		Calendar cal1 = Calendar.getInstance();
149		Calendar cal2 = Calendar.getInstance();
150		cal1.setTime(a);
151		cal2.setTime(b);
152		return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
153			&& cal1.get(Calendar.DAY_OF_YEAR) == cal2
154			.get(Calendar.DAY_OF_YEAR);
155	}
156
157	public static String lastseen(Context context, boolean active, long time) {
158		long difference = (System.currentTimeMillis() - time) / 1000;
159		if (active) {
160			return context.getString(R.string.online_right_now);
161		} else if (difference < 60) {
162			return context.getString(R.string.last_seen_now);
163		} else if (difference < 60 * 2) {
164			return context.getString(R.string.last_seen_min);
165		} else if (difference < 60 * 60) {
166			return context.getString(R.string.last_seen_mins,
167					Math.round(difference / 60.0));
168		} else if (difference < 60 * 60 * 2) {
169			return context.getString(R.string.last_seen_hour);
170		} else if (difference < 60 * 60 * 24) {
171			return context.getString(R.string.last_seen_hours,
172					Math.round(difference / (60.0 * 60.0)));
173		} else if (difference < 60 * 60 * 48) {
174			return context.getString(R.string.last_seen_day);
175		} else {
176			return context.getString(R.string.last_seen_days,
177					Math.round(difference / (60.0 * 60.0 * 24.0)));
178		}
179	}
180
181	public static int getColorForName(String name) {
182		if (name == null || name.isEmpty()) {
183			return 0xFF202020;
184		}
185		return COLORS[getIntForName(name) % COLORS.length];
186	}
187
188	private static int getIntForName(String name) {
189		try {
190			final MessageDigest messageDigest = MessageDigest.getInstance("MD5");
191			messageDigest.update(name.getBytes());
192			byte[] bytes = messageDigest.digest();
193			return Math.abs(new BigInteger(bytes).intValue());
194		} catch (Exception e) {
195			return 0;
196		}
197	}
198
199	public static Pair<String,Boolean> getMessagePreview(final Context context, final Message message) {
200		final Transferable d = message.getTransferable();
201		if (d != null ) {
202			switch (d.getStatus()) {
203				case Transferable.STATUS_CHECKING:
204					return new Pair<>(context.getString(R.string.checking_x,
205									getFileDescriptionString(context,message)),true);
206				case Transferable.STATUS_DOWNLOADING:
207					return new Pair<>(context.getString(R.string.receiving_x_file,
208									getFileDescriptionString(context,message),
209									d.getProgress()),true);
210				case Transferable.STATUS_OFFER:
211				case Transferable.STATUS_OFFER_CHECK_FILESIZE:
212					return new Pair<>(context.getString(R.string.x_file_offered_for_download,
213									getFileDescriptionString(context,message)),true);
214				case Transferable.STATUS_DELETED:
215					return new Pair<>(context.getString(R.string.file_deleted),true);
216				case Transferable.STATUS_FAILED:
217					return new Pair<>(context.getString(R.string.file_transmission_failed),true);
218				case Transferable.STATUS_UPLOADING:
219					if (message.getStatus() == Message.STATUS_OFFERED) {
220						return new Pair<>(context.getString(R.string.offering_x_file,
221								getFileDescriptionString(context, message)), true);
222					} else {
223						return new Pair<>(context.getString(R.string.sending_x_file,
224								getFileDescriptionString(context, message)), true);
225					}
226				default:
227					return new Pair<>("",false);
228			}
229		} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
230			return new Pair<>(context.getString(R.string.pgp_message),true);
231		} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
232			return new Pair<>(context.getString(R.string.decryption_failed), true);
233		} else if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) {
234			if (message.getStatus() == Message.STATUS_RECEIVED) {
235				return new Pair<>(context.getString(R.string.received_x_file,
236							getFileDescriptionString(context, message)), true);
237			} else {
238				return new Pair<>(getFileDescriptionString(context,message),true);
239			}
240		} else {
241			final String body = message.getBody();
242			if (body.startsWith(Message.ME_COMMAND)) {
243				return new Pair<>(body.replaceAll("^" + Message.ME_COMMAND,
244						UIHelper.getMessageDisplayName(message) + " "), false);
245			} else if (message.isGeoUri()) {
246				if (message.getStatus() == Message.STATUS_RECEIVED) {
247					return new Pair<>(context.getString(R.string.received_location), true);
248				} else {
249					return new Pair<>(context.getString(R.string.location), true);
250				}
251			} else if (message.treatAsDownloadable()) {
252				return new Pair<>(context.getString(R.string.x_file_offered_for_download,
253						getFileDescriptionString(context,message)),true);
254			} else {
255				String[] lines = body.split("\n");
256				StringBuilder builder = new StringBuilder();
257				for(String l : lines) {
258					if (l.length() > 0) {
259						char first = l.charAt(0);
260						if ((first != '>' || !isPositionFollowedByQuoteableCharacter(l,0)) && first != '\u00bb') {
261							String line = l.trim();
262							if (line.isEmpty()) {
263								continue;
264							}
265							char last = line.charAt(line.length()-1);
266							if (builder.length() != 0) {
267								builder.append(' ');
268							}
269							builder.append(line);
270							if (!PUNCTIONATION.contains(last)) {
271								break;
272							}
273						}
274					}
275				}
276				if (builder.length() == 0) {
277					builder.append(body.trim());
278				}
279				return new Pair<>(builder.length() > 256 ? builder.substring(0,256) : builder.toString(), false);
280			}
281		}
282	}
283
284	public static boolean isPositionFollowedByQuoteableCharacter(CharSequence body, int pos) {
285		return !isPositionFollowedByNumber(body, pos)
286				&& !isPositionFollowedByEmoticon(body,pos)
287				&& !isPositionFollowedByEquals(body,pos);
288	}
289
290	private static boolean isPositionFollowedByNumber(CharSequence body, int pos) {
291		boolean previousWasNumber = false;
292		for (int i = pos +1; i < body.length(); i++) {
293			char c = body.charAt(i);
294			if (Character.isDigit(body.charAt(i))) {
295				previousWasNumber = true;
296			} else if (previousWasNumber && (c == '.' || c == ',')) {
297				previousWasNumber = false;
298			} else {
299				return (Character.isWhitespace(c) || c == '%' || c == '+') && previousWasNumber;
300			}
301		}
302		return previousWasNumber;
303	}
304
305	private static boolean isPositionFollowedByEquals(CharSequence body, int pos) {
306		return body.length() > pos + 1 && body.charAt(pos+1) == '=';
307	}
308
309	private static boolean isPositionFollowedByEmoticon(CharSequence body, int pos) {
310		if (body.length() <= pos +1) {
311			return false;
312		} else {
313			final char first = body.charAt(pos +1);
314			return first == ';'
315				|| first == ':'
316				|| smallerThanBeforeWhitespace(body,pos+1);
317		}
318	}
319
320	private static boolean smallerThanBeforeWhitespace(CharSequence body, int pos) {
321		for(int i = pos; i < body.length(); ++i) {
322			final char c = body.charAt(i);
323			if (Character.isWhitespace(c)) {
324				return false;
325			} else if (c == '<') {
326				return body.length() == i + 1 || Character.isWhitespace(body.charAt(i + 1));
327			}
328		}
329		return false;
330	}
331
332	public static boolean isPositionFollowedByQuote(CharSequence body, int pos) {
333		if (body.length() <= pos + 1 || Character.isWhitespace(body.charAt(pos+1))) {
334			return false;
335		}
336		boolean previousWasWhitespace = false;
337		for (int i = pos +1; i < body.length(); i++) {
338			char c = body.charAt(i);
339			if (c == '\n' || c == '»') {
340				return false;
341			} else if (c == '«' && !previousWasWhitespace) {
342				return true;
343			} else {
344				previousWasWhitespace = Character.isWhitespace(c);
345			}
346		}
347		return false;
348	}
349
350	public static String getDisplayName(MucOptions.User user) {
351		Contact contact = user.getContact();
352		if (contact != null) {
353			return contact.getDisplayName();
354		} else {
355			final String name = user.getName();
356			if (name != null) {
357				return name;
358			}
359			final Jid realJid = user.getRealJid();
360			if (realJid != null) {
361				return JidHelper.localPartOrFallback(realJid);
362			}
363			return null;
364		}
365	}
366
367	public static String concatNames(List<MucOptions.User> users) {
368		return concatNames(users,users.size());
369	}
370
371	public static String concatNames(List<MucOptions.User> users, int max) {
372		StringBuilder builder = new StringBuilder();
373		final boolean shortNames = users.size() >= 3;
374		for(int i = 0; i < Math.max(users.size(),max); ++i) {
375			if (builder.length() != 0) {
376				builder.append(", ");
377			}
378			final String name = UIHelper.getDisplayName(users.get(i));
379			builder.append(shortNames ? name.split("\\s+")[0] : name);
380		}
381		return builder.toString();
382	}
383
384	public static String getFileDescriptionString(final Context context, final Message message) {
385		if (message.getType() == Message.TYPE_IMAGE) {
386			return context.getString(R.string.image);
387		}
388		final String mime = message.getMimeType();
389		if (mime == null) {
390			return context.getString(R.string.file);
391		} else if (mime.startsWith("audio/")) {
392			return context.getString(R.string.audio);
393		} else if(mime.startsWith("video/")) {
394			return context.getString(R.string.video);
395		} else if (mime.startsWith("image/")) {
396			return context.getString(R.string.image);
397		} else if (mime.contains("pdf")) {
398			return context.getString(R.string.pdf_document)	;
399		} else if (mime.contains("application/vnd.android.package-archive")) {
400			return context.getString(R.string.apk)	;
401		} else if (mime.contains("vcard")) {
402			return context.getString(R.string.vcard)	;
403		} else {
404			return mime;
405		}
406	}
407
408	public static String getMessageDisplayName(final Message message) {
409		final Conversation conversation = message.getConversation();
410		if (message.getStatus() == Message.STATUS_RECEIVED) {
411			final Contact contact = message.getContact();
412			if (conversation.getMode() == Conversation.MODE_MULTI) {
413				if (contact != null) {
414					return contact.getDisplayName();
415				} else {
416					return getDisplayedMucCounterpart(message.getCounterpart());
417				}
418			} else {
419				return contact != null ? contact.getDisplayName() : "";
420			}
421		} else {
422			if (conversation.getMode() == Conversation.MODE_MULTI) {
423				return conversation.getMucOptions().getSelf().getName();
424			} else {
425				final Jid jid = conversation.getAccount().getJid();
426				return jid.hasLocalpart() ? jid.getLocalpart() : jid.toDomainJid().toString();
427			}
428		}
429	}
430
431	public static String getMessageHint(Context context, Conversation conversation) {
432		switch (conversation.getNextEncryption()) {
433			case Message.ENCRYPTION_NONE:
434				if (Config.multipleEncryptionChoices()) {
435					return context.getString(R.string.send_unencrypted_message);
436				} else {
437					return context.getString(R.string.send_message_to_x,conversation.getName());
438				}
439			case Message.ENCRYPTION_OTR:
440				return context.getString(R.string.send_otr_message);
441			case Message.ENCRYPTION_AXOLOTL:
442				AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
443				if (axolotlService != null && axolotlService.trustedSessionVerified(conversation)) {
444					return context.getString(R.string.send_omemo_x509_message);
445				} else {
446					return context.getString(R.string.send_omemo_message);
447				}
448			case Message.ENCRYPTION_PGP:
449				return context.getString(R.string.send_pgp_message);
450			default:
451				return "";
452		}
453	}
454
455	public static String getDisplayedMucCounterpart(final Jid counterpart) {
456		if (counterpart==null) {
457			return "";
458		} else if (!counterpart.isBareJid()) {
459			return counterpart.getResourcepart().trim();
460		} else {
461			return counterpart.toString().trim();
462		}
463	}
464
465	public static boolean receivedLocationQuestion(Message message) {
466		if (message == null
467				|| message.getStatus() != Message.STATUS_RECEIVED
468				|| message.getType() != Message.TYPE_TEXT) {
469			return false;
470		}
471		String body = message.getBody() == null ? null : message.getBody().trim().toLowerCase(Locale.getDefault());
472		body = body.replace("?","").replace("¿","");
473		return LOCATION_QUESTIONS.contains(body);
474	}
475
476	public static ListItem.Tag getTagForStatus(Context context, Presence.Status status) {
477		switch (status) {
478			case CHAT:
479				return new ListItem.Tag(context.getString(R.string.presence_chat), 0xff259b24);
480			case AWAY:
481				return new ListItem.Tag(context.getString(R.string.presence_away), 0xffff9800);
482			case XA:
483				return new ListItem.Tag(context.getString(R.string.presence_xa), 0xfff44336);
484			case DND:
485				return new ListItem.Tag(context.getString(R.string.presence_dnd), 0xfff44336);
486			default:
487				return new ListItem.Tag(context.getString(R.string.presence_online), 0xff259b24);
488		}
489	}
490
491	public static String tranlasteType(Context context, String type) {
492		switch (type.toLowerCase()) {
493			case "pc":
494				return context.getString(R.string.type_pc);
495			case "phone":
496				return context.getString(R.string.type_phone);
497			case "tablet":
498				return context.getString(R.string.type_tablet);
499			case "web":
500				return context.getString(R.string.type_web);
501			case "console":
502				return context.getString(R.string.type_console);
503			default:
504				return type;
505		}
506	}
507
508	public static boolean showIconsInPopup(PopupMenu attachFilePopup) {
509		try {
510			Field field = attachFilePopup.getClass().getDeclaredField("mPopup");
511			field.setAccessible(true);
512			Object menuPopupHelper = field.get(attachFilePopup);
513			Class<?> cls = Class.forName("com.android.internal.view.menu.MenuPopupHelper");
514			Method method = cls.getDeclaredMethod("setForceShowIcon", new Class[]{boolean.class});
515			method.setAccessible(true);
516			method.invoke(menuPopupHelper, new Object[]{true});
517			return true;
518		} catch (Exception e) {
519			return false;
520		}
521	}
522}