diff --git a/app/.gitignore b/app/.gitignore
index 61e52b44..d4026ab8 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -1,3 +1,4 @@
/build
app-release.apk
app-google-release.apk
+src/main/res/raw/keystore_tusky_api.bks
diff --git a/app/build.gradle b/app/build.gradle
index cf42c979..cb33758a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -12,14 +12,6 @@ android {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary true
}
- productFlavors {
- google {
- buildConfigField "boolean", "USES_PUSH_NOTIFICATIONS", "true"
- }
- fdroid {
- buildConfigField "boolean", "USES_PUSH_NOTIFICATIONS", "false"
- }
- }
buildTypes {
release {
minifyEnabled true
@@ -59,10 +51,10 @@ dependencies {
compile 'com.github.arimorty:floatingsearchview:2.0.4'
compile 'com.theartofdev.edmodo:android-image-cropper:2.4.3'
compile 'com.jakewharton:butterknife:8.5.1'
- googleCompile 'com.google.firebase:firebase-messaging:10.2.4'
- googleCompile 'com.google.firebase:firebase-crash:10.2.4'
+ compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0'
+ compile('org.eclipse.paho:org.eclipse.paho.android.service:1.1.1') {
+ exclude module: 'support-v4'
+ }
testCompile 'junit:junit:4.12'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'
}
-
-apply plugin: 'com.google.gms.google-services'
diff --git a/app/google-services.json b/app/google-services.json
deleted file mode 100644
index bfd19f88..00000000
--- a/app/google-services.json
+++ /dev/null
@@ -1,55 +0,0 @@
-{
- "project_info": {
- "project_number": "268851337880",
- "firebase_url": "https://tusky-62772.firebaseio.com",
- "project_id": "tusky-62772",
- "storage_bucket": "tusky-62772.appspot.com"
- },
- "client": [
- {
- "client_info": {
- "mobilesdk_app_id": "1:268851337880:android:fc4111b1d145a00e",
- "android_client_info": {
- "package_name": "com.keylesspalace.tusky"
- }
- },
- "oauth_client": [
- {
- "client_id": "268851337880-eie2ssto2d21bfihn9d1qupcrke8oebf.apps.googleusercontent.com",
- "client_type": 1,
- "android_info": {
- "package_name": "com.keylesspalace.tusky",
- "certificate_hash": "18d196307d6e928e99c2e0bb9818c01c38aff2f9"
- }
- },
- {
- "client_id": "268851337880-n19d05m282nirs1fc9kdd5n4of6je4fk.apps.googleusercontent.com",
- "client_type": 3
- }
- ],
- "api_key": [
- {
- "current_key": "AIzaSyCbJtSjuk4I3Jy8PdUaO3TaQOXubcOUElo"
- }
- ],
- "services": {
- "analytics_service": {
- "status": 1
- },
- "appinvite_service": {
- "status": 2,
- "other_platform_oauth_client": [
- {
- "client_id": "268851337880-n19d05m282nirs1fc9kdd5n4of6je4fk.apps.googleusercontent.com",
- "client_type": 3
- }
- ]
- },
- "ads_service": {
- "status": 2
- }
- }
- }
- ],
- "configuration_version": "1"
-}
\ No newline at end of file
diff --git a/app/src/fdroid/AndroidManifest.xml b/app/src/fdroid/AndroidManifest.xml
deleted file mode 100644
index b7060e6e..00000000
--- a/app/src/fdroid/AndroidManifest.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/fdroid/java/com/keylesspalace/tusky/MessagingService.java b/app/src/fdroid/java/com/keylesspalace/tusky/MessagingService.java
deleted file mode 100644
index eaa1b1b5..00000000
--- a/app/src/fdroid/java/com/keylesspalace/tusky/MessagingService.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/* Copyright 2017 Andrew Dawson
- *
- * This file is a part of Tusky.
- *
- * 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.
- *
- * Tusky 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 Tusky; if not,
- * see . */
-
-package com.keylesspalace.tusky;
-
-import android.app.IntentService;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-import android.text.Spanned;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-
-import com.keylesspalace.tusky.entity.Notification;
-import com.keylesspalace.tusky.json.SpannedTypeAdapter;
-import com.keylesspalace.tusky.json.StringWithEmoji;
-import com.keylesspalace.tusky.json.StringWithEmojiTypeAdapter;
-import com.keylesspalace.tusky.network.MastodonAPI;
-import com.keylesspalace.tusky.util.NotificationMaker;
-import com.keylesspalace.tusky.util.OkHttpUtils;
-
-import java.util.HashSet;
-import java.util.List;
-
-import java.io.IOException;
-import java.util.Set;
-
-import okhttp3.Interceptor;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-import retrofit2.Retrofit;
-import retrofit2.converter.gson.GsonConverterFactory;
-
-public class MessagingService extends IntentService {
- public static final int NOTIFY_ID = 6; // This is an arbitrary number.
-
- private MastodonAPI mastodonAPI;
-
- public MessagingService() {
- super("Tusky Pull Notification Service");
- }
-
- @Override
- protected void onHandleIntent(Intent intent) {
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
- getApplicationContext());
- boolean enabled = preferences.getBoolean("notificationsEnabled", true);
- if (!enabled) {
- return;
- }
-
- createMastodonApi();
-
- mastodonAPI.notifications(null, null, null).enqueue(new Callback>() {
- @Override
- public void onResponse(Call> call,
- Response> response) {
- if (response.isSuccessful()) {
- onNotificationsReceived(response.body());
- }
- }
-
- @Override
- public void onFailure(Call> call, Throwable t) {}
- });
- }
-
- private void createMastodonApi() {
- SharedPreferences preferences = getSharedPreferences(
- getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
- final String domain = preferences.getString("domain", null);
- final String accessToken = preferences.getString("accessToken", null);
-
- OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder()
- .addInterceptor(new Interceptor() {
- @Override
- public okhttp3.Response intercept(Chain chain) throws IOException {
- Request originalRequest = chain.request();
-
- Request.Builder builder = originalRequest.newBuilder()
- .header("Authorization", String.format("Bearer %s", accessToken));
-
- Request newRequest = builder.build();
-
- return chain.proceed(newRequest);
- }
- })
- .build();
-
- Gson gson = new GsonBuilder()
- .registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
- .registerTypeAdapter(StringWithEmoji.class, new StringWithEmojiTypeAdapter())
- .create();
-
- Retrofit retrofit = new Retrofit.Builder()
- .baseUrl("https://" + domain)
- .client(okHttpClient)
- .addConverterFactory(GsonConverterFactory.create(gson))
- .build();
-
- mastodonAPI = retrofit.create(MastodonAPI.class);
- }
-
- private void onNotificationsReceived(List notificationList) {
- SharedPreferences notificationsPreferences = getSharedPreferences(
- "Notifications", Context.MODE_PRIVATE);
- Set currentIds = notificationsPreferences.getStringSet(
- "current_ids", new HashSet());
- for (Notification notification : notificationList) {
- String id = notification.id;
- if (!currentIds.contains(id)) {
- currentIds.add(id);
- NotificationMaker.make(this, NOTIFY_ID, notification);
- }
- }
- notificationsPreferences.edit()
- .putStringSet("current_ids", currentIds)
- .apply();
- }
-
- public static String getInstanceToken() {
- // This is only used for the "google" build flavor, so this version is just a stub method.
- return null;
- }
-}
diff --git a/app/src/google/AndroidManifest.xml b/app/src/google/AndroidManifest.xml
deleted file mode 100644
index 20ecbe94..00000000
--- a/app/src/google/AndroidManifest.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/google/java/com/keylesspalace/tusky/MessagingService.java b/app/src/google/java/com/keylesspalace/tusky/MessagingService.java
deleted file mode 100644
index 4a2a55da..00000000
--- a/app/src/google/java/com/keylesspalace/tusky/MessagingService.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/* Copyright 2017 Andrew Dawson
- *
- * This file is a part of Tusky.
- *
- * 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.
- *
- * Tusky 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 Tusky; if not,
- * see .
- *
- * If you modify this Program, or any covered work, by linking or combining it with Firebase Cloud
- * Messaging and Firebase Crash Reporting (or a modified version of those libraries), containing
- * parts covered by the Google APIs Terms of Service, the licensors of this Program grant you
- * additional permission to convey the resulting work. */
-
-package com.keylesspalace.tusky;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-import android.text.Spanned;
-
-import com.google.firebase.iid.FirebaseInstanceId;
-import com.google.firebase.messaging.FirebaseMessagingService;
-import com.google.firebase.messaging.RemoteMessage;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-
-import com.keylesspalace.tusky.entity.Notification;
-import com.keylesspalace.tusky.json.SpannedTypeAdapter;
-import com.keylesspalace.tusky.json.StringWithEmoji;
-import com.keylesspalace.tusky.json.StringWithEmojiTypeAdapter;
-import com.keylesspalace.tusky.network.MastodonAPI;
-import com.keylesspalace.tusky.util.Log;
-import com.keylesspalace.tusky.util.NotificationMaker;
-import com.keylesspalace.tusky.util.OkHttpUtils;
-
-import java.io.IOException;
-
-import okhttp3.Interceptor;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-import retrofit2.Retrofit;
-import retrofit2.converter.gson.GsonConverterFactory;
-
-public class MessagingService extends FirebaseMessagingService {
- private MastodonAPI mastodonAPI;
- private static final String TAG = "MessagingService";
- public static final int NOTIFY_ID = 666;
-
- @Override
- public void onMessageReceived(RemoteMessage remoteMessage) {
- Log.d(TAG, remoteMessage.getFrom());
- Log.d(TAG, remoteMessage.toString());
-
- String notificationId = remoteMessage.getData().get("notification_id");
-
- if (notificationId == null) {
- Log.e(TAG, "No notification ID in payload!!");
- return;
- }
-
- Log.d(TAG, notificationId);
-
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
- getApplicationContext());
- boolean enabled = preferences.getBoolean("notificationsEnabled", true);
- if (!enabled) {
- return;
- }
-
- createMastodonAPI();
-
- mastodonAPI.notification(notificationId).enqueue(new Callback() {
- @Override
- public void onResponse(Call call, Response response) {
- if (response.isSuccessful()) {
- NotificationMaker.make(MessagingService.this, NOTIFY_ID, response.body());
- }
- }
-
- @Override
- public void onFailure(Call call, Throwable t) {}
- });
- }
-
- private void createMastodonAPI() {
- SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
- final String domain = preferences.getString("domain", null);
- final String accessToken = preferences.getString("accessToken", null);
-
- OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder()
- .addInterceptor(new Interceptor() {
- @Override
- public okhttp3.Response intercept(Chain chain) throws IOException {
- Request originalRequest = chain.request();
-
- Request.Builder builder = originalRequest.newBuilder()
- .header("Authorization", String.format("Bearer %s", accessToken));
-
- Request newRequest = builder.build();
-
- return chain.proceed(newRequest);
- }
- })
- .build();
-
- Gson gson = new GsonBuilder()
- .registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
- .registerTypeAdapter(StringWithEmoji.class, new StringWithEmojiTypeAdapter())
- .create();
-
- Retrofit retrofit = new Retrofit.Builder()
- .baseUrl("https://" + domain)
- .client(okHttpClient)
- .addConverterFactory(GsonConverterFactory.create(gson))
- .build();
-
- mastodonAPI = retrofit.create(MastodonAPI.class);
- }
-
- public static String getInstanceToken() {
- return FirebaseInstanceId.getInstance().getToken();
- }
-}
diff --git a/app/src/google/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java b/app/src/google/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java
deleted file mode 100644
index 14d640d5..00000000
--- a/app/src/google/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/* Copyright 2017 Andrew Dawson
- *
- * This file is a part of Tusky.
- *
- * 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.
- *
- * Tusky 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 Tusky; if not,
- * see .
- *
- * If you modify this Program, or any covered work, by linking or combining it with Firebase Cloud
- * Messaging and Firebase Crash Reporting (or a modified version of those libraries), containing
- * parts covered by the Google APIs Terms of Service, the licensors of this Program grant you
- * additional permission to convey the resulting work. */
-
-package com.keylesspalace.tusky;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-
-import com.google.firebase.iid.FirebaseInstanceId;
-import com.google.firebase.iid.FirebaseInstanceIdService;
-import com.keylesspalace.tusky.network.TuskyAPI;
-import com.keylesspalace.tusky.util.Log;
-import com.keylesspalace.tusky.util.OkHttpUtils;
-
-import okhttp3.ResponseBody;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-import retrofit2.Retrofit;
-
-public class MyFirebaseInstanceIdService extends FirebaseInstanceIdService {
- private static final String TAG = "com.keylesspalace.tusky.MyFirebaseInstanceIdService";
-
- private TuskyAPI tuskyAPI;
-
- protected void createTuskyAPI() {
- Retrofit retrofit = new Retrofit.Builder()
- .baseUrl(getString(R.string.tusky_api_url))
- .client(OkHttpUtils.getCompatibleClient())
- .build();
-
- tuskyAPI = retrofit.create(TuskyAPI.class);
- }
-
- @Override
- public void onTokenRefresh() {
- createTuskyAPI();
-
- String refreshedToken = FirebaseInstanceId.getInstance().getToken();
- SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
- String accessToken = preferences.getString("accessToken", null);
- String domain = preferences.getString("domain", null);
-
- if (accessToken != null && domain != null) {
- tuskyAPI.unregister("https://" + domain, accessToken).enqueue(new Callback() {
- @Override
- public void onResponse(Call call, Response response) {
- Log.d(TAG, response.message());
- }
-
- @Override
- public void onFailure(Call call, Throwable t) {
- Log.d(TAG, t.getMessage());
- }
- });
- tuskyAPI.register("https://" + domain, accessToken, refreshedToken).enqueue(new Callback() {
- @Override
- public void onResponse(Call call, Response response) {
- Log.d(TAG, response.message());
- }
-
- @Override
- public void onFailure(Call call, Throwable t) {
- Log.d(TAG, t.getMessage());
- }
- });
- }
- }
-}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 058d165b..cbe15faa 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,15 +6,18 @@
-
+
+
+
+
+ android:theme="@style/AppTheme">
+
() {
- @Override
- public void onResponse(Call call, retrofit2.Response response) {
- Log.d(TAG, "Enable push notifications response: " + response.message());
+ Callback callback = new Callback() {
+ @Override
+ public void onResponse(Call call,
+ retrofit2.Response response) {
+ if (response.isSuccessful()) {
+ pushNotificationClient.subscribeToTopic(getPushNotificationTopic());
+ pushNotificationClient.connect(BaseActivity.this);
+ } else {
+ onEnablePushNotificationsFailure(response.message());
}
+ }
- @Override
- public void onFailure(Call call, Throwable t) {
- Log.d(TAG, "Enable push notifications failed: " + t.getMessage());
- }
- });
- } else {
- // Start up the MessagingService on a repeating interval for "pull" notifications.
- long checkInterval = 60 * 1000 * 5;
- AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
- Intent intent = new Intent(this, MessagingService.class);
- final int SERVICE_REQUEST_CODE = 8574603; // This number is arbitrary.
- serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE, intent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
- SystemClock.elapsedRealtime(), checkInterval, serviceAlarmIntent);
- }
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ onEnablePushNotificationsFailure(t.getMessage());
+ }
+ };
+ String deviceToken = pushNotificationClient.getDeviceToken();
+ Session session = new Session(getDomain(), getAccessToken(), deviceToken);
+ tuskyApi.register(session)
+ .enqueue(callback);
+ }
+
+ private void onEnablePushNotificationsFailure(String message) {
+ Log.e(TAG, "Enabling push notifications failed. " + message);
}
protected void disablePushNotifications() {
- if (BuildConfig.USES_PUSH_NOTIFICATIONS) {
- tuskyAPI.unregister(getBaseUrl(), getAccessToken()).enqueue(new Callback() {
- @Override
- public void onResponse(Call call, retrofit2.Response response) {
- Log.d(TAG, "Disable push notifications response: " + response.message());
+ Callback callback = new Callback() {
+ @Override
+ public void onResponse(Call call,
+ retrofit2.Response response) {
+ if (response.isSuccessful()) {
+ pushNotificationClient.unsubscribeToTopic(getPushNotificationTopic());
+ } else {
+ onDisablePushNotificationsFailure();
}
+ }
- @Override
- public void onFailure(Call call, Throwable t) {
- Log.d(TAG, "Disable push notifications failed: " + t.getMessage());
- }
- });
- } else if (serviceAlarmIntent != null) {
- // Cancel the repeating call for "pull" notifications.
- AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
- alarmManager.cancel(serviceAlarmIntent);
- }
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ onDisablePushNotificationsFailure();
+ }
+ };
+ String deviceToken = pushNotificationClient.getDeviceToken();
+ Session session = new Session(getDomain(), getAccessToken(), deviceToken);
+ tuskyApi.unregister(session)
+ .enqueue(callback);
+ }
+
+ private void onDisablePushNotificationsFailure() {
+ Log.e(TAG, "Disabling push notifications failed.");
+ }
+
+ private String getPushNotificationTopic() {
+ return String.format("%s/%s/#", getDomain(), getAccessToken());
+ }
+
+ private String getDomain() {
+ return getPrivatePreferences()
+ .getString("domain", null);
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java
index f0089fd2..319257b4 100644
--- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java
@@ -307,7 +307,9 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
if (replyVisibility != null && startingVisibility != null) {
// Lowest possible visibility setting in response
- if (startingVisibility.equals("private") || replyVisibility.equals("private")) {
+ if (startingVisibility.equals("direct") || replyVisibility.equals("direct")) {
+ startingVisibility = "direct";
+ } else if (startingVisibility.equals("private") || replyVisibility.equals("private")) {
startingVisibility = "private";
} else if (startingVisibility.equals("unlisted") || replyVisibility.equals("unlisted")) {
startingVisibility = "unlisted";
diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java
index 3d39b81a..f0a93c23 100644
--- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java
@@ -15,7 +15,6 @@
package com.keylesspalace.tusky;
-import android.app.NotificationManager;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -215,8 +214,7 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove
.putString("current", "[]")
.apply();
- ((NotificationManager) (getSystemService(NOTIFICATION_SERVICE)))
- .cancel(MessagingService.NOTIFY_ID);
+ pushNotificationClient.clearNotifications(this);
/* After editing a profile, the profile header in the navigation drawer needs to be
* refreshed */
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Session.java b/app/src/main/java/com/keylesspalace/tusky/entity/Session.java
new file mode 100644
index 00000000..8a3c5d0b
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Session.java
@@ -0,0 +1,28 @@
+/* Copyright 2017 Andrew Dawson
+ *
+ * This file is a part of Tusky.
+ *
+ * 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.
+ *
+ * Tusky 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 Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.entity;
+
+public class Session {
+ public String instanceUrl;
+ public String accessToken;
+ public String deviceToken;
+
+ public Session(String instanceUrl, String accessToken, String deviceToken) {
+ this.instanceUrl = instanceUrl;
+ this.accessToken = accessToken;
+ this.deviceToken = deviceToken;
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TuskyAPI.java b/app/src/main/java/com/keylesspalace/tusky/network/TuskyApi.java
similarity index 67%
rename from app/src/main/java/com/keylesspalace/tusky/network/TuskyAPI.java
rename to app/src/main/java/com/keylesspalace/tusky/network/TuskyApi.java
index 700de83c..24384dcf 100644
--- a/app/src/main/java/com/keylesspalace/tusky/network/TuskyAPI.java
+++ b/app/src/main/java/com/keylesspalace/tusky/network/TuskyApi.java
@@ -15,17 +15,16 @@
package com.keylesspalace.tusky.network;
+import com.keylesspalace.tusky.entity.Session;
+
import okhttp3.ResponseBody;
import retrofit2.Call;
-import retrofit2.http.Field;
-import retrofit2.http.FormUrlEncoded;
+import retrofit2.http.Body;
import retrofit2.http.POST;
-public interface TuskyAPI {
- @FormUrlEncoded
+public interface TuskyApi {
@POST("/register")
- Call register(@Field("instance_url") String instanceUrl, @Field("access_token") String accessToken, @Field("device_token") String deviceToken);
- @FormUrlEncoded
+ Call register(@Body Session session);
@POST("/unregister")
- Call unregister(@Field("instance_url") String instanceUrl, @Field("access_token") String accessToken);
+ Call unregister(@Body Session session);
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/PushNotificationClient.java b/app/src/main/java/com/keylesspalace/tusky/util/PushNotificationClient.java
new file mode 100644
index 00000000..f3df738f
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/PushNotificationClient.java
@@ -0,0 +1,237 @@
+package com.keylesspalace.tusky.util;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.text.Spanned;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.keylesspalace.tusky.R;
+import com.keylesspalace.tusky.entity.Notification;
+import com.keylesspalace.tusky.json.SpannedTypeAdapter;
+import com.keylesspalace.tusky.json.StringWithEmoji;
+import com.keylesspalace.tusky.json.StringWithEmojiTypeAdapter;
+
+import org.eclipse.paho.android.service.MqttAndroidClient;
+import org.eclipse.paho.client.mqttv3.DisconnectedBufferOptions;
+import org.eclipse.paho.client.mqttv3.IMqttActionListener;
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.IMqttToken;
+import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
+import org.eclipse.paho.client.mqttv3.MqttClient;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+
+import java.io.InputStream;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+
+import static android.content.Context.NOTIFICATION_SERVICE;
+
+public class PushNotificationClient {
+ private static final String TAG = "PushNotificationClient";
+ private static final int NOTIFY_ID = 666;
+
+ private static class QueuedAction {
+ enum Type {
+ SUBSCRIBE,
+ UNSUBSCRIBE,
+ DISCONNECT,
+ }
+
+ Type type;
+ String topic;
+
+ QueuedAction(Type type) {
+ this.type = type;
+ }
+
+ QueuedAction(Type type, String topic) {
+ this.type = type;
+ this.topic = topic;
+ }
+ }
+
+ private MqttAndroidClient mqttAndroidClient;
+ private ArrayDeque queuedActions;
+ private ArrayList subscribedTopics;
+
+ public PushNotificationClient(final @NonNull Context applicationContext,
+ @NonNull String serverUri) {
+ queuedActions = new ArrayDeque<>();
+ subscribedTopics = new ArrayList<>();
+
+ // Create the MQTT client.
+ String clientId = MqttClient.generateClientId();
+ mqttAndroidClient = new MqttAndroidClient(applicationContext, serverUri, clientId);
+ mqttAndroidClient.setCallback(new MqttCallbackExtended() {
+ @Override
+ public void connectComplete(boolean reconnect, String serverURI) {
+ if (reconnect) {
+ flushQueuedActions();
+ for (String topic : subscribedTopics) {
+ subscribeToTopic(topic);
+ }
+ }
+ }
+
+ @Override
+ public void connectionLost(Throwable cause) {
+ onConnectionLost();
+ }
+
+ @Override
+ public void messageArrived(String topic, MqttMessage message) throws Exception {
+ onMessageReceived(applicationContext, new String(message.getPayload()));
+ }
+
+ @Override
+ public void deliveryComplete(IMqttDeliveryToken token) {
+ // This client is read-only, so this is unused.
+ }
+ });
+ }
+
+ private void flushQueuedActions() {
+ while (!queuedActions.isEmpty()) {
+ QueuedAction action = queuedActions.pop();
+ switch (action.type) {
+ case SUBSCRIBE: subscribeToTopic(action.topic); break;
+ case UNSUBSCRIBE: unsubscribeToTopic(action.topic); break;
+ case DISCONNECT: disconnect(); break;
+ }
+ }
+ }
+
+ /** Connect to the MQTT broker. */
+ public void connect(Context context) {
+ MqttConnectOptions options = new MqttConnectOptions();
+ options.setAutomaticReconnect(true);
+ options.setCleanSession(false);
+ try {
+ String password = context.getString(R.string.tusky_api_keystore_password);
+ InputStream keystore = context.getResources().openRawResource(R.raw.keystore_tusky_api);
+ try {
+ options.setSocketFactory(mqttAndroidClient.getSSLSocketFactory(keystore, password));
+ } finally {
+ IOUtils.closeQuietly(keystore);
+ }
+ mqttAndroidClient.connect(options).setActionCallback(new IMqttActionListener() {
+ @Override
+ public void onSuccess(IMqttToken asyncActionToken) {
+ DisconnectedBufferOptions bufferOptions = new DisconnectedBufferOptions();
+ bufferOptions.setBufferEnabled(true);
+ bufferOptions.setBufferSize(100);
+ bufferOptions.setPersistBuffer(false);
+ bufferOptions.setDeleteOldestMessages(false);
+ mqttAndroidClient.setBufferOpts(bufferOptions);
+ onConnectionSuccess();
+ flushQueuedActions();
+ }
+
+ @Override
+ public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+ Log.e(TAG, "An exception occurred while connecting. " + exception.getMessage()
+ + " " + exception.getCause());
+ onConnectionFailure();
+ }
+ });
+ } catch (MqttException e) {
+ Log.e(TAG, "An exception occurred while connecpting. " + e.getMessage());
+ onConnectionFailure();
+ }
+ }
+
+ private void onConnectionSuccess() {
+ Log.v(TAG, "The connection succeeded.");
+ }
+
+ private void onConnectionFailure() {
+ Log.v(TAG, "The connection failed.");
+ }
+
+ private void onConnectionLost() {
+ Log.v(TAG, "The connection was lost.");
+ }
+
+ /** Disconnect from the MQTT broker. */
+ public void disconnect() {
+ if (!mqttAndroidClient.isConnected()) {
+ queuedActions.add(new QueuedAction(QueuedAction.Type.DISCONNECT));
+ return;
+ }
+ try {
+ mqttAndroidClient.disconnect();
+ } catch (MqttException ex) {
+ Log.e(TAG, "An exception occurred while disconnecting.");
+ onDisconnectFailed();
+ }
+ }
+
+ private void onDisconnectFailed() {
+ Log.v(TAG, "Failed while disconnecting from the broker.");
+ }
+
+ /** Subscribe to the push notification topic. */
+ public void subscribeToTopic(final String topic) {
+ if (!mqttAndroidClient.isConnected()) {
+ queuedActions.add(new QueuedAction(QueuedAction.Type.SUBSCRIBE, topic));
+ return;
+ }
+ try {
+ mqttAndroidClient.subscribe(topic, 0, null, new IMqttActionListener() {
+ @Override
+ public void onSuccess(IMqttToken asyncActionToken) {
+ subscribedTopics.add(topic);
+ onConnectionSuccess();
+ }
+
+ @Override
+ public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+ Log.e(TAG, "An exception occurred while subscribing." + exception.getMessage());
+ onConnectionFailure();
+ }
+ });
+ } catch (MqttException e) {
+ Log.e(TAG, "An exception occurred while subscribing." + e.getMessage());
+ onConnectionFailure();
+ }
+ }
+
+ /** Unsubscribe from the push notification topic. */
+ public void unsubscribeToTopic(String topic) {
+ if (!mqttAndroidClient.isConnected()) {
+ queuedActions.add(new QueuedAction(QueuedAction.Type.UNSUBSCRIBE, topic));
+ return;
+ }
+ try {
+ mqttAndroidClient.unsubscribe(topic);
+ subscribedTopics.remove(topic);
+ } catch (MqttException e) {
+ Log.e(TAG, "An exception occurred while unsubscribing." + e.getMessage());
+ onConnectionFailure();
+ }
+ }
+
+ private void onMessageReceived(final Context context, String message) {
+ Log.v(TAG, "Notification received: " + message);
+
+ Gson gson = new GsonBuilder()
+ .registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
+ .registerTypeAdapter(StringWithEmoji.class, new StringWithEmojiTypeAdapter())
+ .create();
+ Notification notification = gson.fromJson(message, Notification.class);
+
+ NotificationMaker.make(context, NOTIFY_ID, notification);
+ }
+
+ public void clearNotifications(Context context) {
+ ((NotificationManager) (context.getSystemService(NOTIFICATION_SERVICE))).cancel(NOTIFY_ID);
+ }
+
+ public String getDeviceToken() {
+ return mqttAndroidClient.getClientId();
+ }
+}
diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml
index b2a8e3b6..ad2d3b37 100644
--- a/app/src/main/res/values/donottranslate.xml
+++ b/app/src/main/res/values/donottranslate.xml
@@ -2,7 +2,8 @@
Tusky
https://tusky.keylesspalace.com
- https://tuskynotifier.keylesspalace.com
+ apitusky.keylesspalace.com
+ your_password_here
oauth2redirect
com.keylesspalace.tusky
diff --git a/build.gradle b/build.gradle
index 80e6fd50..bfed5920 100644
--- a/build.gradle
+++ b/build.gradle
@@ -9,7 +9,6 @@ buildscript {
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
- classpath 'com.google.gms:google-services:3.0.0'
}
}