added initial web push implementation, bug fix

This commit is contained in:
nuclearfog 2023-05-19 23:27:14 +02:00
parent 9eb1943e5d
commit 81c4c64167
No known key found for this signature in database
GPG Key ID: 03488A185C476379
21 changed files with 834 additions and 92 deletions

View File

@ -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'
}

View File

@ -10,14 +10,11 @@
-->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" tools:ignore="ScopedStorage" />
<application
android:name=".ClientApplication"
@ -128,6 +125,19 @@
android:screenOrientation="portrait"
android:theme="@style/AppTheme" />
<receiver
android:name=".receiver.PushNotificationReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/>
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED"/>
</intent-filter>
</receiver>
</application>
</manifest>

View File

@ -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<String> 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();

View File

@ -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<Long> 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<Long> 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
*

View File

@ -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<Long> mediaIds) throws MastodonException {
public Status updateStatus(StatusUpdate update, List<Long> mediaIds) throws MastodonException {
List<String> 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<String> 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<String> 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<String> 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;
}
}

View File

@ -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;
}
}

View File

@ -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<Long> mediaIds) throws TwitterException {
public Status updateStatus(StatusUpdate update, List<Long> mediaIds) throws TwitterException {
List<String> 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<String> 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 {

View File

@ -37,7 +37,7 @@ public class MessageUpdater extends AsyncExecutor<MessageUpdate, MessageUpdater.
// upload media if any
long mediaId = 0L;
if (update.getMediaUpdate() != null) {
mediaId = connection.uploadMedia(update.getMediaUpdate());
mediaId = connection.updateMedia(update.getMediaUpdate());
}
// upload message and media ID
connection.sendDirectmessage(id, update.getMessage(), mediaId);

View File

@ -0,0 +1,42 @@
package org.nuclearfog.twidda.backend.async;
import android.content.Context;
import androidx.annotation.NonNull;
import org.nuclearfog.twidda.backend.api.Connection;
import org.nuclearfog.twidda.backend.api.ConnectionManager;
import org.nuclearfog.twidda.backend.helper.update.PushUpdate;
import org.nuclearfog.twidda.config.GlobalSettings;
import org.nuclearfog.twidda.model.WebPush;
/**
* Async class used to update push information
*
* @author nuclearfog
*/
public class PushUpdater extends AsyncExecutor <PushUpdate, Void> {
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;
}
}

View File

@ -41,12 +41,12 @@ public class StatusUpdater extends AsyncExecutor<StatusUpdate, StatusUpdater.Sta
List<Long> 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);

View File

@ -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() {

View File

@ -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<String> 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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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();
}

View File

@ -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());
}
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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<Intent> activityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), this);

View File

@ -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