Moves Firebase notifications to a "google" build flavor and adds an "fdroid" flavor that uses polling for notifications.

Also, adds a few missing license notices.
This commit is contained in:
Vavassor 2017-04-27 23:29:42 -04:00
parent cc8d8f716a
commit 5f6fab2b70
20 changed files with 653 additions and 373 deletions

View File

@ -12,6 +12,14 @@ android {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary true vectorDrawables.useSupportLibrary true
} }
productFlavors {
google {
buildConfigField "boolean", "USES_PUSH_NOTIFICATIONS", "true"
}
fdroid {
buildConfigField "boolean", "USES_PUSH_NOTIFICATIONS", "false"
}
}
buildTypes { buildTypes {
release { release {
minifyEnabled true minifyEnabled true
@ -51,8 +59,8 @@ dependencies {
compile 'com.github.arimorty:floatingsearchview:2.0.3' compile 'com.github.arimorty:floatingsearchview:2.0.3'
compile 'com.theartofdev.edmodo:android-image-cropper:2.4.0' compile 'com.theartofdev.edmodo:android-image-cropper:2.4.0'
compile 'com.jakewharton:butterknife:8.4.0' compile 'com.jakewharton:butterknife:8.4.0'
compile 'com.google.firebase:firebase-messaging:10.0.1' googleCompile 'com.google.firebase:firebase-messaging:10.0.1'
compile 'com.google.firebase:firebase-crash:10.0.1' googleCompile 'com.google.firebase:firebase-crash:10.0.1'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0' annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
} }

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.keylesspalace.tusky">
<application>
<service
android:name=".MessagingService"
android:enabled="true"
android:exported="true" />
</application>
</manifest>

View File

@ -0,0 +1,114 @@
/* 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 <http://www.gnu.org/licenses>. */
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 java.util.List;
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 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<List<Notification>>() {
@Override
public void onResponse(Call<List<Notification>> call,
Response<List<Notification>> response) {
if (response.isSuccessful()) {
List<Notification> notificationList = response.body();
for (Notification notification : notificationList) {
NotificationMaker.make(MessagingService.this, NOTIFY_ID, notification);
}
}
}
@Override
public void onFailure(Call<List<Notification>> 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);
}
}

View File

@ -0,0 +1,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.keylesspalace.tusky">
<application>
<meta-data android:name="firebase_analytics_collection_enabled" android:value="false" />
<service android:name=".MyFirebaseInstanceIdService" android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter>
</service>
<service android:name=".MessagingService" android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>

View File

@ -0,0 +1,121 @@
/* 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 <http://www.gnu.org/licenses>.
*
* 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.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 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<Notification>() {
@Override
public void onResponse(Call<Notification> call, Response<Notification> response) {
if (response.isSuccessful()) {
NotificationMaker.make(MessagingService.this, NOTIFY_ID, response.body());
}
}
@Override
public void onFailure(Call<Notification> 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);
}
}

View File

@ -33,7 +33,7 @@ import retrofit2.Response;
import retrofit2.Retrofit; import retrofit2.Retrofit;
public class MyFirebaseInstanceIdService extends FirebaseInstanceIdService { public class MyFirebaseInstanceIdService extends FirebaseInstanceIdService {
private static final String TAG = "MyFirebaseInstanceIdService"; private static final String TAG = "com.keylesspalace.tusky.MyFirebaseInstanceIdService";
private TuskyAPI tuskyAPI; private TuskyAPI tuskyAPI;

View File

@ -15,7 +15,7 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:name=".TuskyApplication"> android:name=".TuskyApplication">
<meta-data android:name="firebase_analytics_collection_enabled" android:value="false" />
<activity android:name=".SplashActivity"> <activity android:name=".SplashActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -72,18 +72,6 @@
android:name="com.theartofdev.edmodo.cropper.CropImageActivity" android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
android:theme="@style/Base.Theme.AppCompat" /> android:theme="@style/Base.Theme.AppCompat" />
<service android:name=".MyFirebaseInstanceIdService" android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter>
</service>
<service android:name=".MyFirebaseMessagingService" android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<receiver android:name=".NotificationClearBroadcastReceiver" /> <receiver android:name=".NotificationClearBroadcastReceiver" />
<service <service

View File

@ -15,6 +15,8 @@
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -22,6 +24,7 @@ import android.graphics.Color;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.os.SystemClock;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
@ -32,7 +35,6 @@ import android.view.Menu;
import com.google.firebase.iid.FirebaseInstanceId; import com.google.firebase.iid.FirebaseInstanceId;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.keylesspalace.tusky.entity.Account;
import java.io.IOException; import java.io.IOException;
@ -47,16 +49,13 @@ import retrofit2.Callback;
import retrofit2.Retrofit; import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory; import retrofit2.converter.gson.GsonConverterFactory;
/* There isn't presently a way to globally change the theme of a whole application at runtime, just
* individual activities. So, each activity has to set its theme before any views are created. And
* the most expedient way to accomplish this was to put it in a base class and just have every
* activity extend from it. */
public class BaseActivity extends AppCompatActivity { public class BaseActivity extends AppCompatActivity {
private static final String TAG = "BaseActivity"; // logging tag private static final String TAG = "BaseActivity"; // logging tag
protected MastodonAPI mastodonAPI; protected MastodonAPI mastodonAPI;
protected TuskyAPI tuskyAPI; protected TuskyAPI tuskyAPI;
protected Dispatcher mastodonApiDispatcher; protected Dispatcher mastodonApiDispatcher;
protected PendingIntent serviceAlarmIntent;
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
@ -66,6 +65,9 @@ public class BaseActivity extends AppCompatActivity {
createMastodonAPI(); createMastodonAPI();
createTuskyAPI(); createTuskyAPI();
/* There isn't presently a way to globally change the theme of a whole application at
* runtime, just individual activities. So, each activity has to set its theme before any
* views are created. */
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lightTheme", false)) { if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lightTheme", false)) {
setTheme(R.style.AppTheme_Light); setTheme(R.style.AppTheme_Light);
} }
@ -154,12 +156,14 @@ public class BaseActivity extends AppCompatActivity {
} }
protected void createTuskyAPI() { protected void createTuskyAPI() {
Retrofit retrofit = new Retrofit.Builder() if (BuildConfig.USES_PUSH_NOTIFICATIONS) {
.baseUrl(getString(R.string.tusky_api_url)) Retrofit retrofit = new Retrofit.Builder()
.client(OkHttpUtils.getCompatibleClient()) .baseUrl(getString(R.string.tusky_api_url))
.build(); .client(OkHttpUtils.getCompatibleClient())
.build();
tuskyAPI = retrofit.create(TuskyAPI.class); tuskyAPI = retrofit.create(TuskyAPI.class);
}
} }
protected void redirectIfNotLoggedIn() { protected void redirectIfNotLoggedIn() {
@ -193,30 +197,48 @@ public class BaseActivity extends AppCompatActivity {
} }
protected void enablePushNotifications() { protected void enablePushNotifications() {
tuskyAPI.register(getBaseUrl(), getAccessToken(), FirebaseInstanceId.getInstance().getToken()).enqueue(new Callback<ResponseBody>() { if (BuildConfig.USES_PUSH_NOTIFICATIONS) {
@Override tuskyAPI.register(getBaseUrl(), getAccessToken(), FirebaseInstanceId.getInstance().getToken()).enqueue(new Callback<ResponseBody>() {
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) { @Override
Log.d(TAG, "Enable push notifications response: " + response.message()); public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
} Log.d(TAG, "Enable push notifications response: " + response.message());
}
@Override @Override
public void onFailure(Call<ResponseBody> call, Throwable t) { public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.d(TAG, "Enable push notifications failed: " + t.getMessage()); 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; // * 10;
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);
}
} }
protected void disablePushNotifications() { protected void disablePushNotifications() {
tuskyAPI.unregister(getBaseUrl(), getAccessToken()).enqueue(new Callback<ResponseBody>() { if (BuildConfig.USES_PUSH_NOTIFICATIONS) {
@Override tuskyAPI.unregister(getBaseUrl(), getAccessToken()).enqueue(new Callback<ResponseBody>() {
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) { @Override
Log.d(TAG, "Disable push notifications response: " + response.message()); public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
} Log.d(TAG, "Disable push notifications response: " + response.message());
}
@Override @Override
public void onFailure(Call<ResponseBody> call, Throwable t) { public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.d(TAG, "Disable push notifications failed: " + t.getMessage()); 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);
}
} }
} }

View File

@ -15,7 +15,6 @@
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;

View File

@ -1,3 +1,18 @@
/* 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 <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.Manifest; import android.Manifest;

View File

@ -1,3 +1,18 @@
/* 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 <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;

View File

@ -1,3 +1,18 @@
/* 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 <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
interface LinkListener { interface LinkListener {

View File

@ -187,7 +187,11 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove
} }
// Setup push notifications // Setup push notifications
if (arePushNotificationsEnabled()) enablePushNotifications(); if (arePushNotificationsEnabled()) {
enablePushNotifications();
} else {
disablePushNotifications();
}
composeButton = floatingBtn; composeButton = floatingBtn;
} }
@ -203,7 +207,7 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove
.apply(); .apply();
((NotificationManager) (getSystemService(NOTIFICATION_SERVICE))) ((NotificationManager) (getSystemService(NOTIFICATION_SERVICE)))
.cancel(MyFirebaseMessagingService.NOTIFY_ID); .cancel(MessagingService.NOTIFY_ID);
/* After editing a profile, the profile header in the navigation drawer needs to be /* After editing a profile, the profile header in the navigation drawer needs to be
* refreshed */ * refreshed */

View File

@ -1,319 +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 <http://www.gnu.org/licenses>.
*
* 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.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.Spanned;
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.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.json.JSONArray;
import org.json.JSONException;
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 MyFirebaseMessagingService extends FirebaseMessagingService {
private MastodonAPI mastodonAPI;
private static final String TAG = "MyFirebaseMessagingService";
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<Notification>() {
@Override
public void onResponse(Call<Notification> call, Response<Notification> response) {
if (response.isSuccessful()) {
buildNotification(response.body());
}
}
@Override
public void onFailure(Call<Notification> 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 String truncateWithEllipses(String string, int limit) {
if (string.length() < limit) {
return string;
} else {
return string.substring(0, limit - 3) + "...";
}
}
private static boolean filterNotification(SharedPreferences preferences,
Notification notification) {
switch (notification.type) {
default:
case MENTION: {
return preferences.getBoolean("notificationFilterMentions", true);
}
case FOLLOW: {
return preferences.getBoolean("notificationFilterFollows", true);
}
case REBLOG: {
return preferences.getBoolean("notificationFilterReblogs", true);
}
case FAVOURITE: {
return preferences.getBoolean("notificationFilterFavourites", true);
}
}
}
private void buildNotification(Notification body) {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final SharedPreferences notificationPreferences = getApplicationContext().getSharedPreferences("Notifications", MODE_PRIVATE);
if (!filterNotification(preferences, body)) {
return;
}
String rawCurrentNotifications = notificationPreferences.getString("current", "[]");
JSONArray currentNotifications;
try {
currentNotifications = new JSONArray(rawCurrentNotifications);
} catch (JSONException e) {
currentNotifications = new JSONArray();
}
boolean alreadyContains = false;
for(int i = 0; i < currentNotifications.length(); i++) {
try {
if (currentNotifications.getString(i).equals(body.account.displayName)) {
alreadyContains = true;
}
} catch (JSONException e) {
e.printStackTrace();
}
}
if (!alreadyContains) {
currentNotifications.put(body.account.displayName);
}
SharedPreferences.Editor editor = notificationPreferences.edit();
editor.putString("current", currentNotifications.toString());
editor.commit();
Intent resultIntent = new Intent(this, MainActivity.class);
resultIntent.putExtra("tab_position", 1);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(resultIntent);
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
Intent deleteIntent = new Intent(this, NotificationClearBroadcastReceiver.class);
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(this, 0, deleteIntent, PendingIntent.FLAG_CANCEL_CURRENT);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_notify)
.setContentIntent(resultPendingIntent)
.setDeleteIntent(deletePendingIntent)
.setDefaults(0); // So it doesn't ring twice, notify only in Target callback
if (currentNotifications.length() == 1) {
builder.setContentTitle(titleForType(body))
.setContentText(truncateWithEllipses(bodyForType(body), 40));
Target mTarget = new Target() {
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
builder.setLargeIcon(bitmap);
setupPreferences(preferences, builder);
((NotificationManager) (getSystemService(NOTIFICATION_SERVICE))).notify(NOTIFY_ID, builder.build());
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
}
};
Picasso.with(this)
.load(body.account.avatar)
.placeholder(R.drawable.avatar_default)
.transform(new RoundedTransformation(7, 0))
.into(mTarget);
} else {
setupPreferences(preferences, builder);
try {
builder.setContentTitle(String.format(getString(R.string.notification_title_summary), currentNotifications.length()))
.setContentText(truncateWithEllipses(joinNames(currentNotifications), 40));
} catch (JSONException e) {
e.printStackTrace();
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder.setVisibility(android.app.Notification.VISIBILITY_PRIVATE);
builder.setCategory(android.app.Notification.CATEGORY_SOCIAL);
}
((NotificationManager) (getSystemService(NOTIFICATION_SERVICE))).notify(NOTIFY_ID, builder.build());
}
private void setupPreferences(SharedPreferences preferences, NotificationCompat.Builder builder) {
if (preferences.getBoolean("notificationAlertSound", true)) {
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
}
if (preferences.getBoolean("notificationAlertVibrate", false)) {
builder.setVibrate(new long[] { 500, 500 });
}
if (preferences.getBoolean("notificationAlertLight", false)) {
builder.setLights(0xFF00FF8F, 300, 1000);
}
}
private String joinNames(JSONArray array) throws JSONException {
if (array.length() > 3) {
return String.format(getString(R.string.notification_summary_large), array.get(0), array.get(1), array.get(2), array.length() - 3);
} else if (array.length() == 3) {
return String.format(getString(R.string.notification_summary_medium), array.get(0), array.get(1), array.get(2));
} else if (array.length() == 2) {
return String.format(getString(R.string.notification_summary_small), array.get(0), array.get(1));
}
return null;
}
private String titleForType(Notification notification) {
switch (notification.type) {
case MENTION:
return String.format(getString(R.string.notification_mention_format), notification.account.getDisplayName());
case FOLLOW:
return String.format(getString(R.string.notification_follow_format), notification.account.getDisplayName());
case FAVOURITE:
return String.format(getString(R.string.notification_favourite_format), notification.account.getDisplayName());
case REBLOG:
return String.format(getString(R.string.notification_reblog_format), notification.account.getDisplayName());
}
return null;
}
private String bodyForType(Notification notification) {
switch (notification.type) {
case FOLLOW:
return notification.account.username;
case MENTION:
case FAVOURITE:
case REBLOG:
return notification.status.content.toString();
}
return null;
}
}

View File

@ -0,0 +1,224 @@
/* 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 <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import com.keylesspalace.tusky.entity.Notification;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.json.JSONArray;
import org.json.JSONException;
class NotificationMaker {
static void make(final Context context, final int notifyId, Notification body) {
final SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(context);
final SharedPreferences notificationPreferences = context.getSharedPreferences(
"Notifications", Context.MODE_PRIVATE);
if (!filterNotification(preferences, body)) {
return;
}
String rawCurrentNotifications = notificationPreferences.getString("current", "[]");
JSONArray currentNotifications;
try {
currentNotifications = new JSONArray(rawCurrentNotifications);
} catch (JSONException e) {
currentNotifications = new JSONArray();
}
boolean alreadyContains = false;
for(int i = 0; i < currentNotifications.length(); i++) {
try {
if (currentNotifications.getString(i).equals(body.account.getDisplayName())) {
alreadyContains = true;
}
} catch (JSONException e) {
e.printStackTrace();
}
}
if (!alreadyContains) {
currentNotifications.put(body.account.getDisplayName());
}
notificationPreferences.edit()
.putString("current", currentNotifications.toString())
.commit();
Intent resultIntent = new Intent(context, MainActivity.class);
resultIntent.putExtra("tab_position", 1);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(resultIntent);
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, PendingIntent.FLAG_CANCEL_CURRENT);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_notify)
.setContentIntent(resultPendingIntent)
.setDeleteIntent(deletePendingIntent)
.setDefaults(0); // So it doesn't ring twice, notify only in Target callback
if (currentNotifications.length() == 1) {
builder.setContentTitle(titleForType(context, body))
.setContentText(truncateWithEllipses(bodyForType(body), 40));
Target mTarget = new Target() {
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
builder.setLargeIcon(bitmap);
setupPreferences(preferences, builder);
((NotificationManager) (context.getSystemService(Context.NOTIFICATION_SERVICE)))
.notify(notifyId, builder.build());
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {}
};
Picasso.with(context)
.load(body.account.avatar)
.placeholder(R.drawable.avatar_default)
.transform(new RoundedTransformation(7, 0))
.into(mTarget);
} else {
setupPreferences(preferences, builder);
try {
builder.setContentTitle(String.format(context.getString(R.string.notification_title_summary), currentNotifications.length()))
.setContentText(truncateWithEllipses(joinNames(context, currentNotifications), 40));
} catch (JSONException e) {
e.printStackTrace();
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder.setVisibility(android.app.Notification.VISIBILITY_PRIVATE);
builder.setCategory(android.app.Notification.CATEGORY_SOCIAL);
}
((NotificationManager) (context.getSystemService(Context.NOTIFICATION_SERVICE)))
.notify(notifyId, builder.build());
}
private static boolean filterNotification(SharedPreferences preferences,
Notification notification) {
switch (notification.type) {
default:
case MENTION: {
return preferences.getBoolean("notificationFilterMentions", true);
}
case FOLLOW: {
return preferences.getBoolean("notificationFilterFollows", true);
}
case REBLOG: {
return preferences.getBoolean("notificationFilterReblogs", true);
}
case FAVOURITE: {
return preferences.getBoolean("notificationFilterFavourites", true);
}
}
}
private static String truncateWithEllipses(String string, int limit) {
if (string.length() < limit) {
return string;
} else {
return string.substring(0, limit - 3) + "...";
}
}
private static void setupPreferences(SharedPreferences preferences,
NotificationCompat.Builder builder) {
if (preferences.getBoolean("notificationAlertSound", true)) {
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
}
if (preferences.getBoolean("notificationAlertVibrate", false)) {
builder.setVibrate(new long[] { 500, 500 });
}
if (preferences.getBoolean("notificationAlertLight", false)) {
builder.setLights(0xFF00FF8F, 300, 1000);
}
}
@Nullable
private static String joinNames(Context context, JSONArray array) throws JSONException {
if (array.length() > 3) {
return String.format(context.getString(R.string.notification_summary_large), array.get(0), array.get(1), array.get(2), array.length() - 3);
} else if (array.length() == 3) {
return String.format(context.getString(R.string.notification_summary_medium), array.get(0), array.get(1), array.get(2));
} else if (array.length() == 2) {
return String.format(context.getString(R.string.notification_summary_small), array.get(0), array.get(1));
}
return null;
}
@Nullable
private static String titleForType(Context context, Notification notification) {
switch (notification.type) {
case MENTION:
return String.format(context.getString(R.string.notification_mention_format), notification.account.getDisplayName());
case FOLLOW:
return String.format(context.getString(R.string.notification_follow_format), notification.account.getDisplayName());
case FAVOURITE:
return String.format(context.getString(R.string.notification_favourite_format), notification.account.getDisplayName());
case REBLOG:
return String.format(context.getString(R.string.notification_reblog_format), notification.account.getDisplayName());
}
return null;
}
@Nullable
private static String bodyForType(Notification notification) {
switch (notification.type) {
case FOLLOW:
return notification.account.username;
case MENTION:
case FAVOURITE:
case REBLOG:
return notification.status.content.toString();
}
return null;
}
}

View File

@ -42,7 +42,7 @@ import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
class OkHttpUtils { public class OkHttpUtils {
static final String TAG = "OkHttpUtils"; // logging tag static final String TAG = "OkHttpUtils"; // logging tag
/** /**
@ -58,8 +58,7 @@ class OkHttpUtils {
* TLS 1.1 and 1.2 have to be manually enabled on API levels 16-20. * TLS 1.1 and 1.2 have to be manually enabled on API levels 16-20.
*/ */
@NonNull @NonNull
static OkHttpClient.Builder getCompatibleClientBuilder() { public static OkHttpClient.Builder getCompatibleClientBuilder() {
ConnectionSpec fallback = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) ConnectionSpec fallback = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.allEnabledCipherSuites() .allEnabledCipherSuites()
.supportsTlsExtensions(true) .supportsTlsExtensions(true)
@ -79,7 +78,7 @@ class OkHttpUtils {
} }
@NonNull @NonNull
static OkHttpClient getCompatibleClient() { public static OkHttpClient getCompatibleClient() {
return getCompatibleClientBuilder().build(); return getCompatibleClientBuilder().build();
} }

View File

@ -25,7 +25,7 @@ import com.google.gson.JsonParseException;
import java.lang.reflect.Type; import java.lang.reflect.Type;
class SpannedTypeAdapter implements JsonDeserializer<Spanned> { public class SpannedTypeAdapter implements JsonDeserializer<Spanned> {
@Override @Override
public Spanned deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { public Spanned deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return HtmlUtils.fromHtml(Emojione.shortnameToUnicode(json.getAsString(), false)); return HtmlUtils.fromHtml(Emojione.shortnameToUnicode(json.getAsString(), false));

View File

@ -1,3 +1,18 @@
/* 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 <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
interface StatusRemoveListener { interface StatusRemoveListener {

View File

@ -1,3 +1,18 @@
/* 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 <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
/** /**

View File

@ -1,3 +1,18 @@
/* 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 <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import com.emojione.Emojione; import com.emojione.Emojione;