From c5fdbdd43cd7d4380e1077d02fa57579ee7c9099 Mon Sep 17 00:00:00 2001
From: Marten Gajda <marten@dmfs.org>
Date: Thu, 8 Nov 2018 00:52:16 +0100
Subject: [PATCH] Update SDK target level to 26. Implements #703 (#719)

This also updates the support library version to 26.1.0.
Due to changes in Android 8 we had to limit some background service and broadcast functionality. There are no limitations to the user experience though, but some planned features may require a different solution now (mostly because implicit broadcasts no longer work).
---
 .travis.yml                                   |   7 +-
 dependencies.gradle                           |   2 +-
 gradle.properties                             |   4 +-
 .../dmfs/provider/tasks/ContentOperation.java |   6 +
 .../org/dmfs/provider/tasks/TaskProvider.java |  18 +--
 opentasks/src/main/AndroidManifest.xml        |   2 +-
 .../java/org/dmfs/tasks/TaskListActivity.java |  14 ++-
 .../notification/AlarmBroadcastReceiver.java  |   8 ++
 .../notification/NotificationActionUtils.java |   8 +-
 .../NotificationUpdaterService.java           | 116 ++++++++++++++++--
 .../notification/TaskNotificationHandler.java |  28 ++++-
 .../src/main/res/layout/undo_notification.xml |   4 +-
 opentasks/src/main/res/values-de/strings.xml  |   3 +
 opentasks/src/main/res/values-v21/styles.xml  |   4 +-
 opentasks/src/main/res/values/strings.xml     |   6 +
 opentasks/src/main/res/values/styles.xml      |   4 +-
 16 files changed, 195 insertions(+), 39 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 78486a9b5..806c007b4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,15 +9,17 @@ android:
     - tools
     - platform-tools
     - tools
+    - build-tools-26.0.2
     - build-tools-27.0.1
     - android-24
-    - android-25
     - android-26
+    - extra
     - extra-android-m2repository
     - sys-img-armeabi-v7a-android-24
 
 # Emulator Management: Create, Start and Wait
 before_script:
+  - android list targets
   - echo no | android create avd --force -n test --target android-24 --abi armeabi-v7a
   - QEMU_AUDIO_DRV=none emulator -avd test -no-window &
   - android-wait-for-emulator
@@ -26,3 +28,6 @@ before_script:
 script:
   - android list target
   - ./gradlew check connectedAndroidTest
+
+after_script:
+  - cat /home/travis/build/dmfs/opentasks/opentasks/build/reports/lint-results.xml
\ No newline at end of file
diff --git a/dependencies.gradle b/dependencies.gradle
index 3cb9e2806..17ee39b6a 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -1,4 +1,4 @@
-def support_lib_version = '25.4.0'
+def support_lib_version = '26.1.0'
 def jems_version = '1.15'
 def contentpal_version = '9b087b2' // 9b087b2 -> 2017-12-12
 def support_test_runner_version = '0.5'
diff --git a/gradle.properties b/gradle.properties
index b7460b615..7cb4a36c9 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-COMPILE_SDK_VERSION=25
+COMPILE_SDK_VERSION=26
 BUILD_TOOLS_VERSION=27.0.1
 MIN_SDK_VERSION=15
-TARGET_SDK_VERSION=25
+TARGET_SDK_VERSION=26
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ContentOperation.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ContentOperation.java
index 2022c7fed..f45287e6a 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ContentOperation.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ContentOperation.java
@@ -26,6 +26,7 @@
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Handler;
 import android.util.Log;
 
@@ -183,6 +184,11 @@ private void sendBroadcast(Context context, String action, Uri uri, DateTime dat
                 intent.putExtra(TaskContract.EXTRA_TASK_TIMEZONE, datetime.getTimeZone().getID());
             }
             intent.putExtra(TaskContract.EXTRA_TASK_TITLE, title);
+            if (Build.VERSION.SDK_INT >= 26)
+            {
+                // for now only notify our own package
+                intent.setPackage(context.getPackageName());
+            }
             context.sendBroadcast(intent);
         }
     }),
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java
index 481714174..6441619f2 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java
@@ -32,6 +32,7 @@
 import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.text.TextUtils;
