From 81c4c6416747892a66259839a37decaedb47ace2 Mon Sep 17 00:00:00 2001 From: nuclearfog Date: Fri, 19 May 2023 23:27:14 +0200 Subject: [PATCH] added initial web push implementation, bug fix --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 20 ++- .../nuclearfog/twidda/ClientApplication.java | 21 +++ .../twidda/backend/api/Connection.java | 60 ++++--- .../twidda/backend/api/mastodon/Mastodon.java | 98 +++++++++++- .../api/mastodon/impl/MastodonPush.java | 151 ++++++++++++++++++ .../backend/api/twitter/v1/TwitterV1.java | 12 +- .../twidda/backend/async/MessageUpdater.java | 2 +- .../twidda/backend/async/PushUpdater.java | 42 +++++ .../twidda/backend/async/StatusUpdater.java | 4 +- .../twidda/backend/helper/MediaStatus.java | 31 ++-- .../backend/helper/update/MessageUpdate.java | 21 +-- .../backend/helper/update/ProfileUpdate.java | 35 ++-- .../backend/helper/update/PushUpdate.java | 101 ++++++++++++ .../backend/helper/update/StatusUpdate.java | 26 +-- .../twidda/config/GlobalSettings.java | 41 +++++ .../twidda/config/impl/ConfigPush.java | 142 ++++++++++++++++ .../org/nuclearfog/twidda/model/WebPush.java | 81 ++++++++++ .../receiver/PushNotificationReceiver.java | 33 ++++ .../ui/activities/UserlistActivity.java | 2 +- .../twidda/ui/views/AnimatedImageView.java | 2 + 21 files changed, 834 insertions(+), 92 deletions(-) create mode 100644 app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonPush.java create mode 100644 app/src/main/java/org/nuclearfog/twidda/backend/async/PushUpdater.java create mode 100644 app/src/main/java/org/nuclearfog/twidda/backend/helper/update/PushUpdate.java create mode 100644 app/src/main/java/org/nuclearfog/twidda/config/impl/ConfigPush.java create mode 100644 app/src/main/java/org/nuclearfog/twidda/model/WebPush.java create mode 100644 app/src/main/java/org/nuclearfog/twidda/receiver/PushNotificationReceiver.java diff --git a/app/build.gradle b/app/build.gradle index 16879331..9f7c5795 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -61,4 +61,5 @@ dependencies { implementation 'com.github.nuclearfog:Tagger:2.4' implementation 'com.github.nuclearfog:LinkAndScrollMovement:1.4.1' implementation 'com.github.kyleduo:SwitchButton:2.0.3-SNAPSHOT' + implementation 'com.github.UnifiedPush:android-connector:2.1.1' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 70192d57..00bb8ff5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,14 +10,11 @@ --> - - + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/ClientApplication.java b/app/src/main/java/org/nuclearfog/twidda/ClientApplication.java index 67b1955c..9e4fa868 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ClientApplication.java +++ b/app/src/main/java/org/nuclearfog/twidda/ClientApplication.java @@ -4,12 +4,33 @@ import android.app.Application; import org.nuclearfog.twidda.backend.image.ImageCache; import org.nuclearfog.twidda.backend.image.PicassoBuilder; +import org.unifiedpush.android.connector.ConstantsKt; +import org.unifiedpush.android.connector.UnifiedPush; + +import java.util.ArrayList; /** * @author nuclearfog */ public class ClientApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + ArrayList features = new ArrayList<>(1); + features.add(UnifiedPush.FEATURE_BYTES_MESSAGE); + UnifiedPush.registerApp(this, ConstantsKt.INSTANCE_DEFAULT, features, ""); + } + + + @Override + public void onTerminate() { + super.onTerminate(); + UnifiedPush.unregisterApp(this, ConstantsKt.INSTANCE_DEFAULT); + } + + @Override public void onLowMemory() { ImageCache.clear(); diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java index 07e470a2..0b73f0a1 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java @@ -2,6 +2,7 @@ package org.nuclearfog.twidda.backend.api; import org.nuclearfog.twidda.backend.helper.ConnectionConfig; import org.nuclearfog.twidda.backend.helper.MediaStatus; +import org.nuclearfog.twidda.backend.helper.update.PushUpdate; import org.nuclearfog.twidda.lists.Domains; import org.nuclearfog.twidda.lists.Messages; import org.nuclearfog.twidda.backend.helper.update.ProfileUpdate; @@ -24,6 +25,7 @@ import org.nuclearfog.twidda.model.Translation; import org.nuclearfog.twidda.model.Trend; import org.nuclearfog.twidda.model.User; import org.nuclearfog.twidda.model.UserList; +import org.nuclearfog.twidda.model.WebPush; import java.util.List; @@ -447,15 +449,6 @@ public interface Connection { */ void deleteStatus(long id) throws ConnectionException; - /** - * upload status with additional attachment - * - * @param update status update information - * @param mediaIds IDs of the uploaded media files if any - * @return uploaded status - */ - Status uploadStatus(StatusUpdate update, List mediaIds) throws ConnectionException; - /** * return a list of domain names the current user has blocked * @@ -478,6 +471,15 @@ public interface Connection { */ void unblockDomain(String domain) throws ConnectionException; + /** + * upload status with additional attachment + * + * @param update status update information + * @param mediaIds IDs of the uploaded media files if any + * @return uploaded status + */ + Status updateStatus(StatusUpdate update, List mediaIds) throws ConnectionException; + /** * create userlist * @@ -494,6 +496,30 @@ public interface Connection { */ UserList updateUserlist(UserListUpdate update) throws ConnectionException; + /** + * updates current user's profile + * + * @param update profile update information + * @return updated user information + */ + User updateProfile(ProfileUpdate update) throws ConnectionException; + + /** + * upload media file and generate a media ID + * + * @param mediaUpdate inputstream with MIME type of the media + * @return media ID + */ + long updateMedia(MediaStatus mediaUpdate) throws ConnectionException; + + /** + * create Web push subscription + * + * @param pushUpdate web push update + * @return created web push subscription + */ + WebPush updatePush(PushUpdate pushUpdate) throws ConnectionException; + /** * return userlist information * @@ -622,22 +648,6 @@ public interface Connection { */ MediaStatus downloadImage(String link) throws ConnectionException; - /** - * updates current user's profile - * - * @param update profile update information - * @return updated user information - */ - User updateProfile(ProfileUpdate update) throws ConnectionException; - - /** - * upload media file and generate a media ID - * - * @param mediaUpdate inputstream with MIME type of the media - * @return media ID - */ - long uploadMedia(MediaStatus mediaUpdate) throws ConnectionException; - /** * get notification of the current user * diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java index a0aa6464..ddc9b631 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java @@ -1,6 +1,7 @@ package org.nuclearfog.twidda.backend.api.mastodon; import android.content.Context; +import android.util.Base64; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -17,6 +18,7 @@ import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonInstance; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonList; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonNotification; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonPoll; +import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonPush; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonRelation; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonStatus; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonTranslation; @@ -24,6 +26,7 @@ import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonTrend; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonUser; import org.nuclearfog.twidda.backend.helper.ConnectionConfig; import org.nuclearfog.twidda.backend.helper.MediaStatus; +import org.nuclearfog.twidda.backend.helper.update.PushUpdate; import org.nuclearfog.twidda.lists.Domains; import org.nuclearfog.twidda.lists.Messages; import org.nuclearfog.twidda.backend.helper.update.PollUpdate; @@ -50,10 +53,19 @@ import org.nuclearfog.twidda.model.Translation; import org.nuclearfog.twidda.model.Trend; import org.nuclearfog.twidda.model.User; import org.nuclearfog.twidda.model.UserList; +import org.nuclearfog.twidda.model.WebPush; import java.io.IOException; import java.io.InputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECPoint; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -85,7 +97,7 @@ public class Mastodon implements Connection { /** * scopes used by this app */ - private static final String AUTH_SCOPES = "read%20write%20follow"; + private static final String AUTH_SCOPES = "read%20write%20follow%20push"; /** * oauth no redirect (oob) @@ -126,6 +138,7 @@ public class Mastodon implements Connection { private static final String ENDPOINT_CUSTOM_EMOJIS = "/api/v1/custom_emojis"; private static final String ENDPOINT_POLL = "/api/v1/polls/"; private static final String ENDPOINT_DOMAIN_BLOCK = "/api/v1/domain_blocks"; + private static final String ENDPOINT_PUSH_UPDATE = "/api/v1/push/subscription"; private static final MediaType TYPE_TEXT = MediaType.parse("text/plain"); private static final MediaType TYPE_STREAM = MediaType.parse("application/octet-stream"); @@ -166,7 +179,7 @@ public class Mastodon implements Connection { String client_id = json.getString("client_id"); String client_secret = json.getString("client_secret"); connection.setOauthTokens(client_id, client_secret); - return hostname + ENDPOINT_AUTHORIZE_APP + "?scope=read%20write%20follow&response_type=code&redirect_uri=" + REDIRECT_URI + "&client_id=" + client_id; + return hostname + ENDPOINT_AUTHORIZE_APP + "?scope=" + AUTH_SCOPES + "&response_type=code&redirect_uri=" + REDIRECT_URI + "&client_id=" + client_id; } throw new MastodonException(response); } catch (IOException | JSONException e) { @@ -625,7 +638,7 @@ public class Mastodon implements Connection { @Override - public Status uploadStatus(StatusUpdate update, List mediaIds) throws MastodonException { + public Status updateStatus(StatusUpdate update, List mediaIds) throws MastodonException { List params = new ArrayList<>(); // add identifier to prevent duplicate posts params.add("Idempotency-Key=" + System.currentTimeMillis() / 5000); @@ -972,7 +985,7 @@ public class Mastodon implements Connection { @Override - public long uploadMedia(MediaStatus mediaUpdate) throws MastodonException { + public long updateMedia(MediaStatus mediaUpdate) throws MastodonException { try { List params = new ArrayList<>(); if (!mediaUpdate.getDescription().isEmpty()) @@ -1004,6 +1017,65 @@ public class Mastodon implements Connection { } + @Override + public WebPush updatePush(PushUpdate pushUpdate) throws ConnectionException { + try { + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); + ECGenParameterSpec spec = new ECGenParameterSpec("prime256v1"); + generator.initialize(spec); + KeyPair keyPair = generator.generateKeyPair(); + byte[] privKeyData = keyPair.getPrivate().getEncoded(); + byte[] pubKeyData = keyPair.getPublic().getEncoded(); + byte[] serializedPubKey = serializeRawPublicKey((ECPublicKey) keyPair.getPublic()); + String encodedPublicKey = Base64.encodeToString(serializedPubKey, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + String pushPrivateKey = Base64.encodeToString(privKeyData, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + String pushPublicKey = Base64.encodeToString(pubKeyData, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + String randomString = StringUtils.getRandomString(); + + List params = new ArrayList<>(); + params.add("subscription[endpoint]=" + pushUpdate.getEndpoint()); + params.add("subscription[keys][p256dh]=" + encodedPublicKey); + params.add("subscription[keys][auth]=" + randomString); + if (pushUpdate.enableMentions()) + params.add("data[alerts][mention]=true"); + if (pushUpdate.enableFavorite()) + params.add("data[alerts][favourite]=true"); + if (pushUpdate.enableRepost()) + params.add("data[alerts][reblog]=true"); + if (pushUpdate.enableFollow()) + params.add("data[alerts][follow]=true"); + if (pushUpdate.enableFollowRequest()) + params.add("data[alerts][follow_request]=true"); + if (pushUpdate.enablePoll()) + params.add("data[alerts][poll]=true"); + if (pushUpdate.enableStatus()) + params.add("data[alerts][status]=true"); + if (pushUpdate.enableStatusEdit()) + params.add("data[alerts][update]=true"); + if (pushUpdate.getPolicy() == PushUpdate.POLICY_ALL) + params.add("data[policy]=all"); + else if (pushUpdate.getPolicy() == PushUpdate.POLICY_FOLLOWER) + params.add("data[policy]=follower"); + else if (pushUpdate.getPolicy() == PushUpdate.POLICY_FOLLOWING) + params.add("data[policy]=followed"); + else if (pushUpdate.getPolicy() == PushUpdate.POLICY_NONE) + params.add("data[policy]=none"); + Response response = post(ENDPOINT_PUSH_UPDATE, params); + ResponseBody body = response.body(); + if (response.code() == 200 && body != null) { + JSONObject json = new JSONObject(body.string()); + MastodonPush result = new MastodonPush(json); + result.setKeys(pushPublicKey, pushPrivateKey); + result.setAuthSecret(randomString); + return result; + } + throw new MastodonException(response); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | JSONException | IOException e) { + throw new MastodonException(e); + } + } + + @Override public Notifications getNotifications(long minId, long maxId) throws ConnectionException { List params = new ArrayList<>(); @@ -1624,4 +1696,22 @@ public class Mastodon implements Connection { } return hostname + endpoint; } + + /** + * + */ + private byte[] serializeRawPublicKey(ECPublicKey key) { + ECPoint point = 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; + } } \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonPush.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonPush.java new file mode 100644 index 00000000..ffc42b6c --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonPush.java @@ -0,0 +1,151 @@ +package org.nuclearfog.twidda.backend.api.mastodon.impl; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; +import org.nuclearfog.twidda.model.WebPush; + +/** + * Mastodon push implementation + * + * @author nuclearfog + */ +public class MastodonPush implements WebPush { + + private static final long serialVersionUID = 565081495547561476L; + + private long id; + private String endpoint; + private String serverKey, publicKey, privateKey, authSec; + + /** + * @param json web push json object + */ + public MastodonPush(JSONObject json) throws JSONException { + String id = json.getString("id"); + endpoint = json.getString("endpoint"); + serverKey = json.getString("server_key"); + try { + this.id = Long.parseLong(id); + } catch (NumberFormatException e) { + throw new JSONException("bad ID: " + id); + } + } + + + @Override + public long getId() { + return id; + } + + + @Override + public String getEndpoint() { + return endpoint; + } + + + @Override + public String getServerKey() { + return serverKey; + } + + + @Override + public String getPublicKey() { + return publicKey; + } + + + @Override + public String getPrivateKey() { + return privateKey; + } + + + @Override + public String getAuthSecret() { + return authSec; + } + + + @Override + public boolean alertMentionEnabled() { + return false; + } + + + @Override + public boolean alertStatusEnabled() { + return false; + } + + + @Override + public boolean alertRepostEnabled() { + return false; + } + + + @Override + public boolean alertFollowingEnabled() { + return false; + } + + + @Override + public boolean alertFollowRequestEnabled() { + return false; + } + + + @Override + public boolean alertFavoriteEnabled() { + return false; + } + + + @Override + public boolean alertPollEnabled() { + return false; + } + + + @Override + public boolean alertStatusChangeEnabled() { + return false; + } + + + @NonNull + @Override + public String toString() { + return "id=" + getId() + " url=\"" + getEndpoint() + "\""; + } + + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof WebPush)) + return false; + WebPush push = (WebPush) obj; + return getId() == push.getId() && getEndpoint().equals(push.getEndpoint()); + } + + /** + * set encryption keys + */ + public void setKeys(String publicKey, String privateKey) { + this.publicKey = publicKey; + this.privateKey = privateKey; + } + + /** + * set auth key + */ + public void setAuthSecret(String authSec) { + this.authSec = authSec; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/twitter/v1/TwitterV1.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/twitter/v1/TwitterV1.java index 7dd18d9d..9ad06ca3 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/twitter/v1/TwitterV1.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/twitter/v1/TwitterV1.java @@ -23,6 +23,7 @@ import org.nuclearfog.twidda.backend.api.twitter.v1.impl.UserListV1; import org.nuclearfog.twidda.backend.api.twitter.v1.impl.UserV1; import org.nuclearfog.twidda.backend.helper.ConnectionConfig; import org.nuclearfog.twidda.backend.helper.MediaStatus; +import org.nuclearfog.twidda.backend.helper.update.PushUpdate; import org.nuclearfog.twidda.lists.Domains; import org.nuclearfog.twidda.lists.Messages; import org.nuclearfog.twidda.backend.helper.update.ProfileUpdate; @@ -49,6 +50,7 @@ import org.nuclearfog.twidda.model.Translation; import org.nuclearfog.twidda.model.Trend; import org.nuclearfog.twidda.model.User; import org.nuclearfog.twidda.model.UserList; +import org.nuclearfog.twidda.model.WebPush; import java.io.IOException; import java.io.InputStream; @@ -746,7 +748,7 @@ public class TwitterV1 implements Connection { @Override - public Status uploadStatus(StatusUpdate update, List mediaIds) throws TwitterException { + public Status updateStatus(StatusUpdate update, List mediaIds) throws TwitterException { List params = new ArrayList<>(); if (update.getText() != null) params.add("status=" + StringUtils.encode(update.getText())); @@ -1012,7 +1014,7 @@ public class TwitterV1 implements Connection { @Override - public long uploadMedia(MediaStatus mediaUpdate) throws TwitterException { + public long updateMedia(MediaStatus mediaUpdate) throws TwitterException { List params = new ArrayList<>(); boolean enableChunk; final long mediaId; @@ -1096,6 +1098,12 @@ public class TwitterV1 implements Connection { } + @Override + public WebPush updatePush(PushUpdate pushUpdate) throws TwitterException { + throw new TwitterException("not implemented"); + } + + @Override public MediaStatus downloadImage(String link) throws TwitterException { try { diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/async/MessageUpdater.java b/app/src/main/java/org/nuclearfog/twidda/backend/async/MessageUpdater.java index b56c1232..10a1937e 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/async/MessageUpdater.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/async/MessageUpdater.java @@ -37,7 +37,7 @@ public class MessageUpdater extends AsyncExecutor { + + private Connection connection; + private GlobalSettings settings; + + /** + * + */ + public PushUpdater(Context context) { + connection = ConnectionManager.getDefaultConnection(context); + settings = GlobalSettings.getInstance(context); + } + + + @Override + protected Void doInBackground(@NonNull PushUpdate param) { + try { + WebPush webpush = connection.updatePush(param); + settings.setWebPush(webpush); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/async/StatusUpdater.java b/app/src/main/java/org/nuclearfog/twidda/backend/async/StatusUpdater.java index ea6abe85..c7f012dd 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/async/StatusUpdater.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/async/StatusUpdater.java @@ -41,12 +41,12 @@ public class StatusUpdater extends AsyncExecutor mediaIds = new LinkedList<>(); for (MediaStatus mediaStatus : update.getMediaStatuses()) { if (mediaStatus.isLocal()) { - long mediaId = connection.uploadMedia(mediaStatus); + long mediaId = connection.updateMedia(mediaStatus); mediaIds.add(mediaId); } } // upload status - Status status = connection.uploadStatus(update, mediaIds); + Status status = connection.updateStatus(update, mediaIds); return new StatusUpdateResult(status, null); } catch (ConnectionException exception) { return new StatusUpdateResult(null, exception); diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/helper/MediaStatus.java b/app/src/main/java/org/nuclearfog/twidda/backend/helper/MediaStatus.java index 87a22a2b..dce48986 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/helper/MediaStatus.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/helper/MediaStatus.java @@ -6,6 +6,7 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; @@ -15,7 +16,7 @@ import java.io.Serializable; * * @author nuclearfog */ -public class MediaStatus implements Serializable { +public class MediaStatus implements Serializable, Closeable { private static final long serialVersionUID = 6824278073662885637L; @@ -50,6 +51,20 @@ public class MediaStatus implements Serializable { local = true; } + /** + * close stream + */ + @Override + public void close() { + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + // ignore + } + } + /** * create a stream to upload media file * @@ -113,20 +128,6 @@ public class MediaStatus implements Serializable { return local; } - /** - * close stream - */ - public void close() { - try { - if (inputStream != null) { - inputStream.close(); - } - } catch (IOException e) { - // ignore - } - } - - @NonNull @Override public String toString() { diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/MessageUpdate.java b/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/MessageUpdate.java index 116277f0..73d7eb7d 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/MessageUpdate.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/MessageUpdate.java @@ -11,6 +11,7 @@ import androidx.documentfile.provider.DocumentFile; import org.nuclearfog.twidda.backend.helper.MediaStatus; import org.nuclearfog.twidda.model.Instance; +import java.io.Closeable; import java.io.Serializable; import java.util.Arrays; import java.util.TreeSet; @@ -20,7 +21,7 @@ import java.util.TreeSet; * * @author nuclearfog */ -public class MessageUpdate implements Serializable { +public class MessageUpdate implements Serializable, Closeable { private static final long serialVersionUID = 991295406939128220L; @@ -36,6 +37,16 @@ public class MessageUpdate implements Serializable { private TreeSet supportedFormats = new TreeSet<>(); + /** + * close inputstream of media file + */ + @Override + public void close() { + if (mediaUpdate != null) { + mediaUpdate.close(); + } + } + /** * @param name screen name of the user */ @@ -135,14 +146,6 @@ public class MessageUpdate implements Serializable { return instance; } - /** - * close inputstream of media file - */ - public void close() { - if (mediaUpdate != null) { - mediaUpdate.close(); - } - } @NonNull @Override diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/ProfileUpdate.java b/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/ProfileUpdate.java index c240c728..e74dd8e2 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/ProfileUpdate.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/ProfileUpdate.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.documentfile.provider.DocumentFile; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; @@ -17,7 +18,7 @@ import java.io.InputStream; * * @author nuclearfog */ -public class ProfileUpdate { +public class ProfileUpdate implements Closeable { private Uri[] imageUrls = new Uri[2]; private InputStream[] imageStreams = new InputStream[2]; @@ -27,6 +28,23 @@ public class ProfileUpdate { private String description = ""; private String location = ""; + + /** + * close all image streams + */ + @Override + public void close() { + try { + for (InputStream imageStream : imageStreams) { + if (imageStream != null) { + imageStream.close(); + } + } + } catch (IOException e) { + // ignore + } + } + /** * setup profile information * @@ -151,21 +169,6 @@ public class ProfileUpdate { return true; } - /** - * close all image streams - */ - public void close() { - try { - for (InputStream imageStream : imageStreams) { - if (imageStream != null) { - imageStream.close(); - } - } - } catch (IOException e) { - // ignore - } - } - @NonNull @Override diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/PushUpdate.java b/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/PushUpdate.java new file mode 100644 index 00000000..20b12abc --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/PushUpdate.java @@ -0,0 +1,101 @@ +package org.nuclearfog.twidda.backend.helper.update; + +import java.io.Serializable; + +/** + * Webpush updater class used to create a webpush subscription + * @see org.nuclearfog.twidda.backend.api.Connection + * + * @author nuclearfog + */ +public class PushUpdate implements Serializable { + + private static final long serialVersionUID = -34599486422177957L; + + /** + * show all notifications + */ + public static final int POLICY_ALL = 1; + + /** + * show only notifications of followed users + */ + public static final int POLICY_FOLLOWING = 2; + + /** + * show only notifications of followers + */ + public static final int POLICY_FOLLOWER = 3; + + /** + * disable push notification + */ + public static final int POLICY_NONE = 4; + + private String endpoint; + private boolean notifyMention, notifyStatus, notifyFollow, notifyFollowRequest; + private boolean notifyFavorite, notifyRepost, notifyPoll, notifyEdit; + private int policy; + + /** + * + */ + public PushUpdate(String endpoint) { + int idx = endpoint.indexOf('?'); + if (idx > 0) { + this.endpoint = endpoint.substring(0, idx); + } else { + this.endpoint = endpoint; + } + } + + + public String getEndpoint() { + return endpoint; + } + + + public boolean enableMentions() { + return notifyMention; + } + + + public boolean enableStatus() { + return notifyStatus; + } + + + public boolean enableStatusEdit() { + return notifyEdit; + } + + + public boolean enableRepost() { + return notifyRepost; + } + + + public boolean enableFavorite() { + return notifyFavorite; + } + + + public boolean enablePoll() { + return notifyPoll; + } + + + public boolean enableFollow() { + return notifyFollow; + } + + + public boolean enableFollowRequest() { + return notifyFollowRequest; + } + + + public int getPolicy() { + return policy; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/StatusUpdate.java b/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/StatusUpdate.java index d840516e..12b656b7 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/StatusUpdate.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/StatusUpdate.java @@ -14,6 +14,7 @@ import org.nuclearfog.twidda.model.Instance; import org.nuclearfog.twidda.model.Media; import org.nuclearfog.twidda.model.Status; +import java.io.Closeable; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; @@ -25,7 +26,7 @@ import java.util.TreeSet; * * @author nuclearfog */ -public class StatusUpdate implements Serializable { +public class StatusUpdate implements Serializable, Closeable { private static final long serialVersionUID = -5300983806882462557L; @@ -93,6 +94,18 @@ public class StatusUpdate implements Serializable { private boolean attachmentLimitReached = false; private int attachment = EMPTY; + /** + * close all open streams + */ + @Override + public void close() { + for (MediaStatus mediaUpdate : mediaStatuses) { + if (mediaUpdate != null) { + mediaUpdate.close(); + } + } + } + /** * set informations of an existing status to edit these * @@ -465,17 +478,6 @@ public class StatusUpdate implements Serializable { return true; } - /** - * close all open streams - */ - public void close() { - for (MediaStatus mediaUpdate : mediaStatuses) { - if (mediaUpdate != null) { - mediaUpdate.close(); - } - } - } - @NonNull @Override diff --git a/app/src/main/java/org/nuclearfog/twidda/config/GlobalSettings.java b/app/src/main/java/org/nuclearfog/twidda/config/GlobalSettings.java index 2be24809..b4cbe7d3 100644 --- a/app/src/main/java/org/nuclearfog/twidda/config/GlobalSettings.java +++ b/app/src/main/java/org/nuclearfog/twidda/config/GlobalSettings.java @@ -12,8 +12,10 @@ import androidx.annotation.Nullable; import org.nuclearfog.twidda.config.impl.ConfigAccount; import org.nuclearfog.twidda.config.impl.ConfigLocation; +import org.nuclearfog.twidda.config.impl.ConfigPush; import org.nuclearfog.twidda.model.Account; import org.nuclearfog.twidda.model.Location; +import org.nuclearfog.twidda.model.WebPush; import java.util.LinkedList; import java.util.List; @@ -85,6 +87,12 @@ public class GlobalSettings { private static final String PROXY_PASS = "proxy_pass"; private static final String TREND_LOC = "location"; private static final String TREND_ID = "world_id_long"; + private static final String PUSH_ID = "push_id"; + private static final String PUSH_SERVER_HOST = "push_server_host"; + private static final String PUSH_SERVER_KEY = "push_server_key"; + private static final String PUSH_PUBLIC_KEY = "push_public_key"; + private static final String PUSH_PRIVATE_KEY = "push_private_key"; + private static final String PUSH_AUTH_KEY = "push_auth_key"; private static final String ENABLE_LIKE = "like_enable"; private static final String ENABLE_TWITTER_ALT = "twitter_alt_set"; private static final String FILTER_RESULTS = "filter_results"; @@ -125,6 +133,7 @@ public class GlobalSettings { private SharedPreferences settings; private Location location; + private ConfigPush webPush; private ConfigAccount login; private String proxyHost, proxyPort; private String proxyUser, proxyPass; @@ -547,6 +556,31 @@ public class GlobalSettings { edit.apply(); } + /** + * get used web push instance + */ + public WebPush getWebPush() { + return webPush; + } + + /** + * save web push configuration + * + * @param webPush web push information + */ + public void setWebPush(WebPush webPush) { + this.webPush = new ConfigPush(webPush); + + Editor edit = settings.edit(); + edit.putLong(PUSH_ID, webPush.getId()); + edit.putString(PUSH_SERVER_KEY, webPush.getServerKey()); + edit.putString(PUSH_SERVER_HOST, webPush.getEndpoint()); + edit.putString(PUSH_PUBLIC_KEY, webPush.getPublicKey()); + edit.putString(PUSH_PRIVATE_KEY, webPush.getPrivateKey()); + edit.putString(PUSH_AUTH_KEY, webPush.getAuthSecret()); + edit.apply(); + } + /** * get loading limit of tweets/users @@ -967,7 +1001,14 @@ public class GlobalSettings { proxyPass = settings.getString(PROXY_PASS, ""); String place = settings.getString(TREND_LOC, DEFAULT_LOCATION_NAME); long woeId = settings.getLong(TREND_ID, DEFAULT_LOCATION_ID); + long pushID = settings.getLong(PUSH_ID, 0L); + String pushServerKey = settings.getString(PUSH_SERVER_KEY, ""); + String pushServerHost = settings.getString(PUSH_SERVER_HOST, ""); + String pushPublicKey = settings.getString(PUSH_PUBLIC_KEY, ""); + String pushPrivateKey = settings.getString(PUSH_PRIVATE_KEY, ""); + String pushAuthKey = settings.getString(PUSH_AUTH_KEY, ""); location = new ConfigLocation(woeId, place); + webPush = new ConfigPush(pushID, pushServerHost, pushServerKey, pushPublicKey, pushPrivateKey, pushAuthKey); // login informations initLogin(); } diff --git a/app/src/main/java/org/nuclearfog/twidda/config/impl/ConfigPush.java b/app/src/main/java/org/nuclearfog/twidda/config/impl/ConfigPush.java new file mode 100644 index 00000000..c7fb57a5 --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/config/impl/ConfigPush.java @@ -0,0 +1,142 @@ +package org.nuclearfog.twidda.config.impl; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.nuclearfog.twidda.model.WebPush; + +/** + * @author nuclearfog + */ +public class ConfigPush implements WebPush { + + private static final long serialVersionUID = -6942479639448210795L; + + private long id; + private String endpoint; + private String serverKey, publicKey, privateKey, authKey; + + /** + * @param webPush web push instance to copy information + */ + public ConfigPush(WebPush webPush) { + id = webPush.getId(); + endpoint = webPush.getEndpoint(); + serverKey = webPush.getServerKey(); + publicKey = webPush.getPublicKey(); + privateKey = webPush.getPrivateKey(); + authKey = webPush.getAuthSecret(); + } + + /** + * + */ + public ConfigPush(long id, String endpoint, String serverKey, String publicKey, String privateKey, String authKey) { + this.id = id; + this.endpoint = endpoint; + this.serverKey = serverKey; + this.privateKey = privateKey; + this.publicKey = publicKey; + this.authKey = authKey; + } + + + @Override + public long getId() { + return id; + } + + + @Override + public String getEndpoint() { + return endpoint; + } + + + @Override + public String getServerKey() { + return serverKey; + } + + + @Override + public String getPublicKey() { + return publicKey; + } + + + @Override + public String getPrivateKey() { + return privateKey; + } + + + @Override + public String getAuthSecret() { + return authKey; + } + + + @Override + public boolean alertMentionEnabled() { + return false; + } + + + @Override + public boolean alertStatusEnabled() { + return false; + } + + + @Override + public boolean alertRepostEnabled() { + return false; + } + + + @Override + public boolean alertFollowingEnabled() { + return false; + } + + + @Override + public boolean alertFollowRequestEnabled() { + return false; + } + + + @Override + public boolean alertFavoriteEnabled() { + return false; + } + + + @Override + public boolean alertPollEnabled() { + return false; + } + + + @Override + public boolean alertStatusChangeEnabled() { + return false; + } + + + @NonNull + @Override + public String toString() { + return "id=" + getId() + " url=\"" + getEndpoint() + "\""; + } + + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof WebPush)) + return false; + WebPush push = (WebPush) obj; + return getId() == push.getId() && getEndpoint().equals(push.getEndpoint()); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/model/WebPush.java b/app/src/main/java/org/nuclearfog/twidda/model/WebPush.java new file mode 100644 index 00000000..73a9d2a6 --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/model/WebPush.java @@ -0,0 +1,81 @@ +package org.nuclearfog.twidda.model; + +import java.io.Serializable; + +/** + * Represents a web push subscription. + * + * @author nuclearfog + */ +public interface WebPush extends Serializable { + + /** + * @return ID of the subscription + */ + long getId(); + + /** + * @return webpush host url + */ + String getEndpoint(); + + /** + * @return unique server key set from {@link org.nuclearfog.twidda.backend.api.Connection} + */ + String getServerKey(); + + /** + * @return encryption public key + */ + String getPublicKey(); + + /** + * @return encryption public key + */ + String getPrivateKey(); + + /** + * @return auth secret + */ + String getAuthSecret(); + + /** + * @return true if notification for mentions is enabled + */ + boolean alertMentionEnabled(); + + /** + * @return true if status notification (profile subscription) is enabled + */ + boolean alertStatusEnabled(); + + /** + * @return true if 'status reposted' notification is enabled + */ + boolean alertRepostEnabled(); + + /** + * @return true if 'new follower' notification is enabled + */ + boolean alertFollowingEnabled(); + + /** + * @return true if 'follow request' notification is enabled + */ + boolean alertFollowRequestEnabled(); + + /** + * @return true if 'status favorited' notification is enabled + */ + boolean alertFavoriteEnabled(); + + /** + * @return true if 'poll finished' notification is enabled + */ + boolean alertPollEnabled(); + + /** + * @return true if 'status changed' notification is enabled + */ + boolean alertStatusChangeEnabled(); +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/receiver/PushNotificationReceiver.java b/app/src/main/java/org/nuclearfog/twidda/receiver/PushNotificationReceiver.java new file mode 100644 index 00000000..8fd88b84 --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/receiver/PushNotificationReceiver.java @@ -0,0 +1,33 @@ +package org.nuclearfog.twidda.receiver; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import org.nuclearfog.twidda.backend.async.PushUpdater; +import org.nuclearfog.twidda.backend.helper.update.PushUpdate; +import org.unifiedpush.android.connector.MessagingReceiver; + +/** + * Push notification receiver used to trigger synchronization. + * + * @author nuclearfog + */ +public class PushNotificationReceiver extends MessagingReceiver { + + + @Override + public void onMessage(@NonNull Context context, @NonNull byte[] message, @NonNull String instance) { + super.onMessage(context, message, instance); + // todo add manual synchonization + } + + + @Override + public void onNewEndpoint(@NonNull Context context, @NonNull String endpoint, @NonNull String instance) { + super.onNewEndpoint(context, endpoint, instance); + PushUpdater pushUpdater = new PushUpdater(context); + PushUpdate update = new PushUpdate(endpoint); + pushUpdater.execute(update, null); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/activities/UserlistActivity.java b/app/src/main/java/org/nuclearfog/twidda/ui/activities/UserlistActivity.java index 765acd6a..d295cf57 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ui/activities/UserlistActivity.java +++ b/app/src/main/java/org/nuclearfog/twidda/ui/activities/UserlistActivity.java @@ -76,7 +76,7 @@ public class UserlistActivity extends AppCompatActivity implements ActivityResul * regex pattern to validate username * e.g. username, @username or @username@instance.social */ - private static final Pattern USERNAME_PATTERN = Pattern.compile("@?[\\w\\d]{1,20}(@[\\w\\d.]{1,50})?"); + private static final Pattern USERNAME_PATTERN = Pattern.compile("@?\\w{1,20}(@[\\w.]{1,50})?"); private ActivityResultLauncher activityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), this); diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/views/AnimatedImageView.java b/app/src/main/java/org/nuclearfog/twidda/ui/views/AnimatedImageView.java index c3a588c0..36523a61 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ui/views/AnimatedImageView.java +++ b/app/src/main/java/org/nuclearfog/twidda/ui/views/AnimatedImageView.java @@ -48,6 +48,7 @@ public class AnimatedImageView extends AppCompatImageView { * @inheritDoc */ @Override + @SuppressWarnings("deprecation") public void setImageURI(@Nullable Uri uri) { ContentResolver resolver = getContext().getContentResolver(); String mime = resolver.getType(uri); @@ -66,6 +67,7 @@ public class AnimatedImageView extends AppCompatImageView { * @inheritDoc */ @Override + @SuppressWarnings("deprecation") protected void onDraw(Canvas canvas) { if (movie != null) { // calculate scale and offsets