diff --git a/app/build.gradle b/app/build.gradle index b4f3360cf..686d93680 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -173,6 +173,11 @@ dependencies { //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2' implementation 'com.huangyz0918:androidwm-light:0.1.2' + implementation "com.madgag.spongycastle:bctls-jdk15on:1.58.0.0" + implementation "com.madgag.spongycastle:prov:1.58.0.0" + implementation "com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0" + implementation "com.madgag.spongycastle:bcpg-jdk15on:1.58.0.0" + implementation 'com.github.UnifiedPush:android-connector:1.0.0' //Flavors //Playstore diff --git a/app/src/common/AndroidManifest.xml b/app/src/common/AndroidManifest.xml index d08f233c2..397a7c259 100644 --- a/app/src/common/AndroidManifest.xml +++ b/app/src/common/AndroidManifest.xml @@ -84,5 +84,20 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 140b7d0cb..31711a6bf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -460,6 +460,18 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/activities/BaseMainActivity.java b/app/src/main/java/app/fedilab/android/activities/BaseMainActivity.java index 862e18587..99f48562f 100644 --- a/app/src/main/java/app/fedilab/android/activities/BaseMainActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/BaseMainActivity.java @@ -35,6 +35,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.preference.PreferenceManager; +import android.util.Log; import android.util.Patterns; import android.view.LayoutInflater; import android.view.Menu; @@ -95,6 +96,7 @@ import java.util.regex.Matcher; import app.fedilab.android.BuildConfig; import app.fedilab.android.R; import app.fedilab.android.asynctasks.ManageFiltersAsyncTask; +import app.fedilab.android.asynctasks.PostSubscriptionAsyncTask; import app.fedilab.android.asynctasks.RetrieveAccountsAsyncTask; import app.fedilab.android.asynctasks.RetrieveFeedsAsyncTask; import app.fedilab.android.asynctasks.RetrieveInstanceAsyncTask; @@ -112,6 +114,7 @@ import app.fedilab.android.client.Entities.Error; import app.fedilab.android.client.Entities.Filters; import app.fedilab.android.client.Entities.Instance; import app.fedilab.android.client.Entities.ManageTimelines; +import app.fedilab.android.client.Entities.PushSubscription; import app.fedilab.android.client.Entities.Relationship; import app.fedilab.android.client.Entities.Results; import app.fedilab.android.client.Entities.Status; @@ -137,6 +140,7 @@ import app.fedilab.android.helper.ExpandableHeightListView; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MenuFloating; import app.fedilab.android.interfaces.OnFilterActionInterface; +import app.fedilab.android.interfaces.OnPostSubscription; import app.fedilab.android.interfaces.OnRetrieveEmojiAccountInterface; import app.fedilab.android.interfaces.OnRetrieveFeedsInterface; import app.fedilab.android.interfaces.OnRetrieveInstanceInterface; @@ -161,7 +165,7 @@ import static app.fedilab.android.helper.Helper.changeDrawableColor; public abstract class BaseMainActivity extends BaseActivity - implements NavigationView.OnNavigationItemSelectedListener, OnRetrieveFeedsInterface, OnUpdateAccountInfoInterface, OnRetrieveMetaDataInterface, OnRetrieveInstanceInterface, OnRetrieveRemoteAccountInterface, OnRetrieveEmojiAccountInterface, OnFilterActionInterface, OnSyncTimelineInterface, OnRetrieveRelationshipInterface { + implements NavigationView.OnNavigationItemSelectedListener, OnRetrieveFeedsInterface, OnUpdateAccountInfoInterface, OnRetrieveMetaDataInterface, OnRetrieveInstanceInterface, OnRetrieveRemoteAccountInterface, OnRetrieveEmojiAccountInterface, OnFilterActionInterface, OnSyncTimelineInterface, OnRetrieveRelationshipInterface, OnPostSubscription { public static String currentLocale; @@ -249,6 +253,7 @@ public abstract class BaseMainActivity extends BaseActivity e.printStackTrace(); } + if (account == null) { Helper.logoutCurrentUser(BaseMainActivity.this); Intent myIntent = new Intent(BaseMainActivity.this, LoginActivity.class); @@ -417,6 +422,12 @@ public abstract class BaseMainActivity extends BaseActivity add_new = findViewById(R.id.add_new); main_app_container = findViewById(R.id.main_app_container); + + + Log.v(Helper.TAG, "social: " + social); + if (social == UpdateAccountInfoAsyncTask.SOCIAL.MASTODON || social == UpdateAccountInfoAsyncTask.SOCIAL.PLEROMA) { + new PostSubscriptionAsyncTask(BaseMainActivity.this, this); + } if (social == UpdateAccountInfoAsyncTask.SOCIAL.MASTODON || social == UpdateAccountInfoAsyncTask.SOCIAL.PLEROMA || social == UpdateAccountInfoAsyncTask.SOCIAL.GNU || social == UpdateAccountInfoAsyncTask.SOCIAL.FRIENDICA) { new SyncTimelinesAsyncTask(BaseMainActivity.this, 0, Helper.canFetchList(BaseMainActivity.this, account), BaseMainActivity.this); @@ -2421,6 +2432,11 @@ public abstract class BaseMainActivity extends BaseActivity protected abstract void launchOwnerNotificationsActivity(); + @Override + public void onSubscription(APIResponse apiResponse) { + + } + public enum iconLauncher { BUBBLES, FEDIVERSE, diff --git a/app/src/main/java/app/fedilab/android/asynctasks/PostSubscriptionAsyncTask.java b/app/src/main/java/app/fedilab/android/asynctasks/PostSubscriptionAsyncTask.java new file mode 100644 index 000000000..aca5ff329 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/asynctasks/PostSubscriptionAsyncTask.java @@ -0,0 +1,51 @@ +/* Copyright 2021 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 . */ +package app.fedilab.android.asynctasks; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import java.lang.ref.WeakReference; + +import app.fedilab.android.client.API; +import app.fedilab.android.client.APIResponse; +import app.fedilab.android.interfaces.OnPostSubscription; + + +public class PostSubscriptionAsyncTask { + + private final OnPostSubscription listener; + private final WeakReference contextReference; + private APIResponse apiResponse; + + + public PostSubscriptionAsyncTask(Context context, OnPostSubscription onPostSubscription) { + this.contextReference = new WeakReference<>(context); + this.listener = onPostSubscription; + doInBackground(); + } + + + protected void doInBackground() { + new Thread(() -> { + apiResponse = new API(contextReference.get()).pushSubscription(); + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> listener.onSubscription(apiResponse); + mainHandler.post(myRunnable); + }).start(); + } + +} diff --git a/app/src/main/java/app/fedilab/android/client/API.java b/app/src/main/java/app/fedilab/android/client/API.java index dd312924c..47e8f77dd 100644 --- a/app/src/main/java/app/fedilab/android/client/API.java +++ b/app/src/main/java/app/fedilab/android/client/API.java @@ -22,9 +22,12 @@ import android.os.Build; import android.os.Bundle; import android.text.Html; import android.text.SpannableString; +import android.util.Base64; +import android.util.Log; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -46,6 +49,7 @@ import java.net.URLEncoder; import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; +import java.security.Security; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -58,6 +62,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Random; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -88,6 +93,7 @@ import app.fedilab.android.client.Entities.Notification; import app.fedilab.android.client.Entities.Peertube; import app.fedilab.android.client.Entities.Poll; import app.fedilab.android.client.Entities.PollOptions; +import app.fedilab.android.client.Entities.PushSubscription; import app.fedilab.android.client.Entities.Reaction; import app.fedilab.android.client.Entities.Relationship; import app.fedilab.android.client.Entities.Report; @@ -99,7 +105,9 @@ import app.fedilab.android.client.Entities.Tag; import app.fedilab.android.client.Entities.Trends; import app.fedilab.android.client.Entities.TrendsHistory; import app.fedilab.android.fragments.DisplayNotificationsFragment; +import app.fedilab.android.helper.ECDH; import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.PushNotifications; import app.fedilab.android.sqlite.AccountDAO; import app.fedilab.android.sqlite.Sqlite; import app.fedilab.android.sqlite.TimelineCacheDAO; @@ -555,6 +563,34 @@ public class API { return reactions; } + + /** + * Parse a push notification + * + * @param resobj JSONObject + * @return PushSubscription + */ + private static PushSubscription parsePushNotifications(JSONObject resobj) { + PushSubscription pushSubscription = new PushSubscription(); + try { + pushSubscription.setId(resobj.getString("id")); + pushSubscription.setEndpoint(resobj.getString("endpoint")); + pushSubscription.setServer_key(resobj.getString("server_key")); + JSONObject alertsObject = resobj.getJSONObject("alerts"); + Iterator iter = alertsObject.keys(); + HashMap alertsList = new HashMap<>(); + while (iter.hasNext()) { + String key = iter.next(); + boolean value = (boolean) alertsObject.get(key); + alertsList.put(key, value); + } + pushSubscription.setAlertsList(alertsList); + } catch (JSONException e) { + e.printStackTrace(); + } + return pushSubscription; + } + /** * Parse a reaction * @@ -5745,6 +5781,52 @@ public class API { return apiResponse; } + /** + * Subscribe to push notifications + * + * @return APIResponse + */ + public APIResponse pushSubscription() { + PushSubscription pushSubscription = new PushSubscription(); + final SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); + boolean notif_follow = sharedpreferences.getBoolean(Helper.SET_NOTIF_FOLLOW, true); + boolean notif_add = sharedpreferences.getBoolean(Helper.SET_NOTIF_ADD, true); + boolean notif_mention = sharedpreferences.getBoolean(Helper.SET_NOTIF_MENTION, true); + boolean notif_share = sharedpreferences.getBoolean(Helper.SET_NOTIF_SHARE, true); + boolean notif_status = sharedpreferences.getBoolean(Helper.SET_NOTIF_STATUS, true); + boolean notif_poll = sharedpreferences.getBoolean(Helper.SET_NOTIF_POLL, true); + + HashMap params = new HashMap<>(); + params.put("data[alerts][follow]", String.valueOf(notif_follow)); + params.put("data[alerts][mention]", String.valueOf(notif_mention)); + params.put("data[alerts][favourite]", String.valueOf(notif_add)); + params.put("data[alerts][reblog]", String.valueOf(notif_share)); + params.put("data[alerts][poll]", String.valueOf(notif_poll)); + + params.put("subscription[endpoint]", getAbsoluteUrl("/streaming/user")); + + ECDH ecdh = ECDH.getInstance(); + String pubKey = ecdh.getPublicKey(context); + byte[] randBytes = new byte[16]; + new Random().nextBytes(randBytes); + String auth = Base64.encodeToString(randBytes, Base64.DEFAULT); + params.put("subscription[keys][p256dh]", pubKey); + params.put("subscription[keys][auth]", auth); + Log.v(Helper.TAG, "params: " + params); + try { + String response = new HttpsConnection(context, this.instance).post(getAbsoluteUrl("/push/subscription"), 10, params, prefKeyOauthTokenT); + Log.v(Helper.TAG, "response: " + response); + pushSubscription = parsePushNotifications(new JSONObject(response)); + } catch (HttpsConnection.HttpsConnectionException e) { + setError(e.getStatusCode(), e); + e.printStackTrace(); + } catch (NoSuchAlgorithmException | IOException | KeyManagementException | JSONException e) { + e.printStackTrace(); + } + apiResponse.setPushSubscription(pushSubscription); + return apiResponse; + } + /** * Update a list by its id * diff --git a/app/src/main/java/app/fedilab/android/client/APIResponse.java b/app/src/main/java/app/fedilab/android/client/APIResponse.java index 635eaeed2..62327c10e 100644 --- a/app/src/main/java/app/fedilab/android/client/APIResponse.java +++ b/app/src/main/java/app/fedilab/android/client/APIResponse.java @@ -35,6 +35,7 @@ import app.fedilab.android.client.Entities.PeertubeNotification; import app.fedilab.android.client.Entities.PixelFedStory; import app.fedilab.android.client.Entities.PixelFedStoryItem; import app.fedilab.android.client.Entities.Playlist; +import app.fedilab.android.client.Entities.PushSubscription; import app.fedilab.android.client.Entities.Relationship; import app.fedilab.android.client.Entities.Report; import app.fedilab.android.client.Entities.Results; @@ -56,6 +57,7 @@ public class APIResponse { private List notifications = null; private List relationships = null; private List announcements = null; + private PushSubscription pushSubscription; private String targetedId = null; private Results results = null; private List howToVideos = null; @@ -85,6 +87,14 @@ public class APIResponse { return accounts; } + public PushSubscription getPushSubscription() { + return pushSubscription; + } + + public void setPushSubscription(PushSubscription pushSubscription) { + this.pushSubscription = pushSubscription; + } + public void setAccounts(List accounts) { this.accounts = accounts; } diff --git a/app/src/main/java/app/fedilab/android/client/Entities/PushSubscription.java b/app/src/main/java/app/fedilab/android/client/Entities/PushSubscription.java new file mode 100644 index 000000000..2a3505bde --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/Entities/PushSubscription.java @@ -0,0 +1,57 @@ +package app.fedilab.android.client.Entities; +/* Copyright 2021 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 . */ + +import java.util.HashMap; + +public class PushSubscription { + private String id; + private String endpoint; + private HashMap alerts; + private String server_key; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public HashMap getAlertsList() { + return alerts; + } + + public void setAlertsList(HashMap alertsList) { + this.alerts = alertsList; + } + + public String getServer_key() { + return server_key; + } + + public void setServer_key(String server_key) { + this.server_key = server_key; + } + +} diff --git a/app/src/main/java/app/fedilab/android/client/HttpsConnection.java b/app/src/main/java/app/fedilab/android/client/HttpsConnection.java index 6327b8335..0b71fc085 100644 --- a/app/src/main/java/app/fedilab/android/client/HttpsConnection.java +++ b/app/src/main/java/app/fedilab/android/client/HttpsConnection.java @@ -474,6 +474,63 @@ public class HttpsConnection { } + + String postJson(String urlConnection, int timeout, JSONObject jsonObject, String token) throws IOException, NoSuchAlgorithmException, KeyManagementException, HttpsConnectionException { + + URL url = new URL(urlConnection); + byte[] postDataBytes; + postDataBytes = jsonObject.toString().getBytes(StandardCharsets.UTF_8); + if (proxy != null) + httpURLConnection = (HttpURLConnection) url.openConnection(proxy); + else + httpURLConnection = (HttpURLConnection) url.openConnection(); + httpURLConnection.setRequestProperty("User-Agent", USER_AGENT); + httpURLConnection.setConnectTimeout(timeout * 1000); + httpURLConnection.setDoOutput(true); + if (httpURLConnection instanceof HttpsURLConnection) { + ((HttpsURLConnection) httpURLConnection).setSSLSocketFactory(new TLSSocketFactory(this.instance)); + } + httpURLConnection.setRequestProperty("Content-Type", "application/json"); + httpURLConnection.setRequestProperty("Accept", "application/json"); + httpURLConnection.setRequestMethod("POST"); + setToken(token); + httpURLConnection.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length)); + + + httpURLConnection.getOutputStream().write(postDataBytes); + String response; + if (httpURLConnection.getResponseCode() >= 200 && httpURLConnection.getResponseCode() < 400) { + getSinceMaxId(); + response = converToString(httpURLConnection.getInputStream()); + } else { + String error = null; + if (httpURLConnection.getErrorStream() != null) { + InputStream stream = httpURLConnection.getErrorStream(); + if (stream == null) { + stream = httpURLConnection.getInputStream(); + } + try (Scanner scanner = new Scanner(stream)) { + scanner.useDelimiter("\\Z"); + if (scanner.hasNext()) { + error = scanner.next(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + int responseCode = httpURLConnection.getResponseCode(); + try { + httpURLConnection.getInputStream().close(); + } catch (Exception ignored) { + } + throw new HttpsConnectionException(responseCode, error); + } + getSinceMaxId(); + httpURLConnection.getInputStream().close(); + return response; + + } + @SuppressWarnings("SameParameterValue") String postMisskey(String urlConnection, int timeout, JSONObject paramaters, String token) throws IOException, NoSuchAlgorithmException, KeyManagementException, HttpsConnectionException { URL url = new URL(urlConnection); diff --git a/app/src/main/java/app/fedilab/android/drawers/BaseStatusListAdapter.java b/app/src/main/java/app/fedilab/android/drawers/BaseStatusListAdapter.java index f4106d6f3..e7bbe1524 100644 --- a/app/src/main/java/app/fedilab/android/drawers/BaseStatusListAdapter.java +++ b/app/src/main/java/app/fedilab/android/drawers/BaseStatusListAdapter.java @@ -640,10 +640,27 @@ public abstract class BaseStatusListAdapter extends RecyclerView.Adapter. */ +package app.fedilab.android.interfaces; + +import app.fedilab.android.client.APIResponse; + +public interface OnPostSubscription { + void onSubscription(APIResponse apiResponse); +} diff --git a/app/src/main/java/app/fedilab/android/services/UnifiedPushService.java b/app/src/main/java/app/fedilab/android/services/UnifiedPushService.java new file mode 100644 index 000000000..d8d85fc3e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/services/UnifiedPushService.java @@ -0,0 +1,45 @@ +package app.fedilab.android.services; + +import android.content.Context; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.unifiedpush.android.connector.MessagingReceiver; +import org.unifiedpush.android.connector.MessagingReceiverHandler; + +import app.fedilab.android.helper.PushNotifications; + +class handler implements MessagingReceiverHandler { + @Override + public void onNewEndpoint(@Nullable Context context, @NotNull String s) { + PushNotifications push = new PushNotifications(); + push.registerPushNotifications(context, s); + } + + @Override + public void onRegistrationFailed(@Nullable Context context) { + // Toast ? + } + + @Override + public void onRegistrationRefused(@Nullable Context context) { + // Toast ? + } + + @Override + public void onUnregistered(@Nullable Context context) { + // Remove endpoint & ServerKey + } + + @Override + public void onMessage(@Nullable Context context, @NotNull String s) { + PushNotifications push = new PushNotifications(); + push.displayNotification(context, s); + } +} + +class UnifiedPushService extends MessagingReceiver { + public UnifiedPushService() { + super(new handler()); + } +} diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 16f7dc261..d8fa2cf2d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -130,4 +130,13 @@ #f3f3f3 #606984 + + + #ff0000 + #ffa500 + #ffff00 + #008000 + #0000ff + #4b0082 + #ee82ee diff --git a/build.gradle b/build.gradle index c95742af2..318aa1c47 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' + classpath 'com.android.tools.build:gradle:4.1.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -17,6 +17,7 @@ allprojects { repositories { jcenter() google() + maven { url 'https://jitpack.io' } } }