@@ -1294,6 +1295,12 @@ protected void onEndTransaction(boolean callerIsSyncAdapter)
         }
         // add the change log to the broadcast
         providerChangedIntent.putExtras(mOperationsLog.toBundle(true));
+        if (Build.VERSION.SDK_INT >= 26)
+        {
+            // for now we only notify our own package
+            // we'll have to figure out how to do this correctly on Android 8+, e.g. how is it done by CalendarProvider and ContactsProvider
+            providerChangedIntent.setPackage(getContext().getPackageName());
+        }
         getContext().sendBroadcast(providerChangedIntent);
     }
 
@@ -1313,6 +1320,8 @@ public void onDatabaseCreated(SQLiteDatabase db)
         // notify listeners that the database has been created
         Intent dbInitializedIntent = new Intent(TaskContract.ACTION_DATABASE_INITIALIZED);
         dbInitializedIntent.setDataAndType(TaskContract.getContentUri(mAuthority), TaskContract.MIMETYPE_AUTHORITY);
+        // Android SDK 26 doesn't allow us to send implicit broadcasts, this particular brodcast is only for internal use, so just make it explicit by setting our package name
+        dbInitializedIntent.setPackage(getContext().getPackageName());
         getContext().sendBroadcast(dbInitializedIntent);
     }
 
@@ -1322,14 +1331,7 @@ public void onDatabaseUpdate(SQLiteDatabase db, int oldVersion, int newVersion)
     {
         if (oldVersion < 15)
         {
-            mAsyncHandler.post(new Runnable()
-            {
-                @Override
-                public void run()
-                {
-                    ContentOperation.UPDATE_TIMEZONE.fire(getContext(), null);
-                }
-            });
+            mAsyncHandler.post(() -> ContentOperation.UPDATE_TIMEZONE.fire(getContext(), null));
         }
     }
 
diff --git a/opentasks/src/main/AndroidManifest.xml b/opentasks/src/main/AndroidManifest.xml
index 02b61d25f..716131456 100644
--- a/opentasks/src/main/AndroidManifest.xml
+++ b/opentasks/src/main/AndroidManifest.xml
@@ -207,7 +207,7 @@
         <!-- custom alarm receivers -->
         <receiver
                 android:name="org.dmfs.tasks.notification.AlarmBroadcastReceiver"
-                android:permission="">
+                android:exported="false">
             <intent-filter>
                 <action android:name="org.dmfs.android.tasks.TASK_DUE"/>
 
diff --git a/opentasks/src/main/java/org/dmfs/tasks/TaskListActivity.java b/opentasks/src/main/java/org/dmfs/tasks/TaskListActivity.java
index 608b3c615..a9f4ebd79 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/TaskListActivity.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/TaskListActivity.java
@@ -25,6 +25,7 @@
 import android.os.Build.VERSION;
 import android.os.Bundle;
 import android.os.Handler;
+import android.provider.Settings;
 import android.support.annotation.ColorInt;
 import android.support.annotation.NonNull;
 import android.support.design.widget.AppBarLayout;
@@ -639,7 +640,18 @@ else if (item.getItemId() == R.id.menu_visible_list)
         }
         else if (item.getItemId() == R.id.opentasks_menu_app_settings)
         {
-            startActivity(new Intent(this, AppSettingsActivity.class));
+            if (VERSION.SDK_INT < 26)
+            {
+                startActivity(new Intent(this, AppSettingsActivity.class));
+            }
+            else
+            {
+                // for now just open the notification settings, which is all we currently support anyway
+                Intent intent = new Intent();
+                intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
+                intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
+                startActivity(intent);
+            }
             return true;
         }
         else
diff --git a/opentasks/src/main/java/org/dmfs/tasks/notification/AlarmBroadcastReceiver.java b/opentasks/src/main/java/org/dmfs/tasks/notification/AlarmBroadcastReceiver.java
index 3b01ef36e..91612a1ff 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/notification/AlarmBroadcastReceiver.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/notification/AlarmBroadcastReceiver.java
@@ -22,6 +22,7 @@
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.net.Uri;
+import android.os.Build;
 import android.preference.PreferenceManager;
 
 import org.dmfs.tasks.R;
