feat: add support for firebase and split out google services as a dependency for only the play version of the app. Add support for requests in new pn server

This commit is contained in:
0x330a
2023-04-20 17:12:38 +10:00
parent 2246a5d9ce
commit 8d4f2445f2
21 changed files with 381 additions and 510 deletions

View File

@@ -306,14 +306,6 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
</activity>
<service
android:name="org.thoughtcrime.securesms.notifications.PushNotificationService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
android:exported="false" />
<service

View File

@@ -75,14 +75,13 @@ import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
import org.thoughtcrime.securesms.notifications.FcmUtils;
import org.thoughtcrime.securesms.notifications.PushNotificationManager;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
import org.thoughtcrime.securesms.notifications.PushManager;
import org.thoughtcrime.securesms.notifications.PushNotificationManager;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
@@ -149,6 +148,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
@Inject MessageDataProvider messageDataProvider;
@Inject JobDatabase jobDatabase;
@Inject TextSecurePreferences textSecurePreferences;
@Inject PushManager pushManager;
CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration;
@@ -220,7 +220,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
SnodeModule.Companion.configure(apiDB, broadcaster);
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey != null) {
registerForFCMIfNeeded(false);
registerForPnIfNeeded(false);
}
initializeExpiringMessageManager();
initializeTypingStatusRepository();
@@ -386,7 +386,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
BackgroundPollWorker.schedulePeriodic(this);
if (BuildConfig.PLAY_STORE_DISABLED) {
UpdateApkRefreshListener.schedule(this);
// possibly add update apk job
}
}
@@ -439,30 +439,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private static class ProviderInitializationException extends RuntimeException { }
public void registerForFCMIfNeeded(final Boolean force) {
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive() && !force) return;
if (force && firebaseInstanceIdJob != null) {
firebaseInstanceIdJob.cancel(null);
}
firebaseInstanceIdJob = FcmUtils.getFcmInstanceId(task->{
if (!task.isSuccessful()) {
Log.w("Loki", "FirebaseInstanceId.getInstance().getInstanceId() failed." + task.getException());
return Unit.INSTANCE;
}
String token = task.getResult().getToken();
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey == null) return Unit.INSTANCE;
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
if (TextSecurePreferences.isUsingFCM(this)) {
PushNotificationManager.register(token, userPublicKey, this, force);
} else {
PushNotificationManager.unregister(token, this);
}
});
return Unit.INSTANCE;
});
public void registerForPnIfNeeded(final Boolean force) {
pushManager.register(force);
}
private void setUpPollingIfNeeded() {

View File

@@ -52,6 +52,7 @@ public class IdentityKeyUtil {
public static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key";
public static final String ED25519_SECRET_KEY = "pref_ed25519_secret_key";
public static final String NOTIFICATION_KEY = "pref_notification_key";
public static final String LOKI_SEED = "loki_seed";
public static final String HAS_MIGRATED_KEY = "has_migrated_keys";

View File

@@ -1,4 +0,0 @@
package org.thoughtcrime.securesms.dependencies;
public interface InjectableType {
}

View File

@@ -0,0 +1,12 @@
package org.thoughtcrime.securesms.dependencies
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.thoughtcrime.securesms.notifications.PushManager
@EntryPoint
@InstallIn(SingletonComponent::class)
interface PushComponent {
fun providePushManager(): PushManager
}

View File

@@ -1,11 +1,11 @@
package org.thoughtcrime.securesms.home
import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.ClipData
import android.content.ClipboardManager
import android.os.Bundle
import android.text.SpannableString
import android.widget.Toast
@@ -199,7 +199,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// update things based on TextSecurePrefs (profile info etc)
// Set up remaining components if needed
val application = ApplicationContext.getInstance(this@HomeActivity)
application.registerForFCMIfNeeded(false)
application.registerForPnIfNeeded(false)
if (textSecurePreferences.getLocalNumber() != null) {
OpenGroupManager.startPolling()
JobQueue.shared.resumePendingJobs()

View File

@@ -31,7 +31,6 @@ public final class JobManagerFactories {
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application));
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
put(PrepareAttachmentAudioExtrasJob.KEY, new PrepareAttachmentAudioExtrasJob.Factory());
}};
factoryKeys.addAll(factoryHashMap.keySet());

View File

