Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge UnifiedPush support into main app #368

Merged
merged 13 commits into from
Nov 21, 2024
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