Reverts notifications from MQTT prototype to pull notifications.

This commit is contained in:
Vavassor 2017-07-08 20:59:48 -04:00
parent 14d02e72b7
commit f68f6d7473
9 changed files with 295 additions and 148 deletions

View File

@ -98,7 +98,7 @@
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" /> <receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
<service android:name="org.eclipse.paho.android.service.MqttService" /> <service android:name=".service.PullNotificationService" />
<service <service
tools:targetApi="24" tools:targetApi="24"
android:name="com.keylesspalace.tusky.service.TuskyTileService" android:name="com.keylesspalace.tusky.service.TuskyTileService"

View File

@ -15,6 +15,9 @@
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.app.AlarmManager;
import android.app.NotificationManager;
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,24 +25,23 @@ 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;
import android.text.Spanned; import android.text.Spanned;
import android.util.Log;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.Menu; import android.view.Menu;
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.Session;
import com.keylesspalace.tusky.json.SpannedTypeAdapter; import com.keylesspalace.tusky.json.SpannedTypeAdapter;
import com.keylesspalace.tusky.json.StringWithEmoji; import com.keylesspalace.tusky.json.StringWithEmoji;
import com.keylesspalace.tusky.json.StringWithEmojiTypeAdapter; import com.keylesspalace.tusky.json.StringWithEmojiTypeAdapter;
import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.TuskyApi; import com.keylesspalace.tusky.network.TuskyApi;
import com.keylesspalace.tusky.service.PullNotificationService;
import com.keylesspalace.tusky.util.OkHttpUtils; import com.keylesspalace.tusky.util.OkHttpUtils;
import com.keylesspalace.tusky.util.PushNotificationClient;
import java.io.IOException; import java.io.IOException;
@ -48,18 +50,14 @@ import okhttp3.Interceptor;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Retrofit; import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory; import retrofit2.converter.gson.GsonConverterFactory;
public class BaseActivity extends AppCompatActivity { public class BaseActivity extends AppCompatActivity {
private static final String TAG = "BaseActivity"; // logging tag protected static final int SERVICE_REQUEST_CODE = 8574603; // This number is arbitrary.
public MastodonApi mastodonApi; public MastodonApi mastodonApi;
public TuskyApi tuskyApi; public TuskyApi tuskyApi;
protected PushNotificationClient pushNotificationClient;
protected Dispatcher mastodonApiDispatcher; protected Dispatcher mastodonApiDispatcher;
@Override @Override
@ -69,7 +67,6 @@ public class BaseActivity extends AppCompatActivity {
redirectIfNotLoggedIn(); redirectIfNotLoggedIn();
createMastodonApi(); createMastodonApi();
createTuskyApi(); createTuskyApi();
createPushNotificationClient();
/* There isn't presently a way to globally change the theme of a whole application at /* 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 * runtime, just individual activities. So, each activity has to set its theme before any
@ -173,11 +170,6 @@ public class BaseActivity extends AppCompatActivity {
tuskyApi = retrofit.create(TuskyApi.class); tuskyApi = retrofit.create(TuskyApi.class);
} }
protected void createPushNotificationClient() {
pushNotificationClient = new PushNotificationClient(getApplicationContext(),
"ssl://" + getString(R.string.tusky_api_url) + ":8883");
}
protected void redirectIfNotLoggedIn() { protected void redirectIfNotLoggedIn() {
SharedPreferences preferences = getPrivatePreferences(); SharedPreferences preferences = getPrivatePreferences();
String domain = preferences.getString("domain", null); String domain = preferences.getString("domain", null);
@ -209,66 +201,47 @@ public class BaseActivity extends AppCompatActivity {
} }
protected void enablePushNotifications() { protected void enablePushNotifications() {
Callback<ResponseBody> callback = new Callback<ResponseBody>() { // Start up the PullNotificationService on a repeating interval.
@Override SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
public void onResponse(Call<ResponseBody> call, String minutesString = preferences.getString("pullNotificationCheckInterval", "15");
retrofit2.Response<ResponseBody> response) { long minutes = Long.valueOf(minutesString);
if (response.isSuccessful()) { long checkInterval = 1000 * 60 * minutes;
pushNotificationClient.subscribeToTopic(getPushNotificationTopic()); AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
pushNotificationClient.connect(BaseActivity.this); Intent intent = new Intent(this, PullNotificationService.class);
} else { PendingIntent serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE,
onEnablePushNotificationsFailure(response.message()); intent, PendingIntent.FLAG_UPDATE_CURRENT);
} alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
} SystemClock.elapsedRealtime(), checkInterval, serviceAlarmIntent);
@Override
public void onFailure(Call<ResponseBody> 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() { protected void disablePushNotifications() {
Callback<ResponseBody> callback = new Callback<ResponseBody>() { // Cancel the repeating call for "pull" notifications.
@Override AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
public void onResponse(Call<ResponseBody> call, Intent intent = new Intent(this, PullNotificationService.class);
retrofit2.Response<ResponseBody> response) { PendingIntent serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE,
if (response.isSuccessful()) { intent, PendingIntent.FLAG_UPDATE_CURRENT);
pushNotificationClient.unsubscribeToTopic(getPushNotificationTopic()); alarmManager.cancel(serviceAlarmIntent);
} else {
onDisablePushNotificationsFailure();
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
onDisablePushNotificationsFailure();
}
};
String deviceToken = pushNotificationClient.getDeviceToken();
Session session = new Session(getDomain(), getAccessToken(), deviceToken);
tuskyApi.unregister(session)
.enqueue(callback);
} }
private void onDisablePushNotificationsFailure() { protected void clearNotifications() {
Log.e(TAG, "Disabling push notifications failed."); SharedPreferences notificationPreferences = getApplicationContext()
.getSharedPreferences("Notifications", MODE_PRIVATE);
notificationPreferences.edit()
.putString("current", "[]")
.apply();
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.cancel(PullNotificationService.NOTIFY_ID);
} }
private String getPushNotificationTopic() { protected void setPullNotificationCheckInterval(long minutes) {
return String.format("%s/%s/#", getDomain(), getAccessToken()); long checkInterval = 1000 * 60 * minutes;
} AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(this, PullNotificationService.class);
private String getDomain() { PendingIntent serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE,
return getPrivatePreferences() intent, PendingIntent.FLAG_UPDATE_CURRENT);
.getString("domain", null); alarmManager.cancel(serviceAlarmIntent);
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime(), checkInterval, serviceAlarmIntent);
} }
} }

View File

@ -210,17 +210,19 @@ public class MainActivity extends BaseActivity {
composeButton = floatingBtn; composeButton = floatingBtn;
} }
@Override
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
ArrayList<Integer> pageHistoryList = new ArrayList<>();
pageHistoryList.addAll(pageHistory);
outState.putIntegerArrayList("pageHistory", pageHistoryList);
super.onSaveInstanceState(outState, outPersistentState);
}
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
SharedPreferences notificationPreferences = getApplicationContext() clearNotifications();
.getSharedPreferences("Notifications", MODE_PRIVATE);
notificationPreferences.edit()
.putString("current", "[]")
.apply();
pushNotificationClient.clearNotifications(this);
/* 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 */
@ -234,11 +236,50 @@ public class MainActivity extends BaseActivity {
} }
@Override @Override
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
ArrayList<Integer> pageHistoryList = new ArrayList<>(); if (requestCode == COMPOSE_RESULT && resultCode == ComposeActivity.RESULT_OK) {
pageHistoryList.addAll(pageHistory); Intent intent = new Intent(TimelineReceiver.Types.STATUS_COMPOSED);
outState.putIntegerArrayList("pageHistory", pageHistoryList); LocalBroadcastManager.getInstance(getApplicationContext())
super.onSaveInstanceState(outState, outPersistentState); .sendBroadcast(intent);
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onBackPressed() {
if (drawer != null && drawer.isDrawerOpen()) {
drawer.closeDrawer();
} else if (pageHistory.size() < 2) {
super.onBackPressed();
} else {
pageHistory.pop();
viewPager.setCurrentItem(pageHistory.peek());
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_MENU: {
if (drawer.isDrawerOpen()) {
drawer.closeDrawer();
} else {
drawer.openDrawer();
}
return true;
}
case KeyEvent.KEYCODE_SEARCH: {
startActivity(new Intent(this, SearchActivity.class));
return true;
}
}
return super.onKeyDown(keyCode, event);
}
// Fix for GitHub issues #190, #259 (MainActivity won't restart on screen rotation.)
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
} }
private void tintTab(TabLayout.Tab tab, boolean tinted) { private void tintTab(TabLayout.Tab tab, boolean tinted) {
@ -465,51 +506,4 @@ public class MainActivity extends BaseActivity {
private void onFetchUserInfoFailure(Exception exception) { private void onFetchUserInfoFailure(Exception exception) {
Log.e(TAG, "Failed to fetch user info. " + exception.getMessage()); Log.e(TAG, "Failed to fetch user info. " + exception.getMessage());
} }
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == COMPOSE_RESULT && resultCode == ComposeActivity.RESULT_OK) {
Intent intent = new Intent(TimelineReceiver.Types.STATUS_COMPOSED);
LocalBroadcastManager.getInstance(getApplicationContext())
.sendBroadcast(intent);
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onBackPressed() {
if (drawer != null && drawer.isDrawerOpen()) {
drawer.closeDrawer();
} else if (pageHistory.size() < 2) {
super.onBackPressed();
} else {
pageHistory.pop();
viewPager.setCurrentItem(pageHistory.peek());
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_MENU: {
if (drawer.isDrawerOpen()) {
drawer.closeDrawer();
} else {
drawer.openDrawer();
}
return true;
}
case KeyEvent.KEYCODE_SEARCH: {
startActivity(new Intent(this, SearchActivity.class));
return true;
}
}
return super.onKeyDown(keyCode, event);
}
// Fix for GitHub issues #190, #259 (MainActivity won't restart on screen rotation.)
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
} }

View File

@ -59,24 +59,34 @@ public class PreferencesActivity extends BaseActivity
} }
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals("lightTheme")) { switch (key) {
themeSwitched = true; case "lightTheme": {
// recreate() could be used instead, but it doesn't have an animation B). themeSwitched = true;
Intent intent = getIntent(); // recreate() could be used instead, but it doesn't have an animation B).
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); Intent intent = getIntent();
Bundle savedInstanceState = new Bundle(); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
saveInstanceState(savedInstanceState); Bundle savedInstanceState = new Bundle();
intent.putExtras(savedInstanceState); saveInstanceState(savedInstanceState);
startActivity(intent); intent.putExtras(savedInstanceState);
finish(); startActivity(intent);
overridePendingTransition(R.anim.fade_in, R.anim.fade_out); finish();
} else if (key.equals("notificationsEnabled")) { overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
boolean notificationsEnabled = sharedPreferences.getBoolean("notificationsEnabled", true); break;
}
if (notificationsEnabled) { case "notificationsEnabled": {
enablePushNotifications(); boolean enabled = sharedPreferences.getBoolean("notificationsEnabled", true);
} else { if (enabled) {
disablePushNotifications(); enablePushNotifications();
} else {
disablePushNotifications();
}
break;
}
case "pullNotificationCheckInterval": {
String s = sharedPreferences.getString("pullNotificationCheckInterval", "15");
long minutes = Long.valueOf(s);
setPullNotificationCheckInterval(minutes);
break;
} }
} }
} }

