diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 90d2ef9b..222613d0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -98,7 +98,7 @@ - + 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) { - 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); + // Start up the PullNotificationService on a repeating interval. + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + String minutesString = preferences.getString("pullNotificationCheckInterval", "15"); + long minutes = Long.valueOf(minutesString); + long checkInterval = 1000 * 60 * minutes; + AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(this, PullNotificationService.class); + PendingIntent 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() { - 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) { - 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); + // Cancel the repeating call for "pull" notifications. + AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(this, PullNotificationService.class); + PendingIntent serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE, + intent, PendingIntent.FLAG_UPDATE_CURRENT); + alarmManager.cancel(serviceAlarmIntent); + } + + protected void clearNotifications() { + SharedPreferences notificationPreferences = getApplicationContext() + .getSharedPreferences("Notifications", MODE_PRIVATE); + notificationPreferences.edit() + .putString("current", "[]") + .apply(); + + NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + manager.cancel(PullNotificationService.NOTIFY_ID); + } + + protected void setPullNotificationCheckInterval(long minutes) { + long checkInterval = 1000 * 60 * minutes; + AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(this, PullNotificationService.class); + PendingIntent serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE, + intent, PendingIntent.FLAG_UPDATE_CURRENT); + alarmManager.cancel(serviceAlarmIntent); + alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime(), checkInterval, serviceAlarmIntent); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index 7423d0f4..a4d77f02 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -210,17 +210,19 @@ public class MainActivity extends BaseActivity { composeButton = floatingBtn; } + @Override + public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { + ArrayList pageHistoryList = new ArrayList<>(); + pageHistoryList.addAll(pageHistory); + outState.putIntegerArrayList("pageHistory", pageHistoryList); + super.onSaveInstanceState(outState, outPersistentState); + } + @Override protected void onResume() { super.onResume(); - SharedPreferences notificationPreferences = getApplicationContext() - .getSharedPreferences("Notifications", MODE_PRIVATE); - notificationPreferences.edit() - .putString("current", "[]") - .apply(); - - pushNotificationClient.clearNotifications(this); + clearNotifications(); /* After editing a profile, the profile header in the navigation drawer needs to be * refreshed */ @@ -234,11 +236,50 @@ public class MainActivity extends BaseActivity { } @Override - public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { - ArrayList pageHistoryList = new ArrayList<>(); - pageHistoryList.addAll(pageHistory); - outState.putIntegerArrayList("pageHistory", pageHistoryList); - super.onSaveInstanceState(outState, outPersistentState); + 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); } private void tintTab(TabLayout.Tab tab, boolean tinted) { @@ -465,51 +506,4 @@ public class MainActivity extends BaseActivity { private void onFetchUserInfoFailure(Exception exception) { 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); - } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.java b/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.java index 58c18588..716747ee 100644 --- a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.java @@ -59,24 +59,34 @@ public class PreferencesActivity extends BaseActivity } public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (key.equals("lightTheme")) { - themeSwitched = true; - // recreate() could be used instead, but it doesn't have an animation B). - Intent intent = getIntent(); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); - Bundle savedInstanceState = new Bundle(); - saveInstanceState(savedInstanceState); - intent.putExtras(savedInstanceState); - startActivity(intent); - finish(); - overridePendingTransition(R.anim.fade_in, R.anim.fade_out); - } else if (key.equals("notificationsEnabled")) { - boolean notificationsEnabled = sharedPreferences.getBoolean("notificationsEnabled", true); - - if (notificationsEnabled) { - enablePushNotifications(); - } else { - disablePushNotifications(); + switch (key) { + case "lightTheme": { + themeSwitched = true; + // recreate() could be used instead, but it doesn't have an animation B). + Intent intent = getIntent(); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + Bundle savedInstanceState = new Bundle(); + saveInstanceState(savedInstanceState); + intent.putExtras(savedInstanceState); + startActivity(intent); + finish(); + overridePendingTransition(R.anim.fade_in, R.anim.fade_out); + break; + } + case "notificationsEnabled": { + boolean enabled = sharedPreferences.getBoolean("notificationsEnabled", true); + if (enabled) { + enablePushNotifications(); + } else { + disablePushNotifications(); + } + break; + } + case "pullNotificationCheckInterval": { + String s = sharedPreferences.getString("pullNotificationCheckInterval", "15"); + long minutes = Long.valueOf(s); + setPullNotificationCheckInterval(minutes); + break; } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/service/PullNotificationService.java b/app/src/main/java/com/keylesspalace/tusky/service/PullNotificationService.java new file mode 100644 index 00000000..fa4a18c0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/service/PullNotificationService.java @@ -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 . */ + +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>() { + @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(); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NotificationMaker.java b/app/src/main/java/com/keylesspalace/tusky/util/NotificationMaker.java index e347a5ea..5232cd6f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/NotificationMaker.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/NotificationMaker.java @@ -41,7 +41,7 @@ import com.squareup.picasso.Target; import org.json.JSONArray; import org.json.JSONException; -class NotificationMaker { +public class NotificationMaker { public static final String TAG = "NotificationMaker"; diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml new file mode 100644 index 00000000..8552a0df --- /dev/null +++ b/app/src/main/res/values/array.xml @@ -0,0 +1,27 @@ + + + + 5 minutes + 10 minutes + 15 minutes + 20 minutes + 25 minutes + 30 minutes + 45 minutes + 1 hour + 2 hours + + + + 5 + 10 + 15 + 20 + 25 + 30 + 45 + 60 + 120 + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 518ce9f7..a25916c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -146,7 +146,8 @@ Notifications Edit Notifications - Push notifications + Notifications + Check Interval Alerts Notify with a sound Notify with vibration diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 6b2d0747..214ad9bf 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -54,6 +54,12 @@ android:title="@string/pref_title_notifications_enabled" android:defaultValue="true" /> + +