This commit is contained in:
andrew
2023-07-26 14:36:06 +09:30
parent 55216875ac
commit d3ea4e2e30
18 changed files with 99 additions and 85 deletions

View File

@@ -67,6 +67,7 @@ import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.notifications.PushManager
import org.thoughtcrime.securesms.onboarding.SeedActivity
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
import org.thoughtcrime.securesms.permissions.Permissions
@@ -106,6 +107,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
@Inject lateinit var groupDatabase: GroupDatabase
@Inject lateinit var textSecurePreferences: TextSecurePreferences
@Inject lateinit var configFactory: ConfigFactory
@Inject lateinit var pushManager: PushManager
private val globalSearchViewModel by viewModels<GlobalSearchViewModel>()
private val homeViewModel by viewModels<HomeViewModel>()
@@ -230,8 +232,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
(applicationContext as ApplicationContext).startPollingIfNeeded()
// update things based on TextSecurePrefs (profile info etc)
// Set up remaining components if needed
val application = ApplicationContext.getInstance(this@HomeActivity)
application.registerForPnIfNeeded(false)
pushManager.refresh(false)
if (textSecurePreferences.getLocalNumber() != null) {
OpenGroupManager.startPolling()
JobQueue.shared.resumePendingJobs()

View File

@@ -1,7 +1,10 @@
package org.thoughtcrime.securesms.notifications
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import org.session.libsession.utilities.TextSecurePreferences
import javax.inject.Inject
import javax.inject.Singleton
class ExpiryManager(
private val context: Context,

View File

@@ -1,12 +1,17 @@
package org.thoughtcrime.securesms.notifications
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import org.session.libsession.utilities.TextSecurePreferences
import javax.inject.Inject
import javax.inject.Singleton
class FcmTokenManager(
private val context: Context,
private val expiryManager: ExpiryManager
@Singleton
class FcmTokenManager @Inject constructor(
@ApplicationContext private val context: Context,
) {
private val expiryManager = ExpiryManager(context)
val isUsingFCM get() = TextSecurePreferences.isUsingFCM(context)
var fcmToken

View File

@@ -19,6 +19,7 @@ import org.session.libsession.utilities.ThemeUtil
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.home.HomeActivity
import org.thoughtcrime.securesms.notifications.PushManager
import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.PNModeView
@@ -27,8 +28,12 @@ import org.thoughtcrime.securesms.util.getAccentColor
import org.thoughtcrime.securesms.util.getColorWithID
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
import org.thoughtcrime.securesms.util.show
import javax.inject.Inject
class PNModeActivity : BaseActionBarActivity() {
@Inject lateinit var pushManager: PushManager
private lateinit var binding: ActivityPnModeBinding
private var selectedOptionView: PNModeView? = null
@@ -161,7 +166,7 @@ class PNModeActivity : BaseActionBarActivity() {
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView))
val application = ApplicationContext.getInstance(this)
application.startPollingIfNeeded()
application.registerForPnIfNeeded(true)
pushManager.refresh(true)
val intent = Intent(this, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
intent.putExtra(HomeActivity.FROM_ONBOARDING, true)

View File

@@ -1,10 +1,12 @@
package org.thoughtcrime.securesms.preferences
import android.os.Bundle
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment
@AndroidEntryPoint
class NotificationSettingsActivity : PassphraseRequiredActionBarActivity() {
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {

View File

@@ -23,14 +23,22 @@ import org.session.libsession.utilities.TextSecurePreferences;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.PushManager;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
import network.loki.messenger.R;
@AndroidEntryPoint
public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragment {
@SuppressWarnings("unused")
private static final String TAG = NotificationsPreferenceFragment.class.getSimpleName();
@Inject
PushManager pushManager;
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
@@ -41,7 +49,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
this.findPreference(fcmKey)
.setOnPreferenceChangeListener((preference, newValue) -> {
TextSecurePreferences.setIsUsingFCM(getContext(), (boolean) newValue);
ApplicationContext.getInstance(getContext()).registerForPnIfNeeded(true);
pushManager.refresh(true);
return true;
});

View File

@@ -0,0 +1,96 @@
package org.thoughtcrime.securesms.notifications
import android.content.Context
import com.goterl.lazysodium.utils.KeyPair
import dagger.hilt.android.qualifiers.ApplicationContext
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.combine.and
import org.session.libsession.messaging.sending_receiving.notifications.PushManagerV1
import org.session.libsession.utilities.Device
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.emptyPromise
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import javax.inject.Inject
import javax.inject.Singleton
private const val TAG = "GenericPushManager"
@Singleton
class GenericPushManager @Inject constructor(
@ApplicationContext private val context: Context,
private val device: Device,
private val tokenManager: FcmTokenManager,
private val pushManagerV2: PushManagerV2,
) {
fun refresh(token: String, force: Boolean): Promise<*, Exception> {
Log.d(TAG, "refresh() called")
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return emptyPromise()
val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise()
return when {
tokenManager.isUsingFCM -> register(force, token, userPublicKey, userEdKey)
tokenManager.requiresUnregister -> unregister(token, userPublicKey, userEdKey)
else -> emptyPromise()
}
}
/**
* Register for push notifications if:
* force is true
* there is no FCM Token
* FCM Token has expired
*/
private fun register(
force: Boolean,
token: String,
publicKey: String,
userEd25519Key: KeyPair,
namespaces: List<Int> = listOf(Namespace.DEFAULT)
): Promise<*, Exception> = if (force || tokenManager.isInvalid()) {
register(token, publicKey, userEd25519Key, namespaces)
} else emptyPromise()
/**
* Register for push notifications.
*/
private fun register(
token: String,
publicKey: String,
userEd25519Key: KeyPair,
namespaces: List<Int> = listOf(Namespace.DEFAULT)
): Promise<*, Exception> {
val v1 = PushManagerV1.register(
device = device,
token = token,
publicKey = publicKey
) fail {
Log.e(TAG, "register v1 failed", it)
}
val v2 = pushManagerV2.register(
Device.ANDROID, token, publicKey, userEd25519Key, namespaces
) fail {
Log.e(TAG, "register v2 failed", it)
}
return v1 and v2 success {
Log.d(TAG, "registerBoth success... saving token!!")
tokenManager.fcmToken = token
}
}
private fun unregister(
token: String,
userPublicKey: String,
userEdKey: KeyPair
): Promise<*, Exception> = PushManagerV1.unregister() and pushManagerV2.unregister(
Device.ANDROID, token, userPublicKey, userEdKey
) fail {
Log.e(TAG, "unregisterBoth failed", it)
} success {
tokenManager.fcmToken = null
}
}

View File

@@ -0,0 +1,111 @@
package org.thoughtcrime.securesms.notifications
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.interfaces.Sign
import com.goterl.lazysodium.utils.KeyPair
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map
import okhttp3.MediaType
import okhttp3.Request
import okhttp3.RequestBody
import org.session.libsession.messaging.sending_receiving.notifications.*
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.Version
import org.session.libsession.utilities.Device
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.retryIfNeeded
import javax.inject.Inject
import javax.inject.Singleton
private const val TAG = "PushManagerV2"
private const val maxRetryCount = 4
@Singleton
class PushManagerV2 @Inject constructor(private val pushHandler: PushHandler) {
private val sodium = LazySodiumAndroid(SodiumAndroid())
fun register(
device: Device,
token: String,
publicKey: String,
userEd25519Key: KeyPair,
namespaces: List<Int>
): Promise<SubscriptionResponse, Exception> {
val pnKey = pushHandler.getOrCreateNotificationKey()
val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s
// if we want to support passing namespace list, here is the place to do it
val sigData = "MONITOR${publicKey}${timestamp}1${namespaces.joinToString(separator = ",")}".encodeToByteArray()
val signature = ByteArray(Sign.BYTES)
sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEd25519Key.secretKey.asBytes)
val requestParameters = SubscriptionRequest(
pubkey = publicKey,
session_ed25519 = userEd25519Key.publicKey.asHexString,
namespaces = listOf(Namespace.DEFAULT),
data = true, // only permit data subscription for now (?)
service = device.service,
sig_ts = timestamp,
signature = Base64.encodeBytes(signature),
service_info = mapOf("token" to token),
enc_key = pnKey.asHexString,
).let(Json::encodeToString)
return retryResponseBody<SubscriptionResponse>("subscribe", requestParameters) success {
Log.d(TAG, "registerV2 success")
}
}
fun unregister(
device: Device,
token: String,
userPublicKey: String,
userEdKey: KeyPair
): Promise<UnsubscribeResponse, Exception> {
val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s
// if we want to support passing namespace list, here is the place to do it
val sigData = "UNSUBSCRIBE${userPublicKey}${timestamp}".encodeToByteArray()
val signature = ByteArray(Sign.BYTES)
sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEdKey.secretKey.asBytes)
val requestParameters = UnsubscriptionRequest(
pubkey = userPublicKey,
session_ed25519 = userEdKey.publicKey.asHexString,
service = device.service,
sig_ts = timestamp,
signature = Base64.encodeBytes(signature),
service_info = mapOf("token" to token),
).let(Json::encodeToString)
return retryResponseBody<UnsubscribeResponse>("unsubscribe", requestParameters) success {
Log.d(TAG, "unregisterV2 success")
}
}
private inline fun <reified T: Response> retryResponseBody(path: String, requestParameters: String): Promise<T, Exception> =
retryIfNeeded(maxRetryCount) { getResponseBody(path, requestParameters) }
private inline fun <reified T: Response> getResponseBody(path: String, requestParameters: String): Promise<T, Exception> {
val server = Server.LATEST
val url = "${server.url}/$path"
val body = RequestBody.create(MediaType.get("application/json"), requestParameters)
val request = Request.Builder().url(url).post(body).build()
return OnionRequestAPI.sendOnionRequest(
request,
server.url,
server.publicKey,
Version.V4
).map { response ->
response.body!!.inputStream()
.let { Json.decodeFromStream<T>(it) }
.also { if (it.isFailure()) throw Exception("error: ${it.message}.") }
}
}
}