diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 46b59da..d11b439 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -5,6 +5,7 @@ on: branches: [ "master" ] pull_request: branches: [ "master" ] + workflow_dispatch: jobs: build: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a00d75d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,68 @@ +name: 'Publish new release with Andorid APK' + +on: + push: + tags: ['v*'] +# This workflow will trigger on each push of a tag that starts with a "v" to create or update a GitHub release, build your app, and upload the artifacts to the release. + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Decode Keystore from GitHub Secrets + env: + KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }} + run: | + echo $KEYSTORE_FILE | base64 -d > app/my-release-key.keystore + + - name: Build release APK with Gradle + run: ./gradlew build + env: + SIGNING_KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + SIGNING_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + SIGNING_STORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + + - name: Post Build | Upload binary + uses: actions/upload-artifact@v4 + with: + name: release-apk + path: app/build/outputs/apk/release + retention-days: 3 + if-no-files-found: error + + release: + name: Release + runs-on: ubuntu-latest + needs: build + permissions: + contents: write + steps: + - name: Download binary from previous job + uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: Display structure of downloaded files + run: ls artifacts + + # Upload release asset: https://github.com/actions/upload-release-asset + # which recommends: https://github.com/softprops/action-gh-release + - name: Release + uses: softprops/action-gh-release@v2 + if: github.ref_type == 'tag' + with: + files: artifacts/* diff --git a/.gitignore b/.gitignore index 6a329ed..a1aa86d 100644 --- a/.gitignore +++ b/.gitignore @@ -154,3 +154,9 @@ captures/ # Keystore files *.jks + +# Grade Built files: +app/build/ + +# Intellij APK Release: +app/release/ diff --git a/app/build.gradle b/app/build.gradle index c5e26b4..dbe47d2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,26 +1,47 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 27 - buildToolsVersion "27.0.3" + compileSdkVersion 33 + buildToolsVersion "30.0.3" + namespace 'de.dotwee.micropinner' defaultConfig { applicationId "de.dotwee.micropinner" minSdkVersion 16 - targetSdkVersion 27 + targetSdkVersion 33 versionCode 29 versionName "v2.2.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } - buildTypes { + signingConfigs { + def keystoreFilePath = System.getenv("SIGNING_STORE_FILE") ?: "my-release-key.keystore" + if (file(keystoreFilePath).exists() || + System.getenv("SIGNING_STORE_PASSWORD") || + System.getenv("SIGNING_KEY_ALIAS") || + System.getenv("SIGNING_KEY_PASSWORD")) { + release { + storeFile file(keystoreFilePath) + storePassword System.getenv("SIGNING_STORE_PASSWORD") + keyAlias System.getenv("SIGNING_KEY_ALIAS") + keyPassword System.getenv("SIGNING_KEY_PASSWORD") + } + } + } + + buildTypes { debug { minifyEnabled false + applicationIdSuffix '.debug' } release { + // Apply signingConfig only if it was configured + if (signingConfigs.hasProperty('release')) { + signingConfig signingConfigs.release + } minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } @@ -34,20 +55,20 @@ android { } dependencies { - implementation 'com.android.support:appcompat-v7:27.1.0' + implementation 'androidx.appcompat:appcompat:1.5.1' - androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.1') { + androidTestImplementation('androidx.test.espresso:espresso-core:3.5.0') { // Necessary if your app targets Marshmallow (since Espresso // hasn't moved to Marshmallow yet) exclude group: 'com.android.support', module: 'support-annotations' } - androidTestImplementation('com.android.support.test.espresso:espresso-intents:3.0.1') { + androidTestImplementation('androidx.test.espresso:espresso-intents:3.5.0') { // Necessary to avoid version conflicts exclude group: 'com.android.support', module: 'support-annotations' } - androidTestImplementation('com.android.support.test:runner:1.0.1') { + androidTestImplementation('androidx.test.ext:junit:1.1.4') { // Necessary if your app targets Marshmallow (since the test runner // hasn't moved to Marshmallow yet) exclude group: 'com.android.support', module: 'support-annotations' diff --git a/app/src/androidTest/java/de/dotwee/micropinner/tools/Matches.java b/app/src/androidTest/java/de/dotwee/micropinner/tools/Matches.java index 46c28bf..831e9ad 100644 --- a/app/src/androidTest/java/de/dotwee/micropinner/tools/Matches.java +++ b/app/src/androidTest/java/de/dotwee/micropinner/tools/Matches.java @@ -1,10 +1,11 @@ package de.dotwee.micropinner.tools; import android.graphics.drawable.ColorDrawable; -import android.support.annotation.ColorInt; -import android.support.annotation.NonNull; -import android.support.test.espresso.intent.Checks; -import android.support.test.espresso.matcher.BoundedMatcher; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.test.espresso.Root; +import androidx.test.espresso.intent.Checks; +import androidx.test.espresso.matcher.BoundedMatcher; import android.view.View; import android.widget.TextView; @@ -16,6 +17,15 @@ */ public final class Matches { + @NonNull + public static Matcher isToast() { + return new ToastMatcher(); + } + @NonNull + public static Matcher isToast(int maxRetries) { + return new ToastMatcher(maxRetries); + } + /** * This matcher checks if a TextView displays its text in * a specific color. diff --git a/app/src/androidTest/java/de/dotwee/micropinner/tools/PreferencesHandlerTest.java b/app/src/androidTest/java/de/dotwee/micropinner/tools/PreferencesHandlerTest.java index 543b871..43c5e0a 100644 --- a/app/src/androidTest/java/de/dotwee/micropinner/tools/PreferencesHandlerTest.java +++ b/app/src/androidTest/java/de/dotwee/micropinner/tools/PreferencesHandlerTest.java @@ -2,8 +2,8 @@ import android.content.SharedPreferences; import android.preference.PreferenceManager; -import android.support.test.rule.ActivityTestRule; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Rule; diff --git a/app/src/androidTest/java/de/dotwee/micropinner/tools/TestTools.java b/app/src/androidTest/java/de/dotwee/micropinner/tools/TestTools.java index b8f8bdb..62b09cb 100644 --- a/app/src/androidTest/java/de/dotwee/micropinner/tools/TestTools.java +++ b/app/src/androidTest/java/de/dotwee/micropinner/tools/TestTools.java @@ -1,6 +1,6 @@ package de.dotwee.micropinner.tools; -import android.support.test.rule.ActivityTestRule; +import androidx.test.rule.ActivityTestRule; import de.dotwee.micropinner.view.MainDialog; diff --git a/app/src/androidTest/java/de/dotwee/micropinner/tools/ToastMatcher.java b/app/src/androidTest/java/de/dotwee/micropinner/tools/ToastMatcher.java new file mode 100644 index 0000000..d938791 --- /dev/null +++ b/app/src/androidTest/java/de/dotwee/micropinner/tools/ToastMatcher.java @@ -0,0 +1,68 @@ +package de.dotwee.micropinner.tools; + +import android.os.IBinder; +import androidx.test.espresso.Root; +import android.view.WindowManager.LayoutParams; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +/** + * This class allows to match Toast messages in tests with Espresso. + * + * Idea taken from: Checking toast message in android espresso - Stack Overflow + * + * Usage in test class: + * + *
+ * {@code
+ * import somepkg.ToastMatcher.Companion.onToast;
+ *
+ * // To assert a toast does *not* pop up:
+ * onView(withText("text")).inRoot(new ToastMatcher()).check(doesNotExist());
+ * onView(withText(textId)).inRoot(new ToastMatcher()).check(doesNotExist());
+ *
+ * // To assert a toast does pop up:
+ * onView(withText("text")).inRoot(new ToastMatcher()).check(matches(isDisplayed()));
+ * onView(withText(textId)).inRoot(new ToastMatcher()).check(matches(isDisplayed()));
+ * }
+ */
+public class ToastMatcher extends TypeSafeMatcher {
+
+    /** Default for maximum number of retries to wait for the toast to pop up */
+    private static final int DEFAULT_MAX_FAILURES = 5;
+
+    /** Restrict number of false results from matchesSafely to avoid endless loop */
+    private int failures = 0;
+    private final int maxFailures;
+
+    public ToastMatcher() {
+        this(DEFAULT_MAX_FAILURES);
+    }
+    public ToastMatcher(int maxFailures) {
+        this.maxFailures = maxFailures;
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("is toast");
+    }
+
+    @Override
+    public boolean matchesSafely(Root root) {
+        int type = root.getWindowLayoutParams().get().type;
+        if (type == LayoutParams.TYPE_TOAST || type == LayoutParams.TYPE_APPLICATION_OVERLAY) {
+            IBinder windowToken = root.getDecorView().getWindowToken();
+            IBinder appToken = root.getDecorView().getApplicationWindowToken();
+            if (windowToken == appToken) {
+                // windowToken == appToken means this window isn't contained by any other windows.
+                // if it was a window for an activity, it would have TYPE_BASE_APPLICATION.
+                return true;
+            }
+        }
+        // Method is called again if false is returned which is useful because a toast may take some time to pop up. But for
+        // obvious reasons an infinite wait isn't of help. So false is only returned as often as maxFailures specifies.
+        return (++failures >= maxFailures);
+    }
+
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogNewPinTest.java b/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogNewPinTest.java
index 13d5654..dcde639 100644
--- a/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogNewPinTest.java
+++ b/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogNewPinTest.java
@@ -1,7 +1,7 @@
 package de.dotwee.micropinner.view;
 
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -9,20 +9,21 @@
 
 import de.dotwee.micropinner.R;
 import de.dotwee.micropinner.database.PinDatabase;
+import de.dotwee.micropinner.tools.Matches;
 import de.dotwee.micropinner.tools.PreferencesHandler;
 
-import static android.support.test.espresso.Espresso.onData;
-import static android.support.test.espresso.Espresso.onView;
-import static android.support.test.espresso.action.ViewActions.click;
-import static android.support.test.espresso.action.ViewActions.typeText;
-import static android.support.test.espresso.assertion.ViewAssertions.matches;
-import static android.support.test.espresso.matcher.RootMatchers.withDecorView;
-import static android.support.test.espresso.matcher.ViewMatchers.isChecked;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static android.support.test.espresso.matcher.ViewMatchers.isFocusable;
-import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import static android.support.test.espresso.matcher.ViewMatchers.withSpinnerText;
-import static android.support.test.espresso.matcher.ViewMatchers.withText;
+import static androidx.test.espresso.Espresso.onData;
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.action.ViewActions.typeText;
+import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isChecked;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.isFocusable;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withSpinnerText;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
 import static de.dotwee.micropinner.tools.TestTools.getPreferencesHandler;
 import static de.dotwee.micropinner.tools.TestTools.recreateActivity;
 import static org.hamcrest.Matchers.allOf;
@@ -108,9 +109,12 @@ public void testEmptyTitleToast() throws Exception {
         // click pin button
         onView(withText(R.string.dialog_action_pin)).perform(click());
 
+        // can't see toast if another toast is already present
+        onView(withText(R.string.message_visibility_unsupported)).inRoot(Matches.isToast())
+                .check(doesNotExist());
+
         // verify toast existence
-        onView(withText(R.string.message_empty_title)).inRoot(
-                withDecorView(not(activityTestRule.getActivity().getWindow().getDecorView())))
+        onView(withText(R.string.message_empty_title)).inRoot(Matches.isToast())
                 .check(matches(isDisplayed()));
     }
 
diff --git a/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogParentPinTest.java b/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogParentPinTest.java
index 2acba4f..5fc0cfc 100644
--- a/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogParentPinTest.java
+++ b/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogParentPinTest.java
@@ -3,13 +3,14 @@
 import android.annotation.TargetApi;
 import android.content.Intent;
 import android.os.Build;
-import android.support.test.espresso.intent.Intents;
-import android.support.test.espresso.matcher.ViewMatchers;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.espresso.intent.Intents;
+import androidx.test.espresso.matcher.ViewMatchers;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.After;
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -18,12 +19,12 @@
 import de.dotwee.micropinner.R;
 import de.dotwee.micropinner.tools.NotificationTools;
 
-import static android.support.test.espresso.Espresso.onView;
-import static android.support.test.espresso.assertion.ViewAssertions.matches;
-import static android.support.test.espresso.matcher.ViewMatchers.isChecked;
-import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import static android.support.test.espresso.matcher.ViewMatchers.withSpinnerText;
-import static android.support.test.espresso.matcher.ViewMatchers.withText;
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isChecked;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withSpinnerText;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
 /**
  * Created by Lukas Wolfsteiner on 06.11.2015.
@@ -38,19 +39,11 @@ public class MainDialogParentPinTest {
      * activity to be launched before each test
      */
     @Rule
-    public ActivityTestRule activityTestRule =
-            new ActivityTestRule<>(MainDialog.class);
-
-    @Before
-    public void setUp() {
-
-        final Intent testIntent =
-                new Intent(activityTestRule.getActivity(), MainDialog.class).putExtra(
-                        NotificationTools.EXTRA_INTENT, Constants.testPin);
-
-        Intents.init();
-        activityTestRule.launchActivity(testIntent);
-    }
+    public ActivityScenarioRule activityTestRule =
+            new ActivityScenarioRule<>(
+                    new Intent(ApplicationProvider.getApplicationContext(), MainDialog.class)
+                            .putExtra(NotificationTools.EXTRA_INTENT, Constants.testPin)
+            );
 
     /**
      * @throws Exception
diff --git a/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogThemeTest.java b/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogThemeTest.java
index 05a7eab..4f8c029 100644
--- a/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogThemeTest.java
+++ b/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogThemeTest.java
@@ -2,13 +2,14 @@
 
 import android.content.res.Configuration;
 import android.os.Build;
-import android.support.annotation.ColorInt;
-import android.support.annotation.NonNull;
-import android.support.annotation.RequiresApi;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v4.content.ContextCompat;
-import android.support.v7.app.AppCompatDelegate;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.core.content.ContextCompat;
+import androidx.appcompat.app.AppCompatDelegate;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -18,10 +19,10 @@
 import de.dotwee.micropinner.R;
 import de.dotwee.micropinner.tools.Matches;
 
-import static android.support.test.espresso.Espresso.onView;
-import static android.support.test.espresso.assertion.ViewAssertions.matches;
-import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import static android.support.test.espresso.matcher.ViewMatchers.withText;
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
 import static de.dotwee.micropinner.tools.TestTools.recreateActivity;
 
 /**
@@ -40,6 +41,7 @@ public class MainDialogThemeTest {
             new ActivityTestRule<>(MainDialog.class);
 
     @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
     @ColorInt
     private static int getAccentColor(@NonNull ActivityTestRule activityTestRule, boolean light) {
         Configuration configuration = new Configuration();
@@ -49,6 +51,7 @@ private static int getAccentColor(@NonNull ActivityTestRule activity
     }
 
     @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
     @ColorInt
     private static int getBackgroundColor(@NonNull ActivityTestRule activityTestRule, boolean light) {
         Configuration configuration = new Configuration();
@@ -73,6 +76,7 @@ public void setUp() {
      * This method verifies the light theme's accent.
      */
     @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
     @Test
     public void testThemeLightAccent() throws Exception {
         changeUiMode(activityTestRule, AppCompatDelegate.MODE_NIGHT_NO);
@@ -90,6 +94,7 @@ public void testThemeLightAccent() throws Exception {
      * This method verifies the light theme's background.
      */
     @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
     @Test
     public void testThemeLightBackground() throws Exception {
         changeUiMode(activityTestRule, AppCompatDelegate.MODE_NIGHT_NO);
@@ -101,6 +106,7 @@ public void testThemeLightBackground() throws Exception {
      * This method verifies the light theme's accent.
      */
     @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
     @Test
     public void testThemeDarkAccent() throws Exception {
         changeUiMode(activityTestRule, AppCompatDelegate.MODE_NIGHT_YES);
@@ -120,6 +126,7 @@ public void testThemeDarkAccent() throws Exception {
      * This method verifies the dark theme's background.
      */
     @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
     @Test
     public void testThemeDarkBackground() throws Exception {
         changeUiMode(activityTestRule, AppCompatDelegate.MODE_NIGHT_YES);
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 80ef865..f9b893d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,9 +1,18 @@
 
 
+    xmlns:tools="http://schemas.android.com/tools">
 
     
+    
+
+    
+        
+
+            
+            
+            
+        
+    
 
     
+            android:exported="true"
+            android:launchMode="singleInstance"
+            android:noHistory="true"
+            android:theme="@style/DialogTheme"
+            android:windowSoftInputMode="adjustResize">
             
                 
 
@@ -25,9 +37,7 @@
         
 
         
-
         
-
         
@@ -35,6 +45,14 @@
                 
             
         
+        
+            
+                
+            
+        
     
 
-
+
\ No newline at end of file
diff --git a/app/src/main/java/de/dotwee/micropinner/database/PinDatabase.java b/app/src/main/java/de/dotwee/micropinner/database/PinDatabase.java
index baeb592..580a36f 100644
--- a/app/src/main/java/de/dotwee/micropinner/database/PinDatabase.java
+++ b/app/src/main/java/de/dotwee/micropinner/database/PinDatabase.java
@@ -5,8 +5,8 @@
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
-import android.support.annotation.NonNull;
-import android.support.v4.util.ArrayMap;
+import androidx.annotation.NonNull;
+import androidx.collection.ArrayMap;
 import android.util.Log;
 
 import java.util.Map;
diff --git a/app/src/main/java/de/dotwee/micropinner/database/PinSpec.java b/app/src/main/java/de/dotwee/micropinner/database/PinSpec.java
index 974e0e8..5aa4096 100644
--- a/app/src/main/java/de/dotwee/micropinner/database/PinSpec.java
+++ b/app/src/main/java/de/dotwee/micropinner/database/PinSpec.java
@@ -3,7 +3,8 @@
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
-import android.support.annotation.NonNull;
+import androidx.annotation.NonNull;
+import androidx.core.app.NotificationCompat.NotificationVisibility;
 
 import java.io.Serializable;
 
@@ -90,6 +91,7 @@ private void setContent(@NonNull String content) {
         this.content = content;
     }
 
+    @NotificationVisibility
     public int getVisibility() {
         return visibility;
     }
@@ -145,6 +147,7 @@ public String toClipString() {
         }
     }
 
+    @NonNull
     @Override
     public String toString() {
         return "PinSpec{" +
diff --git a/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenter.java b/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenter.java
index fd4243f..f41f6b3 100644
--- a/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenter.java
+++ b/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenter.java
@@ -1,7 +1,7 @@
 package de.dotwee.micropinner.presenter;
 
 import android.app.Activity;
-import android.support.annotation.NonNull;
+import androidx.annotation.NonNull;
 
 import de.dotwee.micropinner.database.PinSpec;
 
@@ -16,6 +16,15 @@ public interface MainPresenter {
      */
     void onSwitchHold();
 
+    /**
+     * This method handles the result of a permission request that was made on the presenters behalf by an activity.
+     * @param permissions The requested permissions.
+     * @param grantResults The grant results for the corresponding permissions which is either PERMISSION_GRANTED or PERMISSION_DENIED.
+     * @see ActivityCompat.OnRequestPermissionsResultCallback | Android Developers
+     */
+    void onRequestPermissionsResult(@NonNull String[] permissions,
+                                    @NonNull int[] grantResults);
+
     /**
      * This method handles the click on the positive dialog button.
      */
diff --git a/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenterImpl.java b/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenterImpl.java
index e32f587..4f4be32 100644
--- a/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenterImpl.java
+++ b/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenterImpl.java
@@ -1,18 +1,24 @@
 package de.dotwee.micropinner.presenter;
 
+import android.Manifest;
 import android.app.Activity;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.os.Build;
-import android.support.annotation.NonNull;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.widget.SwitchCompat;
+import androidx.core.app.ActivityCompat;
+import androidx.core.app.NotificationCompat;
+
 import android.view.View;
 import android.widget.Button;
 import android.widget.CheckBox;
 import android.widget.EditText;
 import android.widget.Spinner;
-import android.widget.Switch;
 import android.widget.TextView;
 import android.widget.Toast;
 
@@ -33,14 +39,16 @@ public class MainPresenterImpl implements MainPresenter {
     private final PreferencesHandler preferencesHandler;
     private final NotificationManager notificationManager;
     private final Activity activity;
+    private final int activityPermissionRequestCode;
 
     private final PinDatabase pinDatabase;
     private final Intent intent;
     private PinSpec parentPin;
 
-    public MainPresenterImpl(@NonNull Activity activity, @NonNull Intent intent) {
+    public MainPresenterImpl(@NonNull Activity activity, @NonNull Intent intent, int permissionRequestCode) {
         this.preferencesHandler = PreferencesHandler.getInstance(activity);
         this.activity = activity;
+        this.activityPermissionRequestCode = permissionRequestCode;
         this.intent = intent;
 
         pinDatabase = PinDatabase.getInstance(activity.getApplicationContext());
@@ -68,11 +76,39 @@ public void onSwitchHold() {
         Toast.makeText(activity, "Theme will change automatically by day and night.", Toast.LENGTH_SHORT).show();
     }
 
+    @Override
+    public void onRequestPermissionsResult(@NonNull String[] permissions,
+                                           @NonNull int[] grantResults) {
+        if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+            // Permission has been granted.
+            createPin();
+        } else {
+            // Permission request was denied.
+            Toast.makeText(activity,
+                    activity.getResources().getText(R.string.message_notifications_permission_denied),
+                    Toast.LENGTH_LONG).show();
+        }
+    }
+
     /**
-     * This method handles the click on the positive dialog button.
+     * For sdk version 33 we need to request permission to send notifications.
+     *
+     * Requests the {@link android.Manifest.permission#POST_NOTIFICATIONS} permission.
+     * If an additional rationale should be displayed, the user has to launch the request from
+     * a SnackBar that includes additional information.
+     *
+     * @see android - changed targetSdkVersion to 33 from 30 and now notifications are not coming up - Stack Overflow
+     * @see permissions-samples/MainActivity.java ยท android/permissions-samples
+     * @see Request app permissions - Android Developers
      */
-    @Override
-    public void onButtonPositive() {
+    @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
+    private void requestNotificationPermission() {
+        // Request the permission. The result will be received in the activity's onRequestPermissionResult().
+        ActivityCompat.requestPermissions(activity,
+            new String[]{Manifest.permission.POST_NOTIFICATIONS}, activityPermissionRequestCode);
+    }
+
+    private void createPin() {
         PinSpec newPin;
 
         try {
@@ -91,6 +127,24 @@ public void onButtonPositive() {
         }
     }
 
+    /**
+     * This method handles the click on the positive dialog button.
+     */
+    @Override
+    public void onButtonPositive() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+            // No permission needed:
+            createPin();
+        } else if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS)
+                == PackageManager.PERMISSION_GRANTED) {
+            // Permission already granted:
+            createPin();
+        } else {
+            // Permission is missing and must be requested.
+            requestNotificationPermission();
+        }
+    }
+
     /**
      * This method handles the click on the negative dialog button.
      */
@@ -113,7 +167,7 @@ public void restore() {
         // restore the switch's state if advanced is enabled
         if (preferencesHandler.isAdvancedUsed()) {
 
-            Switch advancedSwitch = activity.findViewById(R.id.switchAdvanced);
+            SwitchCompat advancedSwitch = activity.findViewById(R.id.switchAdvanced);
             if (advancedSwitch != null) {
 
                 advancedSwitch.setChecked(true);
@@ -170,7 +224,7 @@ public boolean hasParentPin() {
         if (intent != null) {
             Serializable extra = intent.getSerializableExtra(NotificationTools.EXTRA_INTENT);
 
-            if (extra != null && extra instanceof PinSpec) {
+            if (extra instanceof PinSpec) {
                 this.parentPin = (PinSpec) extra;
                 return true;
             }
@@ -255,21 +309,18 @@ public void handleParentVisibility(@NonNull PinSpec pin) {
             int visibilityPosition;
 
             switch (parentPin.getVisibility()) {
-                case Notification.VISIBILITY_PUBLIC:
+                case NotificationCompat.VISIBILITY_PUBLIC:
+                default:
                     visibilityPosition = 0;
                     break;
 
-                case Notification.VISIBILITY_PRIVATE:
+                case NotificationCompat.VISIBILITY_PRIVATE:
                     visibilityPosition = 1;
                     break;
 
-                case Notification.VISIBILITY_SECRET:
+                case NotificationCompat.VISIBILITY_SECRET:
                     visibilityPosition = 2;
                     break;
-
-                default:
-                    visibilityPosition = 0;
-                    break;
             }
 
             spinnerVisibility.setSelection(visibilityPosition, true);
@@ -284,6 +335,7 @@ public void handleParentPriority(@NonNull PinSpec pin) {
             int priorityPosition;
 
             switch (parentPin.getPriority()) {
+                default:
                 case Notification.PRIORITY_DEFAULT:
                     priorityPosition = 0;
                     break;
@@ -299,10 +351,6 @@ public void handleParentPriority(@NonNull PinSpec pin) {
                 case Notification.PRIORITY_HIGH:
                     priorityPosition = 3;
                     break;
-
-                default:
-                    priorityPosition = 0;
-                    break;
             }
 
             spinnerPriority.setSelection(priorityPosition, true);
diff --git a/app/src/main/java/de/dotwee/micropinner/receiver/OnBootReceiver.java b/app/src/main/java/de/dotwee/micropinner/receiver/OnBootReceiver.java
index e7c6f9e..c6ff0dd 100644
--- a/app/src/main/java/de/dotwee/micropinner/receiver/OnBootReceiver.java
+++ b/app/src/main/java/de/dotwee/micropinner/receiver/OnBootReceiver.java
@@ -3,14 +3,11 @@
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.util.Log;
 
-import java.util.Map;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
-import de.dotwee.micropinner.database.PinDatabase;
-import de.dotwee.micropinner.database.PinSpec;
 import de.dotwee.micropinner.tools.NotificationTools;
 
 public class OnBootReceiver extends BroadcastReceiver {
@@ -31,15 +28,6 @@ public void onReceive(@NonNull Context context, @Nullable Intent intent) {
             return;
         }
 
-        // get all pins
-        final Map pinMap = PinDatabase.getInstance(context).getAllPinsMap();
-
-        // foreach through them all
-        for (Map.Entry entry : pinMap.entrySet()) {
-            PinSpec pin = entry.getValue();
-
-            // create a notification from the object and finally restore it
-            NotificationTools.notify(context, pin);
-        }
+        NotificationTools.restoreNotifications(context);
     }
 }
diff --git a/app/src/main/java/de/dotwee/micropinner/receiver/OnClipReceiver.java b/app/src/main/java/de/dotwee/micropinner/receiver/OnClipReceiver.java
index 03e7a67..89ff67e 100644
--- a/app/src/main/java/de/dotwee/micropinner/receiver/OnClipReceiver.java
+++ b/app/src/main/java/de/dotwee/micropinner/receiver/OnClipReceiver.java
@@ -5,7 +5,7 @@
 import android.content.ClipboardManager;
 import android.content.Context;
 import android.content.Intent;
-import android.support.annotation.NonNull;
+import androidx.annotation.NonNull;
 import android.util.Log;
 import android.widget.Toast;
 
diff --git a/app/src/main/java/de/dotwee/micropinner/receiver/OnDeleteReceiver.java b/app/src/main/java/de/dotwee/micropinner/receiver/OnDeleteReceiver.java
index 5299ee7..2f04163 100644
--- a/app/src/main/java/de/dotwee/micropinner/receiver/OnDeleteReceiver.java
+++ b/app/src/main/java/de/dotwee/micropinner/receiver/OnDeleteReceiver.java
@@ -3,7 +3,7 @@
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-import android.support.annotation.NonNull;
+import androidx.annotation.NonNull;
 import android.util.Log;
 
 import de.dotwee.micropinner.database.PinDatabase;
diff --git a/app/src/main/java/de/dotwee/micropinner/receiver/OnUpdateReceiver.java b/app/src/main/java/de/dotwee/micropinner/receiver/OnUpdateReceiver.java
new file mode 100644
index 0000000..4d648e0
--- /dev/null
+++ b/app/src/main/java/de/dotwee/micropinner/receiver/OnUpdateReceiver.java
@@ -0,0 +1,38 @@
+package de.dotwee.micropinner.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import de.dotwee.micropinner.tools.NotificationTools;
+
+/**
+ * This receiver is used when the app is updated to ensure notifications are restored immediately.
+ *
+ * @see android - Push Notification After App Was Updated - Stack Overflow
+ */
+public class OnUpdateReceiver extends BroadcastReceiver {
+    private final static String TAG = OnBootReceiver.class.getSimpleName();
+
+    @Override
+    public void onReceive(@NonNull Context context, @Nullable Intent intent) {
+        if (intent == null || intent.getAction() == null) {
+            Log.w(TAG,
+                    "Intent (and its action) must be not null to work with it, returning without work");
+            return;
+        }
+
+        if (!intent.getAction().equals(Intent.ACTION_MY_PACKAGE_REPLACED)) {
+            Log.w(TAG, "OnUpdateReceiver's intent actions is not "
+                    + Intent.ACTION_MY_PACKAGE_REPLACED
+                    + ", returning without work");
+            return;
+        }
+
+        NotificationTools.restoreNotifications(context);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
index 7e4089a..5d54e4b 100644
--- a/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
+++ b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
@@ -7,12 +7,24 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.Typeface;
 import android.os.Build;
-import android.support.annotation.NonNull;
-import android.support.v4.app.NotificationCompat;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.app.NotificationCompat;
+
+import android.service.notification.StatusBarNotification;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.StyleSpan;
 import android.util.Log;
 
+import java.util.HashMap;
+import java.util.Map;
+
 import de.dotwee.micropinner.R;
+import de.dotwee.micropinner.database.PinDatabase;
 import de.dotwee.micropinner.database.PinSpec;
 import de.dotwee.micropinner.receiver.OnClipReceiver;
 import de.dotwee.micropinner.receiver.OnDeleteReceiver;
@@ -22,18 +34,93 @@
  * Created by lukas on 10.08.2016.
  */
 public class NotificationTools {
+    /**
+     * Name of extra data inside intents that contains a PinSpec object with data about the parent pin.
+     */
     public final static String EXTRA_INTENT = "IAMAPIN";
 
-    private static final String CHANNEL_NAME = "pin_channel";
+    /**
+     * Used in app version 2.2.0 and earlier.
+     */
+    private static final String CHANNEL_NAME_OLD = "pin_channel";
+    private static final String CHANNEL_NAME_PUBLIC = "pin_channel_public";
+    private static final String CHANNEL_NAME_PRIVATE = "pin_channel_private";
+    private static final String CHANNEL_NAME_SECRET = "pin_channel_secret";
+
     private static final String TAG = NotificationTools.class.getSimpleName();
 
+    /** Needed for later android versions, see:
+     * https://stackoverflow.com/questions/67045607/how-to-resolve-missing-pendingintent-mutability-flag-lint-warning-in-android-a
+     */
+    private static final int FLAG_IMMUTABLE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0;
+
+    /**
+     * Detect the first time the app is started. Used to prevent restoring notifications more than once.
+     *
+     *  @see android - Detect the first time an Activity is opened on this session - Stack Overflow 
+     */
+    private static volatile boolean JUST_STARTED = true;
+
+    public static void restoreNotifications(@NonNull Context context) {
+        if (!JUST_STARTED) {
+            return;
+        }
+        JUST_STARTED = false;
+
+        // get all pins
+        final Map pinMap = PinDatabase.getInstance(context).getAllPinsMap();
+
+        @Nullable final Map activeNotifications = getActiveNotifications(context);
+
+        // foreach through them all
+        for (Map.Entry entry : pinMap.entrySet()) {
+            PinSpec pin = entry.getValue();
+
+            // On API level 23 and above we double check that the notification doesn't already exists before restoring it.
+            if (activeNotifications != null && activeNotifications.containsKey(pin.getIdAsInt())) {
+                Log.i(TAG, "skipped restoring notification with id " + pin.getId());
+                continue;
+            }
+
+            // create a notification from the object and finally restore it
+            NotificationTools.notify(context, pin);
+        }
+    }
+
+    /**
+     * Get active notifications on API 23 and later.
+     * @return Null on API 22 and earlier, otherwise a map with notification ids as keys and info about the notifications as values.
+     * @see android - notificationManager get notification by Id - Stack Overflow
+     */
+    @Nullable
+    private static Map getActiveNotifications(@NonNull Context context) {
+        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
+            return null;
+        }
+
+        NotificationManager notificationManager =
+                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+        if (notificationManager == null) {
+            return null;
+        }
+        StatusBarNotification[] barNotifications = notificationManager.getActiveNotifications();
+
+        Map notificationMap = new HashMap<>();
+        for(StatusBarNotification notification: barNotifications) {
+            notificationMap.put(notification.getId(), notification);
+        }
+
+        return notificationMap;
+    }
+
     @NonNull
     private static PendingIntent getPinIntent(@NonNull Context context, @NonNull PinSpec pin) {
         Intent resultIntent = new Intent(context, MainDialog.class);
         resultIntent.putExtra(EXTRA_INTENT, pin);
 
         return PendingIntent.getActivity(context, (int) pin.getId(), resultIntent,
-                PendingIntent.FLAG_UPDATE_CURRENT);
+                PendingIntent.FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE);
     }
 
     @NonNull
@@ -63,17 +150,80 @@ private static NotificationChannel getNotificationChannel(int pinPriority) {
                 break;
         }
 
-        return new NotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID, CHANNEL_NAME, importance);
+        return new NotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID, CHANNEL_NAME_PUBLIC, importance);
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    private static void createOrUpdateNotificationChannels(@NonNull Context context, @NonNull NotificationManager notificationManager) {
+        // Use low importance in order to not make a sound when creating a notification.
+        // If this is too low then the user should be able to manually change channel settings, so this seems like a sensible default.
+        // See: https://developer.android.com/develop/ui/views/notifications/channels#importance
+        final int importance = NotificationManager.IMPORTANCE_LOW;
+
+        // Delete old channel used in version 2.2.0 and earlier:
+        notificationManager.deleteNotificationChannel(CHANNEL_NAME_OLD);
+
+        // Create one channel per visibility level to allow user to customize how they are shown on the lock screen:
+        NotificationChannel public_channel = new NotificationChannel(CHANNEL_NAME_PUBLIC,
+                context.getResources().getString(R.string.notifications_channel_public),
+                importance);
+        public_channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
+        public_channel.setShowBadge(false);
+        public_channel.enableLights(false);
+        public_channel.enableVibration(false);
+        notificationManager.createNotificationChannel(public_channel);
+
+        NotificationChannel private_channel = new NotificationChannel(CHANNEL_NAME_PRIVATE,
+                context.getResources().getString(R.string.notifications_channel_private),
+                importance);
+        private_channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
+        private_channel.setShowBadge(false);
+        private_channel.enableLights(false);
+        private_channel.enableVibration(false);
+        notificationManager.createNotificationChannel(private_channel);
+
+        NotificationChannel secret_channel = new NotificationChannel(CHANNEL_NAME_SECRET,
+                context.getResources().getString(R.string.notifications_channel_secret),
+                importance);
+        secret_channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
+        secret_channel.setShowBadge(false);
+        secret_channel.enableLights(false);
+        secret_channel.enableVibration(false);
+        notificationManager.createNotificationChannel(secret_channel);
+    }
+
+    private static String getChannelName(@NonNull PinSpec pin) {
+        switch (pin.getVisibility()) {
+            case NotificationCompat.VISIBILITY_PUBLIC:
+                return CHANNEL_NAME_PUBLIC;
+            case NotificationCompat.VISIBILITY_PRIVATE:
+                return CHANNEL_NAME_PRIVATE;
+            case NotificationCompat.VISIBILITY_SECRET:
+                return CHANNEL_NAME_SECRET;
+            default:
+                throw new RuntimeException("Unknown visibility value");
+        }
+    }
+
+    private static CharSequence styledText(CharSequence text, StyleSpan style) {
+        // https://stackoverflow.com/questions/70698860/how-to-bold-title-in-notification
+        Spannable content = new SpannableString(text);
+        content.setSpan(style, 0, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        return content;
     }
 
     public static void notify(@NonNull Context context, @NonNull PinSpec pin) {
         NotificationManager notificationManager =
                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
 
+        String channel_id = getChannelName(pin);
         NotificationCompat.Builder builder =
-                new NotificationCompat.Builder(context, CHANNEL_NAME).setContentTitle(pin.getTitle())
+                new NotificationCompat.Builder(context, channel_id)
+                        .setContentTitle(pin.getTitle())
                         .setContentText(pin.getContent())
                         .setSmallIcon(R.drawable.ic_notif_star)
+                        .setOnlyAlertOnce(true)
+                        .setCategory(NotificationCompat.CATEGORY_REMINDER)
                         .setPriority(pin.getPriority())
                         .setVisibility(pin.getVisibility())
                         .setStyle(new NotificationCompat.BigTextStyle().bigText(pin.getContent()))
@@ -81,25 +231,42 @@ public static void notify(@NonNull Context context, @NonNull PinSpec pin) {
 
                         .setDeleteIntent(PendingIntent.getBroadcast(context, (int) pin.getId(),
                                 new Intent(context, OnDeleteReceiver.class).setAction("notification_cancelled")
-                                        .putExtra(EXTRA_INTENT, pin), PendingIntent.FLAG_CANCEL_CURRENT))
+                                        .putExtra(EXTRA_INTENT, pin), PendingIntent.FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE))
                         .setOngoing(pin.isPersistent());
 
+        if (pin.getVisibility() == NotificationCompat.VISIBILITY_PRIVATE && !pin.getContent().isEmpty()) {
+            // If visibility is hidden then an alternative notification can be shown on the lock screen:
+            // More info: https://developer.android.com/develop/ui/views/notifications/build-notification#lockscreenNotification
+            // More info: https://gabrieltanner.org/blog/android-notifications-overview/
+
+            // Show "Contents hidden" placeholder as italic:
+            // https://stackoverflow.com/questions/70698860/how-to-bold-title-in-notification
+            CharSequence hiddenContent = styledText(
+                    context.getResources().getText(R.string.message_hidden_private_content),
+                    new StyleSpan(Typeface.ITALIC)
+            );
+
+            NotificationCompat.Builder publicBuilder = new NotificationCompat.Builder(context, channel_id)
+                    .setContentTitle(pin.getTitle())
+                    .setContentText(hiddenContent)
+                    .setContentTitle(pin.getTitle())
+                    .setSmallIcon(R.drawable.ic_notif_star)
+                    .setPriority(NotificationCompat.PRIORITY_DEFAULT);
+
+            builder.setPublicVersion(publicBuilder.build());
+        }
+
         if (pin.isShowActions()) {
             builder.addAction(R.drawable.ic_action_clip,
                     context.getString(R.string.message_save_to_clipboard),
                     PendingIntent.getBroadcast(context, (int) pin.getId(),
                             new Intent(context, OnClipReceiver.class).putExtra(EXTRA_INTENT, pin),
-                            PendingIntent.FLAG_CANCEL_CURRENT));
+                            PendingIntent.FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE));
         }
 
         if (notificationManager != null) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-
-                /* Create or update. */
-                NotificationChannel channel = new NotificationChannel(CHANNEL_NAME,
-                        "Pins",
-                        NotificationManager.IMPORTANCE_DEFAULT);
-                notificationManager.createNotificationChannel(channel);
+                createOrUpdateNotificationChannels(context, notificationManager);
             }
 
             Log.i(TAG, "Send notification with pin id " + pin.getIdAsInt() + " to system");
diff --git a/app/src/main/java/de/dotwee/micropinner/tools/PreferencesHandler.java b/app/src/main/java/de/dotwee/micropinner/tools/PreferencesHandler.java
index 59b93e7..2b2ea1b 100644
--- a/app/src/main/java/de/dotwee/micropinner/tools/PreferencesHandler.java
+++ b/app/src/main/java/de/dotwee/micropinner/tools/PreferencesHandler.java
@@ -3,7 +3,7 @@
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
+import androidx.annotation.NonNull;
 
 /**
  * Created by lukas on 18.08.2015 - 16:11
diff --git a/app/src/main/java/de/dotwee/micropinner/view/MainDialog.java b/app/src/main/java/de/dotwee/micropinner/view/MainDialog.java
index 756b877..7392b2e 100644
--- a/app/src/main/java/de/dotwee/micropinner/view/MainDialog.java
+++ b/app/src/main/java/de/dotwee/micropinner/view/MainDialog.java
@@ -6,11 +6,11 @@
 import android.content.res.Configuration;
 import android.os.Build;
 import android.os.Bundle;
-import android.support.annotation.LayoutRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.app.AppCompatDelegate;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.app.AppCompatDelegate;
 import android.util.DisplayMetrics;
 import android.view.View;
 import android.view.ViewGroup;
@@ -22,7 +22,7 @@
 import de.dotwee.micropinner.R;
 import de.dotwee.micropinner.presenter.MainPresenter;
 import de.dotwee.micropinner.presenter.MainPresenterImpl;
-import de.dotwee.micropinner.receiver.OnBootReceiver;
+import de.dotwee.micropinner.tools.NotificationTools;
 import de.dotwee.micropinner.view.custom.DialogContentView;
 import de.dotwee.micropinner.view.custom.DialogFooterView;
 import de.dotwee.micropinner.view.custom.DialogHeaderView;
@@ -33,11 +33,16 @@
 public class MainDialog extends AppCompatActivity implements MainPresenter.Data {
     private static final String TAG = MainDialog.class.getSimpleName();
 
+    /** Used when requesting permission to post notifications. */
+    private static final int PERMISSION_REQUEST_PRESENTER = 0;
+
     static {
         AppCompatDelegate.setDefaultNightMode(
-                AppCompatDelegate.MODE_NIGHT_AUTO);
+                AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
     }
 
+    private MainPresenter mainPresenter;
+
     /**
      * This method checks if the user's device is a tablet, depending on the official resource {@link
      * Configuration}.
@@ -56,7 +61,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
 
         this.setContentView(R.layout.dialog_main);
 
-        MainPresenter mainPresenter = new MainPresenterImpl(this, getIntent());
+        mainPresenter = new MainPresenterImpl(this, getIntent(), PERMISSION_REQUEST_PRESENTER);
 
         DialogHeaderView headerView = findViewById(R.id.dialogHeaderView);
         headerView.setMainPresenter(mainPresenter);
@@ -70,8 +75,19 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
         // restore previous state
         mainPresenter.restore();
 
-        // simulate device-boot by sending a new intent to class OnBootReceiver
-        sendBroadcast(new Intent(this, OnBootReceiver.class));
+        // If app was closed then restore notifications from previous session:
+        NotificationTools.restoreNotifications(this);
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+                                           @NonNull int[] grantResults) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+        if (requestCode == PERMISSION_REQUEST_PRESENTER) {
+            // Request made by main presenter, so let it handle the results:
+            mainPresenter.onRequestPermissionsResult(permissions, grantResults);
+        }
     }
 
     @Override
diff --git a/app/src/main/java/de/dotwee/micropinner/view/custom/AbstractDialogView.java b/app/src/main/java/de/dotwee/micropinner/view/custom/AbstractDialogView.java
index 2a11829..ee20bbf 100644
--- a/app/src/main/java/de/dotwee/micropinner/view/custom/AbstractDialogView.java
+++ b/app/src/main/java/de/dotwee/micropinner/view/custom/AbstractDialogView.java
@@ -1,7 +1,7 @@
 package de.dotwee.micropinner.view.custom;
 
 import android.content.Context;
-import android.support.annotation.NonNull;
+import androidx.annotation.NonNull;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.widget.FrameLayout;
diff --git a/app/src/main/java/de/dotwee/micropinner/view/custom/DialogContentView.java b/app/src/main/java/de/dotwee/micropinner/view/custom/DialogContentView.java
index 6dfc0b3..76a955a 100644
--- a/app/src/main/java/de/dotwee/micropinner/view/custom/DialogContentView.java
+++ b/app/src/main/java/de/dotwee/micropinner/view/custom/DialogContentView.java
@@ -41,6 +41,9 @@ public void init() {
 
         spinnerPriority = findViewById(R.id.spinnerPriority);
         setPriorityAdapter();
+
+        CheckBox showActions = this.findViewById(R.id.checkBoxShowActions);
+        showActions.setOnCheckedChangeListener(this);
     }
 
     private void setVisibilityAdapter() {
@@ -71,11 +74,8 @@ private void setPriorityAdapter() {
     public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
         checkIfPresenterNull();
 
-        switch (compoundButton.getId()) {
-
-            case R.id.checkBoxShowActions:
-                mainPresenter.onShowActions();
-                break;
+        if (compoundButton.getId() == R.id.checkBoxShowActions) {
+            mainPresenter.onShowActions();
         }
     }
 }
diff --git a/app/src/main/java/de/dotwee/micropinner/view/custom/DialogFooterView.java b/app/src/main/java/de/dotwee/micropinner/view/custom/DialogFooterView.java
index 20ef29d..f6aad77 100644
--- a/app/src/main/java/de/dotwee/micropinner/view/custom/DialogFooterView.java
+++ b/app/src/main/java/de/dotwee/micropinner/view/custom/DialogFooterView.java
@@ -1,7 +1,7 @@
 package de.dotwee.micropinner.view.custom;
 
 import android.content.Context;
-import android.support.annotation.NonNull;
+import androidx.annotation.NonNull;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.View;
@@ -50,20 +50,15 @@ public void init() {
     public void onClick(@NonNull View view) {
         checkIfPresenterNull();
 
-        switch (view.getId()) {
-            case R.id.buttonPin:
-                mainPresenter.onButtonPositive();
-                break;
-
-            case R.id.buttonCancel:
-                mainPresenter.onButtonNegative();
-                break;
-
-            default:
-                if (BuildConfig.DEBUG) {
-                    Log.w(TAG, "Registered click on unknown view");
-                }
-                break;
+        int id = view.getId();
+        if (id == R.id.buttonPin) {
+            mainPresenter.onButtonPositive();
+        } else if (id == R.id.buttonCancel) {
+            mainPresenter.onButtonNegative();
+        } else {
+            if (BuildConfig.DEBUG) {
+                Log.w(TAG, "Registered click on unknown view");
+            }
         }
     }
 }
diff --git a/app/src/main/java/de/dotwee/micropinner/view/custom/DialogHeaderView.java b/app/src/main/java/de/dotwee/micropinner/view/custom/DialogHeaderView.java
index 8be7fad..15f1280 100644
--- a/app/src/main/java/de/dotwee/micropinner/view/custom/DialogHeaderView.java
+++ b/app/src/main/java/de/dotwee/micropinner/view/custom/DialogHeaderView.java
@@ -6,7 +6,8 @@
 import android.view.View;
 import android.widget.CompoundButton;
 import android.widget.LinearLayout;
-import android.widget.Switch;
+
+import androidx.appcompat.widget.SwitchCompat;
 
 import de.dotwee.micropinner.BuildConfig;
 import de.dotwee.micropinner.R;
@@ -15,10 +16,10 @@
  * Created by lukas on 25.07.2016.
  */
 public class DialogHeaderView extends AbstractDialogView
-        implements Switch.OnCheckedChangeListener, View.OnClickListener, View.OnLongClickListener {
+        implements SwitchCompat.OnCheckedChangeListener, View.OnClickListener, View.OnLongClickListener {
 
     private static final String TAG = DialogHeaderView.class.getSimpleName();
-    private Switch switchAdvanced;
+    private SwitchCompat switchAdvanced;
 
     public DialogHeaderView(Context context) {
         super(context);
@@ -50,27 +51,19 @@ public void init() {
     @Override
     public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
         checkIfPresenterNull();
-        switch (compoundButton.getId()) {
-
-            case R.id.switchAdvanced:
-                mainPresenter.onViewExpand(isChecked);
-                break;
+        if (compoundButton.getId() == R.id.switchAdvanced) {
+            mainPresenter.onViewExpand(isChecked);
         }
     }
 
     @Override
     public void onClick(View view) {
-        switch (view.getId()) {
-
-            case R.id.linearLayoutHeader:
-                switchAdvanced.performClick();
-                break;
-
-            default:
-                if (BuildConfig.DEBUG) {
-                    Log.w(TAG, "Registered click on unknown view");
-                }
-                break;
+        if (view.getId() == R.id.linearLayoutHeader) {
+            switchAdvanced.performClick();
+        } else {
+            if (BuildConfig.DEBUG) {
+                Log.w(TAG, "Registered click on unknown view");
+            }
         }
     }
 
@@ -84,21 +77,17 @@ public void onClick(View view) {
     public boolean onLongClick(View view) {
         checkIfPresenterNull();
 
-        switch (view.getId()) {
-
-            case R.id.switchAdvanced:
-                mainPresenter.onSwitchHold();
-                return true;
-
-            case R.id.linearLayoutHeader:
-                mainPresenter.onSwitchHold();
-                return true;
-
-            default:
-                if (BuildConfig.DEBUG) {
-                    Log.w(TAG, "Registered long-click on unknown view");
-                }
-                return false;
+        int id = view.getId();
+        if (id == R.id.switchAdvanced) {
+            mainPresenter.onSwitchHold();
+            return true;
+        } else if (id == R.id.linearLayoutHeader) {
+            mainPresenter.onSwitchHold();
+            return true;
+        }
+        if (BuildConfig.DEBUG) {
+            Log.w(TAG, "Registered long-click on unknown view");
         }
+        return false;
     }
 }
diff --git a/app/src/main/res/layout/dialog_main.xml b/app/src/main/res/layout/dialog_main.xml
index fec6754..0b80ff2 100644
--- a/app/src/main/res/layout/dialog_main.xml
+++ b/app/src/main/res/layout/dialog_main.xml
@@ -3,18 +3,25 @@
     style="@style/MainWrapper"
     android:layout_width="wrap_content"
     android:layout_height="match_parent"
-    tools:context=".view.MainDialog">
+    android:layout_margin="16dp"
+    android:background="@color/background"
+    tools:context=".view.MainDialog"
+    tools:ignore="Overdraw">
 
     
+        style="@style/DialogView"
+        android:background="@color/background" />
 
     
+        style="@style/DialogView"
+        android:layout_weight="1"
+        tools:ignore="InefficientWeight" />
 
     
+        style="@style/DialogView"
+        android:background="@color/background" />
 
 
diff --git a/app/src/main/res/layout/dialog_main_content.xml b/app/src/main/res/layout/dialog_main_content.xml
index 4a687f3..ef04f86 100644
--- a/app/src/main/res/layout/dialog_main_content.xml
+++ b/app/src/main/res/layout/dialog_main_content.xml
@@ -1,84 +1,85 @@
 
-
-
-    
-
-        
-
-        
-    
-
-    
-
-        
-
-        
-    
-
-
-    
-
-        
-
-        
-    
-
-    
-
-        
-
-        
-    
-
-    
-
-        
-
-    
-
-    
-
-        
-
-    
-
-
\ No newline at end of file
+
+
+    
+
+        
+
+            
+
+                
+
+                
+
+                
+
+                
+            
+        
+
+        
+
+            
+
+            
+        
+
+        
+
+            
+
+            
+        
+
+        
+
+            
+
+        
+
+        
+
+            
+
+        
+
+    
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_main_head.xml b/app/src/main/res/layout/dialog_main_head.xml
index 37bd85e..9048831 100644
--- a/app/src/main/res/layout/dialog_main_head.xml
+++ b/app/src/main/res/layout/dialog_main_head.xml
@@ -25,7 +25,7 @@
         android:layout_height="wrap_content"
         android:background="@android:color/transparent">
 
-        
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 72d4eac..f5dc079 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -2,4 +2,7 @@
 
     #673AB7
     #512DA8
+    #00FFFFFF
+    #FF5252
+    #FAFAFA
 
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index ffde02f..275d6b8 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -2,7 +2,7 @@
 
 
     24dp
-    24dp
-    24dp
+    20dp
+    16dp
 
 
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7f24afb..cbf6b6a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
 
-
+
     MicroPinner
 
     New Pin
@@ -22,9 +22,15 @@
     Content
 
     Hey! Seems like you are using a older Android Version, where the visibility-setting is unsupported.
+    Permission to send notifications is required for this app to function.
     Pin has been copied to clipboard.
     The title has to contain text.
     Save to clipboard
+    Contents hidden
+
+    Public Pins
+    Private Pins
+    Secret Pins
 
     public
     private
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index d611d8c..00781c9 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,10 +1,12 @@
-
+
 
     
 
@@ -81,7 +90,15 @@
         wrap_content
         wrap_content
         ?attr/colorAccent
-        8dp
+        4dp
+    
+
+