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 {
|
sourceSets {
|
||||||
playstore {
|
playstore {
|
||||||
manifest.srcFile "src/playstore/AndroidManifest.xml"
|
|
||||||
java.srcDirs = ['src/main/java', 'src/playstore/java']
|
java.srcDirs = ['src/main/java', 'src/playstore/java']
|
||||||
res.srcDirs = ['src/main/res', 'src/playstore/res']
|
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-limiter:5.11.3"
|
||||||
implementation "ch.acra:acra-dialog:5.11.3"
|
implementation "ch.acra:acra-dialog:5.11.3"
|
||||||
implementation "com.madgag.spongycastle:bctls-jdk15on:1.58.0.0"
|
implementation "com.madgag.spongycastle:bctls-jdk15on:1.58.0.0"
|
||||||
implementation 'com.github.UnifiedPush:android-connector:2.2.0'
|
implementation 'org.unifiedpush.android:connector:3.0.4'
|
||||||
// implementation 'com.github.UnifiedPush:android-foss_embedded_fcm_distributor:1.0.0-beta1'
|
|
||||||
playstoreImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:2.2.0') {
|
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-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'
|
||||||
|
|
|
@ -467,18 +467,12 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<receiver
|
<service android:name=".mastodon.services.PushServiceImpl"
|
||||||
android:name=".mastodon.services.CustomReceiver"
|
android:exported="false">
|
||||||
android:enabled="true"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="org.unifiedpush.android.connector.MESSAGE" />
|
<action android:name="org.unifiedpush.android.connector.PUSH_EVENT"/>
|
||||||
<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" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</service>
|
||||||
|
|
||||||
|
|
||||||
<activity
|
<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.Activity;
|
||||||
import app.fedilab.android.mastodon.client.entities.api.Emoji;
|
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.Instance;
|
||||||
|
import app.fedilab.android.mastodon.client.entities.api.InstanceV2;
|
||||||
import app.fedilab.android.mastodon.client.entities.api.Tag;
|
import app.fedilab.android.mastodon.client.entities.api.Tag;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.http.GET;
|
import retrofit2.http.GET;
|
||||||
|
@ -32,6 +33,9 @@ public interface MastodonInstanceService {
|
||||||
@GET("instance")
|
@GET("instance")
|
||||||
Call<Instance> instance();
|
Call<Instance> instance();
|
||||||
|
|
||||||
|
@GET("instance")
|
||||||
|
Call<InstanceV2> instanceV2();
|
||||||
|
|
||||||
@GET("instance/peers")
|
@GET("instance/peers")
|
||||||
Call<List<String>> connectedInstance();
|
Call<List<String>> connectedInstance();
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,7 @@ public interface MastodonNotificationsService {
|
||||||
@Field("subscription[endpoint]") String endpoint,
|
@Field("subscription[endpoint]") String endpoint,
|
||||||
@Field("subscription[keys][p256dh]") String keys_p256dh,
|
@Field("subscription[keys][p256dh]") String keys_p256dh,
|
||||||
@Field("subscription[keys][auth]") String keys_auth,
|
@Field("subscription[keys][auth]") String keys_auth,
|
||||||
|
@Field("subscription[standard]") boolean standard,
|
||||||
@Field("data[alerts][follow]") boolean follow,
|
@Field("data[alerts][follow]") boolean follow,
|
||||||
@Field("data[alerts][favourite]") boolean favourite,
|
@Field("data[alerts][favourite]") boolean favourite,
|
||||||
@Field("data[alerts][reblog]") boolean reblog,
|
@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;
|
public String id;
|
||||||
@SerializedName("endpoint")
|
@SerializedName("endpoint")
|
||||||
public String endpoint;
|
public String endpoint;
|
||||||
|
@SerializedName("standard")
|
||||||
|
public String standard;
|
||||||
@SerializedName("policy")
|
@SerializedName("policy")
|
||||||
public String policy;
|
public String policy;
|
||||||
@SerializedName("alerts")
|
@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>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
@ -35,20 +36,26 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
import org.unifiedpush.android.connector.UnifiedPush;
|
import org.unifiedpush.android.connector.UnifiedPush;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.net.IDN;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import app.fedilab.android.R;
|
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.Account;
|
||||||
import app.fedilab.android.mastodon.client.entities.app.BaseAccount;
|
import app.fedilab.android.mastodon.client.entities.app.BaseAccount;
|
||||||
import app.fedilab.android.mastodon.jobs.NotificationsWorker;
|
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 class PushHelper {
|
||||||
|
|
||||||
|
|
||||||
public static void startStreaming(Context context) {
|
public static void startStreaming(Context context) {
|
||||||
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
String typeOfNotification = prefs.getString(context.getString(R.string.SET_NOTIFICATION_TYPE), "PUSH_NOTIFICATIONS");
|
String typeOfNotification = prefs.getString(context.getString(R.string.SET_NOTIFICATION_TYPE), "PUSH_NOTIFICATIONS");
|
||||||
switch (typeOfNotification) {
|
switch (typeOfNotification) {
|
||||||
|
@ -57,8 +64,8 @@ public class PushHelper {
|
||||||
List<BaseAccount> accounts = new Account(context).getPushNotificationAccounts();
|
List<BaseAccount> accounts = new Account(context).getPushNotificationAccounts();
|
||||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
Runnable myRunnable = () -> {
|
Runnable myRunnable = () -> {
|
||||||
List<String> distributors = UnifiedPush.getDistributors(context, new ArrayList<>());
|
List<String> distributors = UnifiedPush.getDistributors(context);
|
||||||
if (distributors.size() == 0) {
|
if (distributors.isEmpty()) {
|
||||||
AlertDialog.Builder alert = new MaterialAlertDialogBuilder(context);
|
AlertDialog.Builder alert = new MaterialAlertDialogBuilder(context);
|
||||||
alert.setTitle(R.string.no_distributors_found);
|
alert.setTitle(R.string.no_distributors_found);
|
||||||
final TextView message = new TextView(context);
|
final TextView message = new TextView(context);
|
||||||
|
@ -95,7 +102,7 @@ public class PushHelper {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
List<BaseAccount> accounts = new Account(context).getPushNotificationAccounts();
|
List<BaseAccount> accounts = new Account(context).getPushNotificationAccounts();
|
||||||
for (BaseAccount account : accounts) {
|
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();
|
}).start();
|
||||||
break;
|
break;
|
||||||
|
@ -108,7 +115,7 @@ public class PushHelper {
|
||||||
List<BaseAccount> accounts = new Account(context).getPushNotificationAccounts();
|
List<BaseAccount> accounts = new Account(context).getPushNotificationAccounts();
|
||||||
if (accounts != null) {
|
if (accounts != null) {
|
||||||
for (BaseAccount account : accounts) {
|
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();
|
}).start();
|
||||||
|
@ -125,13 +132,49 @@ public class PushHelper {
|
||||||
if (accounts == null) {
|
if (accounts == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
List<String> distributors = UnifiedPush.getDistributors(context, new ArrayList<>());
|
List<String> distributors = UnifiedPush.getDistributors(context);
|
||||||
if (distributors.size() == 1 || !UnifiedPush.getDistributor(context).isEmpty()) {
|
if (!distributors.isEmpty()) {
|
||||||
if (distributors.size() == 1) {
|
if (distributors.size() == 1) {
|
||||||
UnifiedPush.saveDistributor(context, distributors.get(0));
|
UnifiedPush.saveDistributor(context, distributors.get(0));
|
||||||
}
|
}
|
||||||
|
final OkHttpClient okHttpClient = Helper.myOkHttpClient(context.getApplicationContext());
|
||||||
for (BaseAccount account : accounts) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -143,7 +186,7 @@ public class PushHelper {
|
||||||
String distributor = distributorsStr[item];
|
String distributor = distributorsStr[item];
|
||||||
UnifiedPush.saveDistributor(context, distributor);
|
UnifiedPush.saveDistributor(context, distributor);
|
||||||
for (BaseAccount account : accounts) {
|
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();
|
dialog.dismiss();
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,14 +15,19 @@ package app.fedilab.android.mastodon.helper;
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
|
||||||
|
import static app.fedilab.android.mastodon.helper.Helper.TAG;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import org.unifiedpush.android.connector.data.PushEndpoint;
|
||||||
|
|
||||||
import java.net.IDN;
|
import java.net.IDN;
|
||||||
|
|
||||||
import app.fedilab.android.R;
|
import app.fedilab.android.R;
|
||||||
|
@ -41,22 +46,13 @@ import retrofit2.converter.gson.GsonConverterFactory;
|
||||||
public class PushNotifications {
|
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
|
SharedPreferences prefs = PreferenceManager
|
||||||
.getDefaultSharedPreferences(context);
|
.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 pubKey = pushEndpoint.getPubKeySet().getPubKey();
|
||||||
String auth = ecdh.getAuthKey();
|
String auth =pushEndpoint.getPubKeySet().getAuth();
|
||||||
|
|
||||||
|
|
||||||
boolean notif_follow = prefs.getBoolean(context.getString(R.string.SET_NOTIF_FOLLOW), true);
|
boolean notif_follow = prefs.getBoolean(context.getString(R.string.SET_NOTIF_FOLLOW), true);
|
||||||
|
@ -84,9 +80,10 @@ public class PushNotifications {
|
||||||
PushSubscription pushSubscription;
|
PushSubscription pushSubscription;
|
||||||
Call<PushSubscription> pushSubscriptionCall = mastodonNotificationsService.pushSubscription(
|
Call<PushSubscription> pushSubscriptionCall = mastodonNotificationsService.pushSubscription(
|
||||||
accountDb.token,
|
accountDb.token,
|
||||||
endpoint,
|
pushEndpoint.getUrl(),
|
||||||
pubKey,
|
pubKey,
|
||||||
auth,
|
auth,
|
||||||
|
true,
|
||||||
notif_follow,
|
notif_follow,
|
||||||
notif_fav,
|
notif_fav,
|
||||||
notif_share,
|
notif_share,
|
||||||
|
@ -101,6 +98,7 @@ public class PushNotifications {
|
||||||
Response<PushSubscription> pushSubscriptionResponse = pushSubscriptionCall.execute();
|
Response<PushSubscription> pushSubscriptionResponse = pushSubscriptionCall.execute();
|
||||||
if (pushSubscriptionResponse.isSuccessful()) {
|
if (pushSubscriptionResponse.isSuccessful()) {
|
||||||
pushSubscription = pushSubscriptionResponse.body();
|
pushSubscription = pushSubscriptionResponse.body();
|
||||||
|
|
||||||
if (pushSubscription != null) {
|
if (pushSubscription != null) {
|
||||||
pushSubscription.server_key = pushSubscription.server_key.replace('/', '_');
|
pushSubscription.server_key = pushSubscription.server_key.replace('/', '_');
|
||||||
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 androidx.preference.PreferenceScreen;
|
||||||
|
|
||||||
import org.unifiedpush.android.connector.UnifiedPush;
|
import org.unifiedpush.android.connector.UnifiedPush;
|
||||||
|
import org.unifiedpush.android.connector.internal.Store;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -138,8 +139,8 @@ public class FragmentNotificationsSettings extends PreferenceFragmentCompat impl
|
||||||
preferenceScreen.removePreferenceRecursively("SET_NOTIFICATION_DELAY_VALUE");
|
preferenceScreen.removePreferenceRecursively("SET_NOTIFICATION_DELAY_VALUE");
|
||||||
}
|
}
|
||||||
if (SET_PUSH_DISTRIBUTOR != null) {
|
if (SET_PUSH_DISTRIBUTOR != null) {
|
||||||
List<String> distributors = UnifiedPush.getDistributors(requireActivity(), new ArrayList<>());
|
List<String> distributors = UnifiedPush.getDistributors(requireActivity());
|
||||||
SET_PUSH_DISTRIBUTOR.setValue(UnifiedPush.getDistributor(requireActivity()));
|
SET_PUSH_DISTRIBUTOR.setValue("");
|
||||||
SET_PUSH_DISTRIBUTOR.setEntries(distributors.toArray(new String[0]));
|
SET_PUSH_DISTRIBUTOR.setEntries(distributors.toArray(new String[0]));
|
||||||
SET_PUSH_DISTRIBUTOR.setEntryValues(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.EmojiInstance;
|
||||||
import app.fedilab.android.mastodon.client.entities.api.Instance;
|
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.InstanceInfo;
|
||||||
|
import app.fedilab.android.mastodon.client.entities.api.InstanceV2;
|
||||||
import app.fedilab.android.mastodon.exception.DBException;
|
import app.fedilab.android.mastodon.exception.DBException;
|
||||||
import app.fedilab.android.mastodon.helper.Helper;
|
import app.fedilab.android.mastodon.helper.Helper;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
|
@ -50,6 +51,7 @@ public class InstancesVM extends AndroidViewModel {
|
||||||
final OkHttpClient okHttpClient = Helper.myOkHttpClient(getApplication().getApplicationContext());
|
final OkHttpClient okHttpClient = Helper.myOkHttpClient(getApplication().getApplicationContext());
|
||||||
private MutableLiveData<EmojiInstance> emojiInstanceMutableLiveData;
|
private MutableLiveData<EmojiInstance> emojiInstanceMutableLiveData;
|
||||||
private MutableLiveData<InstanceInfo> instanceInfoMutableLiveData;
|
private MutableLiveData<InstanceInfo> instanceInfoMutableLiveData;
|
||||||
|
private MutableLiveData<String> vapidMutableLiveData;
|
||||||
|
|
||||||
public InstancesVM(@NonNull Application application) {
|
public InstancesVM(@NonNull Application application) {
|
||||||
super(application);
|
super(application);
|
||||||
|
@ -65,6 +67,16 @@ public class InstancesVM extends AndroidViewModel {
|
||||||
return retrofit.create(MastodonInstanceService.class);
|
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) {
|
public LiveData<EmojiInstance> getEmoji(@NonNull String instance) {
|
||||||
MastodonInstanceService mastodonInstanceService = init(instance);
|
MastodonInstanceService mastodonInstanceService = init(instance);
|
||||||
emojiInstanceMutableLiveData = new MutableLiveData<>();
|
emojiInstanceMutableLiveData = new MutableLiveData<>();
|
||||||
|
@ -146,4 +158,34 @@ public class InstancesVM extends AndroidViewModel {
|
||||||
}).start();
|
}).start();
|
||||||
return instanceInfoMutableLiveData;
|
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,
|
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
import static app.fedilab.android.mastodon.helper.Helper.TAG;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
|
@ -309,7 +312,7 @@ public class NotificationsVM extends AndroidViewModel {
|
||||||
MastodonNotificationsService mastodonNotificationsService = init(instance);
|
MastodonNotificationsService mastodonNotificationsService = init(instance);
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
PushSubscription pushSubscription = null;
|
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) {
|
if (pushSubscriptionCall != null) {
|
||||||
try {
|
try {
|
||||||
Response<PushSubscription> pushSubscriptionResponse = pushSubscriptionCall.execute();
|
Response<PushSubscription> pushSubscriptionResponse = pushSubscriptionCall.execute();
|
||||||
|
|
|
@ -5,17 +5,6 @@
|
||||||
|
|
||||||
|
|
||||||
<application android:name=".MainApplication">
|
<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
|
<activity
|
||||||
android:name=".expandedcontrols.ExpandedControlsActivity"
|
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