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
No known key found for this signature in database
GPG Key ID: 267811D6E6A2698C
21 changed files with 381 additions and 510 deletions

2
.gitignore vendored
View File

@ -15,4 +15,4 @@ signing.properties
ffpr ffpr
*.sh *.sh
pkcs11.password pkcs11.password
play app/play

View File

@ -3,7 +3,6 @@ apply plugin: 'kotlin-android'
apply plugin: 'witness' apply plugin: 'witness'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-parcelize'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'dagger.hilt.android.plugin'
@ -11,6 +10,140 @@ configurations.all {
exclude module: "commons-logging" exclude module: "commons-logging"
} }
def canonicalVersionCode = 335
def canonicalVersionName = "1.16.7"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,
'arm64-v8a' : 2,
'x86' : 3,
'x86_64' : 4,
'universal' : 5]
android {
compileSdkVersion androidCompileSdkVersion
namespace 'network.loki.messenger'
useLibrary 'org.apache.http.legacy'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
packagingOptions {
exclude 'LICENSE.txt'
exclude 'LICENSE'
exclude 'NOTICE'
exclude 'asm-license.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude 'META-INF/proguard/androidx-annotations.pro'
}
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
universalApk true
}
}
defaultConfig {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
minSdkVersion androidMinimumSdkVersion
targetSdkVersion androidTargetSdkVersion
multiDexEnabled = true
vectorDrawables.useSupportLibrary = true
project.ext.set("archivesBaseName", "session")
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "USER_AGENT", "\"OWA\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
resConfigs autoResConfig()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// The following argument makes the Android Test Orchestrator run its
// "pm clear" command after each test invocation. This command ensures
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
}
sourceSets {
String sharedTestDir = 'src/sharedTest/java'
test.java.srcDirs += sharedTestDir
androidTest.java.srcDirs += sharedTestDir
}
buildTypes {
release {
minifyEnabled false
}
debug {
minifyEnabled false
}
}
flavorDimensions "distribution"
productFlavors {
play {
dimension "distribution"
apply plugin: 'com.google.gms.google-services'
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
}
website {
dimension "distribution"
ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
}
}
applicationVariants.all { variant ->
variant.outputs.each { output ->
def abiName = output.getFilter("ABI") ?: 'universal'
def postFix = abiPostFix.get(abiName, 0)
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk"
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
}
}
lintOptions {
abortOnError true
baseline file("lint-baseline.xml")
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
buildFeatures {
dataBinding true
viewBinding true
}
}
dependencies { dependencies {
implementation "androidx.appcompat:appcompat:$appcompatVersion" implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.2.1'
@ -34,7 +167,7 @@ dependencies {
implementation 'androidx.fragment:fragment-ktx:1.5.3' implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation "androidx.core:core-ktx:$coreVersion" implementation "androidx.core:core-ktx:$coreVersion"
implementation "androidx.work:work-runtime-ktx:2.7.1" implementation "androidx.work:work-runtime-ktx:2.7.1"
implementation ("com.google.firebase:firebase-messaging:18.0.0") { playImplementation ("com.google.firebase:firebase-messaging:18.0.0") {
exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
@ -145,137 +278,6 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.4' testImplementation 'org.robolectric:shadows-multidex:4.4'
} }
def canonicalVersionCode = 335
def canonicalVersionName = "1.16.7"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,
'arm64-v8a' : 2,
'x86' : 3,
'x86_64' : 4,
'universal' : 5]
android {
compileSdkVersion androidCompileSdkVersion
namespace 'network.loki.messenger'
useLibrary 'org.apache.http.legacy'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
packagingOptions {
exclude 'LICENSE.txt'
exclude 'LICENSE'
exclude 'NOTICE'
exclude 'asm-license.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude 'META-INF/proguard/androidx-annotations.pro'
}
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
universalApk true
}
}
defaultConfig {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
minSdkVersion androidMinimumSdkVersion
targetSdkVersion androidTargetSdkVersion
multiDexEnabled = true
vectorDrawables.useSupportLibrary = true
project.ext.set("archivesBaseName", "session")
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "USER_AGENT", "\"OWA\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
resConfigs autoResConfig()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// The following argument makes the Android Test Orchestrator run its
// "pm clear" command after each test invocation. This command ensures
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
}
sourceSets {
String sharedTestDir = 'src/sharedTest/java'
test.java.srcDirs += sharedTestDir
androidTest.java.srcDirs += sharedTestDir
}
buildTypes {
release {
minifyEnabled false
}
debug {
minifyEnabled false
}
}
flavorDimensions "distribution"
productFlavors {
play {
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
}
website {
ext.websiteUpdateUrl = "https://github.com/oxen-io/session-android/releases"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
}
}
applicationVariants.all { variant ->
variant.outputs.each { output ->
def abiName = output.getFilter("ABI") ?: 'universal'
def postFix = abiPostFix.get(abiName, 0)
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk"
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
}
}
lintOptions {
abortOnError true
baseline file("lint-baseline.xml")
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
buildFeatures {
dataBinding true
viewBinding true
}
}
static def getLastCommitTimestamp() { static def getLastCommitTimestamp() {
new ByteArrayOutputStream().withStream { os -> new ByteArrayOutputStream().withStream { os ->
return os.toString() + "000" return os.toString() + "000"

View File

@ -306,14 +306,6 @@
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.home.HomeActivity" /> android:value="org.thoughtcrime.securesms.home.HomeActivity" />
</activity> </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" <service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
android:exported="false" /> android:exported="false" />
<service <service

View File

@ -75,14 +75,13 @@ import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger; import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker; import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; 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.NotificationChannels;
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier; 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.providers.BlobProvider;
import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.sskenvironment.ProfileManager; import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager; import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository; import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
@ -149,6 +148,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
@Inject MessageDataProvider messageDataProvider; @Inject MessageDataProvider messageDataProvider;
@Inject JobDatabase jobDatabase; @Inject JobDatabase jobDatabase;
@Inject TextSecurePreferences textSecurePreferences; @Inject TextSecurePreferences textSecurePreferences;
@Inject PushManager pushManager;
CallMessageProcessor callMessageProcessor; CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration; MessagingModuleConfiguration messagingModuleConfiguration;
@ -220,7 +220,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
SnodeModule.Companion.configure(apiDB, broadcaster); SnodeModule.Companion.configure(apiDB, broadcaster);
String userPublicKey = TextSecurePreferences.getLocalNumber(this); String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey != null) { if (userPublicKey != null) {
registerForFCMIfNeeded(false); registerForPnIfNeeded(false);
} }
initializeExpiringMessageManager(); initializeExpiringMessageManager();
initializeTypingStatusRepository(); initializeTypingStatusRepository();
@ -386,7 +386,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
BackgroundPollWorker.schedulePeriodic(this); BackgroundPollWorker.schedulePeriodic(this);
if (BuildConfig.PLAY_STORE_DISABLED) { 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 { } private static class ProviderInitializationException extends RuntimeException { }
public void registerForFCMIfNeeded(final Boolean force) { public void registerForPnIfNeeded(final Boolean force) {
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive() && !force) return; pushManager.register(force);
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;
});
} }
private void setUpPollingIfNeeded() { 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 IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key"; 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 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 LOKI_SEED = "loki_seed";
public static final String HAS_MIGRATED_KEY = "has_migrated_keys"; 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 package org.thoughtcrime.securesms.home
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.ClipData
import android.content.ClipboardManager
import android.os.Bundle import android.os.Bundle
import android.text.SpannableString import android.text.SpannableString
import android.widget.Toast import android.widget.Toast
@ -199,7 +199,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// update things based on TextSecurePrefs (profile info etc) // update things based on TextSecurePrefs (profile info etc)
// Set up remaining components if needed // Set up remaining components if needed
val application = ApplicationContext.getInstance(this@HomeActivity) val application = ApplicationContext.getInstance(this@HomeActivity)
application.registerForFCMIfNeeded(false) application.registerForPnIfNeeded(false)
if (textSecurePreferences.getLocalNumber() != null) { if (textSecurePreferences.getLocalNumber() != null) {
OpenGroupManager.startPolling() OpenGroupManager.startPolling()
JobQueue.shared.resumePendingJobs() JobQueue.shared.resumePendingJobs()

View File

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

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

View File

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

View File

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

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application tools:node="merge">
<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>
</application>
</manifest>

View File

@ -13,7 +13,5 @@ fun getFcmInstanceId(body: (Task<InstanceIdResult>)->Unit): Job = MainScope().la
// wait for task to complete while we are active // wait for task to complete while we are active
} }
if (!isActive) return@launch // don't 'complete' task if we were canceled if (!isActive) return@launch // don't 'complete' task if we were canceled
withContext(Dispatchers.Main) { body(task)
body(task)
}
} }

View File

@ -0,0 +1,142 @@
package org.thoughtcrime.securesms.notifications
import android.content.Context
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.interfaces.AEAD
import com.goterl.lazysodium.interfaces.Sign
import com.goterl.lazysodium.utils.Key
import com.goterl.lazysodium.utils.KeyPair
import kotlinx.coroutines.Job
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.PushNotificationAPI
import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest
import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.Version
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
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 org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
class FirebasePushManager(private val context: Context, private val prefs: TextSecurePreferences): PushManager {
companion object {
private const val maxRetryCount = 4
private const val tokenExpirationInterval = 12 * 60 * 60 * 1000
}
private var firebaseInstanceIdJob: Job? = null
private val sodium = LazySodiumAndroid(SodiumAndroid())
private fun getOrCreateNotificationKey(): Key {
if (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY) == null) {
// generate the key and store it
val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF)
IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString)
}
return Key.fromHexString(
IdentityKeyUtil.retrieve(
context,
IdentityKeyUtil.NOTIFICATION_KEY
)
)
}
override fun register(force: Boolean) {
val currentInstanceIdJob = firebaseInstanceIdJob
if (currentInstanceIdJob != null && currentInstanceIdJob.isActive && !force) return
if (force && currentInstanceIdJob != null) {
currentInstanceIdJob.cancel(null)
}
firebaseInstanceIdJob = getFcmInstanceId { task ->
// context in here is Dispatchers.IO
if (!task.isSuccessful) {
Log.w(
"Loki",
"FirebaseInstanceId.getInstance().getInstanceId() failed." + task.exception
)
return@getFcmInstanceId
}
val token: String = task.result?.token ?: return@getFcmInstanceId
val userPublicKey = getLocalNumber(context) ?: return@getFcmInstanceId
val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return@getFcmInstanceId
if (prefs.isUsingFCM()) {
register(token, userPublicKey, userEdKey, force)
} else {
unregister(token)
}
}
}
override fun unregister(token: String) {
TODO("Not yet implemented")
}
fun register(token: String, publicKey: String, userEd25519Key: KeyPair, force: Boolean, namespaces: List<Int> = listOf(Namespace.DEFAULT)) {
val oldToken = TextSecurePreferences.getFCMToken(context)
val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context)
if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return }
val pnKey = 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 = "firebase",
sig_ts = timestamp,
signature = Base64.encodeBytes(signature),
service_info = mapOf("token" to token),
enc_key = pnKey.asHexString,
)
val url = "${PushNotificationAPI.server}/subscribe"
val body = RequestBody.create(MediaType.get("application/json"), Json.encodeToString(requestParameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
getResponseBody(request.build()).map { response ->
if (response.isSuccess()) {
TextSecurePreferences.setIsUsingFCM(context, true)
TextSecurePreferences.setFCMToken(context, token)
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
} else {
val (_, message) = response.errorInfo()
Log.d("Loki", "Couldn't register for FCM due to error: $message.")
}
}.fail { exception ->
Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.")
}
}
}
private fun getResponseBody(request: Request): Promise<SubscriptionResponse, Exception> {
return OnionRequestAPI.sendOnionRequest(request,
PushNotificationAPI.server,
PushNotificationAPI.serverPublicKey, Version.V4).map { response ->
Json.decodeFromStream(response.body!!.inputStream())
}
}
}

