From de52fa2da138a1e8d85f3d55b56bf7aaf1e9d25f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Thu, 6 Dec 2018 13:35:19 -0500
Subject: [PATCH] notification: add permanent notification to stay in
 background

This should allow scheduled jobs to run correctly and make the application
working even without push notifications.

Change-Id: I3ab5e345039151a361c31be628ba6abfcd50aeed
Gitlab: #525
---
 ring-android/app/src/main/AndroidManifest.xml |   1 +
 .../cx/ring/application/RingApplication.java  |   8 +-
 .../java/cx/ring/service/DRingService.java    |  79 ++++++++-------
 .../services/NotificationServiceImpl.java     |  31 +++++-
 .../SharedPreferencesServiceImpl.java         |   3 +
 .../cx/ring/settings/SettingsFragment.java    |   6 +-
 ...seline_notification_important_black_24.png | Bin 0 -> 237 bytes
 ...seline_notification_important_black_24.png | Bin 0 -> 186 bytes
 ...seline_notification_important_black_24.png | Bin 0 -> 291 bytes
 ...seline_notification_important_black_24.png | Bin 0 -> 399 bytes
 ...seline_notification_important_black_24.png | Bin 0 -> 503 bytes
 .../baseline_notification_important_24.xml    |  10 ++
 .../app/src/main/res/layout/frag_settings.xml |  58 +++++++++++
 .../app/src/main/res/values/strings.xml       |  93 +-----------------
 .../src/main/res/values/strings_account.xml   |   2 +
 .../main/res/values/strings_preferences.xml   |   3 +
 .../src/main/java/cx/ring/model/Settings.java |  10 ++
 .../cx/ring/services/NotificationService.java |  38 ++++---
 18 files changed, 191 insertions(+), 151 deletions(-)
 create mode 100755 ring-android/app/src/main/res/drawable-hdpi/baseline_notification_important_black_24.png
 create mode 100755 ring-android/app/src/main/res/drawable-mdpi/baseline_notification_important_black_24.png
 create mode 100755 ring-android/app/src/main/res/drawable-xhdpi/baseline_notification_important_black_24.png
 create mode 100755 ring-android/app/src/main/res/drawable-xxhdpi/baseline_notification_important_black_24.png
 create mode 100755 ring-android/app/src/main/res/drawable-xxxhdpi/baseline_notification_important_black_24.png
 create mode 100755 ring-android/app/src/main/res/drawable/baseline_notification_important_24.xml

diff --git a/ring-android/app/src/main/AndroidManifest.xml b/ring-android/app/src/main/AndroidManifest.xml
index 03ec0896a..26424f6d0 100644
--- a/ring-android/app/src/main/AndroidManifest.xml
+++ b/ring-android/app/src/main/AndroidManifest.xml
@@ -49,6 +49,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
     <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
 
     <uses-feature
         android:name="android.hardware.wifi"
diff --git a/ring-android/app/src/main/java/cx/ring/application/RingApplication.java b/ring-android/app/src/main/java/cx/ring/application/RingApplication.java
index da60bb5f9..046e96389 100644
--- a/ring-android/app/src/main/java/cx/ring/application/RingApplication.java
+++ b/ring-android/app/src/main/java/cx/ring/application/RingApplication.java
@@ -44,7 +44,6 @@ import javax.inject.Inject;
 import javax.inject.Named;
 
 import androidx.annotation.RequiresApi;
-import androidx.appcompat.app.AppCompatDelegate;
 import cx.ring.BuildConfig;
 import cx.ring.contacts.AvatarFactory;
 import cx.ring.daemon.Ringservice;
