Skip to content

Commit

Permalink
UnifiedPush
Browse files Browse the repository at this point in the history
  • Loading branch information
p1gp1g authored and valldrac committed Nov 16, 2024
1 parent 6c5069e commit d197bc1
Show file tree
Hide file tree
Showing 33 changed files with 1,326 additions and 22 deletions.
10 changes: 10 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ android {
getByName("androidTest") {
java.srcDir("$projectDir/src/testShared")
}

getByName("main") {
java.srcDir("$projectDir/src/unifiedpush/java")
}
}

compileOptions {
Expand Down Expand Up @@ -532,6 +536,12 @@ dependencies {
implementation(libs.molly.glide.webp.decoder)
implementation(libs.gosimple.nbvcxz)
"fossImplementation"("org.osmdroid:osmdroid-android:6.1.16")
implementation(libs.unifiedpush.connector) {
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
}
implementation(libs.unifiedpush.connector.ui) {
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
}

"gmsImplementation"(project(":billing"))

Expand Down
1 change: 1 addition & 0 deletions app/proguard/proguard.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
-dontobfuscate
-keepattributes SourceFile,LineNumberTable
-keep class org.whispersystems.** { *; }
-keep class im.molly.** { *; }
-keep class org.signal.libsignal.net.** { *; }
-keep class org.signal.libsignal.protocol.** { *; }
-keep class org.signal.libsignal.usernames.** { *; }
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,18 @@
</intent-filter>
</receiver>

<receiver
android:name="im.molly.unifiedpush.receiver.UnifiedPushReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE" />
<action android:name="org.unifiedpush.android.connector.UNREGISTERED" />
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT" />
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED" />
</intent-filter>
</receiver>

<service
android:name=".gcm.FcmJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
Expand Down
168 changes: 168 additions & 0 deletions app/src/main/java/im/molly/unifiedpush/MollySocketRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package im.molly.unifiedpush

import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.JsonMappingException
import im.molly.unifiedpush.model.ConnectionRequest
import im.molly.unifiedpush.model.ConnectionResult
import im.molly.unifiedpush.model.MollySocketDevice
import im.molly.unifiedpush.model.Response
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.signal.core.util.Base64
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.util.KeyHelper
import org.thoughtcrime.securesms.AppCapabilities
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.linkdevice.LinkDeviceRepository
import org.thoughtcrime.securesms.push.AccountManagerFactory
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher
import org.thoughtcrime.securesms.util.JsonUtils
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.account.AccountAttributes
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException
import java.io.IOException
import java.net.MalformedURLException

object MollySocketRepository {

private val TAG = Log.tag(MollySocketRepository::class)

private val MEDIA_TYPE_JSON = "application/json; charset=utf-8".toMediaType()

private const val DEVICE_NAME = "MollySocket"

@Throws(IOException::class, DeviceLimitExceededException::class)
fun createDevice(): MollySocketDevice {
Log.d(TAG, "Creating device for MollySocket")

val password = Util.getSecret(18)
val deviceId = verifyNewDevice(password)

return MollySocketDevice(
deviceId = deviceId,
password = password,
)
}

@Throws(IOException::class, DeviceLimitExceededException::class)
private fun verifyNewDevice(password: String): Int {
val verificationCode = AppDependencies.signalServiceAccountManager.newDeviceVerificationCode

val registrationId = KeyHelper.generateRegistrationId(false)
val encryptedDeviceName = DeviceNameCipher.encryptDeviceName(
DEVICE_NAME.toByteArray(), SignalStore.account.aciIdentityKey
)

val notDiscoverable = SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE

val accountAttributes = AccountAttributes(
signalingKey = null,
registrationId = registrationId,
fetchesMessages = true,
registrationLock = null,
unidentifiedAccessKey = null,
unrestrictedUnidentifiedAccess = true,
capabilities = AppCapabilities.getCapabilities(true),
discoverableByPhoneNumber = !notDiscoverable,
name = Base64.encodeWithPadding(encryptedDeviceName),
pniRegistrationId = SignalStore.account.pniRegistrationId,
recoveryPassword = null
)

val aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.aciPreKeys)
val pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.pniIdentityKey, SignalStore.account.pniPreKeys)

val accountManager = AccountManagerFactory.getInstance().createForDeviceLink(AppDependencies.application, password)

return accountManager.finishNewDeviceRegistration(
verificationCode,
accountAttributes,
aciPreKeyCollection, pniPreKeyCollection,
null
).also {
TextSecurePreferences.setMultiDevice(AppDependencies.application, true)
}
}

// If loadDevices() fails, optimistically assume the device is linked
fun MollySocketDevice.isLinked(): Boolean {
return LinkDeviceRepository.loadDevices()?.any {
it.id == deviceId.toLong() && it.name == DEVICE_NAME
} ?: true
}

fun discoverMollySocketServer(url: HttpUrl): Boolean {
try {
val request = Request.Builder().url(url).build()
val client = AppDependencies.okHttpClient.newBuilder().build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
Log.d(TAG, "Unexpected code: $response")
return false
}
val body = response.body ?: run {
Log.d(TAG, "No response body")
return false
}
JsonUtils.fromJson(body.byteStream(), Response::class.java)
}
Log.d(TAG, "URL is OK")
} catch (e: Exception) {
Log.d(TAG, "Exception: $e")
return when (e) {
is MalformedURLException,
is JsonParseException,
is JsonMappingException,
is JsonProcessingException -> false

else -> throw IOException("Can not check server status")
}
}
return true
}

