From 35977c901823adba365051b3a72997ac7c69d5bc Mon Sep 17 00:00:00 2001 From: tom79 Date: Wed, 11 Sep 2019 14:17:01 +0200 Subject: [PATCH] Delayed notifications --- app/src/main/AndroidManifest.xml | 4 +- .../android/activities/BaseMainActivity.java | 5 +- .../fragments/ContentSettingsFragment.java | 13 +- .../LiveNotificationDelayedService.java | 400 ++++++++++++++++++ 4 files changed, 412 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/app/fedilab/android/services/LiveNotificationDelayedService.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 36357e4a5..6abcdc37a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -53,7 +53,7 @@ android:theme="@style/AppThemeDark"> - + diff --git a/app/src/main/java/app/fedilab/android/activities/BaseMainActivity.java b/app/src/main/java/app/fedilab/android/activities/BaseMainActivity.java index b267aeeea..22fe8839e 100644 --- a/app/src/main/java/app/fedilab/android/activities/BaseMainActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/BaseMainActivity.java @@ -115,6 +115,7 @@ import app.fedilab.android.helper.CrossActions; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MenuFloating; import app.fedilab.android.services.BackupStatusService; +import app.fedilab.android.services.LiveNotificationDelayedService; import app.fedilab.android.services.LiveNotificationService; import app.fedilab.android.services.StopLiveNotificationReceiver; import app.fedilab.android.sqlite.AccountDAO; @@ -2521,12 +2522,12 @@ public abstract class BaseMainActivity extends BaseActivity ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE); assert manager != null; for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { - if (LiveNotificationService.class.getName().equals(service.service.getClassName())) { + if (LiveNotificationDelayedService.class.getName().equals(service.service.getClassName())) { return; } } try { - Intent streamingIntent = new Intent(this, LiveNotificationService.class); + Intent streamingIntent = new Intent(this, LiveNotificationDelayedService.class); startService(streamingIntent); } catch (Exception ignored) { } diff --git a/app/src/main/java/app/fedilab/android/fragments/ContentSettingsFragment.java b/app/src/main/java/app/fedilab/android/fragments/ContentSettingsFragment.java index 97713853a..05691169f 100644 --- a/app/src/main/java/app/fedilab/android/fragments/ContentSettingsFragment.java +++ b/app/src/main/java/app/fedilab/android/fragments/ContentSettingsFragment.java @@ -96,6 +96,7 @@ import app.fedilab.android.client.Entities.Status; import app.fedilab.android.filelister.FileListerDialog; import app.fedilab.android.filelister.OnFileSelectedListener; import app.fedilab.android.helper.Helper; +import app.fedilab.android.services.LiveNotificationDelayedService; import app.fedilab.android.services.LiveNotificationService; import app.fedilab.android.services.StopLiveNotificationReceiver; import app.fedilab.android.sqlite.AccountDAO; @@ -1141,7 +1142,7 @@ public class ContentSettingsFragment extends Fragment implements ScreenShotable editor.apply(); if (set_live_notif.isChecked()) { try { - Intent streamingIntent = new Intent(context, LiveNotificationService.class); + Intent streamingIntent = new Intent(context, LiveNotificationDelayedService.class); context.startService(streamingIntent); } catch (Exception ignored) { ignored.printStackTrace(); @@ -1151,7 +1152,7 @@ public class ContentSettingsFragment extends Fragment implements ScreenShotable if (Build.VERSION.SDK_INT >= 26) { NotificationManager notif = ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)); if (notif != null) { - notif.deleteNotificationChannel(LiveNotificationService.CHANNEL_ID); + notif.deleteNotificationChannel(LiveNotificationDelayedService.CHANNEL_ID); } } } @@ -1537,13 +1538,13 @@ public class ContentSettingsFragment extends Fragment implements ScreenShotable editor.putBoolean(Helper.SET_ALLOW_STREAM + userId + instance, set_allow_live_notifications.isChecked()); editor.apply(); if (set_allow_live_notifications.isChecked()) { - LiveNotificationService.totalAccount++; + LiveNotificationDelayedService.totalAccount++; } else { - LiveNotificationService.totalAccount--; + LiveNotificationDelayedService.totalAccount--; } boolean liveNotifications = sharedpreferences.getBoolean(Helper.SET_LIVE_NOTIFICATIONS, true); if (liveNotifications) { - Intent streamingServiceIntent = new Intent(context.getApplicationContext(), LiveNotificationService.class); + Intent streamingServiceIntent = new Intent(context.getApplicationContext(), LiveNotificationDelayedService.class); try { context.startService(streamingServiceIntent); } catch (Exception ignored) { @@ -1779,7 +1780,7 @@ public class ContentSettingsFragment extends Fragment implements ScreenShotable public void onClick(View v) { Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); - intent.putExtra(Settings.EXTRA_CHANNEL_ID, LiveNotificationService.CHANNEL_ID); + intent.putExtra(Settings.EXTRA_CHANNEL_ID, LiveNotificationDelayedService.CHANNEL_ID); startActivity(intent); } }); diff --git a/app/src/main/java/app/fedilab/android/services/LiveNotificationDelayedService.java b/app/src/main/java/app/fedilab/android/services/LiveNotificationDelayedService.java new file mode 100644 index 000000000..3abdc7bbd --- /dev/null +++ b/app/src/main/java/app/fedilab/android/services/LiveNotificationDelayedService.java @@ -0,0 +1,400 @@ +package app.fedilab.android.services; +/* Copyright 2019 Thomas Schneider + * + * This file is a part of Fedilab + * + * 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. + * + * Fedilab 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 Fedilab; if not, + * see . */ + +import android.app.AlarmManager; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.SystemClock; +import android.preference.PreferenceManager; +import android.text.Html; +import android.text.SpannableString; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.SimpleTarget; +import com.bumptech.glide.request.target.Target; +import com.bumptech.glide.request.transition.Transition; + +import java.util.HashMap; +import java.util.List; +import java.util.Objects; + +import app.fedilab.android.R; +import app.fedilab.android.activities.MainActivity; +import app.fedilab.android.client.API; +import app.fedilab.android.client.APIResponse; +import app.fedilab.android.client.Entities.Account; +import app.fedilab.android.client.Entities.Notification; +import app.fedilab.android.fragments.DisplayNotificationsFragment; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.sqlite.AccountDAO; +import app.fedilab.android.sqlite.Sqlite; + +import static androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY; + + +/** + * Created by Thomas on 10/09/2019. + * Manage service for live notifications delayed + */ + +public class LiveNotificationDelayedService extends Service { + + static { + Helper.installProvider(); + } + + public static String CHANNEL_ID = "live_notifications"; + protected Account account; + boolean backgroundProcess; + private NotificationChannel channel; + public static int totalAccount = 0; + public static int eventsCount = 0; + private HashMap since_ids = new HashMap<>(); + private static Thread thread; + + public void onCreate() { + super.onCreate(); + SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); + backgroundProcess = sharedpreferences.getBoolean(Helper.SET_KEEP_BACKGROUND_PROCESS, true); + } + + private void startStream() { + + SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); + backgroundProcess = sharedpreferences.getBoolean(Helper.SET_KEEP_BACKGROUND_PROCESS, true); + boolean liveNotifications = sharedpreferences.getBoolean(Helper.SET_LIVE_NOTIFICATIONS, true); + SQLiteDatabase db = Sqlite.getInstance(getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); + if (liveNotifications) { + List accountStreams = new AccountDAO(getApplicationContext(), db).getAllAccountCrossAction(); + if (accountStreams != null) { + if( thread != null && !thread.isInterrupted()){ + thread.interrupt(); + } + thread = new Thread() { + @Override + public void run() { + //noinspection InfiniteLoopStatement + while (true) { + for (final Account accountStream : accountStreams) { + if (accountStream.getSocial() == null || accountStream.getSocial().equals("MASTODON") || accountStream.getSocial().equals("PLEROMA")) { + taks(accountStream); + } + } + SystemClock.sleep(30000); + } + } + }; + thread.start(); + } + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + + if (intent == null || intent.getBooleanExtra("stop", false)) { + stopSelf(); + } + if (backgroundProcess) { + if (Build.VERSION.SDK_INT >= 26) { + channel = new NotificationChannel(CHANNEL_ID, + "Live notifications", + NotificationManager.IMPORTANCE_DEFAULT); + + ((NotificationManager) Objects.requireNonNull(getSystemService(Context.NOTIFICATION_SERVICE))).createNotificationChannel(channel); + SQLiteDatabase db = Sqlite.getInstance(getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open(); + List accountStreams = new AccountDAO(getApplicationContext(), db).getAllAccountCrossAction(); + totalAccount = 0; + for (Account account : accountStreams) { + if (account.getSocial() == null || account.getSocial().equals("MASTODON") || account.getSocial().equals("PLEROMA")) { + final SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); + boolean allowStream = sharedpreferences.getBoolean(Helper.SET_ALLOW_STREAM + account.getId() + account.getInstance(), true); + if (allowStream) { + totalAccount++; + } + } + } + android.app.Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.top_notification)) + .setSmallIcon(R.drawable.fedilab_notification_icon) + .setContentText(getString(R.string.top_notification_message, String.valueOf(totalAccount), String.valueOf(eventsCount))).build(); + + startForeground(1, notification); + } + startStream(); + return START_STICKY; + } else { + startStream(); + return START_NOT_STICKY; + } + } + + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + super.onTaskRemoved(rootIntent); + if (backgroundProcess) { + restart(); + } + } + + private void restart() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + Intent restartServiceIntent = new Intent(LiveNotificationDelayedService.this, LiveNotificationDelayedService.class); + restartServiceIntent.setPackage(getPackageName()); + PendingIntent restartServicePendingIntent = PendingIntent.getService(getApplicationContext(), 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT); + AlarmManager alarmService = (AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE); + assert alarmService != null; + alarmService.set( + AlarmManager.ELAPSED_REALTIME, + SystemClock.elapsedRealtime() + 1000, + restartServicePendingIntent); + } + } + + private void taks(Account account) { + + if (account != null) { + String key = account.getAcct() + "@" + account.getInstance(); + + APIResponse apiResponse; + API api; + api = new API(getApplicationContext(), account.getInstance(), account.getToken()); + String last_notifid = null; + if( since_ids.containsKey(key) ){ + last_notifid = since_ids.get(key); + } + apiResponse = api.getNotifications(DisplayNotificationsFragment.Type.ALL, null, false); + + if( apiResponse.getNotifications() != null && apiResponse.getNotifications().size() > 0){ + since_ids.put(key, apiResponse.getNotifications().get(0).getId()); + for (Notification notification : apiResponse.getNotifications()) { + if( last_notifid != null && notification.getId().compareTo(last_notifid) > 0) { + onRetrieveStreaming(account, notification); + }else { + break; + } + } + + } + + } + } + + + + private void onRetrieveStreaming(Account account, Notification notification) { + + Bundle b = new Bundle(); + boolean canSendBroadCast = true; + Helper.EventStreaming event; + final SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); + String userId = sharedpreferences.getString(Helper.PREF_KEY_ID, null); + try { + eventsCount++; + if (Build.VERSION.SDK_INT >= 26) { + channel = new NotificationChannel(CHANNEL_ID, + "Live notifications", + NotificationManager.IMPORTANCE_DEFAULT); + ((NotificationManager) Objects.requireNonNull(getSystemService(Context.NOTIFICATION_SERVICE))).createNotificationChannel(channel); + android.app.Notification notificationChannel = new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.top_notification)) + .setSmallIcon(R.drawable.fedilab_notification_icon) + .setContentText(getString(R.string.top_notification_message, String.valueOf(totalAccount), String.valueOf(eventsCount))).build(); + + startForeground(1, notificationChannel); + } + event = Helper.EventStreaming.NOTIFICATION; + boolean liveNotifications = sharedpreferences.getBoolean(Helper.SET_LIVE_NOTIFICATIONS, true); + boolean canNotify = Helper.canNotify(getApplicationContext()); + boolean notify = sharedpreferences.getBoolean(Helper.SET_NOTIFY, true); + String targeted_account = null; + Helper.NotifType notifType = Helper.NotifType.MENTION; + boolean activityRunning = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("isMainActivityRunning", false); + boolean allowStream = sharedpreferences.getBoolean(Helper.SET_ALLOW_STREAM + account.getId() + account.getInstance(), true); + if (!allowStream) { + canNotify = false; + } + if ((userId == null || !userId.equals(account.getId()) || !activityRunning) && liveNotifications && canNotify && notify) { + boolean notif_follow = sharedpreferences.getBoolean(Helper.SET_NOTIF_FOLLOW, true); + boolean notif_add = sharedpreferences.getBoolean(Helper.SET_NOTIF_ADD, true); + boolean notif_mention = sharedpreferences.getBoolean(Helper.SET_NOTIF_MENTION, true); + boolean notif_share = sharedpreferences.getBoolean(Helper.SET_NOTIF_SHARE, true); + boolean notif_poll = sharedpreferences.getBoolean(Helper.SET_NOTIF_POLL, true); + boolean somethingToPush = (notif_follow || notif_add || notif_mention || notif_share || notif_poll); + String message = null; + if (somethingToPush) { + switch (notification.getType()) { + case "mention": + notifType = Helper.NotifType.MENTION; + if (notif_mention) { + if (notification.getAccount().getDisplay_name() != null && notification.getAccount().getDisplay_name().length() > 0) + message = String.format("%s %s", Helper.shortnameToUnicode(notification.getAccount().getDisplay_name(), true), getString(R.string.notif_mention)); + else + message = String.format("@%s %s", notification.getAccount().getAcct(), getString(R.string.notif_mention)); + if (notification.getStatus() != null) { + if (notification.getStatus().getSpoiler_text() != null && notification.getStatus().getSpoiler_text().length() > 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + message = "\n" + new SpannableString(Html.fromHtml(notification.getStatus().getSpoiler_text(), FROM_HTML_MODE_LEGACY)); + else + message = "\n" + new SpannableString(Html.fromHtml(notification.getStatus().getSpoiler_text())); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + message = "\n" + new SpannableString(Html.fromHtml(notification.getStatus().getContent(), FROM_HTML_MODE_LEGACY)); + else + message = "\n" + new SpannableString(Html.fromHtml(notification.getStatus().getContent())); + } + } + } else { + canSendBroadCast = false; + } + break; + case "reblog": + notifType = Helper.NotifType.BOOST; + if (notif_share) { + if (notification.getAccount().getDisplay_name() != null && notification.getAccount().getDisplay_name().length() > 0) + message = String.format("%s %s", Helper.shortnameToUnicode(notification.getAccount().getDisplay_name(), true), getString(R.string.notif_reblog)); + else + message = String.format("@%s %s", notification.getAccount().getAcct(), getString(R.string.notif_reblog)); + } else { + canSendBroadCast = false; + } + break; + case "favourite": + notifType = Helper.NotifType.FAV; + if (notif_add) { + if (notification.getAccount().getDisplay_name() != null && notification.getAccount().getDisplay_name().length() > 0) + message = String.format("%s %s", Helper.shortnameToUnicode(notification.getAccount().getDisplay_name(), true), getString(R.string.notif_favourite)); + else + message = String.format("@%s %s", notification.getAccount().getAcct(), getString(R.string.notif_favourite)); + } else { + canSendBroadCast = false; + } + break; + case "follow": + notifType = Helper.NotifType.FOLLLOW; + if (notif_follow) { + if (notification.getAccount().getDisplay_name() != null && notification.getAccount().getDisplay_name().length() > 0) + message = String.format("%s %s", Helper.shortnameToUnicode(notification.getAccount().getDisplay_name(), true), getString(R.string.notif_follow)); + else + message = String.format("@%s %s", notification.getAccount().getAcct(), getString(R.string.notif_follow)); + targeted_account = notification.getAccount().getId(); + } else { + canSendBroadCast = false; + } + break; + case "poll": + notifType = Helper.NotifType.POLL; + if (notif_poll) { + if (notification.getAccount().getId() != null && notification.getAccount().getId().equals(userId)) + message = getString(R.string.notif_poll_self); + else + message = getString(R.string.notif_poll); + } else { + canSendBroadCast = false; + } + break; + default: + } + //Some others notification + final Intent intent = new Intent(getApplicationContext(), MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Helper.INTENT_ACTION, Helper.NOTIFICATION_INTENT); + intent.putExtra(Helper.PREF_KEY_ID, account.getId()); + intent.putExtra(Helper.PREF_INSTANCE, account.getInstance()); + if (targeted_account != null) { + intent.putExtra(Helper.INTENT_TARGETED_ACCOUNT, targeted_account); + } + final String finalMessage = message; + Handler mainHandler = new Handler(Looper.getMainLooper()); + Helper.NotifType finalNotifType = notifType; + Runnable myRunnable = new Runnable() { + @Override + public void run() { + if (finalMessage != null) { + Glide.with(getApplicationContext()) + .asBitmap() + .load(notification.getAccount().getAvatar()) + .listener(new RequestListener() { + @Override + public boolean onResourceReady(Bitmap resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { + return false; + } + + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + Helper.notify_user(getApplicationContext(), account, intent, BitmapFactory.decodeResource(getResources(), + R.drawable.mastodonlogo), finalNotifType, "@" + notification.getAccount().getAcct(), finalMessage); + return false; + } + }) + .into(new SimpleTarget() { + @Override + public void onResourceReady(@NonNull Bitmap resource, Transition transition) { + + Helper.notify_user(getApplicationContext(), account, intent, resource, finalNotifType, "@" + notification.getAccount().getAcct(), finalMessage); + } + }); + } + } + }; + mainHandler.post(myRunnable); + } + } + + if (canSendBroadCast) { + b.putString("userIdService", account.getId()); + Intent intentBC = new Intent(Helper.RECEIVE_DATA); + intentBC.putExtra("eventStreaming", event); + intentBC.putExtras(b); + b.putParcelable("data", notification); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intentBC); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(Helper.LAST_NOTIFICATION_MAX_ID + account.getId() + account.getInstance(), notification.getId()); + editor.apply(); + } + } catch (Exception ignored) { + } + } + +}