Skip to content

Commit

Permalink
Merge UnifiedPush support into main app (#368)
Browse files Browse the repository at this point in the history
  • Loading branch information
valldrac authored Nov 21, 2024
2 parents f3b75a1 + d5c05fa commit ab43019
Show file tree
Hide file tree
Showing 45 changed files with 1,996 additions and 36 deletions.
7 changes: 7 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,13 @@ 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")
exclude(group = "com.google.protobuf", module = "protobuf-java")
}
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
23 changes: 23 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@

<!-- MOLLY: GMS and Google Maps stuff moved to gms/AndroidManifest.xml -->

<activity
android:name="im.molly.unifiedpush.components.settings.app.notifications.MollySocketQrScannerActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:exported="false" />

<activity
android:name="im.molly.unifiedpush.UnifiedPushDefaultDistributorLinkActivity"
android:theme="@style/TextSecure.DialogActivity"
android:exported="false" />

<meta-data android:name="android.supports_size_changes"
android:value="true" />

Expand Down Expand Up @@ -1302,6 +1313,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
@@ -0,0 +1,55 @@
package im.molly.unifiedpush

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContract
import androidx.appcompat.app.AppCompatActivity
import org.signal.core.util.logging.Log
import org.unifiedpush.android.connector.LinkActivityHelper

class UnifiedPushDefaultDistributorLinkActivity : AppCompatActivity() {
private val helper = LinkActivityHelper(this)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

if (!helper.startLinkActivityForResult()) {
Log.d(TAG, "No distributor with link activity found.")
setResult(RESULT_OK)
finish()
}
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (helper.onLinkActivityResult(requestCode, resultCode, data)) {
// The distributor is saved, you can request registrations with UnifiedPush.registerApp now
Log.d(TAG, "Found a distributor with link activity found.")
val intent = Intent().putExtra(KEY_FOUND, true)
setResult(RESULT_OK, intent)
} else {
// An error occurred, consider no distributor found for the moment
Log.d(TAG, "Found a distributor with link activity found but an error occurred.")
setResult(RESULT_OK)
}
finish()
}

class Contract : ActivityResultContract<Unit, Boolean?>() {
override fun createIntent(context: Context, input: Unit): Intent {
return Intent(context, UnifiedPushDefaultDistributorLinkActivity::class.java)
}

override fun parseResult(resultCode: Int, intent: Intent?): Boolean? {
return intent?.let {
intent.getBooleanExtra(KEY_FOUND, false)
}
}
}

companion object {
private const val KEY_FOUND = "found"
private const val TAG = "UnifiedPushDefaultDistributorLinkActivity"
}
}
48 changes: 48 additions & 0 deletions app/src/main/java/im/molly/unifiedpush/UnifiedPushDistributor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package im.molly.unifiedpush

import android.content.Context
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.unifiedpush.android.connector.UnifiedPush
import org.unifiedpush.android.connector.ui.SelectDistributorDialogsBuilder
import org.unifiedpush.android.connector.ui.UnifiedPushFunctions

object UnifiedPushDistributor {

@JvmStatic
fun registerApp(vapid: String?) {
UnifiedPush.registerApp(AppDependencies.application, vapid = vapid)
}

@JvmStatic
fun unregisterApp() {
UnifiedPush.unregisterApp(AppDependencies.application)
}

fun selectFirstDistributor() {
val context = AppDependencies.application
UnifiedPush.getDistributors(context).firstOrNull()?.also {
UnifiedPush.saveDistributor(context, it)
}
}

@JvmStatic
fun showSelectDistributorDialog(context: Context) {
SelectDistributorDialogsBuilder(
context,
object : UnifiedPushFunctions {
override fun getAckDistributor(): String? = UnifiedPush.getAckDistributor(context)
override fun getDistributors(): List<String> = UnifiedPush.getDistributors(context)
override fun registerApp(instance: String) = UnifiedPush.registerApp(context, instance)
override fun saveDistributor(distributor: String) = UnifiedPush.saveDistributor(context, distributor)
override fun tryUseDefaultDistributor(callback: (Boolean) -> Unit) = UnifiedPush.tryUseDefaultDistributor(context, callback)
}
).apply {
mayUseCurrent = false
mayUseDefault = false
}.run()
}

fun checkIfActive(): Boolean {
return UnifiedPush.getAckDistributor(AppDependencies.application) != null
}
}
Loading

0 comments on commit ab43019

Please # to comment.