XmppActivity.java

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