package org.awesomeapp.messenger.push; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import net.java.otr4j.session.TLV; import net.sqlcipher.database.SQLiteConstraintException; //import org.awesomeapp.messenger.push.gcm.GcmRegistration; import org.awesomeapp.messenger.push.model.PersistedAccount; import org.awesomeapp.messenger.push.model.PersistedDevice; import org.awesomeapp.messenger.push.model.PersistedPushToken; import org.awesomeapp.messenger.push.model.PushDatabase; import org.awesomeapp.messenger.util.AbortableCountDownLatch; import org.awesomeapp.messenger.util.Debug; import org.chatsecure.pushsecure.PushSecureClient; import org.chatsecure.pushsecure.response.Account; import org.chatsecure.pushsecure.response.Device; import org.chatsecure.pushsecure.response.PushToken; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.text.ParseException; import java.util.Date; import java.util.NoSuchElementException; import java.util.concurrent.Executors; import timber.log.Timber; /** * A top-level class for management of ChatSecure-Push. * <p> * Usage: * <pre> * {@code * PushManager manager = new PushManager(context); * manager.authenticateAccount("username", "password", new PushSecureClient.RequestCallback<Account>() { * @Override * public void onSuccess(@NonNull Account account) { * // account has been persisted to the application database * // you may now perform authenticated ChatSecure-Push actions: * * // Create a Whitelist Token Exchange TLV to transmit a token to a peer * // manager.createWhitelistTokenExchangeTlv(...); * * // Send a Push Message to a peer whose token you've received via * // the Whitelist Token Exchange TLV mechanism, or otherwise. * // manager.sendPushMessageToPeer("bob@dukgo.com", new PushSecureClient.RequestCallback<Message>(){...}); * } * * @Override * public void onFailure(@NonNull Throwable throwable) { * // Unable to authenticate ChatSecure-Push account. * * // Check throwable for an error message describing the issue: * // throwable.getMessage(); * } * }); * * } * </pre> * <p> * Created by dbro on 9/18/15. */ public class PushManager { public static final String DEFAULT_PROVIDER = "https://push.zom.im/api/v1/"; enum State {UNAUTHENTICATED, AUTHENTICATED} private State state = State.UNAUTHENTICATED; private Context context; private PushSecureClient client; private String providerUrl; private String deviceName; // <editor-fold desc="Public API"> public PushManager(@NonNull Context context) { this(context, DEFAULT_PROVIDER); } public PushManager(@NonNull Context context, @NonNull String chatsecurePushServerUrl) { this.context = context; this.providerUrl = chatsecurePushServerUrl; client = new PushSecureClient(providerUrl); logAllTokens(); } /** * @return the URL describing the ChatSecure-Push provider */ @NonNull public String getProviderUrl() { return providerUrl; } /** * Create a Whitelist Token Exchange {@link TLV} for transmission to a remote peer over OTR. * This method obtains a new receiving Whitelist Token from ChatSecure-Push if necessary * before notifying {@param callback}. * <p> * The token(s) embedded within the resulting TLV will be marked as issued. */ public void createWhitelistTokenExchangeTlv(@NonNull String issuerIdentifier, @NonNull String recipientIdentifier, @NonNull final PushSecureClient.RequestCallback<TLV> callback, @Nullable final String extraData) throws UnsupportedEncodingException { // if (!assertAuthenticated()) return; if (Debug.DEBUG_ENABLED) Timber.d("createWhitelistTokenExchangeTlv recipient %s issuer %s", recipientIdentifier, issuerIdentifier); // Note that an outgoing Whitelist token must have the host identifier as it's "recipient" final Cursor persistedTokens = getPersistedTokenCursor(issuerIdentifier, recipientIdentifier, false); if (persistedTokens != null && persistedTokens.getCount() > 0) { Timber.d("Got token for identifier %s", issuerIdentifier); markWhitelistTokenIssued(persistedTokens.getInt(persistedTokens.getColumnIndex(PushDatabase.Tokens._ID))); String peerWhitelistToken = persistedTokens.getString(persistedTokens.getColumnIndex(PushDatabase.Tokens.TOKEN)); TLV tokenTlv = createWhitelistTokenExchangeTlvWithToken( new String[]{peerWhitelistToken}, null); callback.onSuccess(tokenTlv); persistedTokens.close(); return; } else if (persistedTokens != null) persistedTokens.close(); if (Debug.DEBUG_ENABLED) Timber.d("Got no token for recipient %s issuer %s. Creating new", recipientIdentifier, issuerIdentifier); createReceivingWhitelistTokenForPeer(issuerIdentifier, recipientIdentifier, new PushSecureClient.RequestCallback<PersistedPushToken>() { @Override public void onSuccess(@NonNull PersistedPushToken response) { try { TLV tlv = new TLV(WhitelistTokenTlv.TLV_WHITELIST_TOKEN, WhitelistTokenTlv.createGson().toJson( new WhitelistTokenTlv( response.providerUrl, new String[]{response.token}, extraData)) .getBytes("UTF-8")); markWhitelistTokenIssued(response.localId); callback.onSuccess(tlv); } catch (UnsupportedEncodingException e) { this.onFailure(e); } } @Override public void onFailure(@NonNull Throwable t) { callback.onFailure(t); } }); } /** * Create a Whitelist Token Exchange {@link TLV} for transmission to a remote peer over OTR. * This method uses the provided Whitelist tokens. See {@link WhitelistTokenTlv} for details * on the TLV data format. * * @param tokens an array of ChatSecure-Push Whitelist Tokens to be packaged in the {@link TLV}. * These tokens are typically issued by a local user for transmission * to a remote user. * @param extraData additional data to be packaged in the {@link TLV} */ public TLV createWhitelistTokenExchangeTlvWithToken(@NonNull String[] tokens, @Nullable String extraData) throws UnsupportedEncodingException { return new TLV(WhitelistTokenTlv.TLV_WHITELIST_TOKEN, WhitelistTokenTlv.createGson().toJson( new WhitelistTokenTlv( providerUrl, tokens, extraData)) .getBytes("UTF-8")); } /** * Authenticate a ChatSecure-Push Account, which includes registering the host device. * When a successful result is delivered to {@param callback} this client may request * Whitelist tokens and immediately begin receiving Push messages addressed to them. * * @param username the ChatSecure-Push Account username * @param password the ChatSecure-Push Account password * @param callback callback to be notified of result on main thread */ public void authenticateAccount(@NonNull final String username, @NonNull final String password, @NonNull final PushSecureClient.RequestCallback<Account> callback) { // If we were previously authenticated, clear that state until this authentication completes state = State.UNAUTHENTICATED; final AbortableCountDownLatch preRequisiteLatch = new AbortableCountDownLatch(2); final String[] gcmToken = new String[1]; final Account[] account = new Account[1]; // This task handles the following flow: // 1a. Create ChatSecure-Push Account // 1b. Obtain a GCM Registration Id // (1a / 1b performed in parallel) // 2. Create or update ChatSecure-Push Device record // 3. Notify callback of success // Note: If any failure occurs in flow, it is also reported to callback // This situation is much more gracefully handled by RxJava, but I don't want // to introduce such a dependency into this project just for me :) new AsyncTask<Void, Void, Account>() { private Throwable taskThrowable; @Override protected Account doInBackground(Void... params) { // Create ChatSecure-Push account client.authenticateAccount(username, password, null /* (optional) email */, new PushSecureClient.RequestCallback<Account>() { @Override public void onSuccess(@NonNull Account response) { if (Debug.DEBUG_ENABLED) Timber.d("Got Account"); account[0] = response; client.setAccount(response); setPersistedAccount(response, password, providerUrl); preRequisiteLatch.countDown(); } @Override public void onFailure(@NonNull Throwable throwable) { if (Debug.DEBUG_ENABLED) Timber.e("Failed to get Account", throwable); taskThrowable = throwable; preRequisiteLatch.abort(); } }); // Fetch GCM Registration Id /** GcmRegistration.getRegistrationIdAsync(context, new GcmRegistration.RegistrationCallback() { @Override public void onRegistration(String gcmRegistrationId) { Timber.d("Got GCM"); gcmToken[0] = gcmRegistrationId; preRequisiteLatch.countDown(); } @Override public void onFailure(Throwable throwable) { Timber.e("Failed to get GCM", throwable); taskThrowable = throwable; preRequisiteLatch.abort(); } });*/ try { // Await the parallel completion of: // (1) ChatSecure-Push Account registration // (2) GCM registration. preRequisiteLatch.await(); if (Debug.DEBUG_ENABLED) Timber.d("Latch - Got GCM and CSP"); final AbortableCountDownLatch deviceRegistrationLatch = new AbortableCountDownLatch(1); PushSecureClient.RequestCallback<Device> deviceCreatedOrUpdatedCallback = new PushSecureClient.RequestCallback<Device>() { @Override public void onSuccess(@NonNull Device response) { Timber.d("Registered Device"); deviceRegistrationLatch.countDown(); } @Override public void onFailure(@NonNull Throwable t) { Timber.e("Failed to register Device", t); taskThrowable = t; deviceRegistrationLatch.abort(); } }; Device persistedDevice = getPersistedDevice(); // Create or Update ChatSecure-Push Device if (persistedDevice == null) { createDeviceWithGcmRegistrationId(gcmToken[0], deviceCreatedOrUpdatedCallback); } else if (!persistedDevice.registrationId.equals(gcmToken[0])) { updateDeviceWithGcmRegistrationId(persistedDevice, gcmToken[0], deviceCreatedOrUpdatedCallback); } else { state = State.AUTHENTICATED; deviceRegistrationLatch.countDown(); } Timber.d("Awaiting device registration"); deviceRegistrationLatch.await(); Timber.d("Latch - Regisered device"); return account[0]; } catch (InterruptedException e) { // This occurs if abort() is called on either of our CountdownLatches // The root cause should be available in taskThrowable Timber.e(e, "Failed to authenticate ChatSecure-Push Account"); } return null; } @Override protected void onPostExecute(Account result) { if (result != null) { Timber.d("authenticateAccount finished with success"); callback.onSuccess(result); } else if (taskThrowable != null) { Timber.e("authenticateAccount failed", taskThrowable); callback.onFailure(taskThrowable); } else { Timber.e("AuthenticateAccount task failed, but no error was reported"); } } }.executeOnExecutor(Executors.newSingleThreadExecutor()); } /** * Create a new Whitelist Token authorizing push access to the local device. * Must be called after {@link #authenticateAccount(String, String, PushSecureClient.RequestCallback)}. * * @param issuerIdentifier a String uniquely identifying the local account that will receive * push messages with the produced Whitelist Token. * @param recipientIdentifier a String uniquely identifying the remote account who will use the * produced Whitelist Token to send push messages to your application. * This is stored internally with the token to enable functionality of * {@link #revokeWhitelistTokensForPeer(String, String, PushSecureClient.RequestCallback)} * @param callback */ public void createReceivingWhitelistTokenForPeer(@NonNull final String issuerIdentifier, @NonNull final String recipientIdentifier, @NonNull final PushSecureClient.RequestCallback<PersistedPushToken> callback) { if (!assertAuthenticated()) return; final PersistedDevice thisDevice = getPersistedDevice(); final String tokenIdentifier = createWhitelistTokenName(recipientIdentifier, thisDevice.name); client.createToken(thisDevice, tokenIdentifier, new PushSecureClient.RequestCallback<PushToken>() { @Override public void onSuccess(@NonNull PushToken response) { ContentValues tokenValues = new ContentValues(6); tokenValues.put(PushDatabase.Tokens.RECIPIENT, recipientIdentifier); tokenValues.put(PushDatabase.Tokens.ISSUER, issuerIdentifier); tokenValues.put(PushDatabase.Tokens.NAME, tokenIdentifier); tokenValues.put(PushDatabase.Tokens.TOKEN, response.token); tokenValues.put(PushDatabase.Tokens.DEVICE, thisDevice.localId); tokenValues.put(PushDatabase.Tokens.CREATED_DATE, PushDatabase.DATE_FORMATTER.format(new Date())); Uri persistedTokenUri = context.getContentResolver().insert(PushDatabase.Tokens.CONTENT_URI, tokenValues); if (Debug.DEBUG_ENABLED) Timber.d("Inserted token %s for recipient %s issuer %s. Uri %s", response.token, recipientIdentifier, issuerIdentifier, persistedTokenUri); //logAllTokens(); String persistedTokenId = persistedTokenUri.getLastPathSegment(); Cursor persistedTokenCursor = context.getContentResolver().query(Uri.withAppendedPath(PushDatabase.Tokens.CONTENT_URI, persistedTokenId), null, null, null, null); if (persistedTokenCursor != null && persistedTokenCursor.moveToFirst()) { PersistedPushToken persistedPushToken = new PersistedPushToken(persistedTokenCursor); callback.onSuccess(persistedPushToken); } else { callback.onFailure(new IOException("Failed to retrieve persisted push token")); } if (persistedTokenCursor != null) persistedTokenCursor.close(); } @Override public void onFailure(@NonNull Throwable t) { callback.onFailure(t); } }); } /** * Persist ChatSecure-Push Whitelist tokens received from a remote peer via the * OTR TLV Token Exchange scheme. These tokens can be later retrieved via * {@link #getPersistedWhitelistToken(String, String, PushSecureClient.RequestCallback)} * * @param tlv The Whitelist Token TLV received from the remote peer * @param recipientIdentifier a String uniquely identifying the local account that received {@param tlv} * @param issuerIdentifier a String uniquely identifying the remote account that issued {@param tlv} */ public void insertReceivedWhitelistTokensTlv(@NonNull WhitelistTokenTlv tlv, @NonNull String recipientIdentifier, @NonNull String issuerIdentifier) { for (int idx = 0; idx < tlv.tokens.length; idx++) { ContentValues tokenValues = new ContentValues(7); tokenValues.put(PushDatabase.Tokens.RECIPIENT, recipientIdentifier); tokenValues.put(PushDatabase.Tokens.ISSUER, issuerIdentifier); tokenValues.put(PushDatabase.Tokens.ISSUED, 1);//they have been issued to you tokenValues.put(PushDatabase.Tokens.PROVIDER, tlv.endpoint); tokenValues.put(PushDatabase.Tokens.NAME, createWhitelistTokenName(recipientIdentifier, issuerIdentifier)); tokenValues.put(PushDatabase.Tokens.TOKEN, tlv.tokens[idx]); tokenValues.put(PushDatabase.Tokens.CREATED_DATE, PushDatabase.DATE_FORMATTER.format(new Date())); try { Uri uri = context.getContentResolver().insert(PushDatabase.Tokens.CONTENT_URI, tokenValues); if (Debug.DEBUG_ENABLED) Timber.d("Inserted token %s for recipient %s issuer %s. Uri %s", tlv.tokens[idx], recipientIdentifier, issuerIdentifier, uri); logAllTokens(); } catch (SQLiteConstraintException e) { // This token is already stored, ignore. Timber.e(e, "Failed to insert token %s.", tlv.tokens[idx], e); } } } /** * Retrieve a persisted Whitelist token for sending a push to {@param pushRecipientIdentifier} on behalf of * {@param pushSenderIdentifier}. * <p> * TODO: Make fully asynchronous or remove * * @param pushRecipientIdentifier a String uniquely identifying the remote peer who should receive * the push message. This identifier will be used to query * @param pushSenderIdentifier a String uniquely identifying the local user which the push message * should be sent on behalf of. * @param callback a callback which will receive the {@link PersistedPushToken} * or a {@link NoSuchElementException} */ public void getPersistedWhitelistToken(@NonNull final String pushRecipientIdentifier, @NonNull final String pushSenderIdentifier, @NonNull final PushSecureClient.RequestCallback<PushToken> callback) { if (Debug.DEBUG_ENABLED) Timber.d("Lookup push token issued by %s received by %s", pushRecipientIdentifier, pushSenderIdentifier); Cursor persistedTokens = getPersistedTokenCursor(pushRecipientIdentifier, pushSenderIdentifier, true); if (persistedTokens != null && persistedTokens.getCount() > 0) { callback.onSuccess(new PersistedPushToken(persistedTokens)); persistedTokens.close(); } else { callback.onFailure(new NoSuchElementException(String.format("No token exists for peer %s", pushRecipientIdentifier))); } if (persistedTokens != null) persistedTokens.close(); } /** * Retrieve a persisted Whitelist token for sending a push to {@param pushRecipientIdentifier} on behalf of * {@param pushSenderIdentifier}. * <p> * TODO: Make fully asynchronous or remove * * @param pushRecipientIdentifier a String uniquely identifying the remote peer who should receive * the push message. This identifier will be used to query * @param pushSenderIdentifier a String uniquely identifying the local user which the push message * should be sent on behalf of. */ public boolean hasPersistedWhitelistToken(@NonNull final String pushRecipientIdentifier, @NonNull final String pushSenderIdentifier) { boolean response = false; if (Debug.DEBUG_ENABLED) Timber.d("Lookup push token issued by %s received by %s", pushRecipientIdentifier, pushSenderIdentifier); Cursor persistedTokens = getPersistedTokenCursor(pushRecipientIdentifier, pushSenderIdentifier, true); response = (persistedTokens != null && persistedTokens.getCount() > 0); if (persistedTokens != null) persistedTokens.close(); return response; } /** * Mark a Whitelist token as issued. This means we should consider it successfully transmitted * to its {@link PushDatabase.Tokens#RECIPIENT}, and it should not be transmitted to any other peers. * * @param tokenLocalId the local database id of the Whitelist token */ public void markWhitelistTokenIssued(final int tokenLocalId) { ContentValues tokenValues = new ContentValues(1); tokenValues.put(PushDatabase.Tokens.ISSUED, 1); int result = context.getContentResolver().update( PushDatabase.Tokens.CONTENT_URI, tokenValues, PushDatabase.Tokens._ID + " = ?", new String[]{String.valueOf(tokenLocalId)}); if (result != 1) Timber.e("Failed to mark token %d as issued", tokenLocalId); else { if (Debug.DEBUG_ENABLED) Timber.d("Marked token %d issued", tokenLocalId); logAllTokens(); } } /** * Mark a Whitelist token as issued. This means we should consider it successfully transmitted * to its {@link PushDatabase.Tokens#RECIPIENT}, and it should not be transmitted to any other peers. * * @param token the token value to make as issued/used */ public void markWhitelistTokenIssued(String token) { ContentValues tokenValues = new ContentValues(1); tokenValues.put(PushDatabase.Tokens.ISSUED, 1); int result = context.getContentResolver().update( PushDatabase.Tokens.CONTENT_URI, tokenValues, PushDatabase.Tokens.TOKEN + " = ?", new String[]{token}); if (result != 1) Timber.e("Failed to mark token %d as issued", token); else { Timber.d("Marked token as issued: " + token); logAllTokens(); } } /** * Revoke Whitelist tokens created by this application install for the given recipient. This method * will only succeed if the matching Whitelist Token(s) was/were created by the ChatSecure-Push account * currently active as a result of {@link #authenticateAccount(String, String, PushSecureClient.RequestCallback)}. * NOTE: This does not currently delete tokens that may have been issued by another application install. * Currently, the only way to do that is to adopt a common naming convention for tokens that incorporates * the recipient OR to delete all tokens the server reports. * (e.g: Created via {@link #createReceivingWhitelistTokenForPeer(String, String, PushSecureClient.RequestCallback)} * Must be called after {@link #authenticateAccount(String, String, PushSecureClient.RequestCallback)}. * * @param issuerIdentifier a String uniquely identifying the local user who issued the tokens to be revoked. * @param recipientIdentifier a String uniquely identifying the remote user who was issued the tokens to be revoked. * @param callback a callback indicating success or failure. */ public void revokeWhitelistTokensForPeer(@NonNull final String issuerIdentifier, @NonNull final String recipientIdentifier, @NonNull final PushSecureClient.RequestCallback<Void> callback) { // if (!assertAuthenticated()) return; final Cursor recipientTokens = getPersistedTokenCursor(issuerIdentifier, recipientIdentifier, false); if (recipientTokens != null && recipientTokens.getCount() > 0) { new AsyncTask<Void, Void, Throwable>() { @Override protected Throwable doInBackground(Void... params) { final AbortableCountDownLatch latch = new AbortableCountDownLatch(recipientTokens.getCount()); do { client.deleteToken(recipientTokens.getString(recipientTokens.getColumnIndex(PushDatabase.Tokens.TOKEN)), new PushSecureClient.RequestCallback<Void>() { @Override public void onSuccess(@NonNull Void response) { Timber.d("Deleted token!"); latch.countDown(); } @Override public void onFailure(@NonNull Throwable t) { Timber.e(t, "Failed to delete token"); latch.abort(); } }); } while (recipientTokens.moveToNext()); try { latch.await(); return null; } catch (InterruptedException e) { Timber.e(e, "Latch interrupted"); return e; } } @Override protected void onPostExecute(Throwable throwable) { if (throwable != null) { callback.onFailure(throwable); } else { callback.onSuccess((Void) new Object()); } } }.execute(); } } /** * Send a ChatSecure-Push push message from {@param issuerIdentifier} to {@param recipientIdentifier} * if a Whitelist Token matching the pair is available. * * @param issuerIdentifier a String uniquely identifying the local user who is issuing the push message. * @param recipientIdentifier a String uniquely identifying the remote user who will receive the push message. * @param callback a callback indicating success or failure */ public void sendPushMessageToPeer(@NonNull final String issuerIdentifier, @NonNull final String recipientIdentifier, @NonNull final PushSecureClient.RequestCallback<org.chatsecure.pushsecure.response.Message> callback) { // if (!assertAuthenticated()) return; if (Debug.DEBUG_ENABLED) Timber.d("Send push to %s from %s", recipientIdentifier, issuerIdentifier); getPersistedWhitelistToken(recipientIdentifier, issuerIdentifier, new PushSecureClient.RequestCallback<PushToken>() { @Override public void onSuccess(@NonNull PushToken response) { PersistedPushToken ppt = (PersistedPushToken)response; sendPushMessageToToken(response.token, ppt.providerUrl, callback); } @Override public void onFailure(@NonNull Throwable t) { callback.onFailure(t); } }); } // </editor-fold desc="Public API"> // <editor-fold desc="Private API"> /** * @param recipientIdentifier the recipient of the token. To receive a token for * transmission to a remote peer, this should be the local host's * identifier. To receive a token for sending a push to a remote * peer, this should be that remote peer's identifier. * @param issuedFilter Filter the returned tokens by those that have * been marked 'issued' or those that have not. When retrieving a * list of tokens to be revoked this should be true. When retrieving * a list of tokens for transmission to remote peers, this should be false. */ @Nullable private Cursor getPersistedTokenCursor(@NonNull String issuerIdentifier, @NonNull String recipientIdentifier, boolean issuedFilter) { String where = PushDatabase.Tokens.RECIPIENT + " = ? " + " AND " + PushDatabase.Tokens.ISSUER + " = ? " + " AND " + PushDatabase.Tokens.ISSUED + " = ?"; String[] whereArgs = new String[]{recipientIdentifier, issuerIdentifier, issuedFilter ? "1" : "0"}; Cursor result = context.getContentResolver().query( PushDatabase.Tokens.CONTENT_URI, null, where, whereArgs, PushDatabase.Tokens.CREATED_DATE + " DESC"); // Most recent tokens first if (Debug.DEBUG_ENABLED) Timber.d("Query token for recipient %s issuer %s isued %b. result: %d", recipientIdentifier, issuerIdentifier, issuedFilter, (result != null ? result.getCount() : 0)); if (result != null) result.moveToFirst(); return result; } @Nullable public PersistedAccount getPersistedAccount() { Cursor accountCursor = context.getContentResolver().query( PushDatabase.Accounts.CONTENT_URI, null, PushDatabase.Accounts._ID + " = ?", new String[]{"0"}, null); if (accountCursor == null) { Timber.w("Persisted Account cursor was null. Maybe CacheWord was not available?"); return null; } else if (!accountCursor.moveToFirst()) { accountCursor.close(); Timber.d("Persisted Account cursor had no rows"); return null; } PersistedAccount account = new PersistedAccount(accountCursor); accountCursor.close(); return account; } private void setPersistedAccount(@NonNull Account account, @NonNull String password, @NonNull String provider) { ContentValues accountValues = new ContentValues(4); accountValues.put(PushDatabase.Accounts._ID, 0); // We should only have one ChatSecure-Push Account accountValues.put(PushDatabase.Accounts.USERNAME, account.username); accountValues.put(PushDatabase.Accounts.PASSWORD, password); accountValues.put(PushDatabase.Accounts.PROVIDER, provider); // Update or Insert if (context.getContentResolver().update(PushDatabase.Accounts.CONTENT_URI, accountValues, PushDatabase.Accounts._ID + " = ?", new String[]{"0"}) == 1) { Timber.d("Updated persisted account"); } else { context.getContentResolver().insert(PushDatabase.Accounts.CONTENT_URI, accountValues); Timber.d("Inserted persisted account"); } } @Nullable private PersistedDevice getPersistedDevice() { // We should only have one persisted device record Cursor deviceCursor = context.getContentResolver().query( PushDatabase.Devices.CONTENT_URI, null, PushDatabase.Devices._ID + " = ?", new String[]{"0"}, null); if (deviceCursor == null) return null; else if (!deviceCursor.moveToFirst()) { deviceCursor.close(); return null; } try { PersistedDevice device = new PersistedDevice(deviceCursor); deviceCursor.close(); return device; } catch (ParseException e) { Timber.e(e, "Failed to create PersistedDevice from Cursor"); return null; } } private void setPersistedDevice(@NonNull Device device) { ContentValues deviceValues = new ContentValues(6); deviceValues.put(PushDatabase.Devices._ID, 0); // We only want to store one record for the host device deviceValues.put(PushDatabase.Devices.NAME, device.name); deviceValues.put(PushDatabase.Devices.DATE_CREATED, PushDatabase.DATE_FORMATTER.format(device.dateCreated)); deviceValues.put(PushDatabase.Devices.REGISTRATION_ID, device.registrationId); deviceValues.put(PushDatabase.Devices.DEVICE_ID, device.deviceId); deviceValues.put(PushDatabase.Devices.SERVER_ID, device.id); deviceValues.put(PushDatabase.Devices.ACTIVE, device.active); // Update or Insert if (context.getContentResolver().update(PushDatabase.Devices.CONTENT_URI, deviceValues, PushDatabase.Devices._ID + " = ?", new String[]{"0"}) == 1) { Timber.d("Updated persisted device"); } else { context.getContentResolver().insert(PushDatabase.Devices.CONTENT_URI, deviceValues); Timber.d("Inserted persisted device"); } } private void createDeviceWithGcmRegistrationId(@NonNull String gcmRegistrationId, @NonNull final PushSecureClient.RequestCallback<Device> callback) { client.createDevice(gcmRegistrationId, getDeviceName(), null /* device identifier */, new PushSecureClient.RequestCallback<Device>() { @Override public void onSuccess(@NonNull Device response) { setPersistedDevice(response); state = State.AUTHENTICATED; callback.onSuccess(response); } @Override public void onFailure(@NonNull Throwable t) { callback.onFailure(t); } }); } private void updateDeviceWithGcmRegistrationId(@NonNull Device deviceToUpdate, @NonNull String gcmRegistrationId, @NonNull final PushSecureClient.RequestCallback<Device> callback) { Timber.d("Updating device"); Device updatedDevice = Device.withUpdatedRegistrationId(deviceToUpdate, gcmRegistrationId); client.updateDevice(updatedDevice, new PushSecureClient.RequestCallback<Device>() { @Override public void onSuccess(@NonNull Device response) { Timber.d("Updated device"); setPersistedDevice(response); state = State.AUTHENTICATED; callback.onSuccess(response); } @Override public void onFailure(@NonNull Throwable t) { Timber.e("Failed to update device", t); callback.onFailure(t); } }); } private boolean assertAuthenticated() { boolean authenticated = state == State.AUTHENTICATED; if (!authenticated) { if (Debug.DEBUG_ENABLED) Timber.w("Not authenticated. Cannot request whitelist token. Did you await the result of #authenticateAccount()"); } return authenticated; } /** * Send a push message to a Whitelist token. * * @param recipientWhitelistToken a raw ChatSecure-Push Whitelist Token string. * @param callback a callback indicating success or failure */ private void sendPushMessageToToken(@NonNull String recipientWhitelistToken,@NonNull String recipientProviderUrl, @NonNull PushSecureClient.RequestCallback<org.chatsecure.pushsecure.response.Message> callback) { // if (!assertAuthenticated()) return; // client = new PushSecureClient(providerUrl); client.sendMessage(recipientWhitelistToken, "" /* push payload */, recipientProviderUrl, callback); } // </editor-fold desc="Private API"> // <editor-fold desc="Utility"> private void logTokens(@NonNull String recipientIdentifier, @NonNull String issuerIdentifier) { StringBuilder log = new StringBuilder(String.format("Tokens issued by %s, received by %s:", issuerIdentifier, recipientIdentifier)); Cursor issuedTokens = getPersistedTokenCursor(issuerIdentifier, recipientIdentifier, true); if (issuedTokens != null && issuedTokens.moveToFirst()) { do { log.append(issuedTokens.getString(issuedTokens.getColumnIndex(PushDatabase.Tokens.TOKEN))); } while (issuedTokens.moveToNext()); } if (issuedTokens != null) issuedTokens.close(); Cursor nonIssuedTokens = getPersistedTokenCursor(issuerIdentifier, recipientIdentifier, false); if (nonIssuedTokens != null && nonIssuedTokens.moveToFirst()) { do { log.append(nonIssuedTokens.getString(nonIssuedTokens.getColumnIndex(PushDatabase.Tokens.TOKEN))); } while (nonIssuedTokens.moveToNext()); } if (nonIssuedTokens != null) nonIssuedTokens.close(); // if (Debug.DEBUG_ENABLED) // Timber.d(log.toString()); } private void logAllTokens() { //if (Debug.DEBUG_ENABLED) { if (false) { StringBuilder log = new StringBuilder("All Whitelist Tokens:\nId\tRecipient\tIssuer\tIssued\tToken\n"); Cursor allTokens = context.getContentResolver().query(PushDatabase.Tokens.CONTENT_URI, null, null, null, PushDatabase.Tokens.CREATED_DATE + " DESC"); if (allTokens != null && allTokens.moveToFirst()) { do { log.append(allTokens.getInt(allTokens.getColumnIndex(PushDatabase.Tokens._ID))); log.append('\t'); log.append(allTokens.getString(allTokens.getColumnIndex(PushDatabase.Tokens.RECIPIENT))); log.append('\t'); log.append(allTokens.getString(allTokens.getColumnIndex(PushDatabase.Tokens.ISSUER))); log.append('\t'); log.append(allTokens.getInt(allTokens.getColumnIndex(PushDatabase.Tokens.ISSUED))); log.append('\t'); log.append(allTokens.getString(allTokens.getColumnIndex(PushDatabase.Tokens.TOKEN))); log.append('\t'); log.append(allTokens.getString(allTokens.getColumnIndex(PushDatabase.Tokens.PROVIDER))); log.append('\t'); log.append('\n'); } while (allTokens.moveToNext()); } if (allTokens != null) allTokens.close(); Timber.d(log.toString()); } } /** * Strip the resource off a JabberID. e.g: a@b.com/foo -> a@b.com * When using JIDs as the identifiers in methods like {@link #getPersistedTokenCursor(String, String, boolean)}, * you should strip the resource off the JID, because if the contact is offline we'll just have the bare JID available. */ public static String stripJabberIdResource(@NonNull String fullJabberId) { int trailingSlashIdx = fullJabberId.lastIndexOf('/'); if (trailingSlashIdx != -1) { return fullJabberId.substring(0, trailingSlashIdx); } return fullJabberId; } /** * @return a String describing a Whitelist token */ private String createWhitelistTokenName(@NonNull String toIdentifier, @NonNull String fromIdentifier) { return toIdentifier + "->" + fromIdentifier; } private String getDeviceName() { String manufacturer = Build.MANUFACTURER; String model = Build.MODEL; if (model.startsWith(manufacturer)) { return capitalize(model); } else { return capitalize(manufacturer) + " " + model; } } private String capitalize(String s) { if (s == null || s.length() == 0) { return ""; } char first = s.charAt(0); if (Character.isUpperCase(first)) { return s; } else { return Character.toUpperCase(first) + s.substring(1); } } // </editor-fold desc="Utility"> }