/*
* Kontalk Android client
* Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kontalk.service.msgcenter;
import java.io.File;
import java.io.InputStream;
import java.io.Reader;
import java.io.Writer;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.ref.WeakReference;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.zip.ZipInputStream;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.StanzaListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.debugger.AbstractDebugger;
import org.jivesoftware.smack.debugger.SmackDebugger;
import org.jivesoftware.smack.debugger.SmackDebuggerFactory;
import org.jivesoftware.smack.filter.StanzaFilter;
import org.jivesoftware.smack.filter.StanzaIdFilter;
import org.jivesoftware.smack.filter.StanzaTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smack.roster.RosterLoadedListener;
import org.jivesoftware.smack.roster.packet.RosterPacket;
import org.jivesoftware.smack.util.Async;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.caps.packet.CapsExtension;
import org.jivesoftware.smackx.chatstates.ChatState;
import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension;
import org.jivesoftware.smackx.csi.ClientStateIndicationManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.disco.packet.DiscoverItems;
import org.jivesoftware.smackx.iqlast.packet.LastActivity;
import org.jivesoftware.smackx.iqversion.VersionManager;
import org.jivesoftware.smackx.iqversion.packet.Version;
import org.jivesoftware.smackx.ping.PingFailedListener;
import org.jivesoftware.smackx.ping.PingManagerV2;
import org.jivesoftware.smackx.receipts.DeliveryReceipt;
import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest;
import org.jxmpp.util.XmppStringUtils;
import android.accounts.Account;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue.IdleHandler;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.Process;
import android.os.SystemClock;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.widget.Toast;
import org.kontalk.BuildConfig;
import org.kontalk.Kontalk;
import org.kontalk.Log;
import org.kontalk.R;
import org.kontalk.authenticator.Authenticator;
import org.kontalk.authenticator.LegacyAuthentication;
import org.kontalk.client.BitsOfBinary;
import org.kontalk.client.BlockingCommand;
import org.kontalk.client.E2EEncryption;
import org.kontalk.client.EndpointServer;
import org.kontalk.client.KontalkConnection;
import org.kontalk.client.OutOfBandData;
import org.kontalk.client.PublicKeyPublish;
import org.kontalk.client.PushRegistration;
import org.kontalk.client.RosterMatch;
import org.kontalk.client.ServerlistCommand;
import org.kontalk.client.SmackInitializer;
import org.kontalk.client.VCard4;
import org.kontalk.crypto.Coder;
import org.kontalk.crypto.PersonalKey;
import org.kontalk.data.Contact;
import org.kontalk.message.CompositeMessage;
import org.kontalk.message.GroupCommandComponent;
import org.kontalk.message.TextComponent;
import org.kontalk.provider.Keyring;
import org.kontalk.provider.MessagesProviderUtils;
import org.kontalk.provider.MyMessages.CommonColumns;
import org.kontalk.provider.MyMessages.Groups;
import org.kontalk.provider.MyMessages.Messages;
import org.kontalk.provider.MyMessages.Threads;
import org.kontalk.provider.MyMessages.Threads.Requests;
import org.kontalk.provider.MyUsers;
import org.kontalk.provider.UsersProvider;
import org.kontalk.service.KeyPairGeneratorService;
import org.kontalk.service.UploadService;
import org.kontalk.service.XMPPConnectionHelper;
import org.kontalk.service.XMPPConnectionHelper.ConnectionHelperListener;
import org.kontalk.service.msgcenter.group.AddRemoveMembersCommand;
import org.kontalk.service.msgcenter.group.CreateGroupCommand;
import org.kontalk.service.msgcenter.group.GroupCommand;
import org.kontalk.service.msgcenter.group.GroupController;
import org.kontalk.service.msgcenter.group.GroupControllerFactory;
import org.kontalk.service.msgcenter.group.KontalkGroupController;
import org.kontalk.service.msgcenter.group.PartCommand;
import org.kontalk.service.msgcenter.group.SetSubjectCommand;
import org.kontalk.ui.MessagingNotification;
import org.kontalk.util.MediaStorage;
import org.kontalk.util.MessageUtils;
import org.kontalk.util.Preferences;
import org.kontalk.util.SystemUtils;
/**
* The Message Center Service.
* Use {@link Intent}s to deliver commands (via {@link Context#startService}).
* Service will broadcast intents when certain events occur.
* @author Daniele Ricci
*/
public class MessageCenterService extends Service implements ConnectionHelperListener {
public static final String TAG = MessageCenterService.class.getSimpleName();
static {
SmackConfiguration.DEBUG = BuildConfig.DEBUG;
// we need our own debugger factory because of our internal logging system
SmackConfiguration.setDebuggerFactory(new SmackDebuggerFactory() {
@Override
public SmackDebugger create(XMPPConnection connection, Writer writer, Reader reader) throws IllegalArgumentException {
return new AbstractDebugger(connection, writer, reader) {
@Override
protected void log(String logMessage) {
Log.d("SMACK", logMessage);
}
};
}
});
}
public static final String ACTION_HOLD = "org.kontalk.action.HOLD";
public static final String ACTION_RELEASE = "org.kontalk.action.RELEASE";
public static final String ACTION_RESTART = "org.kontalk.action.RESTART";
public static final String ACTION_TEST = "org.kontalk.action.TEST";
public static final String ACTION_MESSAGE = "org.kontalk.action.MESSAGE";
public static final String ACTION_PUSH_START = "org.kontalk.push.START";
public static final String ACTION_PUSH_STOP = "org.kontalk.push.STOP";
public static final String ACTION_PUSH_REGISTERED = "org.kontalk.push.REGISTERED";
public static final String ACTION_IDLE = "org.kontalk.action.IDLE";
public static final String ACTION_PING = "org.kontalk.action.PING";
public static final String ACTION_MEDIA_READY = "org.kontalk.action.MEDIA_READY";
/** Request the roster. */
public static final String ACTION_ROSTER = "org.kontalk.action.ROSTER";
/** Request roster match. */
public static final String ACTION_ROSTER_MATCH = "org.kontalk.action.ROSTER_MATCH";
/**
* Broadcasted when we are connected and authenticated to the server.
* Send this intent to receive the same as a broadcast if connected. */
public static final String ACTION_CONNECTED = "org.kontalk.action.CONNECTED";
/**
* Broadcasted when the roster has been loaded.
* Send this intent to receive the same as a broadcast if the roster has
* already been loaded.
*/
public static final String ACTION_ROSTER_LOADED = "org.kontalk.action.ROSTER_LOADED";
/**
* Send this intent to request roster status of any user.
* Broadcasted back in reply to requests.
*/
public static final String ACTION_ROSTER_STATUS = "org.kontalk.action.ROSTER_STATUS";
/**
* Broadcasted when a presence stanza is received.
* Send this intent to broadcast presence.
* Send this intent with type="probe" to request a presence in the roster.
*/
public static final String ACTION_PRESENCE = "org.kontalk.action.PRESENCE";
/**
* Broadcasted when a last activity iq is received.
* Send this intent to request a last activity.
*/
public static final String ACTION_LAST_ACTIVITY = "org.kontalk.action.LAST_ACTIVITY";
/**
* Commence key pair regeneration.
* {@link KeyPairGeneratorService} service will be started to generate the
* key pair. After that, we will send the public key to the server for
* verification and signature. Once the server returns the signed public
* key, it will be installed in the default account.
* Broadcasted when key pair regeneration has completed.
*/
public static final String ACTION_REGENERATE_KEYPAIR = "org.kontalk.action.REGEN_KEYPAIR";
/** Commence key pair import. */
public static final String ACTION_IMPORT_KEYPAIR = "org.kontalk.action.IMPORT_KEYPAIR";
/**
* Broadcasted when a presence subscription has been accepted.
* Send this intent to accept a presence subscription.
*/
public static final String ACTION_SUBSCRIBED = "org.kontalk.action.SUBSCRIBED";
/**
* Broadcasted when receiving a vCard.
* Send this intent to update your own vCard.
*/
public static final String ACTION_VCARD = "org.kontalk.action.VCARD";
/**
* Broadcasted when receiving a public key.
* Send this intent to request a public key.
*/
public static final String ACTION_PUBLICKEY = "org.kontalk.action.PUBLICKEY";
/**
* Broadcasted when receiving the server list.
* Send this intent to request the server list.
*/
public static final String ACTION_SERVERLIST = "org.kontalk.action.SERVERLIST";
/**
* Send this intent to retry to send a pending-user-review message.
*/
public static final String ACTION_RETRY = "org.kontalk.action.RETRY";
/**
* Broadcasted when the blocklist is received.
* Send this intent to request the blocklist.
*/
public static final String ACTION_BLOCKLIST = "org.kontalk.action.BLOCKLIST";
/** Broadcasted when a block request has ben accepted by the server. */
public static final String ACTION_BLOCKED = "org.kontalk.action.BLOCKED";
/** Broadcasted when an unblock request has ben accepted by the server. */
public static final String ACTION_UNBLOCKED = "org.kontalk.action.UNBLOCKED";
/**
* Broadcasted when receiving version information.
* Send this intent to request version information to an entity.
*/
public static final String ACTION_VERSION = "org.kontalk.action.VERSION";
// common parameters
public static final String EXTRA_PACKET_ID = "org.kontalk.packet.id";
public static final String EXTRA_TYPE = "org.kontalk.packet.type";
public static final String EXTRA_ERROR_CONDITION = "org.kontalk.packet.error.condition";
// use with org.kontalk.action.PRESENCE/SUBSCRIBED
public static final String EXTRA_FROM = "org.kontalk.stanza.from";
public static final String EXTRA_TO = "org.kontalk.stanza.to";
public static final String EXTRA_STATUS = "org.kontalk.presence.status";
public static final String EXTRA_SHOW = "org.kontalk.presence.show";
public static final String EXTRA_PRIORITY = "org.kontalk.presence.priority";
public static final String EXTRA_PRIVACY = "org.kontalk.presence.privacy";
public static final String EXTRA_FINGERPRINT = "org.kontalk.presence.fingerprint";
public static final String EXTRA_SUBSCRIBED_FROM = "org.kontalk.presence.subscribed.from";
public static final String EXTRA_SUBSCRIBED_TO = "org.kontalk.presence.subscribed.to";
public static final String EXTRA_STAMP = "org.kontalk.packet.delay";
// use with org.kontalk.action.ROSTER(_MATCH)
public static final String EXTRA_JIDLIST = "org.kontalk.roster.JIDList";
public static final String EXTRA_ROSTER_NAME = "org.kontalk.roster.name";
// use with org.kontalk.action.LAST_ACTIVITY
public static final String EXTRA_SECONDS = "org.kontalk.last.seconds";
// use with org.kontalk.action.VCARD
public static final String EXTRA_PUBLIC_KEY = "org.kontalk.vcard.publicKey";
// used with org.kontalk.action.BLOCKLIST
public static final String EXTRA_BLOCKLIST = "org.kontalk.blocklist";
// used with org.kontalk.action.IMPORT_KEYPAIR
public static final String EXTRA_KEYPACK = "org.kontalk.keypack";
public static final String EXTRA_PASSPHRASE = "org.kontalk.passphrase";
// used with org.kontalk.action.VERSION
public static final String EXTRA_VERSION_NAME = "org.kontalk.version.name";
public static final String EXTRA_VERSION_NUMBER = "org.kontalk.version.number";
// used for org.kontalk.presence.privacy.action extra
/** Accept subscription. */
public static final int PRIVACY_ACCEPT = 0;
/** Block user. */
public static final int PRIVACY_BLOCK = 1;
/** Unblock user. */
public static final int PRIVACY_UNBLOCK = 2;
/** Reject subscription and block. */
public static final int PRIVACY_REJECT = 3;
/** Message URI. */
public static final String EXTRA_MESSAGE = "org.kontalk.message";
// other
public static final String PUSH_REGISTRATION_ID = "org.kontalk.PUSH_REGISTRATION_ID";
private static final String DEFAULT_PUSH_PROVIDER = "gcm";
private static final int GROUP_COMMAND_CREATE = 1;
private static final int GROUP_COMMAND_SUBJECT = 2;
private static final int GROUP_COMMAND_PART = 3;
private static final int GROUP_COMMAND_MEMBERS = 4;
/** Minimal wakeup time. */
public final static int MIN_WAKEUP_TIME = 300000;
/** Normal ping tester timeout. */
private static final int SLOW_PING_TIMEOUT = 10000;
/** Fast ping tester timeout. */
private static final int FAST_PING_TIMEOUT = 3000;
/** Minimal interval between connection tests (5 mins). */
private static final int MIN_TEST_INTERVAL = 5*60*1000;
static final IPushListener sPushListener = PushServiceManager.getDefaultListener();
/** Push service instance. */
private IPushService mPushService;
/** Push notifications enabled flag. */
boolean mPushNotifications;
/** Server push sender id. This is static so the {@link IPushListener} can see it. */
static String sPushSenderId;
/** Push registration id. */
String mPushRegistrationId;
/** Flag marking a currently ongoing push registration cycle (unregister/register) */
boolean mPushRegistrationCycle;
// created in onCreate
private WakeLock mWakeLock;
private WakeLock mPingLock;
LocalBroadcastManager mLocalBroadcastManager;
private AlarmManager mAlarmManager;
private PingFailedListener mPingFailedListener;
/** Cached last used server. */
EndpointServer mServer;
/** The connection helper instance. */
XMPPConnectionHelper mHelper;
/** The connection instance. */
KontalkConnection mConnection;
/** My username (account name). */
String mMyUsername;
/** Supported upload services. */
List<IUploadService> mUploadServices;
/** Roster store. */
private SQLiteRosterStore mRosterStore;
/** Service handler. */
Handler mHandler;
/** Task execution pool. Generally used by packet listeners. */
private ExecutorService mThreadPool;
/** Idle handler. */
IdleConnectionHandler mIdleHandler;
/** Inactive state flag (for CSI). */
private boolean mInactive;
/** Timestamp of last use of {@link #ACTION_TEST}. */
private long mLastTest;
/** Pending intent for idle signaling. */
private PendingIntent mIdleIntent;
private boolean mFirstStart = true;
/** Messages waiting for server receipt (packetId: internalStorageId). */
Map<String, Long> mWaitingReceipt = new HashMap<>();
private RegenerateKeyPairListener mKeyPairRegenerator;
private ImportKeyPairListener mKeyPairImporter;
static final class IdleConnectionHandler extends Handler implements IdleHandler {
/** Idle signal. */
private static final int MSG_IDLE = 1;
/** Inactive signal (for CSI). */
private static final int MSG_INACTIVE = 2;
/** Test signal. */
private static final int MSG_TEST = 3;
/** How much time to wait to enter inactive state. */
private final static int INACTIVE_TIME = 30000;
/** A reference to the message center. */
WeakReference<MessageCenterService> s;
/** Reference counter. */
int mRefCount;
public IdleConnectionHandler(MessageCenterService service, int refCount, Looper looper) {
super(looper);
s = new WeakReference<>(service);
mRefCount = refCount;
// set idle handler for the first idle message
Looper.myQueue().addIdleHandler(this);
}
/**
* Queue idle callback. This gets called just one time to issue the
* first idle message.
*/
@Override
public boolean queueIdle() {
reset();
return false;
}
@Override
public void handleMessage(Message msg) {
MessageCenterService service = s.get();
boolean consumed = false;
if (service != null)
consumed = handleMessage(service, msg);
if (!consumed)
super.handleMessage(msg);
}
private boolean handleMessage(MessageCenterService service, Message msg) {
if (msg.what == MSG_IDLE) {
// push notifications unavailable: set up an alarm for next time
if (service.mPushRegistrationId == null) {
service.setWakeupAlarm();
}
Log.d(TAG, "shutting down message center due to inactivity");
service.stopSelf();
return true;
}
else if (msg.what == MSG_INACTIVE) {
service.inactive();
return true;
}
else if (msg.what == MSG_TEST) {
long now = System.currentTimeMillis();
if ((now - service.getLastReceivedStanza()) >= FAST_PING_TIMEOUT) {
if (!service.fastReply()) {
Log.v(TAG, "test ping failed");
XMPPConnection conn = service.mConnection;
if (conn != null) {
AndroidAdaptiveServerPingManager
.getInstanceFor(conn, service)
.pingFailed();
}
restart(service.getApplicationContext());
}
else {
XMPPConnection conn = service.mConnection;
if (conn != null) {
AndroidAdaptiveServerPingManager
.getInstanceFor(conn, service)
.pingSuccess();
}
}
}
return true;
}
return false;
}
/** Resets the idle timer. */
public void reset(int refCount) {
mRefCount = refCount;
reset();
}
/** Resets the idle timer. */
public void reset() {
removeMessages(MSG_IDLE);
removeMessages(MSG_INACTIVE);
if (mRefCount <= 0 && getLooper().getThread().isAlive()) {
// queue inactive message
queueInactive();
}
}
public void idle() {
sendMessage(obtainMessage(MSG_IDLE));
}
public void hold(boolean activate) {
mRefCount++;
if (mRefCount > 0) {
MessageCenterService service = s.get();
if (service != null && service.isInactive() && service.isConnected()) {
service.active(activate);
}
}
post(new Runnable() {
public void run() {
abortIdle();
}
});
}
public void release() {
mRefCount--;
if (mRefCount <= 0) {
mRefCount = 0;
post(new Runnable() {
public void run() {
removeMessages(MSG_IDLE);
removeMessages(MSG_INACTIVE);
Looper.myQueue().addIdleHandler(IdleConnectionHandler.this);
// trigger inactive timer only if connected
// the authenticated callback will ensure it will trigger anyway
MessageCenterService service = s.get();
if (service != null && !service.isInactive() && service.isConnected()) {
queueInactive();
}
}
});
}
}
public void quit() {
abortIdle();
getLooper().quit();
}
/** Aborts any idle message because we are using the service or quitting. */
void abortIdle() {
Looper.myQueue().removeIdleHandler(IdleConnectionHandler.this);
removeMessages(MSG_IDLE);
removeMessages(MSG_INACTIVE);
MessageCenterService service = s.get();
if (service != null)
service.cancelIdleAlarm();
}
public void forceInactiveIfNeeded() {
post(new Runnable() {
public void run() {
MessageCenterService service = s.get();
if (service != null && mRefCount <= 0 && !service.isInactive()) {
forceInactive();
}
}
});
}
public void forceInactive() {
MessageCenterService service = s.get();
if (service != null) {
removeMessages(MSG_INACTIVE);
service.inactive();
}
}
void queueInactive() {
// send inactive state message only if connected
MessageCenterService service = s.get();
if (service != null && service.isConnected()) {
sendMessageDelayed(obtainMessage(MSG_INACTIVE), INACTIVE_TIME);
}
}
public boolean isHeld() {
return mRefCount > 0;
}
public void test() {
post(new Runnable() {
public void run() {
if (!hasMessages(MSG_TEST)) {
sendMessageDelayed(obtainMessage(MSG_TEST), FAST_PING_TIMEOUT);
}
}
});
}
}
private final BroadcastReceiver mInactivityReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
if (mIdleHandler != null) {
mIdleHandler.forceInactive();
}
if (mHelper != null && mHelper.isStruggling()) {
Log.d(TAG, "connection is not going well, shutting down message center");
stopSelf();
}
}
}
} ;
@Override
public void onCreate() {
configure();
// create the roster store
mRosterStore = new SQLiteRosterStore(this);
// create the global wake lock
PowerManager pwr = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = pwr.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Kontalk.TAG);
mWakeLock.setReferenceCounted(false);
mPingLock = pwr.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Kontalk.TAG + "-Ping");
mPingLock.setReferenceCounted(false);
mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
// cancel any pending alarm intent
cancelIdleAlarm();
mLocalBroadcastManager = LocalBroadcastManager.getInstance(this);
mPushService = PushServiceManager.getInstance(this);
// create idle handler
createIdleHandler();
// create main thread handler
mHandler = new Handler();
// register screen off listener for manual inactivation
registerInactivity();
}
void queueTask(Runnable task) {
if (mThreadPool != null) {
mThreadPool.execute(task);
}
}
private void createIdleHandler() {
HandlerThread thread = new HandlerThread("IdleThread", Process.THREAD_PRIORITY_BACKGROUND);
thread.start();
int refCount = Kontalk.get(this).getReferenceCounter();
mIdleHandler = new IdleConnectionHandler(this, refCount, thread.getLooper());
}
private void registerInactivity() {
IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_OFF);
registerReceiver(mInactivityReceiver, filter);
}
private void unregisterInactivity() {
unregisterReceiver(mInactivityReceiver);
}
void sendPacket(Stanza packet) {
sendPacket(packet, true);
}
/**
* Sends a packet to the connection if found.
* @param bumpIdle true if the idle handler must be notified of this event
*/
void sendPacket(Stanza packet, boolean bumpIdle) {
// reset idler if requested
if (bumpIdle) mIdleHandler.reset();
if (mConnection != null) {
try {
mConnection.sendStanza(packet);
}
catch (NotConnectedException e) {
// ignored
Log.v(TAG, "not connected. Dropping packet " + packet);
}
}
}
private void configure() {
SmackInitializer.initialize(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "Message Center starting - " + intent);
handleIntent(intent);
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
Log.d(TAG, "destroying message center");
quit(false);
// deactivate ping manager
AndroidAdaptiveServerPingManager.onDestroy();
// destroy roster store
mRosterStore.onDestroy();
mRosterStore = null;
// unregister screen off listener for manual inactivation
unregisterInactivity();
// destroy references
mAlarmManager = null;
mLocalBroadcastManager = null;
// also release wakelocks just to be sure
if (mWakeLock != null) {
mWakeLock.release();
mWakeLock = null;
}
if (mPingLock != null) {
mPingLock.release();
mPingLock = null;
}
}
public boolean isStarted() {
return mLocalBroadcastManager != null;
}
private synchronized void quit(boolean restarting) {
if (!restarting) {
// quit the idle handler
mIdleHandler.quit();
mIdleHandler = null;
// destroy the service handler
// (can't stop it because it's the main thread)
mHandler = null;
}
else {
// reset the reference counter
int refCount = ((Kontalk) getApplicationContext()).getReferenceCounter();
mIdleHandler.reset(refCount);
}
// stop all running tasks
if (mThreadPool != null) {
mThreadPool.shutdownNow();
mThreadPool = null;
}
// disable listeners
if (mHelper != null)
mHelper.setListener(null);
if (mConnection != null)
mConnection.removeConnectionListener(this);
// abort connection helper (if any)
if (mHelper != null) {
// this is because of NetworkOnMainThreadException
new AbortThread(mHelper).start();
mHelper = null;
}
// disconnect from server (if any)
if (mConnection != null) {
// disable ping manager
AndroidAdaptiveServerPingManager
.getInstanceFor(mConnection, this)
.setEnabled(false);
PingManagerV2.getInstanceFor(mConnection)
.unregisterPingFailedListener(mPingFailedListener);
// this is because of NetworkOnMainThreadException
new DisconnectThread(mConnection).start();
mConnection = null;
}
// clear cached data from contacts
Contact.invalidateData();
// stop any key pair regeneration service
if (!LegacyAuthentication.isUpgrading())
endKeyPairRegeneration();
// release the wakelock
mWakeLock.release();
}
private static final class AbortThread extends Thread {
private final XMPPConnectionHelper mHelper;
public AbortThread(XMPPConnectionHelper helper) {
mHelper = helper;
}
@Override
public void run() {
try {
mHelper.shutdown();
}
catch (Exception e) {
// ignored
}
}
}
private static final class DisconnectThread extends Thread {
private final AbstractXMPPConnection mConn;
public DisconnectThread(AbstractXMPPConnection conn) {
mConn = conn;
}
@Override
public void run() {
try {
mConn.disconnect();
}
catch (Exception e) {
// ignored
}
}
}
private void handleIntent(Intent intent) {
// stop immediately
if (isOfflineMode(this))
stopSelf();
if (intent != null) {
String action = intent.getAction();
// proceed to start only if network is available
boolean canConnect = canConnect();
boolean doConnect;
switch (action != null ? action : "") {
case ACTION_HOLD:
doConnect = handleHold(intent);
break;
case ACTION_RELEASE:
doConnect = handleRelease();
break;
case ACTION_IDLE:
doConnect = handleIdle();
break;
case ACTION_PUSH_START:
doConnect = handlePushStart();
break;
case ACTION_PUSH_STOP:
doConnect = handlePushStop();
break;
case ACTION_PUSH_REGISTERED:
doConnect = handlePushRegistered(intent);
break;
case ACTION_REGENERATE_KEYPAIR:
doConnect = handleRegenerateKeyPair();
break;
case ACTION_IMPORT_KEYPAIR:
doConnect = handleImportKeyPair(intent);
break;
case ACTION_CONNECTED:
doConnect = handleConnected();
break;
case ACTION_RESTART:
doConnect = handleRestart();
break;
case ACTION_TEST:
doConnect = handleTest(canConnect);
break;
case ACTION_PING:
doConnect = handlePing(canConnect);
break;
case ACTION_MESSAGE:
doConnect = handleMessage(intent, canConnect);
break;
case ACTION_ROSTER:
case ACTION_ROSTER_MATCH:
doConnect = handleRoster(intent, canConnect);
break;
case ACTION_ROSTER_LOADED:
doConnect = handleRosterLoaded();
break;
case ACTION_ROSTER_STATUS:
doConnect = handleRosterStatus(intent);
break;
case ACTION_PRESENCE:
doConnect = handlePresence(intent, canConnect);
break;
case ACTION_LAST_ACTIVITY:
doConnect = handleLastActivity(intent, canConnect);
break;
case ACTION_VCARD:
doConnect = handleVCard(intent, canConnect);
break;
case ACTION_PUBLICKEY:
doConnect = handlePublicKey(intent, canConnect);
break;
case ACTION_SERVERLIST:
doConnect = handleServerList(canConnect);
break;
case ACTION_SUBSCRIBED:
doConnect = handleSubscribed(intent, canConnect);
break;
case ACTION_RETRY:
doConnect = handleRetry();
break;
case ACTION_BLOCKLIST:
doConnect = handleBlocklist();
break;
case ACTION_VERSION:
doConnect = handleVersion(intent);
break;
case ACTION_MEDIA_READY:
doConnect = handleMediaReady(intent);
break;
default:
// no command means normal service start, connect if not connected
doConnect = true;
break;
}
if (canConnect && doConnect)
createConnection();
// no reason to exist
if (!canConnect && !doConnect && !isConnected() && !isConnecting())
stopSelf();
mFirstStart = false;
}
else {
Log.v(TAG, "restarting after service crash");
start(getApplicationContext());
}
}
// methods below handle single intent commands
// the returned value is assigned to doConnect in onStartCommand()
/**
* For documentation purposes only. Used by the command handler to quickly
* find what command string they handle.
*/
@Retention(value = RetentionPolicy.SOURCE)
@Target(value = ElementType.METHOD)
private @interface CommandHandler {
String[] name();
}
@CommandHandler(name = ACTION_HOLD)
private boolean handleHold(Intent intent) {
if (!mFirstStart)
mIdleHandler.hold(intent.getBooleanExtra("org.kontalk.activate", false));
return true;
}
@CommandHandler(name = ACTION_RELEASE)
private boolean handleRelease() {
mIdleHandler.release();
return false;
}
@CommandHandler(name = ACTION_IDLE)
private boolean handleIdle() {
mIdleHandler.idle();
return false;
}
@CommandHandler(name = ACTION_PUSH_START)
private boolean handlePushStart() {
setPushNotifications(true);
return false;
}
@CommandHandler(name = ACTION_PUSH_STOP)
private boolean handlePushStop() {
setPushNotifications(false);
return false;
}
@CommandHandler(name = ACTION_PUSH_REGISTERED)
private boolean handlePushRegistered(Intent intent) {
String regId = intent.getStringExtra(PUSH_REGISTRATION_ID);
// registration cycle under way
if (regId == null && mPushRegistrationCycle) {
mPushRegistrationCycle = false;
pushRegister();
}
else
setPushRegistrationId(regId);
return false;
}
@CommandHandler(name = ACTION_REGENERATE_KEYPAIR)
private boolean handleRegenerateKeyPair() {
beginKeyPairRegeneration();
return true;
}
@CommandHandler(name = ACTION_IMPORT_KEYPAIR)
private boolean handleImportKeyPair(Intent intent) {
// zip file with keys
Uri file = intent.getParcelableExtra(EXTRA_KEYPACK);
// passphrase to decrypt files
String passphrase = intent.getStringExtra(EXTRA_PASSPHRASE);
beginKeyPairImport(file, passphrase);
return false;
}
@CommandHandler(name = ACTION_CONNECTED)
private boolean handleConnected() {
if (isConnected())
broadcast(ACTION_CONNECTED);
return false;
}
@CommandHandler(name = ACTION_RESTART)
private boolean handleRestart() {
quit(true);
return true;
}
@CommandHandler(name = ACTION_TEST)
private boolean handleTest(boolean canConnect) {
if (isConnected()) {
if (canTest()) {
mLastTest = SystemClock.elapsedRealtime();
mIdleHandler.test();
}
return false;
}
else {
if (mHelper != null && mHelper.isBackingOff()) {
// helper is waiting for backoff - restart immediately
quit(true);
}
return canConnect;
}
}
@CommandHandler(name = ACTION_PING)
private boolean handlePing(boolean canConnect) {
if (isConnected()) {
// acquire a wake lock
mPingLock.acquire();
final XMPPConnection connection = mConnection;
final PingManagerV2 pingManager = PingManagerV2.getInstanceFor(connection);
final WakeLock pingLock = mPingLock;
Async.go(new Runnable() {
@Override
public void run() {
try {
if (pingManager.pingMyServer(true, SLOW_PING_TIMEOUT)) {
AndroidAdaptiveServerPingManager
.getInstanceFor(connection, MessageCenterService.this)
.pingSuccess();
}
else {
AndroidAdaptiveServerPingManager
.getInstanceFor(connection, MessageCenterService.this)
.pingFailed();
}
}
catch (NotConnectedException e) {
// ignored
}
finally {
// release the wake lock
if (pingLock != null)
pingLock.release();
}
}
}, "PingServerIfNecessary (" + mConnection.getConnectionCounter() + ')');
return false;
}
else {
return canConnect;
}
}
@CommandHandler(name = ACTION_MESSAGE)
private boolean handleMessage(Intent intent, boolean canConnect) {
if (canConnect && isConnected())
sendMessage(intent.getExtras());
return false;
}
@CommandHandler(name = { ACTION_ROSTER, ACTION_ROSTER_MATCH })
private boolean handleRoster(Intent intent, boolean canConnect) {
if (canConnect && isConnected()) {
Stanza iq;
if (ACTION_ROSTER_MATCH.equals(intent.getAction())) {
iq = new RosterMatch();
String[] list = intent.getStringArrayExtra(EXTRA_JIDLIST);
for (String item : list) {
((RosterMatch) iq).addItem(item);
}
// directed to the probe component
iq.setTo(XmppStringUtils.completeJidFrom("probe", mServer.getNetwork()));
}
else {
iq = new RosterPacket();
}
String id = intent.getStringExtra(EXTRA_PACKET_ID);
iq.setStanzaId(id);
// iq default type is get
sendPacket(iq);
}
return false;
}
@CommandHandler(name = ACTION_ROSTER_LOADED)
private boolean handleRosterLoaded() {
if (isConnected() && isRosterLoaded())
broadcast(ACTION_ROSTER_LOADED);
return false;
}
@CommandHandler(name = ACTION_ROSTER_STATUS)
private boolean handleRosterStatus(Intent intent) {
if (mRosterStore != null) {
final String to = intent.getStringExtra(EXTRA_TO);
RosterPacket.Item entry = mRosterStore.getEntry(to);
if (entry != null) {
final String id = intent.getStringExtra(EXTRA_PACKET_ID);
Intent i = new Intent(ACTION_ROSTER_STATUS);
i.putExtra(EXTRA_FROM, to);
i.putExtra(EXTRA_ROSTER_NAME, entry.getName());
RosterPacket.ItemType subscriptionType = entry.getItemType();
i.putExtra(EXTRA_SUBSCRIBED_FROM, subscriptionType == RosterPacket.ItemType.both ||
subscriptionType == RosterPacket.ItemType.from);
i.putExtra(EXTRA_SUBSCRIBED_TO, subscriptionType == RosterPacket.ItemType.both ||
subscriptionType == RosterPacket.ItemType.to);
// to keep track of request-reply
i.putExtra(EXTRA_PACKET_ID, id);
mLocalBroadcastManager.sendBroadcast(i);
}
}
return false;
}
@CommandHandler(name = ACTION_PRESENCE)
private boolean handlePresence(Intent intent, boolean canConnect) {
if (canConnect && isConnected()) {
final String id = intent.getStringExtra(EXTRA_PACKET_ID);
String type = intent.getStringExtra(EXTRA_TYPE);
final String to = intent.getStringExtra(EXTRA_TO);
if ("probe".equals(type)) {
// probing is actually looking into the roster
final Roster roster = getRoster();
if (to == null) {
for (RosterEntry entry : roster.getEntries()) {
broadcastPresence(roster, entry, id);
}
// broadcast our own presence
broadcastMyPresence(id);
}
else {
queueTask(new Runnable() {
@Override
public void run() {
broadcastPresence(roster, to, id);
}
});
}
}
else {
// FIXME isn't this somewhat the same as createPresence?
String show = intent.getStringExtra(EXTRA_SHOW);
Presence p = new Presence(type != null ? Presence.Type.valueOf(type) : Presence.Type.available);
p.setStanzaId(id);
p.setTo(to);
if (intent.hasExtra(EXTRA_PRIORITY))
p.setPriority(intent.getIntExtra(EXTRA_PRIORITY, 0));
String status = intent.getStringExtra(EXTRA_STATUS);
if (!TextUtils.isEmpty(status))
p.setStatus(status);
if (show != null)
p.setMode(Presence.Mode.valueOf(show));
sendPacket(p);
}
}
return false;
}
@CommandHandler(name = ACTION_LAST_ACTIVITY)
private boolean handleLastActivity(Intent intent, boolean canConnect) {
if (canConnect && isConnected()) {
LastActivity p = new LastActivity();
p.setStanzaId(intent.getStringExtra(EXTRA_PACKET_ID));
p.setTo(intent.getStringExtra(EXTRA_TO));
sendPacket(p);
}
return false;
}
@CommandHandler(name = ACTION_VCARD)
private boolean handleVCard(Intent intent, boolean canConnect) {
if (canConnect && isConnected()) {
VCard4 p = new VCard4();
p.setTo(intent.getStringExtra(EXTRA_TO));
sendPacket(p);
}
return false;
}
@CommandHandler(name = ACTION_PUBLICKEY)
private boolean handlePublicKey(Intent intent, boolean canConnect) {
if (canConnect && isConnected()) {
String to = intent.getStringExtra(EXTRA_TO);
if (to != null) {
// request public key for a specific user
PublicKeyPublish p = new PublicKeyPublish();
p.setStanzaId(intent.getStringExtra(EXTRA_PACKET_ID));
p.setTo(to);
sendPacket(p);
}
else {
// request public keys for the whole roster
Collection<RosterEntry> buddies = getRoster().getEntries();
for (RosterEntry buddy : buddies) {
if (isRosterEntrySubscribed(buddy)) {
PublicKeyPublish p = new PublicKeyPublish();
p.setStanzaId(intent.getStringExtra(EXTRA_PACKET_ID));
p.setTo(buddy.getUser());
sendPacket(p);
}
}
// request our own public key (odd eh?)
PublicKeyPublish p = new PublicKeyPublish();
p.setStanzaId(intent.getStringExtra(EXTRA_PACKET_ID));
p.setTo(XmppStringUtils.parseBareJid(mConnection.getUser()));
sendPacket(p);
}
}
return false;
}
@CommandHandler(name = ACTION_SERVERLIST)
private boolean handleServerList(boolean canConnect) {
if (canConnect && isConnected()) {
ServerlistCommand p = new ServerlistCommand();
p.setTo(XmppStringUtils.completeJidFrom("network", mServer.getNetwork()));
StanzaFilter filter = new StanzaIdFilter(p.getStanzaId());
// TODO cache the listener (it shouldn't change)
mConnection.addAsyncStanzaListener(new StanzaListener() {
public void processPacket(Stanza packet) throws NotConnectedException {
Intent i = new Intent(ACTION_SERVERLIST);
List<String> _items = ((ServerlistCommand.ServerlistCommandData) packet)
.getItems();
if (_items != null && _items.size() != 0 && packet.getError() == null) {
String[] items = new String[_items.size()];
_items.toArray(items);
i.putExtra(EXTRA_FROM, packet.getFrom());
i.putExtra(EXTRA_JIDLIST, items);
}
mLocalBroadcastManager.sendBroadcast(i);
}
}, filter);
sendPacket(p);
}
return false;
}
@CommandHandler(name = ACTION_SUBSCRIBED)
private boolean handleSubscribed(Intent intent, boolean canConnect) {
if (canConnect && isConnected()) {
sendSubscriptionReply(intent.getStringExtra(EXTRA_TO),
intent.getStringExtra(EXTRA_PACKET_ID),
intent.getIntExtra(EXTRA_PRIVACY, PRIVACY_ACCEPT));
}
return false;
}
@CommandHandler(name = ACTION_RETRY)
private boolean handleRetry() {
// TODO we should retry only the requested message(s)
// already connected: resend pending messages
if (isConnected())
resendPendingMessages(false, false);
return false;
}
@CommandHandler(name = ACTION_BLOCKLIST)
private boolean handleBlocklist() {
if (isConnected())
requestBlocklist();
return false;
}
@CommandHandler(name = ACTION_VERSION)
private boolean handleVersion(Intent intent) {
if (isConnected()) {
Version version = new Version(intent.getStringExtra(EXTRA_TO));
version.setStanzaId(intent.getStringExtra(EXTRA_PACKET_ID));
sendPacket(version);
}
return false;
}
@CommandHandler(name = ACTION_MEDIA_READY)
private boolean handleMediaReady(Intent intent) {
long msgId = intent.getLongExtra("org.kontalk.message.msgId", 0);
if (msgId > 0)
sendReadyMedia(msgId);
return true;
}
/** Creates a connection to server if needed. */
private synchronized void createConnection() {
if (mConnection == null && mHelper == null) {
mConnection = null;
// acquire the wakelock
mWakeLock.acquire();
// reset push notification variable
mPushNotifications = Preferences.getPushNotificationsEnabled(this) &&
mPushService != null && mPushService.isServiceAvailable();
// reset waiting messages
mWaitingReceipt.clear();
// setup task execution pool
mThreadPool = Executors.newCachedThreadPool();
mInactive = false;
// retrieve account name
Account acc = Authenticator.getDefaultAccount(this);
mMyUsername = (acc != null) ? acc.name : null;
// get server from preferences
mServer = Preferences.getEndpointServer(this);
mHelper = new XMPPConnectionHelper(this, mServer, false);
mHelper.setListener(this);
mHelper.start();
}
}
@Override
public void connectionClosed() {
Log.v(TAG, "connection closed");
}
@Override
public void connectionClosedOnError(Exception error) {
Log.w(TAG, "connection closed with error", error);
restart(this);
}
@Override
public void authenticationFailed() {
// fire up a notification explaining the situation
MessagingNotification.authenticationError(this);
}
@Override
public void reconnectingIn(int seconds) {
// not used
}
@Override
public void reconnectionFailed(Exception error) {
// not used
}
@Override
public void reconnectionSuccessful() {
// not used
}
@Override
public void aborted(Exception e) {
if (e != null) {
// we are being called from the connection helper because of
// connection errors. Set up a wake up timer to try again
setWakeupAlarm();
}
// unrecoverable error - exit
stopSelf();
}
@Override
public synchronized void created(final XMPPConnection connection) {
Log.v(TAG, "connection created.");
mConnection = (KontalkConnection) connection;
// setup version manager
final VersionManager verMgr = VersionManager.getInstanceFor(connection);
verMgr.setVersion(getString(R.string.app_name), SystemUtils.getVersionFullName(this));
// setup roster
Roster roster = getRoster();
roster.addRosterLoadedListener(new RosterLoadedListener() {
@Override
public void onRosterLoaded(Roster roster) {
final Handler handler = mHandler;
if (handler != null) {
handler.post(new Runnable() {
@Override
public void run() {
// send pending subscription replies
sendPendingSubscriptionReplies();
// resend failed and pending messages
resendPendingMessages(false, false);
// resend failed and pending received receipts
resendPendingReceipts();
// roster has been loaded
broadcast(ACTION_ROSTER_LOADED);
}
});
}
}
});
roster.setRosterStore(mRosterStore);
// enable ping manager
AndroidAdaptiveServerPingManager
.getInstanceFor(connection, this)
.setEnabled(true);
mPingFailedListener = new PingFailedListener() {
@Override
public void pingFailed() {
if (isStarted() && mConnection == connection) {
Log.v(TAG, "ping failed, restarting message center");
// restart message center
restart(getApplicationContext());
}
}
};
PingManagerV2 pingManager = PingManagerV2.getInstanceFor(connection);
pingManager.registerPingFailedListener(mPingFailedListener);
pingManager.setPingInterval(0);
StanzaFilter filter;
filter = new StanzaTypeFilter(Presence.class);
connection.addAsyncStanzaListener(new PresenceListener(this), filter);
filter = new StanzaTypeFilter(RosterMatch.class);
connection.addAsyncStanzaListener(new RosterMatchListener(this), filter);
filter = new StanzaTypeFilter(org.jivesoftware.smack.packet.Message.class);
connection.addSyncStanzaListener(new MessageListener(this), filter);
filter = new StanzaTypeFilter(LastActivity.class);
connection.addAsyncStanzaListener(new LastActivityListener(this), filter);
filter = new StanzaTypeFilter(Version.class);
connection.addAsyncStanzaListener(new VersionListener(this), filter);
filter = new StanzaTypeFilter(PublicKeyPublish.class);
connection.addAsyncStanzaListener(new PublicKeyListener(this), filter);
}
@Override
public void connected(XMPPConnection connection) {
// not used.
}
@Override
public void authenticated(XMPPConnection connection, boolean resumed) {
Log.v(TAG, "authenticated!");
// add message ack listener
if (mConnection.isSmEnabled()) {
mConnection.addStanzaAcknowledgedListener(new MessageAckListener(this));
}
else {
Log.w(TAG, "stream management not available - disabling delivery receipts");
}
// send presence
sendPresence(mIdleHandler.isHeld() ? Presence.Mode.available : Presence.Mode.away);
// clear upload service
if (mUploadServices != null)
mUploadServices.clear();
// discovery
discovery();
// helper is not needed any more
mHelper = null;
broadcast(ACTION_CONNECTED);
// we can now release any pending push notification
Preferences.setLastPushNotification(-1);
// force inactive state if needed
mIdleHandler.forceInactiveIfNeeded();
// update alarm manager
AndroidAdaptiveServerPingManager
.getInstanceFor(connection, this)
.onConnectionCompleted();
// request server key if needed
Async.go(new Runnable() {
@Override
public void run() {
final XMPPConnection conn = mConnection;
if (conn != null && conn.isConnected()) {
String jid = conn.getServiceName();
if (Keyring.getPublicKey(MessageCenterService.this, jid, MyUsers.Keys.TRUST_UNKNOWN) == null) {
PublicKeyPublish pub = new PublicKeyPublish();
pub.setTo(jid);
sendPacket(pub, false);
}
}
}
});
// release the wakelock
mWakeLock.release();
}
void broadcast(String action) {
broadcast(action, null, null);
}
void broadcast(String action, String extraName, String extraValue) {
if (mLocalBroadcastManager != null) {
Intent i = new Intent(action);
if (extraName != null)
i.putExtra(extraName, extraValue);
mLocalBroadcastManager.sendBroadcast(i);
}
}
/** Discovers info and items. */
private void discovery() {
StanzaFilter filter;
DiscoverInfo info = new DiscoverInfo();
info.setTo(mServer.getNetwork());
filter = new StanzaIdFilter(info.getStanzaId());
mConnection.addAsyncStanzaListener(new DiscoverInfoListener(this), filter);
sendPacket(info);
DiscoverItems items = new DiscoverItems();
items.setTo(mServer.getNetwork());
filter = new StanzaIdFilter(items.getStanzaId());
mConnection.addAsyncStanzaListener(new DiscoverItemsListener(this), filter);
sendPacket(items);
}
synchronized void active(boolean available) {
final XMPPConnection connection = mConnection;
if (connection != null) {
cancelIdleAlarm();
if (available) {
if (ClientStateIndicationManager.isSupported(connection)) {
Log.d(TAG, "entering active state");
try {
ClientStateIndicationManager.active(connection);
}
catch (NotConnectedException e) {
return;
}
}
sendPresence(Presence.Mode.available);
mInactive = false;
}
// test ping
mIdleHandler.test();
}
}
synchronized void inactive() {
final XMPPConnection connection = mConnection;
if (connection != null) {
if (!mInactive) {
if (ClientStateIndicationManager.isSupported(connection)) {
Log.d(TAG, "entering inactive state");
try {
ClientStateIndicationManager.inactive(connection);
}
catch (NotConnectedException e) {
cancelIdleAlarm();
return;
}
}
sendPresence(Presence.Mode.away);
}
setIdleAlarm();
mInactive = true;
}
}
boolean isInactive() {
return mInactive;
}
boolean fastReply() {
if (!isConnected()) return false;
try {
return PingManagerV2.getInstanceFor(mConnection)
.pingMyServer(false, FAST_PING_TIMEOUT);
}
catch (NotConnectedException e) {
return false;
}
}
long getLastReceivedStanza() {
return mConnection != null ? mConnection.getLastStanzaReceived() : 0;
}
/** Sends our initial presence. */
private void sendPresence(Presence.Mode mode) {
sendPacket(createPresence(mode));
}
private Presence createPresence(Presence.Mode mode) {
String status = Preferences.getStatusMessage();
Presence p = new Presence(Presence.Type.available);
if (!TextUtils.isEmpty(status))
p.setStatus(status);
if (mode != null)
p.setMode(mode);
// TODO find a place for this
p.addExtension(new CapsExtension("http://www.kontalk.org/", "none", "sha-1"));
return p;
}
private void sendReadyMedia(long databaseId) {
Cursor c = getContentResolver().query(ContentUris
.withAppendedId(Messages.CONTENT_URI, databaseId),
new String[] {
Messages._ID,
Messages.THREAD_ID,
Messages.MESSAGE_ID,
Messages.PEER,
Messages.BODY_CONTENT,
Messages.BODY_MIME,
Messages.SECURITY_FLAGS,
Messages.ATTACHMENT_MIME,
Messages.ATTACHMENT_LOCAL_URI,
Messages.ATTACHMENT_FETCH_URL,
Messages.ATTACHMENT_PREVIEW_PATH,
Messages.ATTACHMENT_LENGTH,
Messages.ATTACHMENT_COMPRESS,
// TODO Messages.ATTACHMENT_SECURITY_FLAGS,
Groups.GROUP_JID,
Groups.SUBJECT,
},
null, null, null);
sendMessages(c, false);
c.close();
}
void resendPendingMessages(boolean retrying, boolean forcePending) {
resendPendingMessages(retrying, forcePending, null);
}
/**
* Queries for pending messages and send them through.
* @param retrying if true, we are retrying to send media messages after
* receiving upload info (non-media messages will be filtered out)
* @param forcePending true to include pending user review messages
* @param to filter by recipient (optional)
*/
void resendPendingMessages(boolean retrying, boolean forcePending, String to) {
String[] filterArgs = null;
StringBuilder filter = new StringBuilder()
.append(Messages.DIRECTION)
.append('=')
.append(Messages.DIRECTION_OUT)
.append(" AND ")
.append(Messages.STATUS)
.append("<>")
.append(Messages.STATUS_SENT)
.append(" AND ")
.append(Messages.STATUS)
.append("<>")
.append(Messages.STATUS_RECEIVED)
.append(" AND ")
.append(Messages.STATUS)
.append("<>")
.append(Messages.STATUS_NOTDELIVERED)
.append(" AND ")
.append(Messages.STATUS)
.append("<>")
.append(Messages.STATUS_QUEUED);
// filter out pending messages
if (!forcePending) filter
.append(" AND ")
.append(Messages.STATUS)
.append("<>")
.append(Messages.STATUS_PENDING);
// filter out non-media non-uploaded messages
if (retrying) filter
.append(" AND ")
.append(Messages.ATTACHMENT_FETCH_URL)
.append(" IS NULL AND ")
.append(Messages.ATTACHMENT_LOCAL_URI)
.append(" IS NOT NULL");
if (to != null) {
filter
.append(" AND (")
.append(Messages.PEER)
.append("=? OR EXISTS (SELECT 1 FROM group_members WHERE ")
.append(Groups.GROUP_JID)
.append("=")
.append(Messages.PEER)
.append(" AND ")
.append(Groups.PEER)
.append("=?))");
filterArgs = new String[] { to, to };
}
Cursor c = getContentResolver().query(Messages.CONTENT_URI,
new String[] {
Messages._ID,
Messages.THREAD_ID,
Messages.MESSAGE_ID,
Messages.PEER,
Messages.BODY_CONTENT,
Messages.BODY_MIME,
Messages.SECURITY_FLAGS,
Messages.ATTACHMENT_MIME,
Messages.ATTACHMENT_LOCAL_URI,
Messages.ATTACHMENT_FETCH_URL,
Messages.ATTACHMENT_PREVIEW_PATH,
Messages.ATTACHMENT_LENGTH,
Messages.ATTACHMENT_COMPRESS,
// TODO Messages.ATTACHMENT_SECURITY_FLAGS,
Groups.GROUP_JID,
Groups.SUBJECT,
},
filter.toString(), filterArgs,
Messages._ID);
sendMessages(c, retrying);
c.close();
}
private void sendMessages(Cursor c, boolean retrying) {
// this set will cache thread IDs within this cursor with
// pending group commands (i.e. just processed group commands)
// This will be looked up when sending consecutive message in the group
// and stop them
Set<Long> pendingGroupCommandThreads = new HashSet<>();
while (c.moveToNext()) {
long id = c.getLong(0);
long threadId = c.getLong(1);
String msgId = c.getString(2);
String peer = c.getString(3);
byte[] textContent = c.getBlob(4);
String bodyMime = c.getString(5);
int securityFlags = c.getInt(6);
String attMime = c.getString(7);
String attFileUri = c.getString(8);
String attFetchUrl = c.getString(9);
String attPreviewPath = c.getString(10);
long attLength = c.getLong(11);
int compress = c.getInt(12);
// TODO int attSecurityFlags = c.getInt(13);
String groupJid = c.getString(13); // 14
String groupSubject = c.getString(14); // 15
if (pendingGroupCommandThreads.contains(threadId)) {
Log.v(TAG, "group message for pending group command - delaying");
continue;
}
final boolean isGroupCommand = GroupCommandComponent.supportsMimeType(bodyMime);
if (isGroupCommand) {
if (groupJid == null) {
// orphan group command waiting to be sent
groupJid = peer;
}
else {
// cache the thread -- it will block future messages until
// this command is received by the server
pendingGroupCommandThreads.add(threadId);
}
}
String[] groupMembers = null;
if (groupJid != null) {
/*
* Huge potential issue here. Selecting all members, regardless of pending flags,
* might e.g. deliver messages to removed users if there is a content message right
* after a remove command.
* However, selecting members with zero flags will make a remove command to be sent
* only to existing members and not to the ones being removed.
*/
groupMembers = MessagesProviderUtils.getGroupMembers(this, groupJid, -1);
if (groupMembers.length == 0) {
// no group member left - skip message
// this might be a pending message that was queued before we realized there were no members left
// since the group might get populated again, we just skip the message but keep it
Log.d(TAG, "no members in group - skipping message");
continue;
}
}
// media message encountered and no upload service available - delay message
if (attFileUri != null && attFetchUrl == null && getUploadService() == null && !retrying) {
Log.w(TAG, "no upload info received yet, delaying media message");
continue;
}
Bundle b = new Bundle();
// mark as retrying
b.putBoolean("org.kontalk.message.retrying", true);
b.putLong("org.kontalk.message.msgId", id);
b.putString("org.kontalk.message.packetId", msgId);
if (groupJid != null) {
b.putString("org.kontalk.message.group.jid", groupJid);
b.putString("org.kontalk.message.group.subject", groupSubject);
// will be replaced by the group command (if any)
b.putStringArray("org.kontalk.message.to", groupMembers);
}
else {
b.putString("org.kontalk.message.to", peer);
}
// TODO shouldn't we pass security flags directly here??
b.putBoolean("org.kontalk.message.encrypt", securityFlags != Coder.SECURITY_CLEARTEXT);
if (isGroupCommand) {
int cmd = 0;
byte[] _command = c.getBlob(4);
String command = new String(_command);
String[] createMembers;
String[] addMembers;
String[] removeMembers = null;
String subject;
if ((createMembers = GroupCommandComponent.getCreateCommandMembers(command)) != null) {
cmd = GROUP_COMMAND_CREATE;
b.putStringArray("org.kontalk.message.to", createMembers);
}
else if (command.equals(GroupCommandComponent.COMMAND_PART)) {
cmd = GROUP_COMMAND_PART;
}
else if ((addMembers = GroupCommandComponent.getAddCommandMembers(command)) != null ||
(removeMembers = GroupCommandComponent.getRemoveCommandMembers(command)) != null) {
cmd = GROUP_COMMAND_MEMBERS;
b.putStringArray("org.kontalk.message.group.add", addMembers);
b.putStringArray("org.kontalk.message.group.remove", removeMembers);
}
else if ((subject = GroupCommandComponent.getSubjectCommand(command)) != null) {
cmd = GROUP_COMMAND_SUBJECT;
b.putString("org.kontalk.message.group.subject", subject);
}
b.putInt("org.kontalk.message.group.command", cmd);
}
else if (textContent != null) {
b.putString("org.kontalk.message.body", MessageUtils.toString(textContent));
}
// message has already been uploaded - just send media
if (attFetchUrl != null) {
b.putString("org.kontalk.message.mime", attMime);
b.putString("org.kontalk.message.fetch.url", attFetchUrl);
b.putString("org.kontalk.message.preview.uri", attFileUri);
b.putString("org.kontalk.message.preview.path", attPreviewPath);
}
// check if the message contains some large file to be sent
else if (attFileUri != null) {
b.putString("org.kontalk.message.mime", attMime);
b.putString("org.kontalk.message.media.uri", attFileUri);
b.putString("org.kontalk.message.preview.path", attPreviewPath);
b.putLong("org.kontalk.message.length", attLength);
b.putInt("org.kontalk.message.compress", compress);
}
Log.v(TAG, "resending pending message " + id);
sendMessage(b);
}
}
void resendPendingReceipts() {
Cursor c = getContentResolver().query(Messages.CONTENT_URI,
new String[] {
Messages._ID,
Messages.MESSAGE_ID,
Messages.PEER,
},
Messages.DIRECTION + " = " + Messages.DIRECTION_IN + " AND " +
Messages.STATUS + " = " + Messages.STATUS_INCOMING,
null, Messages._ID);
while (c.moveToNext()) {
long id = c.getLong(0);
String msgId = c.getString(1);
String peer = c.getString(2);
Bundle b = new Bundle();
b.putLong("org.kontalk.message.msgId", id);
b.putString("org.kontalk.message.packetId", msgId);
b.putString("org.kontalk.message.to", peer);
b.putString("org.kontalk.message.ack", msgId);
Log.v(TAG, "resending pending receipt for message " + id);
sendMessage(b);
}
c.close();
}
void sendPendingSubscriptionReplies() {
Cursor c = getContentResolver().query(Threads.CONTENT_URI,
new String[] {
Threads.PEER,
Threads.REQUEST_STATUS,
},
Threads.REQUEST_STATUS + "=" + Threads.REQUEST_REPLY_PENDING_ACCEPT + " OR " +
Threads.REQUEST_STATUS + "=" + Threads.REQUEST_REPLY_PENDING_BLOCK,
null, Threads._ID);
while (c.moveToNext()) {
String to = c.getString(0);
int reqStatus = c.getInt(1);
int action;
switch (reqStatus) {
case Threads.REQUEST_REPLY_PENDING_ACCEPT:
action = PRIVACY_ACCEPT;
break;
case Threads.REQUEST_REPLY_PENDING_BLOCK:
action = PRIVACY_BLOCK;
break;
case Threads.REQUEST_REPLY_PENDING_UNBLOCK:
action = PRIVACY_UNBLOCK;
break;
default:
// skip this one
continue;
}
sendSubscriptionReply(to, null, action);
}
c.close();
}
Roster getRoster() {
return (mConnection != null) ? Roster.getInstanceFor(mConnection) : null;
}
private boolean isRosterLoaded() {
Roster roster = getRoster();
return roster != null && roster.isLoaded();
}
RosterEntry getRosterEntry(String jid) {
Roster roster = getRoster();
return (roster != null) ? roster.getEntry(jid) : null;
}
private boolean isAuthorized(String jid) {
if (Authenticator.isSelfJID(this, jid))
return true;
RosterEntry entry = getRosterEntry(jid);
return entry != null && isAuthorized(entry);
}
private boolean isAuthorized(RosterEntry entry) {
return (isRosterEntrySubscribed(entry) || Authenticator.isSelfJID(this, entry.getUser()));
}
private boolean isRosterEntrySubscribed(RosterEntry entry) {
return (entry != null && (entry.getType() == RosterPacket.ItemType.to || entry.getType() == RosterPacket.ItemType.both) &&
entry.getStatus() != RosterPacket.ItemStatus.SUBSCRIPTION_PENDING);
}
private void broadcastPresence(Roster roster, RosterEntry entry, String id) {
broadcastPresence(roster, entry, entry.getUser(), id);
}
void broadcastPresence(Roster roster, String jid, String id) {
broadcastPresence(roster, roster.getEntry(jid), jid, id);
}
private void broadcastPresence(Roster roster, RosterEntry entry, String jid, String id) {
// this method might be called async
final LocalBroadcastManager lbm = mLocalBroadcastManager;
if (lbm == null)
return;
Intent i;
// entry present and not pending subscription
if (isRosterEntrySubscribed(entry) || Authenticator.isSelfJID(this, jid)) {
// roster entry found, look for presence
Presence presence = roster.getPresence(jid);
i = PresenceListener.createIntent(this, presence, entry);
}
else {
// null type indicates no roster entry found or not authorized
i = new Intent(ACTION_PRESENCE);
i.putExtra(EXTRA_FROM, jid);
}
// to keep track of request-reply
i.putExtra(EXTRA_PACKET_ID, id);
lbm.sendBroadcast(i);
}
/** A special method to broadcast our own presence. */
private void broadcastMyPresence(String id) {
Presence presence = createPresence(null);
presence.setFrom(mConnection.getUser());
Intent i = PresenceListener.createIntent(this, presence, null);
i.putExtra(EXTRA_FINGERPRINT, getMyFingerprint());
i.putExtra(EXTRA_SUBSCRIBED_FROM, true);
i.putExtra(EXTRA_SUBSCRIBED_TO, true);
// to keep track of request-reply
i.putExtra(EXTRA_PACKET_ID, id);
mLocalBroadcastManager.sendBroadcast(i);
}
private String getMyFingerprint() {
try {
PersonalKey key = Kontalk.get(this).getPersonalKey();
return key.getFingerprint();
}
catch (Exception e) {
// something bad happened
Log.w(TAG, "unable to load personal key");
return null;
}
}
private void sendSubscriptionReply(String to, String packetId, int action) {
if (action == PRIVACY_ACCEPT) {
// standard response: subscribed
Presence p = new Presence(Presence.Type.subscribed);
p.setStanzaId(packetId);
p.setTo(to);
sendPacket(p);
// send a subscription request anyway
p = new Presence(Presence.Type.subscribe);
p.setTo(to);
sendPacket(p);
}
else if (action == PRIVACY_BLOCK || action == PRIVACY_UNBLOCK || action == PRIVACY_REJECT) {
sendPrivacyListCommand(to, action);
}
// clear the request status
ContentValues values = new ContentValues(1);
values.put(Threads.REQUEST_STATUS, Threads.REQUEST_NONE);
getContentResolver().update(Requests.CONTENT_URI,
values, CommonColumns.PEER + "=?", new String[]{to});
}
private void sendPrivacyListCommand(final String to, final int action) {
IQ p;
if (action == PRIVACY_BLOCK || action == PRIVACY_REJECT) {
// blocking command: block
p = BlockingCommand.block(to);
}
else if (action == PRIVACY_UNBLOCK) {
// blocking command: block
p = BlockingCommand.unblock(to);
}
else {
// unsupported action
throw new IllegalArgumentException("unsupported action: " + action);
}
if (action == PRIVACY_REJECT) {
// send unsubscribed too
Presence unsub = new Presence(Presence.Type.unsubscribe);
unsub.setTo(to);
sendPacket(unsub);
}
// setup packet filter for response
StanzaFilter filter = new StanzaIdFilter(p.getStanzaId());
StanzaListener listener = new StanzaListener() {
public void processPacket(Stanza packet) {
if (packet instanceof IQ && ((IQ) packet).getType() == IQ.Type.result) {
UsersProvider.setBlockStatus(MessageCenterService.this,
to, action == PRIVACY_BLOCK || action == PRIVACY_REJECT);
// invalidate cached contact
Contact.invalidate(to);
// broadcast result
broadcast((action == PRIVACY_BLOCK || action == PRIVACY_REJECT) ?
ACTION_BLOCKED : ACTION_UNBLOCKED,
EXTRA_FROM, to);
}
}
};
mConnection.addAsyncStanzaListener(listener, filter);
// send IQ
sendPacket(p);
}
private void requestBlocklist() {
Stanza p = BlockingCommand.blocklist();
String packetId = p.getStanzaId();
// listen for response (TODO cache the listener, it shouldn't change)
StanzaFilter idFilter = new StanzaIdFilter(packetId);
mConnection.addAsyncStanzaListener(new StanzaListener() {
public void processPacket(Stanza packet) {
// we don't need this listener anymore
mConnection.removeAsyncStanzaListener(this);
if (packet instanceof BlockingCommand) {
BlockingCommand blocklist = (BlockingCommand) packet;
Intent i = new Intent(ACTION_BLOCKLIST);
List<String> _list = blocklist.getItems();
if (_list != null) {
String[] list = new String[_list.size()];
i.putExtra(EXTRA_BLOCKLIST, _list.toArray(list));
}
Log.v(TAG, "broadcasting blocklist: " + i);
mLocalBroadcastManager.sendBroadcast(i);
}
}
}, idFilter);
sendPacket(p);
}
private void sendMessage(Bundle data) {
if (!isRosterLoaded()) {
Log.d(TAG, "roster not loaded yet, not sending message");
return;
}
boolean retrying = data.getBoolean("org.kontalk.message.retrying");
final String groupJid = data.getString("org.kontalk.message.group.jid");
String to;
// used for verifying isPaused()
String convJid;
String[] toGroup;
GroupController group = null;
if (groupJid != null) {
toGroup = data.getStringArray("org.kontalk.message.to");
// TODO this should be discovered first
to = XmppStringUtils.completeJidFrom("multicast", mConnection.getServiceName());
convJid = groupJid;
// TODO take type from data
group = GroupControllerFactory
.createController(KontalkGroupController.GROUP_TYPE, mConnection, this);
// check if we can send messages even with some members with no subscriptipn
if (!group.canSendWithNoSubscription()) {
for (String jid : toGroup) {
if (!isAuthorized(jid)) {
Log.i(TAG, "not subscribed to " + jid + ", not sending group message");
return;
}
}
}
}
else {
to = data.getString("org.kontalk.message.to");
toGroup = new String[] { to };
convJid = to;
}
if (group == null && !isAuthorized(to)) {
Log.i(TAG, "not subscribed to " + to + ", not sending message");
// warn user: message will not be sent
if (!retrying && MessagingNotification.isPaused(to)) {
Toast.makeText(this, R.string.warn_not_subscribed,
Toast.LENGTH_LONG).show();
}
return;
}
PersonalKey key;
try {
key = ((Kontalk) getApplicationContext()).getPersonalKey();
}
catch (Exception pgpe) {
Log.w(TAG, "no personal key available - not allowed to send messages");
// warn user: message will not be sent
if (MessagingNotification.isPaused(convJid)) {
Toast.makeText(this, R.string.warn_no_personal_key,
Toast.LENGTH_LONG).show();
}
return;
}
// check if message is already pending
final long msgId = data.getLong("org.kontalk.message.msgId");
if (mWaitingReceipt.containsValue(msgId)) {
Log.v(TAG, "message already queued and waiting - dropping");
return;
}
final String id = data.getString("org.kontalk.message.packetId");
final boolean encrypt = data.getBoolean("org.kontalk.message.encrypt");
final String mime = data.getString("org.kontalk.message.mime");
String _mediaUri = data.getString("org.kontalk.message.media.uri");
if (_mediaUri != null) {
// take the first available upload service :)
IUploadService uploadService = getUploadService();
if (uploadService != null) {
Uri preMediaUri = Uri.parse(_mediaUri);
final String previewPath = data.getString("org.kontalk.message.preview.path");
long fileLength;
try {
// encrypt the file if necessary
if (encrypt) {
InputStream in = getContentResolver().openInputStream(preMediaUri);
File encrypted = MessageUtils.encryptFile(this, in, toGroup);
fileLength = encrypted.length();
preMediaUri = Uri.fromFile(encrypted);
}
else {
fileLength = MediaStorage.getLength(this, preMediaUri);
}
}
catch (Exception e) {
Log.w(TAG, "error preprocessing media: " + preMediaUri, e);
// simulate upload error
UploadService.errorNotification(this,
getString(R.string.notify_ticker_upload_error),
getString(R.string.notify_text_upload_error));
return;
}
final Uri mediaUri = preMediaUri;
// build a filename
String filename = CompositeMessage.getFilename(mime, new Date());
if (filename == null)
filename = MediaStorage.UNKNOWN_FILENAME;
// media message - start upload service
final String uploadTo = to;
final String[] uploadGroupTo = toGroup;
uploadService.getPostUrl(filename, fileLength, mime, new IUploadService.UrlCallback() {
@Override
public void callback(String putUrl, String getUrl) {
// start upload intent service
Intent i = new Intent(MessageCenterService.this, UploadService.class);
i.setData(mediaUri);
i.setAction(UploadService.ACTION_UPLOAD);
i.putExtra(UploadService.EXTRA_POST_URL, putUrl);
i.putExtra(UploadService.EXTRA_GET_URL, getUrl);
i.putExtra(UploadService.EXTRA_DATABASE_ID, msgId);
i.putExtra(UploadService.EXTRA_MESSAGE_ID, id);
i.putExtra(UploadService.EXTRA_MIME, mime);
// this will be used only for out of band data
i.putExtra(UploadService.EXTRA_ENCRYPT, encrypt);
i.putExtra(UploadService.EXTRA_PREVIEW_PATH, previewPath);
// delete original (actually it's the encrypted temp file) if we already encrypted it
i.putExtra(UploadService.EXTRA_DELETE_ORIGINAL, encrypt);
i.putExtra(UploadService.EXTRA_USER, groupJid != null ? uploadGroupTo : uploadTo);
if (groupJid != null)
i.putExtra(UploadService.EXTRA_GROUP, groupJid);
startService(i);
}
});
}
else {
// TODO warn user about this problem
Log.w(TAG, "no upload service - this shouldn't happen!");
}
}
else {
// hold on to message center while we send the message
mIdleHandler.hold(false);
Stanza m, originalStanza;
// pre-process message for group delivery
GroupCommand groupCommand = null;
if (group != null) {
int groupCommandId = data.getInt("org.kontalk.message.group.command", 0);
switch (groupCommandId) {
case GROUP_COMMAND_PART:
groupCommand = group.part();
((PartCommand) groupCommand).setDatabaseId(msgId);
// FIXME careful to this, might need abstraction
groupCommand.setMembers(toGroup);
groupCommand.setGroupJid(groupJid);
break;
case GROUP_COMMAND_CREATE: {
String subject = data.getString("org.kontalk.message.group.subject");
groupCommand = group.createGroup();
((CreateGroupCommand) groupCommand).setSubject(subject);
groupCommand.setMembers(toGroup);
groupCommand.setGroupJid(groupJid);
break;
}
case GROUP_COMMAND_SUBJECT: {
String subject = data.getString("org.kontalk.message.group.subject");
groupCommand = group.setSubject();
((SetSubjectCommand) groupCommand).setSubject(subject);
// FIXME careful to this, might need abstraction
groupCommand.setMembers(toGroup);
groupCommand.setGroupJid(groupJid);
break;
}
case GROUP_COMMAND_MEMBERS: {
String subject = data.getString("org.kontalk.message.group.subject");
String[] added = data.getStringArray("org.kontalk.message.group.add");
String[] removed = data.getStringArray("org.kontalk.message.group.remove");
groupCommand = group.addRemoveMembers();
((AddRemoveMembersCommand) groupCommand).setSubject(subject);
((AddRemoveMembersCommand) groupCommand).setAddedMembers(added);
((AddRemoveMembersCommand) groupCommand).setRemovedMembers(removed);
groupCommand.setMembers(toGroup);
groupCommand.setGroupJid(groupJid);
break;
}
default:
groupCommand = group.info();
// FIXME careful to this, might need abstraction
groupCommand.setMembers(toGroup);
groupCommand.setGroupJid(groupJid);
}
m = group.beforeEncryption(groupCommand, null);
}
else {
// message stanza
m = new org.jivesoftware.smack.packet.Message();
}
originalStanza = m;
boolean isMessage = (m instanceof org.jivesoftware.smack.packet.Message);
if (to != null) m.setTo(to);
// set message id
m.setStanzaId(id);
if (msgId > 0)
mWaitingReceipt.put(id, msgId);
// message server id
String serverId = isMessage ? data.getString("org.kontalk.message.ack") : null;
boolean ackRequest = isMessage &&
!data.getBoolean("org.kontalk.message.standalone", false) &&
group == null;
if (isMessage) {
org.jivesoftware.smack.packet.Message msg = (org.jivesoftware.smack.packet.Message) m;
msg.setType(org.jivesoftware.smack.packet.Message.Type.chat);
String body = data.getString("org.kontalk.message.body");
if (body != null)
msg.setBody(body);
String fetchUrl = data.getString("org.kontalk.message.fetch.url");
// generate preview if needed
String _previewUri = data.getString("org.kontalk.message.preview.uri");
String previewFilename = data.getString("org.kontalk.message.preview.path");
if (_previewUri != null && previewFilename != null) {
File previewPath = new File(previewFilename);
if (!previewPath.isFile()) {
Uri previewUri = Uri.parse(_previewUri);
try {
MediaStorage.cacheThumbnail(this, previewUri, previewPath, true);
}
catch (Exception e) {
Log.w(TAG, "unable to generate preview for media", e);
}
}
m.addExtension(new BitsOfBinary(MediaStorage.THUMBNAIL_MIME_NETWORK, previewPath));
}
// add download url if present
if (fetchUrl != null) {
// in this case we will need the length too
long length = data.getLong("org.kontalk.message.length");
m.addExtension(new OutOfBandData(fetchUrl, mime, length, encrypt));
}
if (encrypt) {
byte[] toMessage = null;
try {
Coder coder = Keyring.getEncryptCoder(this, mServer, key, toGroup);
if (coder != null) {
// no extensions, create a simple text version to save space
if (m.getExtensions().size() == 0) {
toMessage = coder.encryptText(body);
}
// some extension, encrypt whole stanza just to be sure
else {
toMessage = coder.encryptStanza(m.toXML());
}
org.jivesoftware.smack.packet.Message encMsg =
new org.jivesoftware.smack.packet.Message(m.getTo(),
((org.jivesoftware.smack.packet.Message) m).getType());
encMsg.setBody(getString(R.string.text_encrypted));
encMsg.setStanzaId(m.getStanzaId());
encMsg.addExtension(new E2EEncryption(toMessage));
// save the unencrypted stanza for later
originalStanza = m;
m = encMsg;
}
}
// FIXME there is some very ugly code here
// FIXME notify just once per session (store in Kontalk instance?)
catch (IllegalArgumentException noPublicKey) {
// warn user: message will be not sent
if (MessagingNotification.isPaused(convJid)) {
Toast.makeText(this, R.string.warn_no_public_key,
Toast.LENGTH_LONG).show();
}
}
catch (GeneralSecurityException e) {
// warn user: message will not be sent
if (MessagingNotification.isPaused(convJid)) {
Toast.makeText(this, R.string.warn_encryption_failed,
Toast.LENGTH_LONG).show();
}
}
if (toMessage == null) {
// message was not encrypted for some reason, mark it pending user review
ContentValues values = new ContentValues(1);
values.put(Messages.STATUS, Messages.STATUS_PENDING);
getContentResolver().update(ContentUris.withAppendedId
(Messages.CONTENT_URI, msgId), values, null, null);
// do not send the message
if (msgId > 0)
mWaitingReceipt.remove(id);
mIdleHandler.release();
return;
}
}
}
// post-process for group delivery
if (group != null) {
m = group.afterEncryption(groupCommand, m, originalStanza);
}
if (isMessage) {
// received receipt
if (serverId != null) {
m.addExtension(new DeliveryReceipt(serverId));
}
else {
ChatState chatState;
try {
chatState = ChatState.valueOf(data.getString("org.kontalk.message.chatState"));
// add chat state if message is not a received receipt
m.addExtension(new ChatStateExtension(chatState));
}
catch (Exception ignored) {
}
// standalone: no receipt
if (ackRequest)
DeliveryReceiptRequest.addTo((org.jivesoftware.smack.packet.Message) m);
}
}
sendPacket(m);
// no ack request, release message center immediately
if (!ackRequest)
mIdleHandler.release();
}
}
private void ensureUploadServices() {
if (mUploadServices == null)
mUploadServices = new ArrayList<>(2);
}
void addUploadService(IUploadService service) {
ensureUploadServices();
mUploadServices.add(service);
}
void addUploadService(IUploadService service, int priority) {
ensureUploadServices();
mUploadServices.add(priority, service);
}
/** Returns the first available upload service post URL. */
private IUploadService getUploadService() {
return (mUploadServices != null && mUploadServices.size() > 0) ?
mUploadServices.get(0) : null;
}
private void beginKeyPairRegeneration() {
if (mKeyPairRegenerator == null) {
try {
// lock message center while doing this
hold(this, true);
mKeyPairRegenerator = new RegenerateKeyPairListener(this);
mKeyPairRegenerator.run();
}
catch (Exception e) {
Log.e(TAG, "unable to initiate keypair regeneration", e);
// TODO warn user
endKeyPairRegeneration();
}
}
}
void endKeyPairRegeneration() {
if (mKeyPairRegenerator != null) {
mKeyPairRegenerator.abort();
mKeyPairRegenerator = null;
// release message center
release(this);
}
}
/**
* Used by {@link XMPPConnectionHelper} to retrieve the keyring that will
* be used for the next login while upgrading from legacy.
*/
@Override
public PGPKeyPairRingProvider getKeyPairRingProvider() {
return mKeyPairRegenerator;
}
private void beginKeyPairImport(Uri keypack, String passphrase) {
if (mKeyPairImporter == null) {
try {
ZipInputStream zip = new ZipInputStream(getContentResolver()
.openInputStream(keypack));
mKeyPairImporter = new ImportKeyPairListener(this, zip, passphrase);
mKeyPairImporter.run();
}
catch (Exception e) {
Log.e(TAG, "unable to initiate keypair import", e);
Toast.makeText(this, R.string.err_import_keypair_failed,
Toast.LENGTH_LONG).show();
endKeyPairImport();
}
}
}
void endKeyPairImport() {
if (mKeyPairImporter != null) {
mKeyPairImporter.abort();
mKeyPairImporter = null;
}
}
private boolean canTest() {
long now = SystemClock.elapsedRealtime();
return ((now - mLastTest) > MIN_TEST_INTERVAL);
}
public boolean canConnect() {
return SystemUtils.isNetworkConnectionAvailable(this) && !isOfflineMode(this);
}
public boolean isConnected() {
return mConnection != null && mConnection.isAuthenticated();
}
public boolean isConnecting() {
return mHelper != null;
}
public static boolean isOfflineMode(Context context) {
return Preferences.getOfflineMode();
}
private static Intent getStartIntent(Context context) {
return new Intent(context, MessageCenterService.class);
}
public static void start(Context context) {
// check for offline mode
if (isOfflineMode(context)) {
Log.d(TAG, "offline mode enable - abort service start");
return;
}
// check for network state
if (SystemUtils.isNetworkConnectionAvailable(context)) {
Log.d(TAG, "starting message center");
final Intent intent = getStartIntent(context);
context.startService(intent);
}
else
Log.d(TAG, "network not available or background data disabled - abort service start");
}
public static void stop(Context context) {
Log.d(TAG, "shutting down message center");
context.stopService(new Intent(context, MessageCenterService.class));
}
public static void restart(Context context) {
Log.d(TAG, "restarting message center");
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(ACTION_RESTART);
context.startService(i);
}
public static void test(Context context) {
Log.d(TAG, "testing message center connection");
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(ACTION_TEST);
context.startService(i);
}
public static void ping(Context context) {
Log.d(TAG, "ping message center connection");
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(ACTION_PING);
context.startService(i);
}
/**
* Tells the message center we are holding on to it, preventing shutdown for
* inactivity.
* @param activate true to wake up from CSI and send become available.
*/
public static void hold(final Context context, boolean activate) {
// increment the application counter
((Kontalk) context.getApplicationContext()).hold();
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(ACTION_HOLD);
i.putExtra("org.kontalk.activate", activate);
context.startService(i);
}
/**
* Tells the message center we are releasing it, allowing shutdown for
* inactivity.
*/
public static void release(final Context context) {
// decrement the application counter
((Kontalk) context.getApplicationContext()).release();
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(ACTION_RELEASE);
context.startService(i);
}
/** Broadcasts our presence to the server. */
public static void updateStatus(final Context context) {
// FIXME this is what sendPresence already does
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(ACTION_PRESENCE);
i.putExtra(EXTRA_STATUS, Preferences.getStatusMessage());
context.startService(i);
}
/** Sends a chat state message. */
public static void sendChatState(final Context context, String to, ChatState state) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_MESSAGE);
i.putExtra("org.kontalk.message.to", to);
i.putExtra("org.kontalk.message.chatState", state.name());
i.putExtra("org.kontalk.message.standalone", true);
context.startService(i);
}
/** Sends a text message. */
public static void sendTextMessage(final Context context, String to, String text, boolean encrypt, long msgId, String packetId) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_MESSAGE);
i.putExtra("org.kontalk.message.msgId", msgId);
i.putExtra("org.kontalk.message.packetId", packetId);
i.putExtra("org.kontalk.message.mime", TextComponent.MIME_TYPE);
i.putExtra("org.kontalk.message.to", to);
i.putExtra("org.kontalk.message.body", text);
i.putExtra("org.kontalk.message.encrypt", encrypt);
i.putExtra("org.kontalk.message.chatState", ChatState.active.name());
context.startService(i);
}
public static void sendGroupTextMessage(final Context context, String groupJid,
String groupSubject, String[] to,
String text, boolean encrypt, long msgId, String packetId) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_MESSAGE);
i.putExtra("org.kontalk.message.msgId", msgId);
i.putExtra("org.kontalk.message.packetId", packetId);
i.putExtra("org.kontalk.message.mime", TextComponent.MIME_TYPE);
i.putExtra("org.kontalk.message.group.jid", groupJid);
i.putExtra("org.kontalk.message.group.subject", groupSubject);
i.putExtra("org.kontalk.message.to", to);
i.putExtra("org.kontalk.message.body", text);
i.putExtra("org.kontalk.message.encrypt", encrypt);
i.putExtra("org.kontalk.message.chatState", ChatState.active.name());
context.startService(i);
}
public static void createGroup(final Context context, String groupJid,
String groupSubject, String[] to, boolean encrypt, long msgId, String packetId) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_MESSAGE);
i.putExtra("org.kontalk.message.msgId", msgId);
i.putExtra("org.kontalk.message.packetId", packetId);
i.putExtra("org.kontalk.message.mime", GroupCommandComponent.MIME_TYPE);
i.putExtra("org.kontalk.message.group.jid", groupJid);
i.putExtra("org.kontalk.message.group.subject", groupSubject);
i.putExtra("org.kontalk.message.group.command", GROUP_COMMAND_CREATE);
i.putExtra("org.kontalk.message.to", to);
i.putExtra("org.kontalk.message.encrypt", encrypt);
i.putExtra("org.kontalk.message.chatState", ChatState.active.name());
context.startService(i);
}
public static void leaveGroup(final Context context, String groupJid,
String[] to, boolean encrypt, long msgId, String packetId) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_MESSAGE);
i.putExtra("org.kontalk.message.msgId", msgId);
i.putExtra("org.kontalk.message.packetId", packetId);
i.putExtra("org.kontalk.message.mime", GroupCommandComponent.MIME_TYPE);
i.putExtra("org.kontalk.message.group.jid", groupJid);
i.putExtra("org.kontalk.message.group.command", GROUP_COMMAND_PART);
i.putExtra("org.kontalk.message.to", to);
i.putExtra("org.kontalk.message.encrypt", encrypt);
i.putExtra("org.kontalk.message.chatState", ChatState.active.name());
context.startService(i);
}
public static void addGroupMembers(final Context context, String groupJid,
String groupSubject, String[] to, String[] members, boolean encrypt, long msgId, String packetId) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_MESSAGE);
i.putExtra("org.kontalk.message.msgId", msgId);
i.putExtra("org.kontalk.message.packetId", packetId);
i.putExtra("org.kontalk.message.mime", GroupCommandComponent.MIME_TYPE);
i.putExtra("org.kontalk.message.group.jid", groupJid);
i.putExtra("org.kontalk.message.group.subject", groupSubject);
i.putExtra("org.kontalk.message.group.command", GROUP_COMMAND_MEMBERS);
i.putExtra("org.kontalk.message.group.add", members);
i.putExtra("org.kontalk.message.to", to);
i.putExtra("org.kontalk.message.encrypt", encrypt);
i.putExtra("org.kontalk.message.chatState", ChatState.active.name());
context.startService(i);
}
public static void removeGroupMembers(final Context context, String groupJid,
String groupSubject, String[] to, String[] members, boolean encrypt, long msgId, String packetId) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_MESSAGE);
i.putExtra("org.kontalk.message.msgId", msgId);
i.putExtra("org.kontalk.message.packetId", packetId);
i.putExtra("org.kontalk.message.mime", GroupCommandComponent.MIME_TYPE);
i.putExtra("org.kontalk.message.group.jid", groupJid);
i.putExtra("org.kontalk.message.group.subject", groupSubject);
i.putExtra("org.kontalk.message.group.command", GROUP_COMMAND_MEMBERS);
i.putExtra("org.kontalk.message.group.remove", members);
i.putExtra("org.kontalk.message.to", to);
i.putExtra("org.kontalk.message.encrypt", encrypt);
i.putExtra("org.kontalk.message.chatState", ChatState.active.name());
context.startService(i);
}
public static void setGroupSubject(final Context context, String groupJid,
String groupSubject, String[] to, boolean encrypt, long msgId, String packetId) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_MESSAGE);
i.putExtra("org.kontalk.message.msgId", msgId);
i.putExtra("org.kontalk.message.packetId", packetId);
i.putExtra("org.kontalk.message.mime", GroupCommandComponent.MIME_TYPE);
i.putExtra("org.kontalk.message.group.jid", groupJid);
i.putExtra("org.kontalk.message.group.subject", groupSubject);
i.putExtra("org.kontalk.message.group.command", GROUP_COMMAND_SUBJECT);
i.putExtra("org.kontalk.message.to", to);
i.putExtra("org.kontalk.message.encrypt", encrypt);
i.putExtra("org.kontalk.message.chatState", ChatState.active.name());
context.startService(i);
}
/** Sends a binary message. */
public static void sendBinaryMessage(final Context context,
String to,
String mime, Uri localUri, long length, String previewPath,
boolean encrypt, int compress,
long msgId, String packetId) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_MESSAGE);
i.putExtra("org.kontalk.message.msgId", msgId);
i.putExtra("org.kontalk.message.packetId", packetId);
i.putExtra("org.kontalk.message.mime", mime);
i.putExtra("org.kontalk.message.to", to);
i.putExtra("org.kontalk.message.media.uri", localUri.toString());
i.putExtra("org.kontalk.message.length", length);
i.putExtra("org.kontalk.message.preview.path", previewPath);
i.putExtra("org.kontalk.message.compress", compress);
i.putExtra("org.kontalk.message.encrypt", encrypt);
i.putExtra("org.kontalk.message.chatState", ChatState.active.name());
context.startService(i);
}
public static void sendGroupBinaryMessage(final Context context, String groupJid, String[] to,
String mime, Uri localUri, long length, String previewPath,
boolean encrypt, int compress, long msgId, String packetId) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_MESSAGE);
i.putExtra("org.kontalk.message.msgId", msgId);
i.putExtra("org.kontalk.message.packetId", packetId);
i.putExtra("org.kontalk.message.mime", mime);
i.putExtra("org.kontalk.message.group.jid", groupJid);
i.putExtra("org.kontalk.message.to", to);
i.putExtra("org.kontalk.message.media.uri", localUri.toString());
i.putExtra("org.kontalk.message.length", length);
i.putExtra("org.kontalk.message.preview.path", previewPath);
i.putExtra("org.kontalk.message.compress", compress);
i.putExtra("org.kontalk.message.encrypt", encrypt);
i.putExtra("org.kontalk.message.chatState", ChatState.active.name());
context.startService(i);
}
public static void sendGroupUploadedMedia(final Context context, String groupJid, String[] to,
String mime, Uri localUri, long length, String previewPath, String fetchUrl,
boolean encrypt, long msgId, String packetId) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_MESSAGE);
i.putExtra("org.kontalk.message.msgId", msgId);
i.putExtra("org.kontalk.message.packetId", packetId);
i.putExtra("org.kontalk.message.mime", mime);
i.putExtra("org.kontalk.message.group.jid", groupJid);
i.putExtra("org.kontalk.message.to", to);
i.putExtra("org.kontalk.message.preview.uri", localUri.toString());
i.putExtra("org.kontalk.message.length", length);
i.putExtra("org.kontalk.message.preview.path", previewPath);
i.putExtra("org.kontalk.message.body", fetchUrl);
i.putExtra("org.kontalk.message.fetch.url", fetchUrl);
i.putExtra("org.kontalk.message.encrypt", encrypt);
i.putExtra("org.kontalk.message.chatState", ChatState.active.name());
context.startService(i);
}
public static void sendUploadedMedia(final Context context, String to,
String mime, Uri localUri, long length, String previewPath, String fetchUrl,
boolean encrypt, long msgId, String packetId) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_MESSAGE);
i.putExtra("org.kontalk.message.msgId", msgId);
i.putExtra("org.kontalk.message.packetId", packetId);
i.putExtra("org.kontalk.message.mime", mime);
i.putExtra("org.kontalk.message.to", to);
i.putExtra("org.kontalk.message.preview.uri", localUri.toString());
i.putExtra("org.kontalk.message.length", length);
i.putExtra("org.kontalk.message.preview.path", previewPath);
i.putExtra("org.kontalk.message.body", fetchUrl);
i.putExtra("org.kontalk.message.fetch.url", fetchUrl);
i.putExtra("org.kontalk.message.encrypt", encrypt);
i.putExtra("org.kontalk.message.chatState", ChatState.active.name());
context.startService(i);
}
public static void sendMedia(final Context context, long msgId) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_MEDIA_READY);
i.putExtra("org.kontalk.message.msgId", msgId);
context.startService(i);
}
public static void retryMessage(final Context context, Uri uri, boolean chatEncryptionEnabled) {
boolean encrypted = Preferences.getEncryptionEnabled(context) && chatEncryptionEnabled;
MessagesProviderUtils.retryMessage(context, uri, encrypted);
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_RETRY);
// TODO not implemented yet
i.putExtra(MessageCenterService.EXTRA_MESSAGE, uri);
context.startService(i);
}
public static void retryMessagesTo(final Context context, String to) {
MessagesProviderUtils.retryMessagesTo(context, to);
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_RETRY);
// TODO not implemented yet
i.putExtra(MessageCenterService.EXTRA_TO, to);
context.startService(i);
}
public static void retryAllMessages(final Context context) {
MessagesProviderUtils.retryAllMessages(context);
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_RETRY);
context.startService(i);
}
public static String messageId() {
return StringUtils.randomString(30);
}
/** Replies to a presence subscription request. */
public static void replySubscription(final Context context, String to, int action) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_SUBSCRIBED);
i.putExtra(EXTRA_TO, to);
i.putExtra(EXTRA_PRIVACY, action);
context.startService(i);
}
public static void regenerateKeyPair(final Context context) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_REGENERATE_KEYPAIR);
context.startService(i);
}
public static void importKeyPair(final Context context, Uri keypack, String passphrase) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_IMPORT_KEYPAIR);
i.putExtra(EXTRA_KEYPACK, keypack);
i.putExtra(EXTRA_PASSPHRASE, passphrase);
context.startService(i);
}
public static void requestConnectionStatus(final Context context) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_CONNECTED);
context.startService(i);
}
public static void requestRosterStatus(final Context context) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_ROSTER_LOADED);
context.startService(i);
}
public static void requestRosterEntryStatus(final Context context, String to) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_ROSTER_STATUS);
i.putExtra(MessageCenterService.EXTRA_TO, to);
context.startService(i);
}
public static void requestPresence(final Context context, String to) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_PRESENCE);
i.putExtra(MessageCenterService.EXTRA_TO, to);
i.putExtra(MessageCenterService.EXTRA_TYPE, Presence.Type.probe.name());
context.startService(i);
}
public static void requestLastActivity(final Context context, String to, String id) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_LAST_ACTIVITY);
i.putExtra(EXTRA_TO, to);
i.putExtra(EXTRA_PACKET_ID, id);
context.startService(i);
}
public static void requestVersionInfo(final Context context, String to, String id) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_VERSION);
i.putExtra(EXTRA_TO, to);
i.putExtra(EXTRA_PACKET_ID, id);
context.startService(i);
}
public static void requestVCard(final Context context, String to) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_VCARD);
i.putExtra(EXTRA_TO, to);
context.startService(i);
}
public static void requestPublicKey(final Context context, String to) {
requestPublicKey(context, to, null);
}
public static void requestPublicKey(final Context context, String to, String id) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_PUBLICKEY);
i.putExtra(EXTRA_TO, to);
i.putExtra(EXTRA_PACKET_ID, id);
context.startService(i);
}
public static void requestServerList(final Context context) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_SERVERLIST);
context.startService(i);
}
/** Starts the push notifications registration process. */
public static void enablePushNotifications(Context context) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(ACTION_PUSH_START);
context.startService(i);
}
/** Starts the push notifications unregistration process. */
public static void disablePushNotifications(Context context) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(ACTION_PUSH_STOP);
context.startService(i);
}
/** Caches the given registration Id for use with push notifications. */
public static void registerPushNotifications(Context context, String registrationId) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(ACTION_PUSH_REGISTERED);
i.putExtra(PUSH_REGISTRATION_ID, registrationId);
context.startService(i);
}
public void setPushNotifications(boolean enabled) {
mPushNotifications = enabled;
if (mPushNotifications) {
if (mPushRegistrationId == null)
pushRegister();
}
else {
pushUnregister();
}
}
void pushRegister() {
if (sPushSenderId != null) {
if (mPushService != null && mPushService.isServiceAvailable()) {
// senderId will be given by serverinfo if any
mPushRegistrationId = mPushService.getRegistrationId();
if (TextUtils.isEmpty(mPushRegistrationId))
// start registration
mPushService.register(sPushListener, sPushSenderId);
else
// already registered - send registration id to server
setPushRegistrationId(mPushRegistrationId);
}
}
}
private void pushUnregister() {
if (mPushService != null) {
if (mPushService.isRegistered())
// start unregistration
mPushService.unregister(sPushListener);
else
// force unregistration
setPushRegistrationId(null);
}
}
private void setPushRegistrationId(String regId) {
mPushRegistrationId = regId;
// notify the server about the change
if (canConnect() && isConnected()) {
if (regId != null) {
sendPushRegistration(regId);
}
else {
sendPushUnregistration();
}
}
}
private void sendPushRegistration(final String regId) {
IQ iq = PushRegistration.register(DEFAULT_PUSH_PROVIDER, regId);
iq.setTo("push@" + mServer.getNetwork());
try {
mConnection.sendIqWithResponseCallback(iq, new StanzaListener() {
@Override
public void processPacket(Stanza packet) throws NotConnectedException {
if (mPushService != null)
mPushService.setRegisteredOnServer(regId != null);
}
});
}
catch (NotConnectedException e) {
// ignored
}
}
private void sendPushUnregistration() {
IQ iq = PushRegistration.unregister(DEFAULT_PUSH_PROVIDER);
iq.setTo("push@" + mServer.getNetwork());
try {
mConnection.sendIqWithResponseCallback(iq, new StanzaListener() {
@Override
public void processPacket(Stanza packet) throws NotConnectedException {
if (mPushService != null)
mPushService.setRegisteredOnServer(false);
}
});
}
catch (NotConnectedException e) {
// ignored
}
}
public static String getPushSenderId() {
return sPushSenderId;
}
void setWakeupAlarm() {
long delay = Preferences.getWakeupTimeMillis(this,
MIN_WAKEUP_TIME);
// start message center pending intent
PendingIntent pi = PendingIntent.getService(
getApplicationContext(), 0, getStartIntent(this),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT);
// we don't use the shared alarm manager instance here
// since this can happen after the service has begun to stop
AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + delay, pi);
}
private void ensureIdleAlarm() {
if (mIdleIntent == null) {
Intent i = getStartIntent(this);
i.setAction(ACTION_IDLE);
mIdleIntent = PendingIntent.getService(
getApplicationContext(), 0, i,
PendingIntent.FLAG_UPDATE_CURRENT);
}
}
void cancelIdleAlarm() {
// synchronized access since we might get a call from IdleThread
final AlarmManager alarms = mAlarmManager;
if (alarms != null) {
ensureIdleAlarm();
alarms.cancel(mIdleIntent);
}
}
private void setIdleAlarm() {
// even if this is called from IdleThread, we don't need to synchronize
// because at that point mConnection is null so we never get called
long delay = Preferences.getIdleTimeMillis(this, 0);
if (delay > 0) {
ensureIdleAlarm();
mAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + delay, delay, mIdleIntent);
}
}
}