@@ -1,271 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.session.libsession.messaging.utilities.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.service.UpdateApkReadyListener;
import org.session.libsession.utilities.FileUtils;
import org.session.libsignal.utilities.Hex;
import org.session.libsignal.utilities.JsonUtil;
import org.session.libsession.utilities.TextSecurePreferences;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import network.loki.messenger.BuildConfig;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class UpdateApkJob extends BaseJob {
public static final String KEY = "UpdateApkJob";
private static final String TAG = UpdateApkJob.class.getSimpleName();
public UpdateApkJob() {
this(new Job.Parameters.Builder()
.setQueue("UpdateApkJob")
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(3)
.build());
}
private UpdateApkJob(@NonNull Job.Parameters parameters) {
super(parameters);
}
@Override
public @NonNull
Data serialize() {
return Data.EMPTY;
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws IOException, PackageManager.NameNotFoundException {
if (!BuildConfig.PLAY_STORE_DISABLED) return;
Log.i(TAG, "Checking for APK update...");
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(String.format("%s/latest.json", BuildConfig.NOPLAY_UPDATE_URL)).build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
throw new IOException("Bad response: " + response.message());
}
UpdateDescriptor updateDescriptor = JsonUtil.fromJson(response.body().string(), UpdateDescriptor.class);
byte[] digest = Hex.fromStringCondensed(updateDescriptor.getDigest());
Log.i(TAG, "Got descriptor: " + updateDescriptor);
if (updateDescriptor.getVersionCode() > getVersionCode()) {
DownloadStatus downloadStatus = getDownloadStatus(updateDescriptor.getUrl(), digest);
Log.i(TAG, "Download status: " + downloadStatus.getStatus());
if (downloadStatus.getStatus() == DownloadStatus.Status.COMPLETE) {
Log.i(TAG, "Download status complete, notifying...");
handleDownloadNotify(downloadStatus.getDownloadId());
} else if (downloadStatus.getStatus() == DownloadStatus.Status.MISSING) {
Log.i(TAG, "Download status missing, starting download...");
handleDownloadStart(updateDescriptor.getUrl(), updateDescriptor.getVersionName(), digest);
}
}
}
@Override
public boolean onShouldRetry(@NonNull Exception e) {
return e instanceof IOException;
}
@Override
public void onCanceled() {
Log.w(TAG, "Update check failed");
}
private int getVersionCode() throws PackageManager.NameNotFoundException {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionCode;
}
private DownloadStatus getDownloadStatus(String uri, byte[] theirDigest) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterByStatus(DownloadManager.STATUS_PAUSED | DownloadManager.STATUS_PENDING | DownloadManager.STATUS_RUNNING | DownloadManager.STATUS_SUCCESSFUL);
long pendingDownloadId = TextSecurePreferences.getUpdateApkDownloadId(context);
byte[] pendingDigest = getPendingDigest(context);
Cursor cursor = downloadManager.query(query);
try {
DownloadStatus status = new DownloadStatus(DownloadStatus.Status.MISSING, -1);
while (cursor != null && cursor.moveToNext()) {
int jobStatus = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
String jobRemoteUri = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_URI));
long downloadId = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
byte[] digest = getDigestForDownloadId(downloadId);
if (jobRemoteUri != null && jobRemoteUri.equals(uri) && downloadId == pendingDownloadId) {
if (jobStatus == DownloadManager.STATUS_SUCCESSFUL &&
digest != null && pendingDigest != null &&
MessageDigest.isEqual(pendingDigest, theirDigest) &&
MessageDigest.isEqual(digest, theirDigest))
{
return new DownloadStatus(DownloadStatus.Status.COMPLETE, downloadId);
} else if (jobStatus != DownloadManager.STATUS_SUCCESSFUL) {
status = new DownloadStatus(DownloadStatus.Status.PENDING, downloadId);
}
}
}
return status;
} finally {
if (cursor != null) cursor.close();
}
}
private void handleDownloadStart(String uri, String versionName, byte[] digest) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(uri));
downloadRequest.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
downloadRequest.setTitle("Downloading Signal update");
downloadRequest.setDescription("Downloading Signal " + versionName);
downloadRequest.setVisibleInDownloadsUi(false);
downloadRequest.setDestinationInExternalFilesDir(context, null, "signal-update.apk");
downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
long downloadId = downloadManager.enqueue(downloadRequest);
TextSecurePreferences.setUpdateApkDownloadId(context, downloadId);
TextSecurePreferences.setUpdateApkDigest(context, Hex.toStringCondensed(digest));
}
private void handleDownloadNotify(long downloadId) {
Intent intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
new UpdateApkReadyListener().onReceive(context, intent);
}
private @Nullable byte[] getDigestForDownloadId(long downloadId) {
try {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
FileInputStream fin = new FileInputStream(downloadManager.openDownloadedFile(downloadId).getFileDescriptor());
byte[] digest = FileUtils.getFileDigest(fin);
fin.close();
return digest;
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}
private @Nullable byte[] getPendingDigest(Context context) {
try {
String encodedDigest = TextSecurePreferences.getUpdateApkDigest(context);
if (encodedDigest == null) return null;
return Hex.fromStringCondensed(encodedDigest);
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}
private static class UpdateDescriptor {
@JsonProperty
private int versionCode;
@JsonProperty
private String versionName;
@JsonProperty
private String url;
@JsonProperty
private String sha256sum;
public int getVersionCode() {
return versionCode;
}
public String getVersionName() {
return versionName;
}
public String getUrl() {
return url;
}
public @NonNull String toString() {
return "[" + versionCode + ", " + versionName + ", " + url + "]";
}
public String getDigest() {
return sha256sum;
}
}
private static class DownloadStatus {
enum Status {
PENDING,
COMPLETE,
MISSING
}
private final Status status;
private final long downloadId;
DownloadStatus(Status status, long downloadId) {
this.status = status;
this.downloadId = downloadId;
}
public Status getStatus() {
return status;
}
public long getDownloadId() {
return downloadId;
}
}
public static final class Factory implements Job.Factory<UpdateApkJob> {
@Override
public @NonNull UpdateApkJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new UpdateApkJob(parameters);
}
}
}