View File

@ -0,0 +1,136 @@
/* 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.service;
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.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 com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.util.OkHttpUtils;
import com.keylesspalace.tusky.util.NotificationMaker;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
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 PullNotificationService extends IntentService {
public static final int NOTIFY_ID = 6; // This is an arbitrary number.
private MastodonApi mastodonApi;
public PullNotificationService() {
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()) {
onNotificationsReceived(response.body());
}
}
@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);
}
private void onNotificationsReceived(List<Notification> notificationList) {
SharedPreferences notificationsPreferences = getSharedPreferences(
"Notifications", Context.MODE_PRIVATE);
Set<String> currentIds = notificationsPreferences.getStringSet(
"current_ids", new HashSet<String>());
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();
}
}

View File

@ -41,7 +41,7 @@ import com.squareup.picasso.Target;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
class NotificationMaker { public class NotificationMaker {
public static final String TAG = "NotificationMaker"; public static final String TAG = "NotificationMaker";

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pull_notification_check_interval_names">
<item>5 minutes</item>
<item>10 minutes</item>
<item>15 minutes</item>
<item>20 minutes</item>
<item>25 minutes</item>
<item>30 minutes</item>
<item>45 minutes</item>
<item>1 hour</item>
<item>2 hours</item>
</string-array>
<string-array name="pull_notification_check_intervals" inputType="integer">
<item>5</item>
<item>10</item>
<item>15</item>
<item>20</item>
<item>25</item>
<item>30</item>
<item>45</item>
<item>60</item>
<item>120</item>
</string-array>
</resources>

View File

@ -146,7 +146,8 @@
<string name="pref_title_notification_settings">Notifications</string> <string name="pref_title_notification_settings">Notifications</string>
<string name="pref_title_edit_notification_settings">Edit Notifications</string> <string name="pref_title_edit_notification_settings">Edit Notifications</string>
<string name="pref_title_notifications_enabled">Push notifications</string> <string name="pref_title_notifications_enabled">Notifications</string>
<string name="pref_title_pull_notification_check_interval">Check Interval</string>
<string name="pref_title_notification_alerts">Alerts</string> <string name="pref_title_notification_alerts">Alerts</string>
<string name="pref_title_notification_alert_sound">Notify with a sound</string> <string name="pref_title_notification_alert_sound">Notify with a sound</string>
<string name="pref_title_notification_alert_vibrate">Notify with vibration</string> <string name="pref_title_notification_alert_vibrate">Notify with vibration</string>

View File

@ -54,6 +54,12 @@
android:title="@string/pref_title_notifications_enabled" android:title="@string/pref_title_notifications_enabled"
android:defaultValue="true" /> android:defaultValue="true" />
<ListPreference android:key="pullNotificationCheckInterval"
android:title="@string/pref_title_pull_notification_check_interval"
android:entries="@array/pull_notification_check_interval_names"
android:entryValues="@array/pull_notification_check_intervals"
android:defaultValue="15" />
<PreferenceCategory <PreferenceCategory
android:dependency="notificationsEnabled" android:dependency="notificationsEnabled"
android:title="@string/pref_title_notification_filters"> android:title="@string/pref_title_notification_filters">