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		StringBuilder builder = new StringBuilder();
369		final boolean shortNames = users.size() >= 3;
370		for(MucOptions.User user : users) {
371			if (builder.length() != 0) {
372				builder.append(", ");
373			}
374			final String name = UIHelper.getDisplayName(user);
375			builder.append(shortNames ? name.split("\\s+")[0] : name);
376		}
377		return builder.toString();
378	}
379
380	public static String getFileDescriptionString(final Context context, final Message message) {
381		if (message.getType() == Message.TYPE_IMAGE) {
382			return context.getString(R.string.image);
383		}
384		final String mime = message.getMimeType();
385		if (mime == null) {
386			return context.getString(R.string.file);
387		} else if (mime.startsWith("audio/")) {
388			return context.getString(R.string.audio);
389		} else if(mime.startsWith("video/")) {
390			return context.getString(R.string.video);
391		} else if (mime.startsWith("image/")) {
392			return context.getString(R.string.image);
393		} else if (mime.contains("pdf")) {
394			return context.getString(R.string.pdf_document)	;
395		} else if (mime.contains("application/vnd.android.package-archive")) {
396			return context.getString(R.string.apk)	;
397		} else if (mime.contains("vcard")) {
398			return context.getString(R.string.vcard)	;
399		} else {
400			return mime;
401		}
402	}
403
404	public static String getMessageDisplayName(final Message message) {
405		final Conversation conversation = message.getConversation();
406		if (message.getStatus() == Message.STATUS_RECEIVED) {
407			final Contact contact = message.getContact();
408			if (conversation.getMode() == Conversation.MODE_MULTI) {
409				if (contact != null) {
410					return contact.getDisplayName();
411				} else {
412					return getDisplayedMucCounterpart(message.getCounterpart());
413				}
414			} else {
415				return contact != null ? contact.getDisplayName() : "";
416			}
417		} else {
418			if (conversation.getMode() == Conversation.MODE_MULTI) {
419				return conversation.getMucOptions().getSelf().getName();
420			} else {
421				final Jid jid = conversation.getAccount().getJid();
422				return jid.hasLocalpart() ? jid.getLocalpart() : jid.toDomainJid().toString();
423			}
424		}
425	}
426
427	public static String getMessageHint(Context context, Conversation conversation) {
428		switch (conversation.getNextEncryption()) {
429			case Message.ENCRYPTION_NONE:
430				if (Config.multipleEncryptionChoices()) {
431					return context.getString(R.string.send_unencrypted_message);
432				} else {
433					return context.getString(R.string.send_message_to_x,conversation.getName());
434				}
435			case Message.ENCRYPTION_OTR:
436				return context.getString(R.string.send_otr_message);
437			case Message.ENCRYPTION_AXOLOTL:
438				AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
439				if (axolotlService != null && axolotlService.trustedSessionVerified(conversation)) {
440					return context.getString(R.string.send_omemo_x509_message);
441				} else {
442					return context.getString(R.string.send_omemo_message);
443				}
444			case Message.ENCRYPTION_PGP:
445				return context.getString(R.string.send_pgp_message);
446			default:
447				return "";
448		}
449	}
450
451	public static String getDisplayedMucCounterpart(final Jid counterpart) {
452		if (counterpart==null) {
453			return "";
454		} else if (!counterpart.isBareJid()) {
455			return counterpart.getResourcepart().trim();
456		} else {
457			return counterpart.toString().trim();
458		}
459	}
460
461	public static boolean receivedLocationQuestion(Message message) {
462		if (message == null
463				|| message.getStatus() != Message.STATUS_RECEIVED
464				|| message.getType() != Message.TYPE_TEXT) {
465			return false;
466		}
467		String body = message.getBody() == null ? null : message.getBody().trim().toLowerCase(Locale.getDefault());
468		body = body.replace("?","").replace("¿","");
469		return LOCATION_QUESTIONS.contains(body);
470	}
471
472	public static ListItem.Tag getTagForStatus(Context context, Presence.Status status) {
473		switch (status) {
474			case CHAT:
475				return new ListItem.Tag(context.getString(R.string.presence_chat), 0xff259b24);
476			case AWAY:
477				return new ListItem.Tag(context.getString(R.string.presence_away), 0xffff9800);
478			case XA:
479				return new ListItem.Tag(context.getString(R.string.presence_xa), 0xfff44336);
480			case DND:
481				return new ListItem.Tag(context.getString(R.string.presence_dnd), 0xfff44336);
482			default:
483				return new ListItem.Tag(context.getString(R.string.presence_online), 0xff259b24);
484		}
485	}
486
487	public static String tranlasteType(Context context, String type) {
488		switch (type.toLowerCase()) {
489			case "pc":
490				return context.getString(R.string.type_pc);
491			case "phone":
492				return context.getString(R.string.type_phone);
493			case "tablet":
494				return context.getString(R.string.type_tablet);
495			case "web":
496				return context.getString(R.string.type_web);
497			case "console":
498				return context.getString(R.string.type_console);
499			default:
500				return type;
501		}
502	}
503
504	public static boolean showIconsInPopup(PopupMenu attachFilePopup) {
505		try {
506			Field field = attachFilePopup.getClass().getDeclaredField("mPopup");
507			field.setAccessible(true);
508			Object menuPopupHelper = field.get(attachFilePopup);
509			Class<?> cls = Class.forName("com.android.internal.view.menu.MenuPopupHelper");
510			Method method = cls.getDeclaredMethod("setForceShowIcon", new Class[]{boolean.class});
511			method.setAccessible(true);
512			method.invoke(menuPopupHelper, new Object[]{true});
513			return true;
514		} catch (Exception e) {
515			return false;
516		}
517	}
518}