-
-
Notifications
You must be signed in to change notification settings - Fork 96
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge UnifiedPush support into main app (#368)
- Loading branch information
Showing
45 changed files
with
1,996 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
168 changes: 168 additions & 0 deletions
168
app/src/main/java/im/molly/unifiedpush/MollySocketRepository.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
app/src/main/java/im/molly/unifiedpush/UnifiedPushDefaultDistributorLinkActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
48
app/src/main/java/im/molly/unifiedpush/UnifiedPushDistributor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.