View File

@@ -1,19 +0,0 @@
@file:JvmName("FcmUtils")
package org.thoughtcrime.securesms.notifications
import com.google.android.gms.tasks.Task
import com.google.firebase.iid.FirebaseInstanceId
import com.google.firebase.iid.InstanceIdResult
import kotlinx.coroutines.*
fun getFcmInstanceId(body: (Task<InstanceIdResult>)->Unit): Job = MainScope().launch(Dispatchers.IO) {
val task = FirebaseInstanceId.getInstance().instanceId
while (!task.isComplete && isActive) {
// wait for task to complete while we are active
}
if (!isActive) return@launch // don't 'complete' task if we were canceled
withContext(Dispatchers.Main) {
body(task)
}
}

View File

@@ -0,0 +1,6 @@
package org.thoughtcrime.securesms.notifications
interface PushManager {
fun register(force: Boolean)
fun unregister(token: String)
}

View File

@@ -4,6 +4,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import dagger.hilt.android.AndroidEntryPoint
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveParameters
@@ -11,14 +12,18 @@ import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Log
import javax.inject.Inject
@AndroidEntryPoint
class PushNotificationService : FirebaseMessagingService() {
@Inject lateinit var pushManager: PushManager
override fun onNewToken(token: String) {
super.onNewToken(token)
Log.d("Loki", "New FCM token: $token.")
val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return
PushNotificationManager.register(token, userPublicKey, this, false)
TextSecurePreferences.getLocalNumber(this) ?: return
pushManager.register(true)
}
override fun onMessageReceived(message: RemoteMessage) {

View File

@@ -160,7 +160,7 @@ class PNModeActivity : BaseActionBarActivity() {
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView))
val application = ApplicationContext.getInstance(this)
application.startPollingIfNeeded()
application.registerForFCMIfNeeded(true)
application.registerForPnIfNeeded(true)
val intent = Intent(this, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
show(intent)

View File

@@ -39,7 +39,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
this.findPreference(fcmKey)
.setOnPreferenceChangeListener((preference, newValue) -> {
TextSecurePreferences.setIsUsingFCM(getContext(), (boolean) newValue);
ApplicationContext.getInstance(getContext()).registerForFCMIfNeeded(true);
ApplicationContext.getInstance(getContext()).registerForPnIfNeeded(true);
return true;
});

View File

@@ -1,47 +0,0 @@
package org.thoughtcrime.securesms.service;
import android.content.Context;
import android.content.Intent;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import network.loki.messenger.BuildConfig;
import org.thoughtcrime.securesms.jobs.UpdateApkJob;
import org.session.libsession.utilities.TextSecurePreferences;
import java.util.concurrent.TimeUnit;
public class UpdateApkRefreshListener extends PersistentAlarmManagerListener {
private static final String TAG = UpdateApkRefreshListener.class.getSimpleName();
private static final long INTERVAL = TimeUnit.HOURS.toMillis(6);
@Override
protected long getNextScheduledExecutionTime(Context context) {
return TextSecurePreferences.getUpdateApkRefreshTime(context);
}
@Override
protected long onAlarm(Context context, long scheduledTime) {
Log.i(TAG, "onAlarm...");
if (scheduledTime != 0 && BuildConfig.PLAY_STORE_DISABLED) {
Log.i(TAG, "Queueing APK update job...");
ApplicationContext.getInstance(context)
.getJobManager()
.add(new UpdateApkJob());
}
long newTime = System.currentTimeMillis() + INTERVAL;
TextSecurePreferences.setUpdateApkRefreshTime(context, newTime);
return newTime;
}
public static void schedule(Context context) {
new UpdateApkRefreshListener().onReceive(context, new Intent());
}
}