@@ -47,6 +48,7 @@ public void onReceive(Context context, Intent intent)
         {
             if (isNotificationEnabled(context))
             {
+                NotificationUpdaterService.createChannels(context);
                 Uri taskUri = intent.getData();
 
                 boolean noSignal = intent.getBooleanExtra(NotificationActionUtils.EXTRA_NOTIFICATION_NO_SIGNAL, false);
@@ -72,6 +74,7 @@ else if (intent.getAction().equals(TaskContract.ACTION_BROADCAST_TASK_DUE))
         {
             if (isNotificationEnabled(context))
             {
+                NotificationUpdaterService.createChannels(context);
                 Uri taskUri = intent.getData();
 
                 boolean noSignal = intent.getBooleanExtra(NotificationActionUtils.EXTRA_NOTIFICATION_NO_SIGNAL, false);
@@ -100,6 +103,11 @@ else if (intent.getAction().equals(TaskContract.ACTION_BROADCAST_TASK_DUE))
 
     public boolean isNotificationEnabled(Context context)
     {
+        if (Build.VERSION.SDK_INT >= 26)
+        {
+            // on Android 8+ we leave this decision to Android and always attempt to show the notification
+            return true;
+        }
         SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
         return settings.getBoolean(context.getString(R.string.opentasks_pref_notification_enabled), true);
 
diff --git a/opentasks/src/main/java/org/dmfs/tasks/notification/NotificationActionUtils.java b/opentasks/src/main/java/org/dmfs/tasks/notification/NotificationActionUtils.java
index 3eb2105b2..ac7e4a1d2 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/notification/NotificationActionUtils.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/notification/NotificationActionUtils.java
@@ -92,7 +92,8 @@ public static void sendDueAlarmNotification(Context context, String title, Uri t
         }
 
         // build notification
-        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context).setSmallIcon(R.drawable.ic_notification)
+        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, NotificationUpdaterService.CHANNEL_DUE_DATES).setSmallIcon(
+                R.drawable.ic_notification)
                 .setContentTitle(title).setContentText(dueString);
 
         // color
@@ -172,7 +173,8 @@ public static void sendStartNotification(Context context, String title, Uri task
         NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
 
         // build notification
-        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context).setSmallIcon(R.drawable.ic_notification)
+        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, NotificationUpdaterService.CHANNEL_DUE_DATES).setSmallIcon(
+                R.drawable.ic_notification)
                 .setContentTitle(title).setContentText(startString);
 
         // color
@@ -251,7 +253,7 @@ public static PendingIntent getNotificationActionPendingIntent(Context context,
      */
     public static void createUndoNotification(final Context context, NotificationAction action)
     {
-        NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationUpdaterService.CHANNEL_DUE_DATES);
         builder.setContentTitle(context.getString(action.getActionTextResId()
 
         ));
diff --git a/opentasks/src/main/java/org/dmfs/tasks/notification/NotificationUpdaterService.java b/opentasks/src/main/java/org/dmfs/tasks/notification/NotificationUpdaterService.java
index b8bdcdcb7..53306cc7f 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/notification/NotificationUpdaterService.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/notification/NotificationUpdaterService.java
@@ -16,9 +16,12 @@
 
 package org.dmfs.tasks.notification;
 
+import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.app.AlarmManager;
 import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
 import android.content.ContentProviderOperation;
@@ -68,6 +71,9 @@
  */
 public class NotificationUpdaterService extends Service
 {
+    public static final String CHANNEL_PINNED = "org.dmfs.opentasks.PINNED";
+    public static final String CHANNEL_DUE_DATES = "org.dmfs.opentasks.DUE_DATES";
+
     private static final String TAG = "NotificationUpdaterSe";
 
     /**
@@ -79,6 +85,8 @@ public class NotificationUpdaterService extends Service
     private static final int REQUEST_CODE_DELAY = 2;
     private static final int REQUEST_CODE_UNPIN = 3;
 
+    private static final int DUMMY_NOTIFICATION_ID = -10;
+
     // actions
     public static final String ACTION_PINNED_TASK_DUE = "org.dmfs.tasks.intent.ACTION_PINNED_TASK_DUE";
     public static final String ACTION_PINNED_TASK_START = "org.dmfs.tasks.intent.ACTION_PINNED_TASK_START";
@@ -97,11 +105,13 @@ public class NotificationUpdaterService extends Service
     public static final String EXTRA_TIMEZONE = "org.dmfs.tasks.extras.notification.TIMEZONE";
     public static final String EXTRA_ALLDAY = "org.dmfs.tasks.extras.notification.ALLDAY";
 
-    private final NotificationCompat.Builder mBuilder = new Builder(this);
+    private final NotificationCompat.Builder mBuilder = new Builder(this, NotificationUpdaterService.CHANNEL_DUE_DATES);
     private PendingIntent mDateChangePendingIntent;
     ArrayList<ContentSet> mTasksToPin;
     private String mAuthority;
 
+    private int mForegroundPinned = -1;
+
 
     @Override
     public IBinder onBind(Intent intent)
@@ -117,10 +127,50 @@ public NotificationUpdaterService()
     }
 
 
+    public static void createChannels(Context context)
+    {
+        if (VERSION.SDK_INT >= 26)
+        {
+            NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+            NotificationChannel pinnedChannel = new NotificationChannel(CHANNEL_PINNED, context.getString(R.string.opentasks_notification_channel_pinned_tasks),
+                    NotificationManager.IMPORTANCE_DEFAULT);
+            // pinned Notifications should not get a badge, but they may vibrate
+            pinnedChannel.setShowBadge(false);
+            pinnedChannel.enableLights(false);
+            pinnedChannel.enableVibration(true);
+            pinnedChannel.setVibrationPattern(new long[] { 0, 100, 100, 100, 0 });
+            nm.createNotificationChannel(pinnedChannel);
+
+            NotificationChannel dueDates = new NotificationChannel(NotificationUpdaterService.CHANNEL_DUE_DATES,
+                    context.getString(R.string.opentasks_notification_channel_due_dates), NotificationManager.IMPORTANCE_HIGH);
+            dueDates.setShowBadge(true);
+            dueDates.enableLights(true);
+            dueDates.enableVibration(true);
+            nm.createNotificationChannel(dueDates);
+        }
+    }
+
+
+    @SuppressLint("WrongConstant")
     @Override
     public void onCreate()
     {
         mAuthority = AuthorityUtil.taskAuthority(this);
+
+        if (VERSION.SDK_INT >= 26)
+        {
+            createChannels(this);
+
+            // note this notification is to make Android happy, it should never be visible to the user because it's either replaced or
+            // discarded in onStartCommand
+            startForeground(DUMMY_NOTIFICATION_ID, new NotificationCompat.Builder(this, NotificationUpdaterService.CHANNEL_PINNED).setTicker("OpenTasks")
+                    .setSmallIcon(R.drawable.ic_24_opentasks)
+                    .setSound(null)
+                    .setVibrate(null)
+                    .setPriority(NotificationCompat.PRIORITY_MIN)
+                    .build());
+        }
+
         super.onCreate();
         updateNextDayAlarm();
     }
@@ -129,7 +179,6 @@ public void onCreate()
     @Override
     public int onStartCommand(Intent intent, int flags, int startId)
     {
-
         String intentAction = intent.getAction();
         boolean noSignal = intent.getBooleanExtra(NotificationActionUtils.EXTRA_NOTIFICATION_NO_SIGNAL, false);
         if (intentAction != null)
@@ -200,13 +249,39 @@ public int onStartCommand(Intent intent, int flags, int startId)
         // check if the service needs to kept alive
         if (mTasksToPin == null || mTasksToPin.isEmpty())
         {
+            if (VERSION.SDK_INT >= 26)
+            {
+                this.stopForeground(true);
+                mForegroundPinned = -1;
+            }
             this.stopSelf();
         }
+        else if (VERSION.SDK_INT >= 26 && (mForegroundPinned < 0 || !isPinned(mForegroundPinned)))
+        {
+            // on Android 8+ we have to create or change the foreground notification if we didn't do that yet or of the respective task is no longer pinned
+            mBuilder.setChannelId(CHANNEL_PINNED);
+            ContentSet task = mTasksToPin.get(0);
+            startForeground(TaskFieldAdapters.TASK_ID.get(task), makePinNotification(this, mBuilder, task, true, false, false));
+            mForegroundPinned = TaskFieldAdapters.TASK_ID.get(task);
+        }
         return Service.START_NOT_STICKY;
 
     }
 
 
+    private boolean isPinned(long id)
+    {
+        for (ContentSet task : mTasksToPin)
+        {
+            if (id == TaskFieldAdapters.TASK_ID.get(task))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+
     private void updateNotifications(boolean isReboot, boolean noSignal, boolean withHeadsUpNotification)
     {
         // update pinned tasks
@@ -225,6 +300,7 @@ private void updatePinnedNotifications(ArrayList<ContentSet> tasksToPin, boolean
         {
             boolean isAlreadyShown = pinnedTaskUris.contains(taskContentSet.getUri());
             Integer taskId = TaskFieldAdapters.TASK_ID.get(taskContentSet);
+            mBuilder.setChannelId(CHANNEL_PINNED);
             notificationManager.notify(taskId,
                     makePinNotification(this, mBuilder, taskContentSet, isAlreadyShown && noSignal, isAlreadyShown && noSignal, withHeadsUpNotification));
         }
@@ -241,12 +317,7 @@ private void updatePinnedNotifications(ArrayList<ContentSet> tasksToPin, boolean
                 long taskId = ContentUris.parseId(uri);
                 if (taskId > -1 == !containsTask(tasksToPin, uri))
                 {
-
-                    Integer notificationId = Long.valueOf(ContentUris.parseId(uri)).intValue();
-                    if (notificationId != null)
-                    {
-                        notificationManager.cancel(notificationId);
-                    }
+                    notificationManager.cancel((int) taskId);
                 }
             }
         }
@@ -314,6 +385,7 @@ private static Notification makePinNotification(Context context, Builder builder
     {
         Resources resources = context.getResources();
 
+        builder.setChannelId(CHANNEL_PINNED);
         // reset actions
         builder.mActions = new ArrayList<Action>(2);
 
@@ -392,7 +464,11 @@ private static Notification makePinNotification(Context context, Builder builder
         // unpin action
         builder.addAction(NotificationUpdaterService.getUnpinAction(context, TaskFieldAdapters.TASK_ID.get(task), task.getUri()));
 
-        builder.setDefaults(new Conditional(!noSignal, context).value());
+        if (VERSION.SDK_INT < 26)
+        {
+            builder.setDefaults(new Conditional(!noSignal, context).value());
+        }
+        builder.setOnlyAlertOnce(noSignal);
 
         return builder.build();
     }
@@ -480,6 +556,11 @@ private void resendNotification(NotificationAction notificationAction)
             startIntent.putExtra(TaskContract.EXTRA_TASK_TITLE, notificationAction.title());
             startIntent.putExtra(TaskContract.EXTRA_TASK_TIMESTAMP, notificationAction.getWhen());
             startIntent.putExtra(NotificationActionUtils.EXTRA_NOTIFICATION_NO_SIGNAL, true);
+            if (Build.VERSION.SDK_INT >= 26)
+            {
+                // for now only notify our own package
+                startIntent.setPackage(getPackageName());
+            }
             sendBroadcast(startIntent);
 
             // Due broadcast
@@ -489,6 +570,11 @@ private void resendNotification(NotificationAction notificationAction)
             dueIntent.putExtra(TaskContract.EXTRA_TASK_TITLE, notificationAction.title());
             dueIntent.putExtra(TaskContract.EXTRA_TASK_TIMESTAMP, notificationAction.getWhen());
             dueIntent.putExtra(NotificationActionUtils.EXTRA_NOTIFICATION_NO_SIGNAL, true);
+            if (Build.VERSION.SDK_INT >= 26)
+            {
+                // for now only notify our own package
+                dueIntent.setPackage(getPackageName());
+            }
             sendBroadcast(dueIntent);
         }
     }
@@ -660,14 +746,18 @@ private void delayTask(Uri taskUri, Time dueTime)
     private void delayedCancelHeadsUpNotification()
     {
         Handler handler = new Handler(Looper.getMainLooper());
-        final Runnable r = new Runnable()
+        final Runnable r = () ->
         {
-            public void run()
+            Intent intent = new Intent(getBaseContext(), NotificationUpdaterService.class);
+            intent.setAction(ACTION_CANCEL_HEADUP_NOTIFICATION);
+            if (VERSION.SDK_INT < 26)
             {
-                Intent intent = new Intent(getBaseContext(), NotificationUpdaterService.class);
-                intent.setAction(ACTION_CANCEL_HEADUP_NOTIFICATION);
                 startService(intent);
             }
+            else
+            {
+                startForegroundService(intent);
+            }
         };
         handler.postDelayed(r, HEAD_UP_NOTIFICATION_DURATION);
     }
diff --git a/opentasks/src/main/java/org/dmfs/tasks/notification/TaskNotificationHandler.java b/opentasks/src/main/java/org/dmfs/tasks/notification/TaskNotificationHandler.java
index 82fcaca8c..fb9835fd4 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/notification/TaskNotificationHandler.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/notification/TaskNotificationHandler.java
@@ -23,6 +23,7 @@
 import android.content.SharedPreferences.Editor;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.preference.PreferenceManager;
 
 import org.dmfs.provider.tasks.AuthorityUtil;
@@ -99,7 +100,14 @@ private static void startPinnedTaskService(Context context, Uri taskUri, String
         intent.setData(taskUri);
         intent.setAction(action);
         intent.putExtra(NotificationUpdaterService.EXTRA_NEW_PINNED_TASK, task);
-        context.startService(intent);
+        if (Build.VERSION.SDK_INT < 26)
+        {
+            context.startService(intent);
+        }
+        else
+        {
+            context.startForegroundService(intent);
+        }
     }
 
 
@@ -191,7 +199,14 @@ public static void sendPinnedTaskStartNotification(Context context, Uri taskUri,
         intent.setAction(NotificationUpdaterService.ACTION_PINNED_TASK_START);
         intent.putExtra(NotificationActionUtils.EXTRA_NOTIFICATION_NO_SIGNAL, noSignal);
         intent.setData(taskUri);
-        context.startService(intent);
+        if (Build.VERSION.SDK_INT < 26)
+        {
+            context.startService(intent);
+        }
+        else
+        {
+            context.startForegroundService(intent);
+        }
     }
 
 
@@ -201,7 +216,14 @@ public static void sendPinnedTaskDueNotification(Context context, Uri taskUri, b
         intent.setAction(NotificationUpdaterService.ACTION_PINNED_TASK_DUE);
         intent.putExtra(NotificationActionUtils.EXTRA_NOTIFICATION_NO_SIGNAL, noSignal);
         intent.setData(taskUri);
-        context.startService(intent);
+        if (Build.VERSION.SDK_INT < 26)
+        {
+            context.startService(intent);
+        }
+        else
+        {
+            context.startForegroundService(intent);
+        }
     }
 
 }
diff --git a/opentasks/src/main/res/layout/undo_notification.xml b/opentasks/src/main/res/layout/undo_notification.xml
index 359252990..c0a85e0c1 100644
--- a/opentasks/src/main/res/layout/undo_notification.xml
+++ b/opentasks/src/main/res/layout/undo_notification.xml
@@ -27,7 +27,7 @@
             android:ellipsize="marquee"
             android:fadingEdge="horizontal"
             android:gravity="center_vertical"
-            android:textAppearance="@style/TextAppearance.StatusBar.EventContent.Title.colorFix"/>
+            android:textAppearance="@style/TextAppearance.AppCompat.Notification.Title.colorFix"/>
 
     <ImageView
             android:layout_width="1dip"
@@ -53,6 +53,6 @@
             android:paddingRight="16dip"
             android:text="@string/notification_undo"
             android:textAllCaps="true"
-            android:textAppearance="@style/TextAppearance.StatusBar.EventContent.Action"/>
+            android:textAppearance="@style/TextAppearance.AppCompat.Notification.Action"/>
 
 </LinearLayout>
\ No newline at end of file
diff --git a/opentasks/src/main/res/values-de/strings.xml b/opentasks/src/main/res/values-de/strings.xml
index a8f73aa8c..e6f1b7dd3 100644
--- a/opentasks/src/main/res/values-de/strings.xml
+++ b/opentasks/src/main/res/values-de/strings.xml
@@ -274,4 +274,7 @@
     <!-- Format indicating a date and time. Example: "23 Aug, 11:00 am" -->
     <string name="opentasks_date_time">%1$s, %2$s</string>
 
+    <string name="opentasks_notification_channel_pinned_tasks">Angeheftete Aufgaben</string>
+    <string name="opentasks_notification_channel_due_dates">Beginn und Fälligkeiten</string>
+
 </resources>
diff --git a/opentasks/src/main/res/values-v21/styles.xml b/opentasks/src/main/res/values-v21/styles.xml
index 8aaca4435..71fb88770 100644
--- a/opentasks/src/main/res/values-v21/styles.xml
+++ b/opentasks/src/main/res/values-v21/styles.xml
@@ -11,13 +11,13 @@
         <item name="actionBarItemBackground">@drawable/selectable_background_white</item>
     </style>
 
-    <style name="TextAppearance.StatusBar.EventContent.Title.colorFix">
+    <style name="TextAppearance.AppCompat.Notification.Title.colorFix">
         <item name="android:textColor">@color/abc_primary_text_material_light</item>
         <item name="android:fontFamily">sans-serif</item>
         <item name="android:textStyle">normal</item>
     </style>
 
-    <style name="TextAppearance.StatusBar.EventContent.Action">
+    <style name="TextAppearance.AppCompat.Notification.Action">
         <item name="android:textColor">@color/abc_secondary_text_material_light</item>
         <item name="android:fontFamily">sans-serif-light</item>
         <item name="android:textStyle">bold</item>
diff --git a/opentasks/src/main/res/values/strings.xml b/opentasks/src/main/res/values/strings.xml
index 27933bfcc..5f0023057 100644
--- a/opentasks/src/main/res/values/strings.xml
+++ b/opentasks/src/main/res/values/strings.xml
@@ -280,4 +280,10 @@
     <!-- Format indicating a date and time. Example: "23 Aug, 11:00 am" -->
     <string name="opentasks_date_time">%1$s, %2$s</string>
 
+    <!-- Title of the notification channel for pinned tasks -->
+    <string name="opentasks_notification_channel_pinned_tasks">Pinned tasks</string>
+
+    <!-- Title of the notification channel for start and due alerts -->
+    <string name="opentasks_notification_channel_due_dates">Start and due dates </string>
+
 </resources>
diff --git a/opentasks/src/main/res/values/styles.xml b/opentasks/src/main/res/values/styles.xml
index 61b01cb2f..0673b2f1f 100644
--- a/opentasks/src/main/res/values/styles.xml
+++ b/opentasks/src/main/res/values/styles.xml
@@ -348,9 +348,9 @@
 
     <!-- styles for actionbar / toolbar -->
 
-    <style name="TextAppearance.StatusBar.EventContent.Title.colorFix"/>
+    <style name="TextAppearance.AppCompat.Notification.Title.colorFix"/>
 
-    <style name="TextAppearance.StatusBar.EventContent.Action"/>
+    <style name="TextAppearance.AppCompat.Notification.Action"/>
 
     <style name="ot_notification_icon_undo">
         <item name="android:src">@drawable/ic_24_undo_white</item>