@Throws(IOException::class)
fun registerDeviceOnServer(
url: HttpUrl,
device: MollySocketDevice,
endpoint: String,
ping: Boolean = false,
): ConnectionResult? {
val requestData = ConnectionRequest(
uuid = SignalStore.account.requireAci().toString(),
deviceId = device.deviceId,
password = device.password,
endpoint = endpoint,
ping = ping,
)

val postBody = JsonUtils.toJson(requestData).toRequestBody(MEDIA_TYPE_JSON)
val request = Request.Builder().url(url).post(postBody).build()
val client = AppDependencies.okHttpClient.newBuilder().build()

client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
Log.d(TAG, "Unexpected code: $response")
return null
}
val body = response.body ?: run {
Log.d(TAG, "No response body")
return null
}

val resp = JsonUtils.fromJson(body.byteStream(), Response::class.java)

val status = resp.mollySocket.status
Log.d(TAG, "Status: $status")

return status
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration;
import org.thoughtcrime.securesms.keyvalue.SettingsValues.NotificationDeliveryMethod;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger;
Expand Down Expand Up @@ -127,6 +128,8 @@
import java.util.Map;
import java.util.concurrent.TimeUnit;

import im.molly.unifiedpush.UnifiedPushDistributor;
import im.molly.unifiedpush.jobs.UnifiedPushRefreshJob;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
import io.reactivex.rxjava3.exceptions.UndeliverableException;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
Expand Down Expand Up @@ -504,18 +507,35 @@ public void updatePushNotificationServices() {
return;
}

NotificationDeliveryMethod method = SignalStore.settings().getPreferredNotificationMethod();

boolean fcmEnabled = SignalStore.account().isFcmEnabled();
boolean forceWebSocket = SignalStore.internal().isWebsocketModeForced();
boolean forceFcm = !forceWebSocket && SignalStore.internal().isFcmModeForced();
boolean unifiedPushEnabled = SignalStore.unifiedpush().isEnabled();

if (forceWebSocket || !BuildConfig.USE_PLAY_SERVICES) {
if (method != NotificationDeliveryMethod.FCM || !BuildConfig.USE_PLAY_SERVICES) {
if (fcmEnabled) {
Log.i(TAG, "Play Services not allowed. Disabling FCM.");
updateFcmStatus(false);
} else {
Log.d(TAG, "FCM is disabled.");
Log.d(TAG, "FCM is already disabled.");
}
if (method == NotificationDeliveryMethod.UNIFIEDPUSH) {
if (SignalStore.account().isLinkedDevice()) {
Log.i(TAG, "UnifiedPush not supported in linked devices.");
updateUnifiedPushStatus(false);
} else if (!unifiedPushEnabled) {
Log.i(TAG, "Switching to UnifiedPush.");
updateUnifiedPushStatus(true);
} else {
AppDependencies.getJobManager().add(new UnifiedPushRefreshJob());
}
} else {
if (unifiedPushEnabled) {
Log.i(TAG, "Switching to WebSocket.");
updateUnifiedPushStatus(false);
}
}
} else if (forceFcm && !fcmEnabled && BuildConfig.USE_PLAY_SERVICES) {
} else if (!fcmEnabled) {
Log.i(TAG, "FCM preferred. Updating to use FCM.");
updateFcmStatus(true);
} else {
Expand Down Expand Up @@ -545,6 +565,16 @@ private void updateFcmStatus(boolean fcmEnabled) {
.enqueue();
}

private void updateUnifiedPushStatus(boolean enabled) {
SignalStore.unifiedpush().setEnabled(enabled);
if (enabled) {
UnifiedPushDistributor.registerApp();
} else {
UnifiedPushDistributor.unregisterApp();
}
AppDependencies.getJobManager().add(new UnifiedPushRefreshJob());
}

private void initializeExpiringMessageManager() {
AppDependencies.getExpiringMessageManager().checkSchedule();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.WindowUtil;

import im.molly.unifiedpush.UnifiedPushDistributor;

public class MainActivity extends PassphraseRequiredActivity implements VoiceNoteMediaControllerOwner {

private static final String KEY_STARTING_TAB = "STARTING_TAB";
Expand Down Expand Up @@ -131,6 +133,9 @@ private void presentVitalsState(VitalsViewModel.State state) {
switch (state) {
case NONE:
break;
case PROMPT_UNIFIEDPUSH_SELECT_DISTRIBUTOR_DIALOG:
UnifiedPushDistributor.showSelectDistributorDialog(this);
break;
case PROMPT_SPECIFIC_BATTERY_SAVER_DIALOG:
DeviceSpecificNotificationBottomSheet.show(getSupportFragmentManager());
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,22 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
}
)

clickPref(
title = DSLSettingsText.from("Delete UnifiedPush ping"),
summary = DSLSettingsText.from("Make as Molly never received the ping from MollySocket. Will cause UnifiedPush to stop and Websocket to restart."),
onClick = {
MaterialAlertDialogBuilder(requireContext())
.setTitle("Delete UnifiedPush ping?")
.setMessage("Are you sure?")
.setPositiveButton(android.R.string.ok) { _, _ ->
SignalStore.unifiedpush.lastReceivedTime = 0
Toast.makeText(requireContext(), "UnifiedPush ping deleted!", Toast.LENGTH_SHORT).show()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
)

dividerPref()

sectionHeaderPref(DSLSettingsText.from("Logging"))
Expand Down
Loading

0 comments on commit d197bc1

Please sign in to comment.