@@ -235,7 +234,12 @@ public abstract class RingApplication extends Application {
     public void startDaemon() {
         if (!DRingService.isRunning) {
             try {
-                startService(new Intent(this, DRingService.class));
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+                        && mPreferencesService.getSettings().isAllowPersistentNotification()) {
+                    startForegroundService(new Intent(this, DRingService.class));
+                } else {
+                    startService(new Intent(this, DRingService.class));
+                }
             } catch (Exception e) {
                 Log.w(TAG, "Error starting daemon service");
             }
diff --git a/ring-android/app/src/main/java/cx/ring/service/DRingService.java b/ring-android/app/src/main/java/cx/ring/service/DRingService.java
index be443d88e..82a425ee1 100644
--- a/ring-android/app/src/main/java/cx/ring/service/DRingService.java
+++ b/ring-android/app/src/main/java/cx/ring/service/DRingService.java
@@ -23,6 +23,7 @@
  */
 package cx.ring.service;
 
+import android.app.Notification;
 import android.app.Service;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -54,6 +55,7 @@ import java.util.concurrent.ScheduledExecutorService;
 
 import javax.inject.Inject;
 import javax.inject.Named;
+import javax.inject.Singleton;
 
 import cx.ring.BuildConfig;
 import cx.ring.application.RingApplication;
@@ -63,6 +65,7 @@ import cx.ring.facades.ConversationFacade;
 import cx.ring.fragments.ConversationFragment;
 import cx.ring.model.Account;
 import cx.ring.model.Codec;
+import cx.ring.model.Settings;
 import cx.ring.model.SipCall;
 import cx.ring.model.Uri;
 import cx.ring.services.AccountService;
@@ -74,13 +77,13 @@ import cx.ring.services.DeviceRuntimeService;
 import cx.ring.services.HardwareService;
 import cx.ring.services.HistoryService;
 import cx.ring.services.NotificationService;
-import cx.ring.services.NotificationServiceImpl;
 import cx.ring.services.PreferencesService;
 import cx.ring.tv.call.TVCallActivity;
 import cx.ring.utils.DeviceUtils;
 import io.reactivex.disposables.CompositeDisposable;
 
 public class DRingService extends Service {
+    private static final String TAG = DRingService.class.getSimpleName();
 
     public static final String ACTION_TRUST_REQUEST_ACCEPT = BuildConfig.APPLICATION_ID + ".action.TRUST_REQUEST_ACCEPT";
     public static final String ACTION_TRUST_REQUEST_REFUSE = BuildConfig.APPLICATION_ID + ".action.TRUST_REQUEST_REFUSE";
@@ -107,20 +110,40 @@ public class DRingService extends Service {
     public static final String PUSH_RECEIVED_FIELD_DATA = "data";
     public static final String PUSH_TOKEN_FIELD_TOKEN = "token";
 
-    private static final String TAG = DRingService.class.getSimpleName();
+    private static final int NOTIFICATION_ID = 1;
+
     private final ContactsContentObserver contactContentObserver = new ContactsContentObserver();
-    @Inject
+    @Inject @Singleton
     protected DaemonService mDaemonService;
-    @Inject
+    @Inject @Singleton
     protected CallService mCallService;
-    @Inject
+    @Inject @Singleton
     protected ConferenceService mConferenceService;
-    @Inject
+    @Inject @Singleton
     protected AccountService mAccountService;
-    @Inject
+    @Inject @Singleton
     protected HardwareService mHardwareService;
-    @Inject
+    @Inject @Singleton
     protected HistoryService mHistoryService;
+    @Inject @Singleton
+    protected DeviceRuntimeService mDeviceRuntimeService;
+    @Inject @Singleton
+    protected NotificationService mNotificationService;
+    @Inject @Singleton
+    protected ContactService mContactService;
+    @Inject @Singleton
+    protected PreferencesService mPreferencesService;
+    @Inject @Singleton
+    protected ConversationFacade mConversationFacade;
+    @Inject @Singleton
+    @Named("DaemonExecutor")
+    protected ScheduledExecutorService mExecutor;
+
+    private final Handler mHandler = new Handler();
+    private final CompositeDisposable mDisposableBag = new CompositeDisposable();
+    private final Runnable mConnectivityChecker = this::updateConnectivityState;
+    public static boolean isRunning = false;
+
     protected final IDRingService.Stub mBinder = new IDRingService.Stub() {
 
         @Override
@@ -499,29 +522,6 @@ public class DRingService extends Service {
             mAccountService.registerName(account, password, name);
         }
     };
-    @Inject
-    protected DeviceRuntimeService mDeviceRuntimeService;
-    @Inject
-    protected NotificationService mNotificationService;
-    @Inject
-    protected ContactService mContactService;
-    @Inject
-    protected PreferencesService mPreferencesService;
-    @Inject
-    protected ConversationFacade mConversationFacade;
-    @Inject
-    @Named("DaemonExecutor")
-    protected ScheduledExecutorService mExecutor;
-
-    private final Handler mHandler = new Handler();
-    private final Runnable mConnectivityChecker = new Runnable() {
-        @Override
-        public void run() {
-            updateConnectivityState();
-        }
-    };
-
-    public static boolean isRunning = false;
 
     private final BroadcastReceiver receiver = new BroadcastReceiver() {
         @Override
@@ -550,8 +550,6 @@ public class DRingService extends Service {
         }
     };
 
-    private final CompositeDisposable mDisposableBag = new CompositeDisposable();
-
     @Override
     public void onCreate() {
         Log.i(TAG, "onCreated");
@@ -578,6 +576,7 @@ public class DRingService extends Service {
         mDisposableBag.add(mPreferencesService.getSettingsSubject().subscribe(s -> {
             refreshContacts();
             updateConnectivityState();
+            showSystemNotification(s);
         }));
         mDisposableBag.add(mCallService.getCallSubject().subscribe(call -> {
             SipCall.State newState = call.getCallState();
@@ -608,6 +607,14 @@ public class DRingService extends Service {
         isRunning = false;
     }
 
+    private void showSystemNotification(Settings settings) {
+        if (settings.isAllowPersistentNotification()) {
+            startForeground(NOTIFICATION_ID, (Notification) mNotificationService.getServiceNotification());
+        } else {
+            stopForeground(true);
+        }
+    }
+
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         Log.i(TAG, "onStartCommand " + (intent == null ? "null" : intent.getAction()) + " " + flags + " " + startId);
@@ -732,8 +739,8 @@ public class DRingService extends Service {
     }
 
     private void handleTrustRequestAction(String action, Bundle extras) {
-        String account = extras.getString(NotificationServiceImpl.TRUST_REQUEST_NOTIFICATION_ACCOUNT_ID);
-        Uri from = new Uri(extras.getString(NotificationServiceImpl.TRUST_REQUEST_NOTIFICATION_FROM));
+        String account = extras.getString(NotificationService.TRUST_REQUEST_NOTIFICATION_ACCOUNT_ID);
+        Uri from = new Uri(extras.getString(NotificationService.TRUST_REQUEST_NOTIFICATION_FROM));
         if (account != null) {
             mNotificationService.cancelTrustRequestNotification(account);
             switch (action) {
@@ -752,7 +759,7 @@ public class DRingService extends Service {
     }
 
     private void handleCallAction(String action, Bundle extras) {
-        String callId = extras.getString(NotificationServiceImpl.KEY_CALL_ID);
+        String callId = extras.getString(NotificationService.KEY_CALL_ID);
 
         if (callId == null || callId.isEmpty()) {
             return;
diff --git a/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java
index 55ef1aa07..e0c7fa93a 100644
--- a/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java
+++ b/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java
@@ -70,7 +70,7 @@ import cx.ring.utils.FileUtils;
 import cx.ring.utils.Log;
 import cx.ring.utils.ResourceMapper;
 
-public class NotificationServiceImpl extends NotificationService {
+public class NotificationServiceImpl implements NotificationService {
 
     private static final String TAG = NotificationServiceImpl.class.getSimpleName();
 
@@ -84,6 +84,7 @@ public class NotificationServiceImpl extends NotificationService {
     private static final String NOTIF_CHANNEL_REQUEST = "requests";
     private static final String NOTIF_CHANNEL_FILE_TRANSFER = "file_transfer";
     private static final String NOTIF_CHANNEL_MISSED_CALL = "missed_call";
+    private static final String NOTIF_CHANNEL_SERVICE = "service";
 
     private final SparseArray<NotificationCompat.Builder> mNotificationBuilders = new SparseArray<>();
     @Inject
@@ -152,6 +153,15 @@ public class NotificationServiceImpl extends NotificationService {
         missedCallsChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
         missedCallsChannel.setSound(null, null);
         notificationManager.createNotificationChannel(missedCallsChannel);
+
+        // Background service channel
+        NotificationChannel backgroundChannel = new NotificationChannel(NOTIF_CHANNEL_SERVICE, mContext.getString(R.string.notif_channel_background_service), NotificationManager.IMPORTANCE_LOW);
+        backgroundChannel.setDescription(mContext.getString(R.string.notif_channel_background_service_descr));
+        backgroundChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
+        backgroundChannel.enableLights(false);
+        backgroundChannel.enableVibration(false);
+        backgroundChannel.setShowBadge(false);
+        notificationManager.createNotificationChannel(backgroundChannel);
     }
 
     @Override
@@ -543,6 +553,25 @@ public class NotificationServiceImpl extends NotificationService {
         notificationManager.notify(notificationId, messageNotificationBuilder.build());
     }
 
+    @Override
+    public Object getServiceNotification() {
+        Intent intentHome = new Intent(Intent.ACTION_VIEW)
+                .setClass(mContext, HomeActivity.class)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        PendingIntent pendIntent = PendingIntent.getActivity(mContext, 0, intentHome, PendingIntent.FLAG_UPDATE_CURRENT);
+        NotificationCompat.Builder messageNotificationBuilder = new NotificationCompat.Builder(mContext, NotificationServiceImpl.NOTIF_CHANNEL_SERVICE);
+        messageNotificationBuilder
+                .setContentTitle(mContext.getText(R.string.app_name))
+                .setContentText(mContext.getText(R.string.notif_background_service))
+                .setSmallIcon(R.drawable.ic_ring_logo_white)
+                .setContentIntent(pendIntent)
+                .setVisibility(NotificationCompat.VISIBILITY_SECRET)
+                .setPriority(NotificationCompat.PRIORITY_MIN)
+                .setOngoing(true)
+                .setCategory(NotificationCompat.CATEGORY_SERVICE);
+        return messageNotificationBuilder.build();
+    }
+
     @Override
     public void cancelTextNotification(Uri contact) {
         if (contact == null) {
diff --git a/ring-android/app/src/main/java/cx/ring/services/SharedPreferencesServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/SharedPreferencesServiceImpl.java
index 7260c3124..bd01b9598 100644
--- a/ring-android/app/src/main/java/cx/ring/services/SharedPreferencesServiceImpl.java
+++ b/ring-android/app/src/main/java/cx/ring/services/SharedPreferencesServiceImpl.java
@@ -42,6 +42,7 @@ public class SharedPreferencesServiceImpl extends PreferencesService {
     private static final String RING_REQUESTS = "ring_requests";
     private static final String RING_MOBILE_DATA = "mobile_data";
     private static final String RING_PUSH_NOTIFICATIONS = "push_notifs";
+    private static final String RING_PERSISTENT_NOTIFICATION = "persistent_notif";
     private static final String RING_HD = "hd_upload";
     private static final String RING_SYSTEM_CONTACTS = "system_contacts";
     private static final String RING_PLACE_CALLS = "place_calls";
@@ -61,6 +62,7 @@ public class SharedPreferencesServiceImpl extends PreferencesService {
         edit.putBoolean(RING_PLACE_CALLS, settings.isAllowPlaceSystemCalls());
         edit.putBoolean(RING_ON_STARTUP, settings.isAllowRingOnStartup());
         edit.putBoolean(RING_PUSH_NOTIFICATIONS, settings.isAllowPushNotifications());
+        edit.putBoolean(RING_PERSISTENT_NOTIFICATION, settings.isAllowPersistentNotification());
         edit.putBoolean(RING_HD, settings.isHD());
         edit.apply();
     }
@@ -77,6 +79,7 @@ public class SharedPreferencesServiceImpl extends PreferencesService {
         settings.setAllowPlaceSystemCalls(appPrefs.getBoolean(RING_PLACE_CALLS, false));
         settings.setAllowRingOnStartup(appPrefs.getBoolean(RING_ON_STARTUP, true));
         settings.setAllowPushNotifications(appPrefs.getBoolean(RING_PUSH_NOTIFICATIONS, false));
+        settings.setAllowPersistentNotification(appPrefs.getBoolean(RING_PERSISTENT_NOTIFICATION, false));
         settings.setHD(appPrefs.getBoolean(RING_HD, false));
         return settings;
     }
diff --git a/ring-android/app/src/main/java/cx/ring/settings/SettingsFragment.java b/ring-android/app/src/main/java/cx/ring/settings/SettingsFragment.java
index 3bbbf798f..b88958ae3 100644
--- a/ring-android/app/src/main/java/cx/ring/settings/SettingsFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/settings/SettingsFragment.java
@@ -65,6 +65,8 @@ public class SettingsFragment extends BaseFragment<SettingsPresenter> implements
     Switch mViewPlaceCall;
     @BindView(R.id.settings_startup)
     Switch mViewStartup;
+    @BindView(R.id.settings_persistNotification)
+    Switch mViewPersistNotif;
     private boolean mIsRefreshingViewFromPresenter;
     @BindView(R.id.settings_hd)
     Switch mViewHD;
@@ -107,7 +109,7 @@ public class SettingsFragment extends BaseFragment<SettingsPresenter> implements
         menu.clear();
     }
 
-    @OnCheckedChanged({R.id.settings_mobile_data, R.id.settings_push_notifications, R.id.settings_hd, R.id.settings_contacts, R.id.settings_place_call, R.id.settings_startup})
+    @OnCheckedChanged({R.id.settings_mobile_data, R.id.settings_push_notifications, R.id.settings_hd, R.id.settings_contacts, R.id.settings_place_call, R.id.settings_startup, R.id.settings_persistNotification})
     public void onSettingsCheckedChanged(CompoundButton button, boolean isChecked) {
 
         String neededPermission = null;
@@ -156,6 +158,7 @@ public class SettingsFragment extends BaseFragment<SettingsPresenter> implements
         newSettings.setAllowPlaceSystemCalls(mViewPlaceCall.isChecked());
         newSettings.setAllowRingOnStartup(mViewStartup.isChecked());
         newSettings.setAllowPushNotifications(mViewPushNotifications.isChecked());
+        newSettings.setAllowPersistentNotification(mViewPersistNotif.isChecked());
         newSettings.setHD(mViewHD.isChecked());
 
         // save settings according to UI inputs
@@ -236,6 +239,7 @@ public class SettingsFragment extends BaseFragment<SettingsPresenter> implements
         mIsRefreshingViewFromPresenter = true;
         mViewMobileData.setChecked(viewModel.isAllowMobileData());
         mViewPushNotifications.setChecked(viewModel.isAllowPushNotifications());
+        mViewPersistNotif.setChecked(viewModel.isAllowPersistentNotification());
         mViewContacts.setChecked(viewModel.isAllowSystemContacts());
         mViewPlaceCall.setChecked(viewModel.isAllowPlaceSystemCalls());
         mViewStartup.setChecked(viewModel.isAllowRingOnStartup());
diff --git a/ring-android/app/src/main/res/drawable-hdpi/baseline_notification_important_black_24.png b/ring-android/app/src/main/res/drawable-hdpi/baseline_notification_important_black_24.png
new file mode 100755
index 0000000000000000000000000000000000000000..8cbb5f6e737e90094b5ed4210e743b5e97384ba9
GIT binary patch
literal 237
zcmeAS@N?(olHy`uVBq!ia0y~yU{C>J4i*Lm25-&)VFm_<EuJopAr*{IFEp|p3Xp02
zsK3{3uY;|lgTh{K7PW?H3z!lT`mbJ4Hc4POJg2R@v-JP=?u~!#82I=uZ_7}=C#(3*
zB}(!7ERE<Vw?1uGl~gU7nxeybe4<osXtBZt&aYDh5}cc>vTh`pJa4W0lG1ikuGEXs
zBXGH_+Rk!~<4Zp9iOvYQw~$M?vfbpxgzO&YcZ&^q=1yEF($6C4&13vAm}^bA)C~cz
qCso2v8)M8Do!JqzEq2B_{>NX7jxEcZ^^k#qfx*+&&t;ucLK6T(R9pZ6

literal 0
HcmV?d00001

diff --git a/ring-android/app/src/main/res/drawable-mdpi/baseline_notification_important_black_24.png b/ring-android/app/src/main/res/drawable-mdpi/baseline_notification_important_black_24.png
new file mode 100755
index 0000000000000000000000000000000000000000..267af2dc9cdf1b1b786a188cd4e0915ffff472a8
GIT binary patch
literal 186
zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4i*Lm2CurW#S9D#jh-%!Ar*{APj6&o3}j%r
zxYBXiDsKi=4aN&hUsQy=8J;jOa5K~}d{?`B)a+oWet~jJ`|?x)rNhBYA{W;wG0J_|
z<e;wmp?PMr;I51w;l&2GSQo@<_G~iex9r(v&ac~3@_E7W_E>vc273#Y2@8)j|11`_
oeX(1LBk*yi*w0Y0Z4pdsE-ZesZ!xPR0|Nttr>mdKI;Vst02dKIC;$Ke

literal 0
HcmV?d00001

diff --git a/ring-android/app/src/main/res/drawable-xhdpi/baseline_notification_important_black_24.png b/ring-android/app/src/main/res/drawable-xhdpi/baseline_notification_important_black_24.png
new file mode 100755
index 0000000000000000000000000000000000000000..e6b9a7bdaab985bb2ad1ad1cc0efc310cec3013e
GIT binary patch
literal 291
zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4i*LmhQHi~JPZsBuRL8GLn;{GUfSx&;3(pH
zvD>@Jy2ODcRv?g>bph*xX%EE`c@qq0DWpD__ps*9*R>0(<Lt^)D|7@n7S8&5I#jqx
zf7XWErjNV-{yreLZ-e{i7&}>irnOHV&Ww4sA>7PWPj1Bnp6cY~ArFGoza5-au#n~V
z5v~G__QU?HVVy@OCcc@x+TGpe_>IkXH>TfKW{MR2_Oc<cV~s!87NzJ(Ox!2JmLAaB
zq^w!cab`)w-$@59vohu$)csZ4sO1wgwQkRM&c7A53+46I&)ogP|0j9%jm2vp>^!L6
w$Y?YDlvUa8P@xZwY{GA1@}}@G?LWafvujGii|rL93=9kmp00i_>zopr09W*To&W#<

literal 0
HcmV?d00001

diff --git a/ring-android/app/src/main/res/drawable-xxhdpi/baseline_notification_important_black_24.png b/ring-android/app/src/main/res/drawable-xxhdpi/baseline_notification_important_black_24.png
new file mode 100755
index 0000000000000000000000000000000000000000..41719ffa0d8d01048f46778d97f7c9a4f4069755
GIT binary patch
literal 399
zcmeAS@N?(olHy`uVBq!ia0y~yVDJE84i*LmhW}5h&oD4BhIzU;hEy=Vz1-;5Tqx1{
z(fx`W<I)w`E4o*>GVrvzr14Hn6xyKZuxr9K(Uge+rHtAW)PfXdiQf3ecKA!=wSAla
zwtp|rd(hY{-BPUQI?qP)*dZO!HJiDLS;CL)DsxLXePjJvfe<~RDatE?R8~B3lr_#N
zJXfyrqT6V;k8zM^--_h=H<O*0NlfI8+i3Ugkd^X;rLA9gsztWFk@)?*i|3-8*?}_2
zNug29tfrC9{hnDj8miNTb~1@2S00&kC7HE$f=d?r>z#Smif?<w9^>2PkTI|9%>py-
z6H7MUaCDQJDeQSc(<pq>g6tGIRY(34d%T<OO~~|ax^-fP$_Xac%7C8A3G9^tGj3Od
zC_&-19S)4b>l992R6AMx#q*a;?Ib(*`6<qE_BJPWvWdy^HtjsT=<&P+qll6ej<lxR
z9_M(4S+=-{zK)ve%H&;eK)5Y1c|J?>Im_?Nr%lhXN2^@uXJBAp@O1TaS?83{1OO)e
Bql^Fm

literal 0
HcmV?d00001

diff --git a/ring-android/app/src/main/res/drawable-xxxhdpi/baseline_notification_important_black_24.png b/ring-android/app/src/main/res/drawable-xxxhdpi/baseline_notification_important_black_24.png
new file mode 100755
index 0000000000000000000000000000000000000000..d1c18282036a7c83d30e907b8f91a100f169f130
GIT binary patch
literal 503
zcmeAS@N?(olHy`uVBq!ia0y~yU`POA4i*Lm29JsRH#0CW?(=kU45?szdzH~8B~YaO
zqyC48o2(BuZf+6nVqjucQBdOWSm2_lpgMtxL8O5}i-BqBLM@Gut^ffIQ!cl?!VX5;
zSKZtBe(s$6@(kzNUq7l-c|NCDFR`bX<>4Gdp&n05$GA5wBF<4a+~RU)i=B~ho0TJB
zTsE8SP2w@<%MI0MCLDP5p}|CALQv6MpN;J-(>I<Lk)0AGZheA1d~tWs$_+kkHA#}E
zmrQY)YW1o0>dGJ{W|Il}>kcxo&UV<hvyjWA<;ey|uArxe(;L|Ciaf9{_wd`ju=hol
zM_;u1raG19Uj+jXrM_bnS8*}!(#UDa_%^Zp`>Z{?WtB4BLzr637DWVb&rt3%-r5kX
z^rLqLD@T36_0>`rns-TFZOFW`b|u@Ih3l3{ZD`$<e6>OOO6*Fun1y=5ye9(GS1~?f
zluNKzu;Kr}073?`59~LxS2Tdo8KqORx7OBK96a;$A&c?zcHuK~<oyha>nsi)``O6C
z{9Ieb<KSUtk!OrmbBaq_KD?T8@8E34_9^!c&1U>O<(~b#2?yq_NH`E=kRT{r$9nG|
zBd6PzO<rw#B93vboe`8ci8pU&+GNjviw;;mpHt1oa5}I{=a#`_1_lNOPgg&ebxsLQ
E07ewh$N&HU

literal 0
HcmV?d00001

diff --git a/ring-android/app/src/main/res/drawable/baseline_notification_important_24.xml b/ring-android/app/src/main/res/drawable/baseline_notification_important_24.xml
new file mode 100755
index 000000000..0c7c03b6d
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable/baseline_notification_important_24.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2zM13,16h-2v-2h2v2zM13,12h-2L11,8h2v4zM12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2z"/>
+</vector>
diff --git a/ring-android/app/src/main/res/layout/frag_settings.xml b/ring-android/app/src/main/res/layout/frag_settings.xml
index edac7fbb8..45edd2d9e 100644
--- a/ring-android/app/src/main/res/layout/frag_settings.xml
+++ b/ring-android/app/src/main/res/layout/frag_settings.xml
@@ -407,6 +407,64 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
                 <!-- Privacy -->
 
+                <RelativeLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:orientation="horizontal"
+                    android:padding="8dp"
+                    android:weightSum="1">
+
+                    <LinearLayout
+                        android:id="@+id/system_persistNotification_image"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_alignParentStart="true"
+                        android:layout_centerVertical="true">
+
+
+                        <ImageView
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_gravity="start"
+                            android:contentDescription="@string/pref_persistNotification_summary"
+                            app:srcCompat="@drawable/baseline_notification_important_black_24" />
+
+                    </LinearLayout>
+
+                    <LinearLayout
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_centerVertical="true"
+                        android:layout_toStartOf="@+id/settings_persistNotification"
+                        android:layout_toEndOf="@+id/system_persistNotification_image"
+                        android:orientation="vertical"
+                        android:paddingLeft="16dp"
+                        android:paddingRight="16dp">
+
+                        <TextView
+                            style="@style/ListPrimary"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:ellipsize="end"
+                            android:lines="1"
+                            android:text="@string/pref_persistNotification_title" />
+
+                        <TextView
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:text="@string/pref_persistNotification_summary" />
+
+                    </LinearLayout>
+
+                    <Switch
+                        android:id="@+id/settings_persistNotification"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_alignParentEnd="true"
+                        android:checked="false" />
+
+                </RelativeLayout>
+
                 <TextView
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
diff --git a/ring-android/app/src/main/res/values/strings.xml b/ring-android/app/src/main/res/values/strings.xml
index 8ef4a9663..e2dcf240f 100644
--- a/ring-android/app/src/main/res/values/strings.xml
+++ b/ring-android/app/src/main/res/values/strings.xml
@@ -107,6 +107,9 @@ along with this program; if not, write to the Free Software
     <string name="notif_channel_messages">Messages</string>
     <string name="notif_channel_requests">Requests</string>
     <string name="notif_channel_file_transfer">File transfer</string>
+    <string name="notif_channel_background_service">Background service</string>
+    <string name="notif_channel_background_service_descr">Allows receiving calls and messages anytime.</string>
+    <string name="notif_background_service">Ring is currently running in background</string>
     <string name="notif_mark_as_read">Mark as read</string>
     <string name="notif_dismiss">Dismiss</string>
     <string name="notif_reply">Reply</string>
@@ -267,95 +270,5 @@ along with this program; if not, write to the Free Software
     <string name="menu_file_download">Download file</string>
     <string name="menu_file_delete">Delete file</string>
     <string name="menu_file_share">Share file</string>
-    <string name="large_text" translatable="false">
-        "Material is the metaphor.\n\n"
-
-        "A material metaphor is the unifying theory of a rationalized space and a system of motion."
-        "The material is grounded in tactile reality, inspired by the study of paper and ink, yet "
-        "technologically advanced and open to imagination and magic.\n"
-        "Surfaces and edges of the material provide visual cues that are grounded in reality. The "
-        "use of familiar tactile attributes helps users quickly understand affordances. Yet the "
-        "flexibility of the material creates new affordances that supercede those in the physical "
-        "world, without breaking the rules of physics.\n"
-        "The fundamentals of light, surface, and movement are key to conveying how objects move, "
-        "interact, and exist in space and in relation to each other. Realistic lighting shows "
-        "seams, divides space, and indicates moving parts.\n\n"
-
-        "Bold, graphic, intentional.\n\n"
-
-        "The foundational elements of print based design typography, grids, space, scale, color, "
-        "and use of imagery guide visual treatments. These elements do far more than please the "
-        "eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge "
-        "imagery, large scale typography, and intentional white space create a bold and graphic "
-        "interface that immerse the user in the experience.\n"
-        "An emphasis on user actions makes core functionality immediately apparent and provides "
-        "waypoints for the user.\n\n"
-
-        "Motion provides meaning.\n\n"
-
-        "Motion respects and reinforces the user as the prime mover. Primary user actions are "
-        "inflection points that initiate motion, transforming the whole design.\n"
-        "All action takes place in a single environment. Objects are presented to the user without "
-        "breaking the continuity of experience even as they transform and reorganize.\n"
-        "Motion is meaningful and appropriate, serving to focus attention and maintain continuity. "
-        "Feedback is subtle yet clear. Transitions are efficient yet coherent.\n\n"
-
-        "3D world.\n\n"
-
-        "The material environment is a 3D space, which means all objects have x, y, and z "
-        "dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the "
-        "positive z-axis extending towards the viewer. Every sheet of material occupies a single "
-        "position along the z-axis and has a standard 1dp thickness.\n"
-        "On the web, the z-axis is used for layering and not for perspective. The 3D world is "
-        "emulated by manipulating the y-axis.\n\n"
-
-        "Light and shadow.\n\n"
-
-        "Within the material environment, virtual lights illuminate the scene. Key lights create "
-        "directional shadows, while ambient light creates soft shadows from all angles.\n"
-        "Shadows in the material environment are cast by these two light sources. In Android "
-        "development, shadows occur when light sources are blocked by sheets of material at "
-        "various positions along the z-axis. On the web, shadows are depicted by manipulating the "
-        "y-axis only. The following example shows the card with a height of 6dp.\n\n"
-
-        "Resting elevation.\n\n"
-
-        "All material objects, regardless of size, have a resting elevation, or default elevation "
-        "that does not change. If an object changes elevation, it should return to its resting "
-        "elevation as soon as possible.\n\n"
-
-        "Component elevations.\n\n"
-
-        "The resting elevation for a component type is consistent across apps (e.g., FAB elevation "
-        "does not vary from 6dp in one app to 16dp in another app).\n"
-        "Components may have different resting elevations across platforms, depending on the depth "
-        "of the environment (e.g., TV has a greater depth than mobile or desktop).\n\n"
-
-        "Responsive elevation and dynamic elevation offsets.\n\n"
-
-        "Some component types have responsive elevation, meaning they change elevation in response "
-        "to user input (e.g., normal, focused, and pressed) or system events. These elevation "
-        "changes are consistently implemented using dynamic elevation offsets.\n"
-        "Dynamic elevation offsets are the goal elevation that a component moves towards, relative "
-        "to the component’s resting state. They ensure that elevation changes are consistent "
-        "across actions and component types. For example, all components that lift on press have "
-        "the same elevation change relative to their resting elevation.\n"
-        "Once the input event is completed or cancelled, the component will return to its resting "
-        "elevation.\n\n"
-
-        "Avoiding elevation interference.\n\n"
-
-        "Components with responsive elevations may encounter other components as they move between "
-        "their resting elevations and dynamic elevation offsets. Because material cannot pass "
-        "through other material, components avoid interfering with one another any number of ways, "
-        "whether on a per component basis or using the entire app layout.\n"
-        "On a component level, components can move or be removed before they cause interference. "
-        "For example, a floating action button (FAB) can disappear or move off screen before a "
-        "user picks up a card, or it can move if a snackbar appears.\n"
-        "On the layout level, design your app layout to minimize opportunities for interference. "
-        "For example, position the FAB to one side of stream of a cards so the FAB won’t interfere "
-        "when a user tries to pick up one of cards.\n\n"
-    </string>
-    <string name="action_settings">Settings</string>
 
 </resources>
diff --git a/ring-android/app/src/main/res/values/strings_account.xml b/ring-android/app/src/main/res/values/strings_account.xml
index 8f54b3133..e730486b2 100644
--- a/ring-android/app/src/main/res/values/strings_account.xml
+++ b/ring-android/app/src/main/res/values/strings_account.xml
@@ -37,6 +37,8 @@ along with this program; if not, write to the Free Software
     <string name="action_create">Create account</string>
     <string name="action_create_short">Create</string>
     <string name="action_register">Register</string>
+    <string name="action_settings">Settings</string>
+
     <string name="error_field_required">This field is required</string>
     <string name="dialog_wait_create">Adding account</string>
     <string name="dialog_wait_create_details">Please wait while your new account is added…</string>
diff --git a/ring-android/app/src/main/res/values/strings_preferences.xml b/ring-android/app/src/main/res/values/strings_preferences.xml
index f7708d112..bc91ced6e 100644
--- a/ring-android/app/src/main/res/values/strings_preferences.xml
+++ b/ring-android/app/src/main/res/values/strings_preferences.xml
@@ -30,4 +30,7 @@
     <string name="pref_clearHistory_title">Clear history</string>
     <string name="pref_clearHistory_summary">Clear all the conversations history. This action can not be undone.</string>
 
+    <string name="pref_persistNotification_title">Run in background</string>
+    <string name="pref_persistNotification_summary">Allow running in background to receive calls and messages.</string>
+
 </resources>
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/Settings.java b/ring-android/libringclient/src/main/java/cx/ring/model/Settings.java
index 3b4d345cb..3434f1085 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/Settings.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/Settings.java
@@ -22,6 +22,7 @@ package cx.ring.model;
 public class Settings {
     private boolean mAllowMobileData;
     private boolean mAllowPushNotifications;
+    private boolean mAllowPersistentNotification;
     private boolean mAllowSystemContacts;
     private boolean mAllowPlaceSystemCalls;
     private boolean mAllowRingOnStartup;
@@ -32,6 +33,7 @@ public class Settings {
     public Settings(Settings s) {
         mAllowMobileData = s.mAllowMobileData;
         mAllowPushNotifications = s.mAllowPushNotifications;
+        mAllowPersistentNotification = s.mAllowPersistentNotification;
         mAllowSystemContacts = s.mAllowSystemContacts;
         mAllowPlaceSystemCalls = s.mAllowPlaceSystemCalls;
         mAllowRingOnStartup = s.mAllowRingOnStartup;
@@ -84,4 +86,12 @@ public class Settings {
     public void setHD(boolean hd) {
         mHdUpload = hd;
     }
+
+    public void setAllowPersistentNotification(boolean checked) {
+        this.mAllowPersistentNotification = checked;
+    }
+
+    public boolean isAllowPersistentNotification() {
+        return mAllowPersistentNotification;
+    }
 }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java b/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java
index 8eb6d8df6..232708a0f 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java
@@ -2,6 +2,7 @@
  *  Copyright (C) 2004-2018 Savoir-faire Linux Inc.
  *
  *  Author: Aline Bonnet <aline.bonnet@savoirfairelinux.com>
+ *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
  *
  *  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
@@ -17,47 +18,42 @@
  *  along with this program; if not, write to the Free Software
  *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  */
-
 package cx.ring.services;
 
-import java.util.TreeMap;
-
 import cx.ring.model.Account;
 import cx.ring.model.CallContact;
 import cx.ring.model.Conference;
 import cx.ring.model.Conversation;
 import cx.ring.model.DataTransfer;
-import cx.ring.model.DataTransferEventCode;
 import cx.ring.model.SipCall;
-import cx.ring.model.TextMessage;
 import cx.ring.model.Uri;
 
-public abstract class NotificationService {
-
-    public static final String TRUST_REQUEST_NOTIFICATION_ACCOUNT_ID = "trustRequestNotificationAccountId";
-    public static final String TRUST_REQUEST_NOTIFICATION_FROM = "trustRequestNotificationFrom";
+public interface NotificationService {
+    String TRUST_REQUEST_NOTIFICATION_ACCOUNT_ID = "trustRequestNotificationAccountId";
+    String TRUST_REQUEST_NOTIFICATION_FROM = "trustRequestNotificationFrom";
+    String KEY_CALL_ID = "callId";
 
-    public static final String KEY_CALL_ID = "callId";
+    void showCallNotification(Conference conference);
 
-    public abstract void showCallNotification(Conference conference);
+    void showTextNotification(String accountId, Conversation conversation);
 
-    public abstract void showTextNotification(String accountId, Conversation conversation);
+    void cancelCallNotification(int notificationId);
 
-    public abstract void cancelCallNotification(int notificationId);
+    void cancelTextNotification(Uri contact);
 
-    public abstract void cancelTextNotification(Uri contact);
+    void cancelTextNotification(String ringId);
 
-    public abstract void cancelTextNotification(String ringId);
+    void cancelAll();
 
-    public abstract void cancelAll();
+    void showIncomingTrustRequestNotification(Account account);
 
-    public abstract void showIncomingTrustRequestNotification(Account account);
+    void cancelTrustRequestNotification(String accountID);
 
-    public abstract void cancelTrustRequestNotification(String accountID);
+    void showFileTransferNotification(DataTransfer info, CallContact contact);
 
-    public abstract void showFileTransferNotification(DataTransfer info, CallContact contact);
+    void showMissedCallNotification(SipCall call);
 
-    public abstract void showMissedCallNotification(SipCall call);
+    void cancelFileNotification(long id);
 
-    public abstract void cancelFileNotification(long id);
+    Object getServiceNotification();
 }
-- 
GitLab