mirror of
https://codeberg.org/tom79/Fedilab.git
synced 2025-02-22 17:09:44 +02:00
Push notifications
This commit is contained in:
parent
318a566e76
commit
38a630a4e5
17 changed files with 272 additions and 688 deletions
|
@ -52,7 +52,6 @@ android {
|
|||
}
|
||||
sourceSets {
|
||||
playstore {
|
||||
manifest.srcFile "src/playstore/AndroidManifest.xml"
|
||||
java.srcDirs = ['src/main/java', 'src/playstore/java']
|
||||
res.srcDirs = ['src/main/res', 'src/playstore/res']
|
||||
}
|
||||
|
@ -157,9 +156,9 @@ dependencies {
|
|||
implementation "ch.acra:acra-limiter:5.11.3"
|
||||
implementation "ch.acra:acra-dialog:5.11.3"
|
||||
implementation "com.madgag.spongycastle:bctls-jdk15on:1.58.0.0"
|
||||
implementation 'com.github.UnifiedPush:android-connector:2.2.0'
|
||||
// implementation 'com.github.UnifiedPush:android-foss_embedded_fcm_distributor:1.0.0-beta1'
|
||||
playstoreImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:2.2.0') {
|
||||
implementation 'org.unifiedpush.android:connector:3.0.4'
|
||||
|
||||
playstoreImplementation('org.unifiedpush.android:embedded-fcm-distributor:3.0.0') {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
|
|
|
@ -467,18 +467,12 @@
|
|||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".mastodon.services.CustomReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<service android:name=".mastodon.services.PushServiceImpl"
|
||||
android:exported="false">
|
||||
<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" />
|
||||
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED" />
|
||||
<action android:name="org.unifiedpush.android.connector.PUSH_EVENT"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</service>
|
||||
|
||||
|
||||
<activity
|
||||
|
|
|
@ -21,6 +21,7 @@ import app.fedilab.android.mastodon.client.entities.api.Account;
|
|||
import app.fedilab.android.mastodon.client.entities.api.Activity;
|
||||
import app.fedilab.android.mastodon.client.entities.api.Emoji;
|
||||
import app.fedilab.android.mastodon.client.entities.api.Instance;
|
||||
import app.fedilab.android.mastodon.client.entities.api.InstanceV2;
|
||||
import app.fedilab.android.mastodon.client.entities.api.Tag;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.GET;
|
||||
|
@ -32,6 +33,9 @@ public interface MastodonInstanceService {
|
|||
@GET("instance")
|
||||
Call<Instance> instance();
|
||||
|
||||
@GET("instance")
|
||||
Call<InstanceV2> instanceV2();
|
||||
|
||||
@GET("instance/peers")
|
||||
Call<List<String>> connectedInstance();
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@ public interface MastodonNotificationsService {
|
|||
@Field("subscription[endpoint]") String endpoint,
|
||||
@Field("subscription[keys][p256dh]") String keys_p256dh,
|
||||
@Field("subscription[keys][auth]") String keys_auth,
|
||||
@Field("subscription[standard]") boolean standard,
|
||||
@Field("data[alerts][follow]") boolean follow,
|
||||
@Field("data[alerts][favourite]") boolean favourite,
|
||||
@Field("data[alerts][reblog]") boolean reblog,
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
package app.fedilab.android.mastodon.client.entities.api;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/* Copyright 2025 Thomas Schneider
|
||||
*
|
||||
* This file is a part of Fedilab
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
public class InstanceV2 implements Serializable {
|
||||
|
||||
@SerializedName("domain")
|
||||
public String domain;
|
||||
@SerializedName("title")
|
||||
public String title;
|
||||
@SerializedName("version")
|
||||
public String version;
|
||||
@SerializedName("source_url")
|
||||
public String sourceUrl;
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
@SerializedName("configuration")
|
||||
public Configuration configuration;
|
||||
|
||||
|
||||
|
||||
public static String serialize(InstanceV2 instance) {
|
||||
Gson gson = new Gson();
|
||||
try {
|
||||
return gson.toJson(instance);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static InstanceV2 restore(String serialized) {
|
||||
Gson gson = new Gson();
|
||||
try {
|
||||
return gson.fromJson(serialized, InstanceV2.class);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Configuration implements Serializable {
|
||||
@SerializedName("vapid")
|
||||
public VapId vapId;
|
||||
}
|
||||
public static class VapId implements Serializable {
|
||||
@SerializedName("public_key")
|
||||
public String publicKey;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -22,6 +22,8 @@ public class PushSubscription {
|
|||
public String id;
|
||||
@SerializedName("endpoint")
|
||||
public String endpoint;
|
||||
@SerializedName("standard")
|
||||
public String standard;
|
||||
@SerializedName("policy")
|
||||
public String policy;
|
||||
@SerializedName("alerts")
|
||||
|
|
|
@ -1,289 +0,0 @@
|
|||
package app.fedilab.android.mastodon.helper;
|
||||
/* Copyright 2022 Thomas Schneider
|
||||
*
|
||||
* This file is a part of Fedilab
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.spongycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.spongycastle.asn1.x9.ECNamedCurveTable;
|
||||
import org.spongycastle.asn1.x9.X9ECParameters;
|
||||
import org.spongycastle.crypto.params.ECNamedDomainParameters;
|
||||
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.spongycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.spongycastle.jce.spec.ECNamedCurveSpec;
|
||||
import org.spongycastle.jce.spec.ECParameterSpec;
|
||||
import org.spongycastle.jce.spec.ECPrivateKeySpec;
|
||||
import org.spongycastle.jce.spec.ECPublicKeySpec;
|
||||
import org.spongycastle.math.ec.ECCurve;
|
||||
import org.spongycastle.math.ec.ECPoint;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Security;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyAgreement;
|
||||
|
||||
|
||||
public class ECDH {
|
||||
|
||||
|
||||
public static final String kp_public = "kp_public";
|
||||
public static final String peer_public = "peer_public";
|
||||
public static final String PROVIDER = org.spongycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;
|
||||
|
||||
public static final String kp_private = "kp_private";
|
||||
public static final String KEGEN_ALG = "ECDH";
|
||||
|
||||
public static final String name = "prime256v1";
|
||||
|
||||
private static final String kp_public_affine_x = "kp_public_affine_x";
|
||||
private static final String kp_public_affine_y = "kp_public_affine_y";
|
||||
|
||||
private static ECDH instance;
|
||||
|
||||
static {
|
||||
Security.addProvider(new org.spongycastle.jce.provider.BouncyCastleProvider());
|
||||
}
|
||||
|
||||
public final KeyFactory kf;
|
||||
private final KeyPairGenerator kpg;
|
||||
private final String slug;
|
||||
|
||||
public ECDH(String slug) throws Exception {
|
||||
if (slug == null) {
|
||||
throw new Exception("slug cannot be null");
|
||||
}
|
||||
try {
|
||||
kf = KeyFactory.getInstance(KEGEN_ALG, PROVIDER);
|
||||
kpg = KeyPairGenerator.getInstance(KEGEN_ALG, PROVIDER);
|
||||
this.slug = slug;
|
||||
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized ECDH getInstance(String slug) throws Exception {
|
||||
if (instance == null) {
|
||||
instance = new ECDH(slug);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static String base64Encode(byte[] b) {
|
||||
return Base64.encodeToString(
|
||||
b, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP);
|
||||
}
|
||||
|
||||
static byte[] base64Decode(String str) {
|
||||
return Base64.decode(str, Base64.URL_SAFE);
|
||||
}
|
||||
|
||||
synchronized KeyPair generateKeyPair()
|
||||
throws Exception {
|
||||
ECGenParameterSpec ecParamSpec = new ECGenParameterSpec(name);
|
||||
kpg.initialize(ecParamSpec);
|
||||
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
private byte[] generateSecret(PrivateKey myPrivKey, PublicKey otherPubKey) throws Exception {
|
||||
KeyAgreement keyAgreement = KeyAgreement.getInstance(KEGEN_ALG);
|
||||
keyAgreement.init(myPrivKey);
|
||||
keyAgreement.doPhase(otherPubKey, true);
|
||||
|
||||
return keyAgreement.generateSecret();
|
||||
}
|
||||
|
||||
|
||||
synchronized KeyPair readKeyPair(Context context)
|
||||
throws Exception {
|
||||
return new KeyPair(readMyPublicKey(context), readMyPrivateKey(context));
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
public KeyPair newPair(Context context) {
|
||||
SharedPreferences.Editor prefsEditor = PreferenceManager
|
||||
.getDefaultSharedPreferences(context).edit();
|
||||
KeyPair kp;
|
||||
try {
|
||||
kp = generateKeyPair();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
ECPublicKey key = (ECPublicKey) kp.getPublic();
|
||||
byte[] x = key.getW().getAffineX().toByteArray();
|
||||
byte[] y = key.getW().getAffineY().toByteArray();
|
||||
BigInteger xbi = new BigInteger(1, x);
|
||||
BigInteger ybi = new BigInteger(1, y);
|
||||
X9ECParameters x9 = ECNamedCurveTable.getByName(name);
|
||||
ASN1ObjectIdentifier oid = ECNamedCurveTable.getOID(name);
|
||||
|
||||
ECCurve curve = x9.getCurve();
|
||||
ECPoint point = curve.createPoint(xbi, ybi);
|
||||
ECNamedDomainParameters dParams = new ECNamedDomainParameters(oid,
|
||||
x9.getCurve(), x9.getG(), x9.getN(), x9.getH(), x9.getSeed());
|
||||
|
||||
ECPublicKeyParameters pubKey = new ECPublicKeyParameters(point, dParams);
|
||||
|
||||
|
||||
ECPrivateKeyParameters privateKey = new ECPrivateKeyParameters(new BigInteger(kp.getPrivate().getEncoded()), pubKey.getParameters());
|
||||
byte[] privateKeyBytes = privateKey.getD().toByteArray();
|
||||
|
||||
String keyString = base64Encode(pubKey.getQ().getEncoded(false));
|
||||
String keypString = base64Encode(privateKeyBytes);
|
||||
prefsEditor.putString(kp_public + slug, keyString);
|
||||
prefsEditor.putString(kp_public_affine_x + slug, key.getW().getAffineX().toString());
|
||||
prefsEditor.putString(kp_public_affine_y + slug, key.getW().getAffineY().toString());
|
||||
prefsEditor.putString(kp_private + slug, keypString);
|
||||
prefsEditor.commit();
|
||||
return kp;
|
||||
}
|
||||
|
||||
|
||||
synchronized PublicKey readMyPublicKey(Context context) throws Exception {
|
||||
|
||||
X9ECParameters x9 = ECNamedCurveTable.getByName(name);
|
||||
ASN1ObjectIdentifier oid = ECNamedCurveTable.getOID(name);
|
||||
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
BigInteger xbi = new BigInteger(prefs.getString(kp_public_affine_x + slug, "0"));
|
||||
BigInteger ybi = new BigInteger(prefs.getString(kp_public_affine_y + slug, "0"));
|
||||
|
||||
ECNamedDomainParameters dParams = new ECNamedDomainParameters(oid,
|
||||
x9.getCurve(), x9.getG(), x9.getN(), x9.getH(), x9.getSeed());
|
||||
|
||||
|
||||
ECNamedCurveSpec ecNamedCurveSpec = new ECNamedCurveSpec(name, dParams.getCurve(), dParams.getG(), dParams.getN());
|
||||
java.security.spec.ECPoint w = new java.security.spec.ECPoint(xbi, ybi);
|
||||
return kf.generatePublic(new java.security.spec.ECPublicKeySpec(w, ecNamedCurveSpec));
|
||||
}
|
||||
|
||||
|
||||
public String uncryptMessage(Context context, String cyphered) {
|
||||
byte[] privateKey = getSharedSecret(context);
|
||||
try {
|
||||
Cipher outCipher = Cipher.getInstance("ECIES", PROVIDER);
|
||||
PrivateKey ddd = readPrivateKey(privateKey);
|
||||
outCipher.init(Cipher.DECRYPT_MODE, readPrivateKey(privateKey));
|
||||
byte[] plaintext = outCipher.doFinal(base64Decode(cyphered));
|
||||
return new String(plaintext);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return "";
|
||||
|
||||
}
|
||||
|
||||
|
||||
public PublicKey readPublicKey(String keyStr) throws Exception {
|
||||
ECParameterSpec parameterSpec = org.spongycastle.jce.ECNamedCurveTable.getParameterSpec(name);
|
||||
ECCurve curve = parameterSpec.getCurve();
|
||||
ECPoint point = curve.decodePoint(base64Decode(keyStr));
|
||||
ECPublicKeySpec pubSpec = new ECPublicKeySpec(point, parameterSpec);
|
||||
return kf.generatePublic(pubSpec);
|
||||
}
|
||||
|
||||
|
||||
public PrivateKey readPrivateKey(byte[] key) throws Exception {
|
||||
ECParameterSpec parameterSpec = org.spongycastle.jce.ECNamedCurveTable.getParameterSpec(name);
|
||||
ECPrivateKeySpec pubSpec = new ECPrivateKeySpec(new BigInteger(1, key), parameterSpec);
|
||||
return kf.generatePrivate(pubSpec);
|
||||
}
|
||||
|
||||
synchronized PrivateKey readMyPrivateKey(Context context) throws Exception {
|
||||
X9ECParameters x9 = ECNamedCurveTable.getByName(name);
|
||||
ASN1ObjectIdentifier oid = ECNamedCurveTable.getOID(name);
|
||||
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
BigInteger ybi = new BigInteger(prefs.getString(kp_public_affine_y + slug, "0"));
|
||||
ECNamedDomainParameters dParams = new ECNamedDomainParameters(oid,
|
||||
x9.getCurve(), x9.getG(), x9.getN(), x9.getH(), x9.getSeed());
|
||||
ECNamedCurveSpec ecNamedCurveSpec = new ECNamedCurveSpec(name, dParams.getCurve(), dParams.getG(), dParams.getN());
|
||||
return kf.generatePrivate(new java.security.spec.ECPrivateKeySpec(ybi, ecNamedCurveSpec));
|
||||
}
|
||||
|
||||
|
||||
private synchronized KeyPair getPair(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
String strPub = prefs.getString(kp_public + slug, "");
|
||||
String strPriv = prefs.getString(kp_private + slug, "");
|
||||
if (strPub.trim().isEmpty() || strPriv.trim().isEmpty()) {
|
||||
return newPair(context);
|
||||
}
|
||||
try {
|
||||
return readKeyPair(context);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
PublicKey getServerKey(Context context) throws Exception {
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
String serverKey = prefs.getString(peer_public + slug, "");
|
||||
return readPublicKey(serverKey);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unused", "RedundantSuppression"})
|
||||
public byte[] getSharedSecret(Context context) {
|
||||
try {
|
||||
KeyPair keyPair = getPair(context);
|
||||
if (keyPair != null) {
|
||||
return generateSecret(keyPair.getPrivate(), getServerKey(context));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getPublicKey(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
|
||||
return prefs.getString(kp_public + slug, "");
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
public void saveServerKey(Context context, String strPeerPublic) {
|
||||
SharedPreferences.Editor prefsEditor = PreferenceManager
|
||||
.getDefaultSharedPreferences(context).edit();
|
||||
|
||||
prefsEditor.putString(peer_public + slug, strPeerPublic);
|
||||
prefsEditor.commit();
|
||||
}
|
||||
}
|
|
@ -1,239 +0,0 @@
|
|||
package app.fedilab.android.mastodon.helper;
|
||||
/* Copyright 2022 Thomas Schneider
|
||||
*
|
||||
* This file is a part of Fedilab
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
import static app.fedilab.android.mastodon.client.entities.app.StatusCache.restoreNotificationFromString;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.security.spec.ECPoint;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.KeyAgreement;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import app.fedilab.android.mastodon.client.entities.api.Notification;
|
||||
|
||||
|
||||
public class ECDHFedilab {
|
||||
|
||||
|
||||
public static final String kp_public = "kp_public";
|
||||
public static final String peer_public = "peer_public";
|
||||
|
||||
public static final String name = "prime256v1";
|
||||
private static final byte[] P256_HEAD = new byte[]{(byte) 0x30, (byte) 0x59, (byte) 0x30, (byte) 0x13, (byte) 0x06, (byte) 0x07, (byte) 0x2a,
|
||||
(byte) 0x86, (byte) 0x48, (byte) 0xce, (byte) 0x3d, (byte) 0x02, (byte) 0x01, (byte) 0x06, (byte) 0x08, (byte) 0x2a, (byte) 0x86,
|
||||
(byte) 0x48, (byte) 0xce, (byte) 0x3d, (byte) 0x03, (byte) 0x01, (byte) 0x07, (byte) 0x03, (byte) 0x42, (byte) 0x00};
|
||||
|
||||
static {
|
||||
Security.addProvider(new org.spongycastle.jce.provider.BouncyCastleProvider());
|
||||
}
|
||||
|
||||
private final KeyPairGenerator kpg;
|
||||
private final PublicKey publicKey;
|
||||
private final String encodedPublicKey;
|
||||
private final byte[] authKey;
|
||||
private final String slug;
|
||||
private final String pushPublicKey;
|
||||
private final String encodedAuthKey;
|
||||
private final String pushAccountID;
|
||||
private final String pushPrivateKey;
|
||||
PrivateKey privateKey;
|
||||
private String pushPrivateKe;
|
||||
|
||||
public ECDHFedilab(Context context, String slug) throws Exception {
|
||||
if (slug == null) {
|
||||
throw new Exception("slug cannot be null");
|
||||
}
|
||||
try {
|
||||
kpg = KeyPairGenerator.getInstance("EC");
|
||||
ECGenParameterSpec spec = new ECGenParameterSpec("prime256v1");
|
||||
kpg.initialize(spec);
|
||||
KeyPair keyPair = kpg.generateKeyPair();
|
||||
publicKey = keyPair.getPublic();
|
||||
privateKey = keyPair.getPrivate();
|
||||
encodedPublicKey = Base64.encodeToString(serializeRawPublicKey(publicKey), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
|
||||
authKey = new byte[16];
|
||||
SecureRandom secureRandom = new SecureRandom();
|
||||
secureRandom.nextBytes(authKey);
|
||||
byte[] randomAccountID = new byte[16];
|
||||
secureRandom.nextBytes(randomAccountID);
|
||||
pushPrivateKey = Base64.encodeToString(privateKey.getEncoded(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
|
||||
pushPublicKey = Base64.encodeToString(publicKey.getEncoded(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
|
||||
encodedAuthKey = Base64.encodeToString(authKey, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
|
||||
pushAccountID = Base64.encodeToString(randomAccountID, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
|
||||
SharedPreferences.Editor prefsEditor = PreferenceManager
|
||||
.getDefaultSharedPreferences(context).edit();
|
||||
prefsEditor.putString("pushPrivateKey" + slug, pushPrivateKey);
|
||||
prefsEditor.putString("pushPublicKey" + slug, pushPublicKey);
|
||||
prefsEditor.putString("encodedAuthKey" + slug, encodedAuthKey);
|
||||
prefsEditor.putString("pushAccountID" + slug, pushAccountID);
|
||||
prefsEditor.apply();
|
||||
this.slug = slug;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getServerKey(Context context, String slug) {
|
||||
SharedPreferences sharedPreferences = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
return sharedPreferences.getString("server_key" + slug, null);
|
||||
}
|
||||
|
||||
private static byte[] serializeRawPublicKey(PublicKey key) {
|
||||
ECPoint point = ((ECPublicKey) key).getW();
|
||||
byte[] x = point.getAffineX().toByteArray();
|
||||
byte[] y = point.getAffineY().toByteArray();
|
||||
if (x.length > 32)
|
||||
x = Arrays.copyOfRange(x, x.length - 32, x.length);
|
||||
if (y.length > 32)
|
||||
y = Arrays.copyOfRange(y, y.length - 32, y.length);
|
||||
byte[] result = new byte[65];
|
||||
result[0] = 4;
|
||||
System.arraycopy(x, 0, result, 1 + (32 - x.length), x.length);
|
||||
System.arraycopy(y, 0, result, result.length - y.length, y.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Notification decryptNotification(Context context, String slug, byte[] messageEncrypted) {
|
||||
|
||||
|
||||
SharedPreferences sharedPreferences = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
String pushPrivateKey = sharedPreferences.getString("pushPrivateKey" + slug, null);
|
||||
String pushPublicKey = sharedPreferences.getString("pushPublicKey" + slug, null);
|
||||
String encodedAuthKey = sharedPreferences.getString("encodedAuthKey" + slug, null);
|
||||
sharedPreferences.getString("pushAccountID" + slug, null);
|
||||
|
||||
|
||||
PublicKey serverKey = null;
|
||||
serverKey = deserializeRawPublicKey(Base64.decode(getServerKey(context, slug), Base64.URL_SAFE));
|
||||
PrivateKey privateKey;
|
||||
PublicKey publicKey;
|
||||
byte[] authKey;
|
||||
try {
|
||||
KeyFactory kf = KeyFactory.getInstance("EC");
|
||||
privateKey = kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.decode(pushPrivateKey, Base64.URL_SAFE)));
|
||||
publicKey = kf.generatePublic(new X509EncodedKeySpec(Base64.decode(pushPublicKey, Base64.URL_SAFE)));
|
||||
authKey = Base64.decode(encodedAuthKey, Base64.URL_SAFE);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
byte[] sharedSecret;
|
||||
try {
|
||||
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
|
||||
keyAgreement.init(privateKey);
|
||||
keyAgreement.doPhase(serverKey, true);
|
||||
sharedSecret = keyAgreement.generateSecret();
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
byte[] secondSaltInfo = "Content-Encoding: auth\0".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] deriveKey;
|
||||
try {
|
||||
deriveKey = deriveKey(authKey, sharedSecret, secondSaltInfo, 32);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
String decryptedStr;
|
||||
try {
|
||||
|
||||
SecretKeySpec aesKey = new SecretKeySpec(deriveKey, "AES");
|
||||
byte[] iv = Arrays.copyOfRange(messageEncrypted, 0, 12);
|
||||
byte[] ciphertext = Arrays.copyOfRange(messageEncrypted, 12, messageEncrypted.length); // Separate ciphertext (the MAC is implicitly separated from the ciphertext)
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
GCMParameterSpec gCMParameterSpec = new GCMParameterSpec(128, iv);
|
||||
cipher.init(Cipher.DECRYPT_MODE, aesKey, gCMParameterSpec);
|
||||
byte[] decrypted = cipher.doFinal(ciphertext);
|
||||
decryptedStr = new String(decrypted, 2, decrypted.length - 2, StandardCharsets.UTF_8);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException |
|
||||
InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException |
|
||||
IllegalBlockSizeException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
return restoreNotificationFromString(decryptedStr);
|
||||
}
|
||||
|
||||
protected static PublicKey deserializeRawPublicKey(byte[] rawBytes) {
|
||||
if (rawBytes.length != 65 && rawBytes.length != 64)
|
||||
return null;
|
||||
try {
|
||||
KeyFactory kf = KeyFactory.getInstance("EC");
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
os.write(P256_HEAD);
|
||||
if (rawBytes.length == 64)
|
||||
os.write(4);
|
||||
os.write(rawBytes);
|
||||
return kf.generatePublic(new X509EncodedKeySpec(os.toByteArray()));
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static byte[] deriveKey(byte[] firstSalt, byte[] secondSalt, byte[] info, int length) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
Mac hmacContext = Mac.getInstance("HmacSHA256");
|
||||
hmacContext.init(new SecretKeySpec(firstSalt, "HmacSHA256"));
|
||||
byte[] hmac = hmacContext.doFinal(secondSalt);
|
||||
hmacContext.init(new SecretKeySpec(hmac, "HmacSHA256"));
|
||||
hmacContext.update(info);
|
||||
byte[] result = hmacContext.doFinal(new byte[]{1});
|
||||
return result.length <= length ? result : Arrays.copyOfRange(result, 0, length);
|
||||
}
|
||||
|
||||
public String getPublicKey() {
|
||||
return this.encodedPublicKey;
|
||||
}
|
||||
|
||||
public String getAuthKey() {
|
||||
return this.encodedAuthKey;
|
||||
}
|
||||
|
||||
}
|
|
@ -15,6 +15,7 @@ package app.fedilab.android.mastodon.helper;
|
|||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
@ -35,20 +36,26 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|||
|
||||
import org.unifiedpush.android.connector.UnifiedPush;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.net.IDN;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import app.fedilab.android.R;
|
||||
import app.fedilab.android.mastodon.client.endpoints.MastodonInstanceService;
|
||||
import app.fedilab.android.mastodon.client.entities.api.InstanceV2;
|
||||
import app.fedilab.android.mastodon.client.entities.app.Account;
|
||||
import app.fedilab.android.mastodon.client.entities.app.BaseAccount;
|
||||
import app.fedilab.android.mastodon.jobs.NotificationsWorker;
|
||||
import okhttp3.OkHttpClient;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Response;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
public class PushHelper {
|
||||
|
||||
|
||||
public static void startStreaming(Context context) {
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String typeOfNotification = prefs.getString(context.getString(R.string.SET_NOTIFICATION_TYPE), "PUSH_NOTIFICATIONS");
|
||||
switch (typeOfNotification) {
|
||||
|
@ -57,8 +64,8 @@ public class PushHelper {
|
|||
List<BaseAccount> accounts = new Account(context).getPushNotificationAccounts();
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
Runnable myRunnable = () -> {
|
||||
List<String> distributors = UnifiedPush.getDistributors(context, new ArrayList<>());
|
||||
if (distributors.size() == 0) {
|
||||
List<String> distributors = UnifiedPush.getDistributors(context);
|
||||
if (distributors.isEmpty()) {
|
||||
AlertDialog.Builder alert = new MaterialAlertDialogBuilder(context);
|
||||
alert.setTitle(R.string.no_distributors_found);
|
||||
final TextView message = new TextView(context);
|
||||
|
@ -95,7 +102,7 @@ public class PushHelper {
|
|||
new Thread(() -> {
|
||||
List<BaseAccount> accounts = new Account(context).getPushNotificationAccounts();
|
||||
for (BaseAccount account : accounts) {
|
||||
((Activity) context).runOnUiThread(() -> UnifiedPush.unregisterApp(context, account.user_id + "@" + account.instance));
|
||||
((Activity) context).runOnUiThread(() -> UnifiedPush.unregister(context, account.user_id + "@" + account.instance));
|
||||
}
|
||||
}).start();
|
||||
break;
|
||||
|
@ -108,7 +115,7 @@ public class PushHelper {
|
|||
List<BaseAccount> accounts = new Account(context).getPushNotificationAccounts();
|
||||
if (accounts != null) {
|
||||
for (BaseAccount account : accounts) {
|
||||
((Activity) context).runOnUiThread(() -> UnifiedPush.unregisterApp(context, account.user_id + "@" + account.instance));
|
||||
((Activity) context).runOnUiThread(() -> UnifiedPush.unregister(context, account.user_id + "@" + account.instance));
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
@ -125,13 +132,49 @@ public class PushHelper {
|
|||
if (accounts == null) {
|
||||
return;
|
||||
}
|
||||
List<String> distributors = UnifiedPush.getDistributors(context, new ArrayList<>());
|
||||
if (distributors.size() == 1 || !UnifiedPush.getDistributor(context).isEmpty()) {
|
||||
List<String> distributors = UnifiedPush.getDistributors(context);
|
||||
if (!distributors.isEmpty()) {
|
||||
if (distributors.size() == 1) {
|
||||
UnifiedPush.saveDistributor(context, distributors.get(0));
|
||||
}
|
||||
final OkHttpClient okHttpClient = Helper.myOkHttpClient(context.getApplicationContext());
|
||||
for (BaseAccount account : accounts) {
|
||||
UnifiedPush.registerApp(context, account.user_id + "@" + account.instance, new ArrayList<>(), "");
|
||||
|
||||
new Thread(()->{
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl("https://" + (account.instance != null ? IDN.toASCII(account.instance, IDN.ALLOW_UNASSIGNED) : null) + "/api/v2/")
|
||||
.addConverterFactory(GsonConverterFactory.create(Helper.getDateBuilder()))
|
||||
.client(okHttpClient)
|
||||
.build();
|
||||
MastodonInstanceService mastodonInstanceService = retrofit.create(MastodonInstanceService.class);
|
||||
Call<InstanceV2> instanceV2Call = mastodonInstanceService.instanceV2();
|
||||
String vapid = null;
|
||||
if (instanceV2Call != null) {
|
||||
try {
|
||||
Response<InstanceV2> instanceResponse = instanceV2Call.execute();
|
||||
if (instanceResponse.isSuccessful()) {
|
||||
InstanceV2 instanceV2 = instanceResponse.body();
|
||||
if (instanceV2 != null && instanceV2.configuration.vapId != null) {
|
||||
vapid = instanceV2.configuration.vapId.publicKey;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
String finalVapid = vapid!=null?vapid.replaceAll("=",""):null;
|
||||
Runnable myRunnable = () -> {
|
||||
try {
|
||||
UnifiedPush.register(context, account.user_id + "@" + account.instance, null, finalVapid);
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
};
|
||||
mainHandler.post(myRunnable);
|
||||
}).start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -143,7 +186,7 @@ public class PushHelper {
|
|||
String distributor = distributorsStr[item];
|
||||
UnifiedPush.saveDistributor(context, distributor);
|
||||
for (BaseAccount account : accounts) {
|
||||
UnifiedPush.registerApp(context, account.user_id + "@" + account.instance, new ArrayList<>(), "");
|
||||
UnifiedPush.register(context, account.user_id + "@" + account.instance, null, null);
|
||||
}
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
|
|
@ -15,14 +15,19 @@ package app.fedilab.android.mastodon.helper;
|
|||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
|
||||
import static app.fedilab.android.mastodon.helper.Helper.TAG;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.unifiedpush.android.connector.data.PushEndpoint;
|
||||
|
||||
import java.net.IDN;
|
||||
|
||||
import app.fedilab.android.R;
|
||||
|
@ -41,22 +46,13 @@ import retrofit2.converter.gson.GsonConverterFactory;
|
|||
public class PushNotifications {
|
||||
|
||||
|
||||
public static void registerPushNotifications(Context context, String endpoint, String slug) {
|
||||
public static void registerPushNotifications(Context context, PushEndpoint pushEndpoint, String slug) {
|
||||
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
ECDHFedilab ecdh = null;
|
||||
try {
|
||||
ecdh = new ECDHFedilab(context, slug);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (ecdh == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String pubKey = ecdh.getPublicKey();
|
||||
String auth = ecdh.getAuthKey();
|
||||
String pubKey = pushEndpoint.getPubKeySet().getPubKey();
|
||||
String auth =pushEndpoint.getPubKeySet().getAuth();
|
||||
|
||||
|
||||
boolean notif_follow = prefs.getBoolean(context.getString(R.string.SET_NOTIF_FOLLOW), true);
|
||||
|
@ -84,9 +80,10 @@ public class PushNotifications {
|
|||
PushSubscription pushSubscription;
|
||||
Call<PushSubscription> pushSubscriptionCall = mastodonNotificationsService.pushSubscription(
|
||||
accountDb.token,
|
||||
endpoint,
|
||||
pushEndpoint.getUrl(),
|
||||
pubKey,
|
||||
auth,
|
||||
true,
|
||||
notif_follow,
|
||||
notif_fav,
|
||||
notif_share,
|
||||
|
@ -101,6 +98,7 @@ public class PushNotifications {
|
|||
Response<PushSubscription> pushSubscriptionResponse = pushSubscriptionCall.execute();
|
||||
if (pushSubscriptionResponse.isSuccessful()) {
|
||||
pushSubscription = pushSubscriptionResponse.body();
|
||||
|
||||
if (pushSubscription != null) {
|
||||
pushSubscription.server_key = pushSubscription.server_key.replace('/', '_');
|
||||
pushSubscription.server_key = pushSubscription.server_key.replace('+', '-');
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
package app.fedilab.android.mastodon.services;
|
||||
/* Copyright 2022 Thomas Schneider
|
||||
*
|
||||
* This file is a part of Fedilab
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.unifiedpush.android.connector.MessagingReceiver;
|
||||
|
||||
import app.fedilab.android.R;
|
||||
import app.fedilab.android.mastodon.helper.NotificationsHelper;
|
||||
import app.fedilab.android.mastodon.helper.PushNotifications;
|
||||
|
||||
|
||||
public class CustomReceiver extends MessagingReceiver {
|
||||
|
||||
|
||||
public CustomReceiver() {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onMessage(@NotNull Context context, @NotNull byte[] message, @NotNull String slug) {
|
||||
// Called when a new message is received. The message contains the full POST body of the push message
|
||||
new Thread(() -> {
|
||||
try {
|
||||
/*Notification notification = ECDHFedilab.decryptNotification(context, slug, message);
|
||||
Log.v(Helper.TAG,"notification: " + notification);
|
||||
if(notification != null) {
|
||||
Log.v(Helper.TAG,"id: " + notification.id);
|
||||
}
|
||||
*/
|
||||
NotificationsHelper.task(context, slug);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(@NonNull Context context, @NonNull Intent intent) {
|
||||
super.onReceive(context, intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewEndpoint(@Nullable Context context, @NotNull String endpoint, @NotNull String slug) {
|
||||
if (context != null) {
|
||||
synchronized (this) {
|
||||
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String storedEnpoint = sharedpreferences.getString(context.getString(R.string.SET_STORED_ENDPOINT) + slug, null);
|
||||
if (storedEnpoint == null || !storedEnpoint.equals(endpoint)) {
|
||||
PushNotifications
|
||||
.registerPushNotifications(context, endpoint, slug);
|
||||
SharedPreferences.Editor editor = sharedpreferences.edit();
|
||||
editor.putString(context.getString(R.string.SET_STORED_ENDPOINT) + slug, endpoint);
|
||||
editor.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onRegistrationFailed(@Nullable Context context, @NotNull String s) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnregistered(@Nullable Context context, @NotNull String s) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package app.fedilab.android.mastodon.services;
|
||||
|
||||
import static app.fedilab.android.mastodon.helper.Helper.TAG;
|
||||
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
|
||||
import org.unifiedpush.android.connector.FailedReason;
|
||||
import org.unifiedpush.android.connector.PushService;
|
||||
import org.unifiedpush.android.connector.data.PushEndpoint;
|
||||
import org.unifiedpush.android.connector.data.PushMessage;
|
||||
|
||||
import app.fedilab.android.R;
|
||||
|
||||
import app.fedilab.android.mastodon.helper.NotificationsHelper;
|
||||
import app.fedilab.android.mastodon.helper.PushNotifications;
|
||||
|
||||
public class PushServiceImpl extends PushService {
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(@NonNull PushMessage pushMessage, @NonNull String slug) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
/*if( pushMessage.getDecrypted()) {
|
||||
String decryptedMessage = new String(pushMessage.getContent(), StandardCharsets.UTF_8);
|
||||
JSONObject decryptedMessageJSON = new JSONObject(decryptedMessage);
|
||||
} else {
|
||||
|
||||
}*/
|
||||
NotificationsHelper.task(getApplicationContext(), slug);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewEndpoint(@NonNull PushEndpoint pushEndpoint, @NonNull String slug) {
|
||||
if (getApplicationContext() != null) {
|
||||
synchronized (this) {
|
||||
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||
String storedEnpoint = sharedpreferences.getString(getApplicationContext().getString(R.string.SET_STORED_ENDPOINT) + slug, null);
|
||||
if (storedEnpoint == null || !storedEnpoint.equals(pushEndpoint.getUrl())) {
|
||||
PushNotifications
|
||||
.registerPushNotifications(getApplicationContext(), pushEndpoint, slug);
|
||||
SharedPreferences.Editor editor = sharedpreferences.edit();
|
||||
editor.putString(getApplicationContext().getString(R.string.SET_STORED_ENDPOINT) + slug, pushEndpoint.getUrl());
|
||||
editor.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegistrationFailed(@NonNull FailedReason failedReason, @NonNull String s) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnregistered(@NonNull String s) {
|
||||
}
|
||||
}
|
|
@ -35,6 +35,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import org.unifiedpush.android.connector.UnifiedPush;
|
||||
import org.unifiedpush.android.connector.internal.Store;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -138,8 +139,8 @@ public class FragmentNotificationsSettings extends PreferenceFragmentCompat impl
|
|||
preferenceScreen.removePreferenceRecursively("SET_NOTIFICATION_DELAY_VALUE");
|
||||
}
|
||||
if (SET_PUSH_DISTRIBUTOR != null) {
|
||||
List<String> distributors = UnifiedPush.getDistributors(requireActivity(), new ArrayList<>());
|
||||
SET_PUSH_DISTRIBUTOR.setValue(UnifiedPush.getDistributor(requireActivity()));
|
||||
List<String> distributors = UnifiedPush.getDistributors(requireActivity());
|
||||
SET_PUSH_DISTRIBUTOR.setValue("");
|
||||
SET_PUSH_DISTRIBUTOR.setEntries(distributors.toArray(new String[0]));
|
||||
SET_PUSH_DISTRIBUTOR.setEntryValues(distributors.toArray(new String[0]));
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import app.fedilab.android.mastodon.client.entities.api.Emoji;
|
|||
import app.fedilab.android.mastodon.client.entities.api.EmojiInstance;
|
||||
import app.fedilab.android.mastodon.client.entities.api.Instance;
|
||||
import app.fedilab.android.mastodon.client.entities.api.InstanceInfo;
|
||||
import app.fedilab.android.mastodon.client.entities.api.InstanceV2;
|
||||
import app.fedilab.android.mastodon.exception.DBException;
|
||||
import app.fedilab.android.mastodon.helper.Helper;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
@ -50,6 +51,7 @@ public class InstancesVM extends AndroidViewModel {
|
|||
final OkHttpClient okHttpClient = Helper.myOkHttpClient(getApplication().getApplicationContext());
|
||||
private MutableLiveData<EmojiInstance> emojiInstanceMutableLiveData;
|
||||
private MutableLiveData<InstanceInfo> instanceInfoMutableLiveData;
|
||||
private MutableLiveData<String> vapidMutableLiveData;
|
||||
|
||||
public InstancesVM(@NonNull Application application) {
|
||||
super(application);
|
||||
|
@ -65,6 +67,16 @@ public class InstancesVM extends AndroidViewModel {
|
|||
return retrofit.create(MastodonInstanceService.class);
|
||||
}
|
||||
|
||||
private MastodonInstanceService initV2(String instance) {
|
||||
Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").create();
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl("https://" + (instance != null ? IDN.toASCII(instance, IDN.ALLOW_UNASSIGNED) : null) + "/api/v2/")
|
||||
.addConverterFactory(GsonConverterFactory.create(Helper.getDateBuilder()))
|
||||
.client(okHttpClient)
|
||||
.build();
|
||||
return retrofit.create(MastodonInstanceService.class);
|
||||
}
|
||||
|
||||
public LiveData<EmojiInstance> getEmoji(@NonNull String instance) {
|
||||
MastodonInstanceService mastodonInstanceService = init(instance);
|
||||
emojiInstanceMutableLiveData = new MutableLiveData<>();
|
||||
|
@ -146,4 +158,34 @@ public class InstancesVM extends AndroidViewModel {
|
|||
}).start();
|
||||
return instanceInfoMutableLiveData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public LiveData<String> getInstanceVapid(@NonNull String instance) {
|
||||
MastodonInstanceService mastodonInstanceV2Service = initV2(instance);
|
||||
vapidMutableLiveData = new MutableLiveData<>();
|
||||
new Thread(() -> {
|
||||
String vapid = null;
|
||||
Call<InstanceV2> instanceV2Call = mastodonInstanceV2Service.instanceV2();
|
||||
if (instanceV2Call != null) {
|
||||
try {
|
||||
Response<InstanceV2> instanceResponse = instanceV2Call.execute();
|
||||
if (instanceResponse.isSuccessful()) {
|
||||
InstanceV2 instanceV2 = instanceResponse.body();
|
||||
if (instanceV2 != null && instanceV2.configuration.vapId != null) {
|
||||
vapid = instanceV2.configuration.vapId.publicKey;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
String finalVapid = vapid;
|
||||
Runnable myRunnable = () -> vapidMutableLiveData.setValue(finalVapid);
|
||||
mainHandler.post(myRunnable);
|
||||
}).start();
|
||||
return vapidMutableLiveData;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,9 +14,12 @@ package app.fedilab.android.mastodon.viewmodel.mastodon;
|
|||
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
import static app.fedilab.android.mastodon.helper.Helper.TAG;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
|
@ -309,7 +312,7 @@ public class NotificationsVM extends AndroidViewModel {
|
|||
MastodonNotificationsService mastodonNotificationsService = init(instance);
|
||||
new Thread(() -> {
|
||||
PushSubscription pushSubscription = null;
|
||||
Call<PushSubscription> pushSubscriptionCall = mastodonNotificationsService.pushSubscription(token, endpoint, keys_p256dh, keys_auth, follow, favourite, reblog, mention, poll, status, updates, signup, report, "all");
|
||||
Call<PushSubscription> pushSubscriptionCall = mastodonNotificationsService.pushSubscription(token, endpoint, keys_p256dh, keys_auth, true, follow, favourite, reblog, mention, poll, status, updates, signup, report, "all");
|
||||
if (pushSubscriptionCall != null) {
|
||||
try {
|
||||
Response<PushSubscription> pushSubscriptionResponse = pushSubscriptionCall.execute();
|
||||
|
|
|
@ -5,17 +5,6 @@
|
|||
|
||||
|
||||
<application android:name=".MainApplication">
|
||||
<receiver
|
||||
android:name=".services.EmbeddedDistrib"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="org.unifiedpush.android.distributor.feature.BYTES_MESSAGE" />
|
||||
<action android:name="org.unifiedpush.android.distributor.REGISTER" />
|
||||
<action android:name="org.unifiedpush.android.distributor.UNREGISTER" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".expandedcontrols.ExpandedControlsActivity"
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
package app.fedilab.android.services;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.unifiedpush.android.embedded_fcm_distributor.EmbeddedDistributorReceiver;
|
||||
|
||||
public class EmbeddedDistrib extends EmbeddedDistributorReceiver {
|
||||
@Override
|
||||
public @NotNull
|
||||
String getEndpoint(@Nullable Context context, @NotNull String token, @NotNull String instance) {
|
||||
return "https://gotify.fedilab.app/FCM?v2&token=" + token + "&instance=" + instance;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue