XmppActivity.java

  1package eu.siacs.conversations.ui;
  2
  3import java.io.FileNotFoundException;
  4import java.lang.ref.WeakReference;
  5import java.util.List;
  6import java.util.concurrent.RejectedExecutionException;
  7
  8import eu.siacs.conversations.Config;
  9import eu.siacs.conversations.R;
 10import eu.siacs.conversations.entities.Account;
 11import eu.siacs.conversations.entities.Contact;
 12import eu.siacs.conversations.entities.Conversation;
 13import eu.siacs.conversations.entities.Message;
 14import eu.siacs.conversations.entities.Presences;
 15import eu.siacs.conversations.services.AvatarService;
 16import eu.siacs.conversations.services.XmppConnectionService;
 17import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
 18import eu.siacs.conversations.utils.ExceptionHelper;
 19import android.annotation.SuppressLint;
 20import android.app.Activity;
 21import android.app.AlertDialog;
 22import android.app.PendingIntent;
 23import android.app.AlertDialog.Builder;
 24import android.content.ComponentName;
 25import android.content.Context;
 26import android.content.DialogInterface;
 27import android.content.SharedPreferences;
 28import android.content.DialogInterface.OnClickListener;
 29import android.content.IntentSender.SendIntentException;
 30import android.content.pm.PackageManager;
 31import android.content.pm.ResolveInfo;
 32import android.content.res.Resources;
 33import android.content.Intent;
 34import android.content.ServiceConnection;
 35import android.graphics.Bitmap;
 36import android.graphics.drawable.BitmapDrawable;
 37import android.graphics.drawable.Drawable;
 38import android.net.Uri;
 39import android.os.AsyncTask;
 40import android.os.Bundle;
 41import android.os.IBinder;
 42import android.preference.PreferenceManager;
 43import android.text.InputType;
 44import android.util.DisplayMetrics;
 45import android.util.Log;
 46import android.view.MenuItem;
 47import android.view.View;
 48import android.view.inputmethod.InputMethodManager;
 49import android.widget.EditText;
 50import android.widget.ImageView;
 51
 52public abstract class XmppActivity extends Activity {
 53
 54	protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
 55	protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
 56
 57	public XmppConnectionService xmppConnectionService;
 58	public boolean xmppConnectionServiceBound = false;
 59	protected boolean handledViewIntent = false;
 60
 61	protected int mPrimaryTextColor;
 62	protected int mSecondaryTextColor;
 63	protected int mSecondaryBackgroundColor;
 64	protected int mColorRed;
 65	protected int mColorOrange;
 66	protected int mColorGreen;
 67	protected int mPrimaryColor;
 68
 69	protected boolean mUseSubject = true;
 70
 71	private DisplayMetrics metrics;
 72
 73	protected interface OnValueEdited {
 74		public void onValueEdited(String value);
 75	}
 76
 77	public interface OnPresenceSelected {
 78		public void onPresenceSelected();
 79	}
 80
 81	protected ServiceConnection mConnection = new ServiceConnection() {
 82
 83		@Override
 84		public void onServiceConnected(ComponentName className, IBinder service) {
 85			XmppConnectionBinder binder = (XmppConnectionBinder) service;
 86			xmppConnectionService = binder.getService();
 87			xmppConnectionServiceBound = true;
 88			onBackendConnected();
 89		}
 90
 91		@Override
 92		public void onServiceDisconnected(ComponentName arg0) {
 93			xmppConnectionServiceBound = false;
 94		}
 95	};
 96
 97	@Override
 98	protected void onStart() {
 99		super.onStart();
100		if (!xmppConnectionServiceBound) {
101			connectToBackend();
102		}
103	}
104
105	public void connectToBackend() {
106		Intent intent = new Intent(this, XmppConnectionService.class);
107		intent.setAction("ui");
108		startService(intent);
109		bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
110	}
111
112	@Override
113	protected void onStop() {
114		super.onStop();
115		if (xmppConnectionServiceBound) {
116			unbindService(mConnection);
117			xmppConnectionServiceBound = false;
118		}
119	}
120
121	protected void hideKeyboard() {
122		InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
123
124		View focus = getCurrentFocus();
125
126		if (focus != null) {
127
128			inputManager.hideSoftInputFromWindow(focus.getWindowToken(),
129					InputMethodManager.HIDE_NOT_ALWAYS);
130		}
131	}
132
133	public boolean hasPgp() {
134		return xmppConnectionService.getPgpEngine() != null;
135	}
136
137	public void showInstallPgpDialog() {
138		Builder builder = new AlertDialog.Builder(this);
139		builder.setTitle(getString(R.string.openkeychain_required));
140		builder.setIconAttribute(android.R.attr.alertDialogIcon);
141		builder.setMessage(getText(R.string.openkeychain_required_long));
142		builder.setNegativeButton(getString(R.string.cancel), null);
143		builder.setNeutralButton(getString(R.string.restart),
144				new OnClickListener() {
145
146					@Override
147					public void onClick(DialogInterface dialog, int which) {
148						if (xmppConnectionServiceBound) {
149							unbindService(mConnection);
150							xmppConnectionServiceBound = false;
151						}
152						stopService(new Intent(XmppActivity.this,
153								XmppConnectionService.class));
154						finish();
155					}
156				});
157		builder.setPositiveButton(getString(R.string.install),
158				new OnClickListener() {
159
160					@Override
161					public void onClick(DialogInterface dialog, int which) {
162						Uri uri = Uri
163								.parse("market://details?id=org.sufficientlysecure.keychain");
164						Intent marketIntent = new Intent(Intent.ACTION_VIEW,
165								uri);
166						PackageManager manager = getApplicationContext()
167								.getPackageManager();
168						List<ResolveInfo> infos = manager
169								.queryIntentActivities(marketIntent, 0);
170						if (infos.size() > 0) {
171							startActivity(marketIntent);
172						} else {
173							uri = Uri.parse("http://www.openkeychain.org/");
174							Intent browserIntent = new Intent(
175									Intent.ACTION_VIEW, uri);
176							startActivity(browserIntent);
177						}
178						finish();
179					}
180				});
181		builder.create().show();
182	}
183
184	abstract void onBackendConnected();
185
186	public boolean onOptionsItemSelected(MenuItem item) {
187		switch (item.getItemId()) {
188		case R.id.action_settings:
189			startActivity(new Intent(this, SettingsActivity.class));
190			break;
191		case R.id.action_accounts:
192			startActivity(new Intent(this, ManageAccountActivity.class));
193			break;
194		case android.R.id.home:
195			finish();
196			break;
197		}
198		return super.onOptionsItemSelected(item);
199	}
200
201	@Override
202	protected void onCreate(Bundle savedInstanceState) {
203		super.onCreate(savedInstanceState);
204		metrics = getResources().getDisplayMetrics();
205		ExceptionHelper.init(getApplicationContext());
206		mPrimaryTextColor = getResources().getColor(R.color.primarytext);
207		mSecondaryTextColor = getResources().getColor(R.color.secondarytext);
208		mColorRed = getResources().getColor(R.color.red);
209		mColorOrange = getResources().getColor(R.color.orange);
210		mColorGreen = getResources().getColor(R.color.green);
211		mPrimaryColor = getResources().getColor(R.color.primary);
212		mSecondaryBackgroundColor = getResources().getColor(
213				R.color.secondarybackground);
214		if (getPreferences().getBoolean("use_larger_font", false)) {
215			setTheme(R.style.ConversationsTheme_LargerText);
216		}
217		mUseSubject = getPreferences().getBoolean("use_subject", true);
218	}
219
220	protected SharedPreferences getPreferences() {
221		return PreferenceManager
222				.getDefaultSharedPreferences(getApplicationContext());
223	}
224
225	public boolean useSubjectToIdentifyConference() {
226		return mUseSubject;
227	}
228
229	public void switchToConversation(Conversation conversation) {
230		switchToConversation(conversation, null, false);
231	}
232
233	public void switchToConversation(Conversation conversation, String text,
234			boolean newTask) {
235		Intent viewConversationIntent = new Intent(this,
236				ConversationActivity.class);
237		viewConversationIntent.setAction(Intent.ACTION_VIEW);
238		viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
239				conversation.getUuid());
240		if (text != null) {
241			viewConversationIntent.putExtra(ConversationActivity.TEXT, text);
242		}
243		viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
244		if (newTask) {
245			viewConversationIntent.setFlags(viewConversationIntent.getFlags()
246					| Intent.FLAG_ACTIVITY_NEW_TASK
247					| Intent.FLAG_ACTIVITY_SINGLE_TOP);
248		} else {
249			viewConversationIntent.setFlags(viewConversationIntent.getFlags()
250					| Intent.FLAG_ACTIVITY_CLEAR_TOP);
251		}
252		startActivity(viewConversationIntent);
253		finish();
254	}
255
256	public void switchToContactDetails(Contact contact) {
257		Intent intent = new Intent(this, ContactDetailsActivity.class);
258		intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
259		intent.putExtra("account", contact.getAccount().getJid());
260		intent.putExtra("contact", contact.getJid());
261		startActivity(intent);
262	}
263
264	public void switchToAccount(Account account) {
265		Intent intent = new Intent(this, EditAccountActivity.class);
266		intent.putExtra("jid", account.getJid());
267		startActivity(intent);
268	}
269
270	protected void inviteToConversation(Conversation conversation) {
271		Intent intent = new Intent(getApplicationContext(),
272				ChooseContactActivity.class);
273		intent.putExtra("conversation", conversation.getUuid());
274		startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION);
275	}
276
277	protected void announcePgp(Account account, final Conversation conversation) {
278		xmppConnectionService.getPgpEngine().generateSignature(account,
279				"online", new UiCallback<Account>() {
280
281					@Override
282					public void userInputRequried(PendingIntent pi,
283							Account account) {
284						try {
285							startIntentSenderForResult(pi.getIntentSender(),
286									REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
287						} catch (SendIntentException e) {
288						}
289					}
290
291					@Override
292					public void success(Account account) {
293						xmppConnectionService.databaseBackend
294								.updateAccount(account);
295						xmppConnectionService.sendPresencePacket(account,
296								xmppConnectionService.getPresenceGenerator()
297										.sendPresence(account));
298						if (conversation != null) {
299							conversation
300									.setNextEncryption(Message.ENCRYPTION_PGP);
301							xmppConnectionService.databaseBackend
302									.updateConversation(conversation);
303						}
304					}
305
306					@Override
307					public void error(int error, Account account) {
308						displayErrorDialog(error);
309					}
310				});
311	}
312
313	protected void displayErrorDialog(final int errorCode) {
314		runOnUiThread(new Runnable() {
315
316			@Override
317			public void run() {
318				AlertDialog.Builder builder = new AlertDialog.Builder(
319						XmppActivity.this);
320				builder.setIconAttribute(android.R.attr.alertDialogIcon);
321				builder.setTitle(getString(R.string.error));
322				builder.setMessage(errorCode);
323				builder.setNeutralButton(R.string.accept, null);
324				builder.create().show();
325			}
326		});
327
328	}
329
330	protected void showAddToRosterDialog(final Conversation conversation) {
331		String jid = conversation.getContactJid();
332		AlertDialog.Builder builder = new AlertDialog.Builder(this);
333		builder.setTitle(jid);
334		builder.setMessage(getString(R.string.not_in_roster));
335		builder.setNegativeButton(getString(R.string.cancel), null);
336		builder.setPositiveButton(getString(R.string.add_contact),
337				new DialogInterface.OnClickListener() {
338
339					@Override
340					public void onClick(DialogInterface dialog, int which) {
341						String jid = conversation.getContactJid();
342						Account account = conversation.getAccount();
343						Contact contact = account.getRoster().getContact(jid);
344						xmppConnectionService.createContact(contact);
345						switchToContactDetails(contact);
346					}
347				});
348		builder.create().show();
349	}
350
351	private void showAskForPresenceDialog(final Contact contact) {
352		AlertDialog.Builder builder = new AlertDialog.Builder(this);
353		builder.setTitle(contact.getJid());
354		builder.setMessage(R.string.request_presence_updates);
355		builder.setNegativeButton(R.string.cancel, null);
356		builder.setPositiveButton(R.string.request_now,
357				new DialogInterface.OnClickListener() {
358
359					@Override
360					public void onClick(DialogInterface dialog, int which) {
361						if (xmppConnectionServiceBound) {
362							xmppConnectionService.sendPresencePacket(contact
363									.getAccount(), xmppConnectionService
364									.getPresenceGenerator()
365									.requestPresenceUpdatesFrom(contact));
366						}
367					}
368				});
369		builder.create().show();
370	}
371
372	private void warnMutalPresenceSubscription(final Conversation conversation,
373			final OnPresenceSelected listener) {
374		AlertDialog.Builder builder = new AlertDialog.Builder(this);
375		builder.setTitle(conversation.getContact().getJid());
376		builder.setMessage(R.string.without_mutual_presence_updates);
377		builder.setNegativeButton(R.string.cancel, null);
378		builder.setPositiveButton(R.string.ignore, new OnClickListener() {
379
380			@Override
381			public void onClick(DialogInterface dialog, int which) {
382				conversation.setNextPresence(null);
383				if (listener != null) {
384					listener.onPresenceSelected();
385				}
386			}
387		});
388		builder.create().show();
389	}
390
391	protected void quickEdit(String previousValue, OnValueEdited callback) {
392		quickEdit(previousValue, callback, false);
393	}
394
395	protected void quickPasswordEdit(String previousValue,
396			OnValueEdited callback) {
397		quickEdit(previousValue, callback, true);
398	}
399
400	@SuppressLint("InflateParams")
401	private void quickEdit(final String previousValue,
402			final OnValueEdited callback, boolean password) {
403		AlertDialog.Builder builder = new AlertDialog.Builder(this);
404		View view = (View) getLayoutInflater()
405				.inflate(R.layout.quickedit, null);
406		final EditText editor = (EditText) view.findViewById(R.id.editor);
407		OnClickListener mClickListener = new OnClickListener() {
408
409			@Override
410			public void onClick(DialogInterface dialog, int which) {
411				String value = editor.getText().toString();
412				if (!previousValue.equals(value) && value.trim().length() > 0) {
413					callback.onValueEdited(value);
414				}
415			}
416		};
417		if (password) {
418			editor.setInputType(InputType.TYPE_CLASS_TEXT
419					| InputType.TYPE_TEXT_VARIATION_PASSWORD);
420			editor.setHint(R.string.password);
421			builder.setPositiveButton(R.string.accept, mClickListener);
422		} else {
423			builder.setPositiveButton(R.string.edit, mClickListener);
424		}
425		editor.requestFocus();
426		editor.setText(previousValue);
427		builder.setView(view);
428		builder.setNegativeButton(R.string.cancel, null);
429		builder.create().show();
430	}
431
432	public void selectPresence(final Conversation conversation,
433			final OnPresenceSelected listener) {
434		Contact contact = conversation.getContact();
435		if (!contact.showInRoster()) {
436			showAddToRosterDialog(conversation);
437		} else {
438			Presences presences = contact.getPresences();
439			if (presences.size() == 0) {
440				if (!contact.getOption(Contact.Options.TO)
441						&& !contact.getOption(Contact.Options.ASKING)
442						&& contact.getAccount().getStatus() == Account.STATUS_ONLINE) {
443					showAskForPresenceDialog(contact);
444				} else if (!contact.getOption(Contact.Options.TO)
445						|| !contact.getOption(Contact.Options.FROM)) {
446					warnMutalPresenceSubscription(conversation, listener);
447				} else {
448					conversation.setNextPresence(null);
449					listener.onPresenceSelected();
450				}
451			} else if (presences.size() == 1) {
452				String presence = (String) presences.asStringArray()[0];
453				conversation.setNextPresence(presence);
454				listener.onPresenceSelected();
455			} else {
456				final StringBuilder presence = new StringBuilder();
457				AlertDialog.Builder builder = new AlertDialog.Builder(this);
458				builder.setTitle(getString(R.string.choose_presence));
459				final String[] presencesArray = presences.asStringArray();
460				int preselectedPresence = 0;
461				for (int i = 0; i < presencesArray.length; ++i) {
462					if (presencesArray[i].equals(contact.lastseen.presence)) {
463						preselectedPresence = i;
464						break;
465					}
466				}
467				presence.append(presencesArray[preselectedPresence]);
468				builder.setSingleChoiceItems(presencesArray,
469						preselectedPresence,
470						new DialogInterface.OnClickListener() {
471
472							@Override
473							public void onClick(DialogInterface dialog,
474									int which) {
475								presence.delete(0, presence.length());
476								presence.append(presencesArray[which]);
477							}
478						});
479				builder.setNegativeButton(R.string.cancel, null);
480				builder.setPositiveButton(R.string.ok, new OnClickListener() {
481
482					@Override
483					public void onClick(DialogInterface dialog, int which) {
484						conversation.setNextPresence(presence.toString());
485						listener.onPresenceSelected();
486					}
487				});
488				builder.create().show();
489			}
490		}
491	}
492
493	protected void onActivityResult(int requestCode, int resultCode,
494			final Intent data) {
495		super.onActivityResult(requestCode, resultCode, data);
496		if (requestCode == REQUEST_INVITE_TO_CONVERSATION
497				&& resultCode == RESULT_OK) {
498			String contactJid = data.getStringExtra("contact");
499			String conversationUuid = data.getStringExtra("conversation");
500			Conversation conversation = xmppConnectionService
501					.findConversationByUuid(conversationUuid);
502			if (conversation.getMode() == Conversation.MODE_MULTI) {
503				xmppConnectionService.invite(conversation, contactJid);
504			}
505			Log.d(Config.LOGTAG, "inviting " + contactJid + " to "
506					+ conversation.getName());
507		}
508	}
509
510	public int getSecondaryTextColor() {
511		return this.mSecondaryTextColor;
512	}
513
514	public int getPrimaryTextColor() {
515		return this.mPrimaryTextColor;
516	}
517
518	public int getWarningTextColor() {
519		return this.mColorRed;
520	}
521
522	public int getPrimaryColor() {
523		return this.mPrimaryColor;
524	}
525
526	public int getSecondaryBackgroundColor() {
527		return this.mSecondaryBackgroundColor;
528	}
529
530	public int getPixel(int dp) {
531		DisplayMetrics metrics = getResources().getDisplayMetrics();
532		return ((int) (dp * metrics.density));
533	}
534
535	public AvatarService avatarService() {
536		return xmppConnectionService.getAvatarService();
537	}
538
539	class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
540		private final WeakReference<ImageView> imageViewReference;
541		private Message message = null;
542
543		public BitmapWorkerTask(ImageView imageView) {
544			imageViewReference = new WeakReference<ImageView>(imageView);
545		}
546
547		@Override
548		protected Bitmap doInBackground(Message... params) {
549			message = params[0];
550			try {
551				return xmppConnectionService.getFileBackend().getThumbnail(
552						message, (int) (metrics.density * 288), false);
553			} catch (FileNotFoundException e) {
554				return null;
555			}
556		}
557
558		@Override
559		protected void onPostExecute(Bitmap bitmap) {
560			if (imageViewReference != null && bitmap != null) {
561				final ImageView imageView = imageViewReference.get();
562				if (imageView != null) {
563					imageView.setImageBitmap(bitmap);
564					imageView.setBackgroundColor(0x00000000);
565				}
566			}
567		}
568	}
569
570	public void loadBitmap(Message message, ImageView imageView) {
571		Bitmap bm;
572		try {
573			bm = xmppConnectionService.getFileBackend().getThumbnail(message,
574					(int) (metrics.density * 288), true);
575		} catch (FileNotFoundException e) {
576			bm = null;
577		}
578		if (bm != null) {
579			imageView.setImageBitmap(bm);
580			imageView.setBackgroundColor(0x00000000);
581		} else {
582			if (cancelPotentialWork(message, imageView)) {
583				imageView.setBackgroundColor(0xff333333);
584				final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
585				final AsyncDrawable asyncDrawable = new AsyncDrawable(
586						getResources(), null, task);
587				imageView.setImageDrawable(asyncDrawable);
588				try {
589					task.execute(message);
590				} catch (RejectedExecutionException e) {
591					return;
592				}
593			}
594		}
595	}
596
597	public static boolean cancelPotentialWork(Message message,
598			ImageView imageView) {
599		final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
600
601		if (bitmapWorkerTask != null) {
602			final Message oldMessage = bitmapWorkerTask.message;
603			if (oldMessage == null || message != oldMessage) {
604				bitmapWorkerTask.cancel(true);
605			} else {
606				return false;
607			}
608		}
609		return true;
610	}
611
612	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
613		if (imageView != null) {
614			final Drawable drawable = imageView.getDrawable();
615			if (drawable instanceof AsyncDrawable) {
616				final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
617				return asyncDrawable.getBitmapWorkerTask();
618			}
619		}
620		return null;
621	}
622
623	static class AsyncDrawable extends BitmapDrawable {
624		private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
625
626		public AsyncDrawable(Resources res, Bitmap bitmap,
627				BitmapWorkerTask bitmapWorkerTask) {
628			super(res, bitmap);
629			bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(
630					bitmapWorkerTask);
631		}
632
633		public BitmapWorkerTask getBitmapWorkerTask() {
634			return bitmapWorkerTaskReference.get();
635		}
636	}
637}