View File

@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.notifications
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.session.libsession.utilities.TextSecurePreferences
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object FirebasePushModule {
@Provides
@Singleton
fun provideFirebasePushManager(
@ApplicationContext context: Context,
prefs: TextSecurePreferences,
): PushManager = FirebasePushManager(context, prefs)
}

View File

@ -1,7 +1,15 @@
package org.session.libsession.messaging.sending_receiving.notifications package org.session.libsession.messaging.sending_receiving.notifications
import com.goterl.lazysodium.utils.Key
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/**
* N.B. all of these variable names will be named the same as the actual JSON utf-8 request/responses expected from the server.
* Changing the variable names will break how data is serialized/deserialized.
* If it's less than ideally named we can use [SerialName]
*/
@Serializable @Serializable
data class SubscriptionRequest( data class SubscriptionRequest(
/** the 33-byte account being subscribed to; typically a session ID */ /** the 33-byte account being subscribed to; typically a session ID */
@ -9,7 +17,7 @@ data class SubscriptionRequest(
/** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */ /** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */
val session_ed25519: String?, val session_ed25519: String?,
/** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */ /** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */
val subkey_tag: String?, val subkey_tag: String? = null,
/** array of integer namespaces to subscribe to, **must be sorted in ascending order** */ /** array of integer namespaces to subscribe to, **must be sorted in ascending order** */
val namespaces: List<Int>, val namespaces: List<Int>,
/** if provided and true then notifications will include the body of the message (as long as it isn't too large) */ /** if provided and true then notifications will include the body of the message (as long as it isn't too large) */
@ -46,7 +54,18 @@ data class SubscriptionResponse(
const val GENERIC_ERROR = 4 const val GENERIC_ERROR = 4
} }
fun isSuccess() = success == true && error == null fun isSuccess() = success == true && error == null
fun errorInfo() = if (success == false && error != null) { fun errorInfo() = if (success != true && error != null) {
true to message error to message
} else false to null } else null to null
}
@Serializable
data class PushNotificationServerObject(
val enc_payload: String,
val spns: Int,
) {
fun decryptPayload(key: Key): Any {
TODO()
}
} }

View File

@ -9,15 +9,17 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.Version import org.session.libsession.snode.Version
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.retryIfNeeded
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.retryIfNeeded
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
object PushNotificationAPI { object PushNotificationAPI {
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val server = "https://push.getsession.org" val server = "https://push.getsession.org"
val serverPublicKey: String = TODO("get the new server pubkey here") val serverPublicKey: String = TODO("get the new server pubkey here")
private val legacyServer = "https://live.apns.getsession.org"
private val legacyServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
private val maxRetryCount = 4 private val maxRetryCount = 4
private val tokenExpirationInterval = 12 * 60 * 60 * 1000 private val tokenExpirationInterval = 12 * 60 * 60 * 1000
@ -94,7 +96,7 @@ object PushNotificationAPI {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body) val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) { retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response -> OnionRequestAPI.sendOnionRequest(request.build(), legacyServer, legacyServerPublicKey, Version.V2).map { response ->
val code = response.info["code"] as? Int val code = response.info["code"] as? Int
if (code == null || code == 0) { if (code == null || code == 0) {
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${response.info["message"] as? String ?: "null"}.") Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${response.info["message"] as? String ?: "null"}.")