Added mention/reply notifications provided by a background service.
This commit is contained in:
parent
b00a3cf443
commit
83f8b4303c
8 changed files with 312 additions and 16 deletions
|
@ -4,6 +4,7 @@
|
|||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
@ -33,6 +34,10 @@
|
|||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
<activity android:name=".ViewVideoActivity" />
|
||||
<activity android:name=".ViewThreadActivity" />
|
||||
<service
|
||||
android:name=".NotificationService"
|
||||
android:description="@string/notification_service_description"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -15,9 +15,12 @@
|
|||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.SystemClock;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
|
@ -27,6 +30,10 @@ import android.view.Menu;
|
|||
import android.view.MenuItem;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private AlarmManager alarmManager;
|
||||
private PendingIntent serviceAlarmIntent;
|
||||
private boolean notificationServiceEnabled;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -35,6 +42,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
// Setup the tabs and timeline pager.
|
||||
TimelinePagerAdapter adapter = new TimelinePagerAdapter(getSupportFragmentManager());
|
||||
String[] pageTitles = {
|
||||
getString(R.string.title_home),
|
||||
|
@ -46,6 +54,25 @@ public class MainActivity extends AppCompatActivity {
|
|||
viewPager.setAdapter(adapter);
|
||||
TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout);
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
|
||||
// Retrieve notification update preference.
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
notificationServiceEnabled = preferences.getBoolean("notificationService", true);
|
||||
long notificationCheckInterval =
|
||||
preferences.getLong("notificationCheckInterval", 5 * 60 * 1000);
|
||||
// Start up the NotificationsService.
|
||||
alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
|
||||
Intent intent = new Intent(this, NotificationService.class);
|
||||
final int SERVICE_REQUEST_CODE = 8574603; // This number is arbitrary.
|
||||
serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
if (notificationServiceEnabled) {
|
||||
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||
SystemClock.elapsedRealtime(), notificationCheckInterval, serviceAlarmIntent);
|
||||
} else {
|
||||
alarmManager.cancel(serviceAlarmIntent);
|
||||
}
|
||||
}
|
||||
|
||||
private void compose() {
|
||||
|
@ -54,6 +81,9 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void logOut() {
|
||||
if (notificationServiceEnabled) {
|
||||
alarmManager.cancel(serviceAlarmIntent);
|
||||
}
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
|
|
|
@ -17,6 +17,13 @@ package com.keylesspalace.tusky;
|
|||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Notification {
|
||||
public enum Type {
|
||||
MENTION,
|
||||
|
@ -61,4 +68,27 @@ public class Notification {
|
|||
|| type == Type.FAVOURITE
|
||||
|| type == Type.REBLOG;
|
||||
}
|
||||
|
||||
public static List<Notification> parse(JSONArray array) throws JSONException {
|
||||
List<Notification> notifications = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
JSONObject object = array.getJSONObject(i);
|
||||
String id = object.getString("id");
|
||||
Notification.Type type = Notification.Type.valueOf(
|
||||
object.getString("type").toUpperCase());
|
||||
JSONObject account = object.getJSONObject("account");
|
||||
String displayName = account.getString("display_name");
|
||||
if (displayName.isEmpty()) {
|
||||
displayName = account.getString("username");
|
||||
}
|
||||
Notification notification = new Notification(type, id, displayName);
|
||||
if (notification.hasStatusType()) {
|
||||
JSONObject statusObject = object.getJSONObject("status");
|
||||
Status status = Status.parse(statusObject, false);
|
||||
notification.setStatus(status);
|
||||
}
|
||||
notifications.add(notification);
|
||||
}
|
||||
return notifications;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,228 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is part of Tusky.
|
||||
*
|
||||
* Tusky 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.*;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.VolleyError;
|
||||
import com.android.volley.toolbox.ImageRequest;
|
||||
import com.android.volley.toolbox.JsonArrayRequest;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class NotificationService extends IntentService {
|
||||
private final int NOTIFY_ID = 6; // This is an arbitrary number.
|
||||
|
||||
public NotificationService() {
|
||||
super("Tusky Notification Service");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
String domain = preferences.getString("domain", null);
|
||||
String accessToken = preferences.getString("accessToken", null);
|
||||
long date = preferences.getLong("lastUpdate", 0);
|
||||
Date lastUpdate = null;
|
||||
if (date != 0) {
|
||||
lastUpdate = new Date(date);
|
||||
}
|
||||
assert(domain != null);
|
||||
assert(accessToken != null);
|
||||
checkNotifications(domain, accessToken, lastUpdate);
|
||||
}
|
||||
|
||||
private void checkNotifications(final String domain, final String accessToken,
|
||||
final Date lastUpdate) {
|
||||
String endpoint = getString(R.string.endpoint_notifications);
|
||||
String url = "https://" + domain + endpoint;
|
||||
JsonArrayRequest request = new JsonArrayRequest(url,
|
||||
new Response.Listener<JSONArray>() {
|
||||
@Override
|
||||
public void onResponse(JSONArray response) {
|
||||
List<Notification> notifications;
|
||||
try {
|
||||
notifications = Notification.parse(response);
|
||||
} catch (JSONException e) {
|
||||
onCheckNotificationsFailure();
|
||||
return;
|
||||
}
|
||||
onCheckNotificationsSuccess(notifications, lastUpdate);
|
||||
}
|
||||
}, new Response.ErrorListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
onCheckNotificationsFailure();
|
||||
}
|
||||
}) {
|
||||
@Override
|
||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Authorization", "Bearer " + accessToken);
|
||||
return headers;
|
||||
}
|
||||
};
|
||||
VolleySingleton.getInstance(this).addToRequestQueue(request);
|
||||
}
|
||||
|
||||
private void onCheckNotificationsSuccess(List<Notification> notifications, Date lastUpdate) {
|
||||
Date newest = null;
|
||||
List<MentionResult> mentions = new ArrayList<>();
|
||||
for (Notification notification : notifications) {
|
||||
if (notification.getType() == Notification.Type.MENTION) {
|
||||
Status status = notification.getStatus();
|
||||
if (status != null) {
|
||||
Date createdAt = status.getCreatedAt();
|
||||
if (lastUpdate == null || createdAt.after(lastUpdate)) {
|
||||
MentionResult mention = new MentionResult();
|
||||
mention.content = status.getContent().toString();
|
||||
mention.displayName = notification.getDisplayName();
|
||||
mention.avatarUrl = status.getAvatar();
|
||||
mentions.add(mention);
|
||||
}
|
||||
if (newest == null || createdAt.after(newest)) {
|
||||
newest = createdAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
long now = new Date().getTime();
|
||||
if (mentions.size() > 0) {
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
editor.putLong("lastUpdate", now);
|
||||
editor.apply();
|
||||
loadAvatar(mentions, mentions.get(0).avatarUrl);
|
||||
} else if (newest != null) {
|
||||
long hoursAgo = (now - newest.getTime()) / (60 * 60 * 1000);
|
||||
if (hoursAgo >= 1) {
|
||||
dismissStaleNotifications();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onCheckNotificationsFailure() {
|
||||
//TODO: not sure if just logging here is enough?
|
||||
Log.e("Error", "Could not check notifications in the service.");
|
||||
}
|
||||
|
||||
private static class MentionResult {
|
||||
public String displayName;
|
||||
public String content;
|
||||
public String avatarUrl;
|
||||
}
|
||||
|
||||
private String truncateWithEllipses(String string, int limit) {
|
||||
if (string.length() < limit) {
|
||||
return string;
|
||||
} else {
|
||||
return string.substring(0, limit - 3) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
private void loadAvatar(final List<MentionResult> mentions, String url) {
|
||||
ImageRequest request = new ImageRequest(url, new Response.Listener<Bitmap>() {
|
||||
@Override
|
||||
public void onResponse(Bitmap response) {
|
||||
updateNotification(mentions, response);
|
||||
}
|
||||
}, 0, 0, null, null, new Response.ErrorListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
updateNotification(mentions, null);
|
||||
}
|
||||
});
|
||||
VolleySingleton.getInstance(this).addToRequestQueue(request);
|
||||
}
|
||||
|
||||
private void updateNotification(List<MentionResult> mentions, @Nullable Bitmap icon) {
|
||||
final int NOTIFICATION_CONTENT_LIMIT = 40;
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
String title;
|
||||
if (mentions.size() > 1) {
|
||||
title = String.format(
|
||||
getString(R.string.notification_service_several_mentions),
|
||||
mentions.size());
|
||||
} else {
|
||||
title = String.format(
|
||||
getString(R.string.notification_service_one_mention),
|
||||
mentions.get(0).displayName);
|
||||
}
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
|
||||
.setSmallIcon(R.drawable.ic_notify_mention)
|
||||
.setContentTitle(title);
|
||||
if (icon != null) {
|
||||
builder.setLargeIcon(icon);
|
||||
}
|
||||
if (preferences.getBoolean("notificationAlertSound", false)) {
|
||||
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
|
||||
}
|
||||
if (preferences.getBoolean("notificationStyleVibrate", false)) {
|
||||
builder.setVibrate(new long[] { 500, 500 });
|
||||
}
|
||||
if (preferences.getBoolean("notificationStyleLight", false)) {
|
||||
builder.setLights(0xFF00FF8F, 300, 1000);
|
||||
}
|
||||
for (int i = 0; i < mentions.size(); i++) {
|
||||
MentionResult mention = mentions.get(i);
|
||||
String text = truncateWithEllipses(mention.content, NOTIFICATION_CONTENT_LIMIT);
|
||||
builder.setContentText(text)
|
||||
.setNumber(i);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
builder.setVisibility(android.app.Notification.VISIBILITY_PRIVATE);
|
||||
builder.setCategory(android.app.Notification.CATEGORY_SOCIAL);
|
||||
}
|
||||
Intent resultIntent = new Intent(this, SplashActivity.class);
|
||||
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
|
||||
stackBuilder.addParentStack(SplashActivity.class);
|
||||
stackBuilder.addNextIntent(resultIntent);
|
||||
PendingIntent resultPendingIntent =
|
||||
stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
builder.setContentIntent(resultPendingIntent);
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.notify(NOTIFY_ID, builder.build());
|
||||
}
|
||||
|
||||
private void dismissStaleNotifications() {
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(NOTIFY_ID);
|
||||
}
|
||||
}
|
|
@ -106,23 +106,8 @@ public class NotificationsFragment extends SFragment implements
|
|||
new Response.Listener<JSONArray>() {
|
||||
@Override
|
||||
public void onResponse(JSONArray response) {
|
||||
List<Notification> notifications = new ArrayList<>();
|
||||
try {
|
||||
for (int i = 0; i < response.length(); i++) {
|
||||
JSONObject object = response.getJSONObject(i);
|
||||
String id = object.getString("id");
|
||||
Notification.Type type = Notification.Type.valueOf(
|
||||
object.getString("type").toUpperCase());
|
||||
JSONObject account = object.getJSONObject("account");
|
||||
String displayName = account.getString("display_name");
|
||||
Notification notification = new Notification(type, id, displayName);
|
||||
if (notification.hasStatusType()) {
|
||||
JSONObject statusObject = object.getJSONObject("status");
|
||||
Status status = Status.parse(statusObject, false);
|
||||
notification.setStatus(status);
|
||||
}
|
||||
notifications.add(notification);
|
||||
}
|
||||
List<Notification> notifications = Notification.parse(response);
|
||||
onFetchNotificationsSuccess(notifications, fromId != null);
|
||||
} catch (JSONException e) {
|
||||
onFetchNotificationsFailure();
|
||||
|
|
|
@ -214,6 +214,9 @@ public class Status {
|
|||
JSONObject account = object.getJSONObject("account");
|
||||
String accountId = account.getString("id");
|
||||
String displayName = account.getString("display_name");
|
||||
if (displayName.isEmpty()) {
|
||||
displayName = account.getString("username");
|
||||
}
|
||||
String username = account.getString("acct");
|
||||
String avatar = account.getString("avatar");
|
||||
|
||||
|
|
11
app/src/main/res/drawable/ic_notify_mention.xml
Normal file
11
app/src/main/res/drawable/ic_notify_mention.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector android:height="24dp" android:viewportHeight="637.7953"
|
||||
android:viewportWidth="637.7953" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="1" android:fillColor="#ffffff"
|
||||
android:pathData="M638.4,462C518.5,688.5 338.4,648.4 266,598.8 159,525.6 124,422.3 242.9,288.3 455.8,48.3 302.5,13.2 302.5,13.2c0,0 182.3,30.4 27.3,256.1 -80.8,117.6 -110.4,189.8 -8,253.1 110,56.5 231,-61.9 257,-103z"
|
||||
android:strokeAlpha="1" android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="1"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#ffffff"
|
||||
android:pathData="m263.5,4.2c-41,12.5 -74,22 -103,59.8C60.4,194.3 29.6,305.7 128.9,407.1 251.6,489.6 425.3,370.6 425.3,370.6l38.5,66.6c0,0 -194.3,142.4 -338,52.1C79.6,460.2 -85.6,403.7 64.9,121.1 122.5,12.7 176.6,-10.9 263.5,4.2Z"
|
||||
android:strokeAlpha="1" android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="1"/>
|
||||
</vector>
|
|
@ -86,4 +86,8 @@
|
|||
<string name="visibility_private">Private</string>
|
||||
<string name="visibility_unlisted">Unlisted</string>
|
||||
|
||||
<string name="notification_service_description">Allows Tusky to check for Mastodon notifications.</string>
|
||||
<string name="notification_service_several_mentions">%d new mentions</string>
|
||||
<string name="notification_service_one_mention">Mention from %s</string>
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue