From d4031c67b2d9eefb1b5235e7d16ea346d68b0538 Mon Sep 17 00:00:00 2001 From: Lej77 <31554212+Lej77@users.noreply.github.com> Date: Mon, 26 Dec 2022 17:42:34 +0100 Subject: [PATCH 01/35] Bump compileSdk and targetSdk to sdk 33, updated dependencies and migrate to "androidx" from "android.support" --- app/build.gradle | 16 +++--- .../de/dotwee/micropinner/tools/Matches.java | 8 +-- .../tools/PreferencesHandlerTest.java | 4 +- .../dotwee/micropinner/tools/TestTools.java | 2 +- .../view/MainDialogNewPinTest.java | 28 +++++------ .../view/MainDialogParentPinTest.java | 21 ++++---- .../micropinner/view/MainDialogThemeTest.java | 22 ++++---- app/src/main/AndroidManifest.xml | 3 +- .../micropinner/database/PinDatabase.java | 4 +- .../dotwee/micropinner/database/PinSpec.java | 2 +- .../micropinner/presenter/MainPresenter.java | 2 +- .../presenter/MainPresenterImpl.java | 2 +- .../micropinner/receiver/OnBootReceiver.java | 4 +- .../micropinner/receiver/OnClipReceiver.java | 2 +- .../receiver/OnDeleteReceiver.java | 2 +- .../micropinner/tools/NotificationTools.java | 15 ++++-- .../micropinner/tools/PreferencesHandler.java | 2 +- .../dotwee/micropinner/view/MainDialog.java | 12 ++--- .../view/custom/AbstractDialogView.java | 2 +- .../view/custom/DialogContentView.java | 7 +-- .../view/custom/DialogFooterView.java | 25 ++++------ .../view/custom/DialogHeaderView.java | 50 +++++++------------ build.gradle | 2 +- gradle.properties | 4 +- gradle/wrapper/gradle-wrapper.properties | 4 +- 25 files changed, 117 insertions(+), 128 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c5e26b4..5d98fe0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,17 +1,17 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 27 - buildToolsVersion "27.0.3" + compileSdkVersion 33 + buildToolsVersion "30.0.3" 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 { @@ -34,20 +34,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..46ab00b 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,10 @@ 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.intent.Checks; +import androidx.test.espresso.matcher.BoundedMatcher; import android.view.View; import android.widget.TextView; 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/view/MainDialogNewPinTest.java b/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogNewPinTest.java index 13d5654..16dd494 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; @@ -11,18 +11,18 @@ import de.dotwee.micropinner.database.PinDatabase; 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.matches; +import static androidx.test.espresso.matcher.RootMatchers.withDecorView; +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; 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..7513559 100644 --- a/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogParentPinTest.java +++ b/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogParentPinTest.java @@ -3,10 +3,11 @@ 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.espresso.intent.Intents; +import androidx.test.espresso.matcher.ViewMatchers; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.rule.ActivityTestRule; import org.junit.After; import org.junit.Before; @@ -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. 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..4238057 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,13 @@ 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.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 +18,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; /** diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 80ef865..834b0b3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,7 +16,8 @@ android:name=".view.MainDialog" android:excludeFromRecents="true" android:label="@string/main_name" - android:theme="@style/DialogTheme"> + android:theme="@style/DialogTheme" + android:exported="true"> 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..3694a37 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,7 @@ import android.content.ContentValues; import android.database.Cursor; import android.database.DatabaseUtils; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import java.io.Serializable; 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..06a2180 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; 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..624945e 100644 --- a/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenterImpl.java +++ b/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenterImpl.java @@ -6,7 +6,7 @@ import android.content.Context; import android.content.Intent; import android.os.Build; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import android.view.View; import android.widget.Button; import android.widget.CheckBox; 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..8065bab 100644 --- a/app/src/main/java/de/dotwee/micropinner/receiver/OnBootReceiver.java +++ b/app/src/main/java/de/dotwee/micropinner/receiver/OnBootReceiver.java @@ -3,8 +3,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import android.util.Log; import java.util.Map; 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/tools/NotificationTools.java b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java index 7e4089a..bd018da 100644 --- a/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java +++ b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java @@ -8,8 +8,8 @@ import android.content.Context; import android.content.Intent; import android.os.Build; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; import android.util.Log; import de.dotwee.micropinner.R; @@ -27,13 +27,18 @@ public class NotificationTools { private static final String CHANNEL_NAME = "pin_channel"; 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; + @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 @@ -81,7 +86,7 @@ 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.isShowActions()) { @@ -89,7 +94,7 @@ public static void notify(@NonNull Context context, @NonNull PinSpec pin) { 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) { 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..26b66ab 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; @@ -35,7 +35,7 @@ public class MainDialog extends AppCompatActivity implements MainPresenter.Data static { AppCompatDelegate.setDefaultNightMode( - AppCompatDelegate.MODE_NIGHT_AUTO); + AppCompatDelegate.MODE_NIGHT_AUTO_TIME); } /** 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..28b9f82 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 @@ -71,11 +71,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..e05a5e0 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 @@ -50,27 +50,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 +76,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/build.gradle b/build.gradle index 52e98c3..c75282e 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.2' + classpath 'com.android.tools.build:gradle:7.3.1' } } diff --git a/gradle.properties b/gradle.properties index 1d3591c..915f0e6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,4 +15,6 @@ # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true \ No newline at end of file +# org.gradle.parallel=true +android.enableJetifier=true +android.useAndroidX=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index efa061c..26ec0a3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Apr 04 18:51:18 CEST 2018 +#Sat Dec 24 18:42:03 CET 2022 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip From de3acb956538f2cfb608fd954aa1b0418ea9cb0e Mon Sep 17 00:00:00 2001 From: Lej77 <31554212+Lej77@users.noreply.github.com> Date: Mon, 26 Dec 2022 17:53:38 +0100 Subject: [PATCH 02/35] Request permission to send notifications on Android 13 (sdk 33) --- app/src/main/AndroidManifest.xml | 1 + .../micropinner/presenter/MainPresenter.java | 9 +++ .../presenter/MainPresenterImpl.java | 61 +++++++++++++++++-- .../dotwee/micropinner/view/MainDialog.java | 18 +++++- app/src/main/res/values/strings.xml | 1 + 5 files changed, 85 insertions(+), 5 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 834b0b3..d0f9b99 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ package="de.dotwee.micropinner"> + 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 624945e..e5949ed 100644 --- a/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenterImpl.java +++ b/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenterImpl.java @@ -1,12 +1,17 @@ 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 androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.core.app.ActivityCompat; + import android.view.View; import android.widget.Button; import android.widget.CheckBox; @@ -33,14 +38,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 +75,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 +126,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. */ 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 26b66ab..ff132b4 100644 --- a/app/src/main/java/de/dotwee/micropinner/view/MainDialog.java +++ b/app/src/main/java/de/dotwee/micropinner/view/MainDialog.java @@ -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_TIME); } + 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); @@ -74,6 +79,17 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { sendBroadcast(new Intent(this, OnBootReceiver.class)); } + @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 public void setContentView(@LayoutRes int layoutResID) { if (isTablet(this)) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7f24afb..947d945 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,6 +22,7 @@ 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 From c30cacfa5a34a7fb41f6105c7198f4d57aac3638 Mon Sep 17 00:00:00 2001 From: Lej77 <31554212+Lej77@users.noreply.github.com> Date: Mon, 26 Dec 2022 17:55:20 +0100 Subject: [PATCH 03/35] Allow more actions when selecting text --- app/src/main/AndroidManifest.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d0f9b99..b1e39fb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,14 @@ + + + + + + + + Date: Mon, 26 Dec 2022 18:00:51 +0100 Subject: [PATCH 04/35] Improve tests, ParentPinTest failed because of timeout on newer androids, ThemeTest is now ignored on too old Android versions, NewPinTest has better error messages for failed toast test --- .../de/dotwee/micropinner/tools/Matches.java | 10 +++ .../micropinner/tools/ToastMatcher.java | 68 +++++++++++++++++++ .../view/MainDialogNewPinTest.java | 10 ++- .../view/MainDialogParentPinTest.java | 22 ++---- .../micropinner/view/MainDialogThemeTest.java | 7 ++ 5 files changed, 99 insertions(+), 18 deletions(-) create mode 100644 app/src/androidTest/java/de/dotwee/micropinner/tools/ToastMatcher.java 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 46ab00b..831e9ad 100644 --- a/app/src/androidTest/java/de/dotwee/micropinner/tools/Matches.java +++ b/app/src/androidTest/java/de/dotwee/micropinner/tools/Matches.java @@ -3,6 +3,7 @@ import android.graphics.drawable.ColorDrawable; 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; @@ -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/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 16dd494..dcde639 100644
--- a/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogNewPinTest.java
+++ b/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogNewPinTest.java
@@ -9,14 +9,15 @@
 
 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 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.RootMatchers.withDecorView;
 import static androidx.test.espresso.matcher.ViewMatchers.isChecked;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static androidx.test.espresso.matcher.ViewMatchers.isFocusable;
@@ -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 7513559..5fc0cfc 100644
--- a/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogParentPinTest.java
+++ b/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogParentPinTest.java
@@ -4,13 +4,13 @@
 import android.content.Intent;
 import android.os.Build;
 
+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 androidx.test.rule.ActivityTestRule;
 
 import org.junit.After;
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -39,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 4238057..4f8c029 100644
--- a/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogThemeTest.java
+++ b/app/src/androidTest/java/de/dotwee/micropinner/view/MainDialogThemeTest.java
@@ -5,6 +5,7 @@
 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;
@@ -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);

From 41ce1549ef70f5b997b3dd57122c2b800841a3b6 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Mon, 26 Dec 2022 18:09:19 +0100
Subject: [PATCH 05/35] Fix issue #20 which caused issues when selecting text

---
 app/src/main/res/layout/dialog_main.xml | 2 ++
 app/src/main/res/values/colors.xml      | 1 +
 app/src/main/res/values/styles.xml      | 4 +++-
 3 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/app/src/main/res/layout/dialog_main.xml b/app/src/main/res/layout/dialog_main.xml
index fec6754..0d4a67b 100644
--- a/app/src/main/res/layout/dialog_main.xml
+++ b/app/src/main/res/layout/dialog_main.xml
@@ -3,6 +3,8 @@
     style="@style/MainWrapper"
     android:layout_width="wrap_content"
     android:layout_height="match_parent"
+    android:layout_margin="16dp"
+    android:background="@color/background"
     tools:context=".view.MainDialog">
 
     
     #673AB7
     #512DA8
+    #00FFFFFF
 
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index d611d8c..f4531cd 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -4,7 +4,9 @@
         @color/accent
         @color/primary
         @color/primary_dark
-        @color/background
+
+        @color/activityBackground
+        true
 
         false
         true

From 517eb9aebcb226d98519dfa12717aa99f4e694e7 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Mon, 26 Dec 2022 18:10:50 +0100
Subject: [PATCH 06/35] Follow system's night mode setting

---
 app/src/main/java/de/dotwee/micropinner/view/MainDialog.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 ff132b4..692b36c 100644
--- a/app/src/main/java/de/dotwee/micropinner/view/MainDialog.java
+++ b/app/src/main/java/de/dotwee/micropinner/view/MainDialog.java
@@ -38,7 +38,7 @@ public class MainDialog extends AppCompatActivity implements MainPresenter.Data
 
     static {
         AppCompatDelegate.setDefaultNightMode(
-                AppCompatDelegate.MODE_NIGHT_AUTO_TIME);
+                AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
     }
 
     private MainPresenter mainPresenter;

From 1c88fd97d5b12e79b0055195477ce7f57af4a192 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Mon, 26 Dec 2022 18:12:37 +0100
Subject: [PATCH 07/35] Register event listener for "show notification actions"
 to correctly save it, fixes issue #18

---
 .../de/dotwee/micropinner/view/custom/DialogContentView.java   | 3 +++
 1 file changed, 3 insertions(+)

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 28b9f82..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() {

From 8fca2394d6eb0b6afb8e99b70e5c8a84674690db Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Mon, 26 Dec 2022 18:22:43 +0100
Subject: [PATCH 08/35] Handle overflow when text gets too long, fixes issue
 #19

---
 app/src/main/AndroidManifest.xml              |   1 +
 app/src/main/res/layout/dialog_main.xml       |   9 +-
 .../main/res/layout/dialog_main_content.xml   | 129 +++++++++---------
 3 files changed, 73 insertions(+), 66 deletions(-)

diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b1e39fb..bc8dde5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -26,6 +26,7 @@
             android:excludeFromRecents="true"
             android:label="@string/main_name"
             android:theme="@style/DialogTheme"
+            android:windowSoftInputMode="adjustResize"
             android:exported="true">
             
                 
diff --git a/app/src/main/res/layout/dialog_main.xml b/app/src/main/res/layout/dialog_main.xml
index 0d4a67b..2e3c8d4 100644
--- a/app/src/main/res/layout/dialog_main.xml
+++ b/app/src/main/res/layout/dialog_main.xml
@@ -9,14 +9,17 @@
 
     
+        style="@style/DialogView"
+        android:background="@color/background" />
 
     
+        style="@style/DialogView"
+        android:layout_weight="1" />
 
     
+        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..73a4fb1 100644
--- a/app/src/main/res/layout/dialog_main_content.xml
+++ b/app/src/main/res/layout/dialog_main_content.xml
@@ -1,84 +1,87 @@
 
-
+
 
-    
+    
 
-        
+        
 
-        
-    
+            
 
-    
+            
+        
 
-        
+        
 
-        
-    
+            
 
+            
+        
 
-    
 
-        
+        
 
-        
-    
+            
 
-    
+            
+        
 
-        
+        
 
-        
-    
+            
 
-    
+            
+        
 
-        
+        
 
-    
+            
 
-    
+        
 
-        
+        
 
-    
+            
 
-
\ No newline at end of file
+        
+
+    
+
\ No newline at end of file

From ef6b2a1a34c2db0c72e06633af11ba2d01606991 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Mon, 26 Dec 2022 18:27:16 +0100
Subject: [PATCH 09/35] Create one notification channel for each visibility
 level and provide a "public" notification version for private notifications

---
 .../micropinner/tools/NotificationTools.java  | 87 +++++++++++++++++--
 app/src/main/res/values/strings.xml           |  5 ++
 2 files changed, 83 insertions(+), 9 deletions(-)

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 bd018da..913360d 100644
--- a/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
+++ b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
@@ -7,9 +7,15 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.Typeface;
 import android.os.Build;
 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
 import androidx.core.app.NotificationCompat;
+
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.StyleSpan;
 import android.util.Log;
 
 import de.dotwee.micropinner.R;
@@ -22,9 +28,15 @@
  * 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";
+    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:
@@ -68,15 +80,55 @@ 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) {
+        NotificationChannel public_channel = new NotificationChannel(CHANNEL_NAME_PUBLIC,
+                context.getResources().getString(R.string.notifications_channel_public),
+                NotificationManager.IMPORTANCE_DEFAULT);
+        notificationManager.createNotificationChannel(public_channel);
+
+        NotificationChannel private_channel = new NotificationChannel(CHANNEL_NAME_PRIVATE,
+                context.getResources().getString(R.string.notifications_channel_private),
+                NotificationManager.IMPORTANCE_DEFAULT);
+        notificationManager.createNotificationChannel(private_channel);
+
+        NotificationChannel secret_channel = new NotificationChannel(CHANNEL_NAME_SECRET,
+                context.getResources().getString(R.string.notifications_channel_secret),
+                NotificationManager.IMPORTANCE_DEFAULT);
+        notificationManager.createNotificationChannel(secret_channel);
+    }
+
+    private static String getChannelName(@NonNull PinSpec pin) {
+        switch (pin.getVisibility()) {
+            case Notification.VISIBILITY_PUBLIC:
+                return CHANNEL_NAME_PUBLIC;
+            case Notification.VISIBILITY_PRIVATE:
+                return CHANNEL_NAME_PRIVATE;
+            case Notification.VISIBILITY_SECRET:
+                return CHANNEL_NAME_SECRET;
+            default:
+                throw new RuntimeException("Unknown visibility value");
+        }
+    }
+
+    private static Spannable 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)
                         .setPriority(pin.getPriority())
@@ -89,6 +141,28 @@ public static void notify(@NonNull Context context, @NonNull PinSpec pin) {
                                         .putExtra(EXTRA_INTENT, pin), PendingIntent.FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE))
                         .setOngoing(pin.isPersistent());
 
+        if (pin.getVisibility() == Notification.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
+            Spannable 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),
@@ -99,12 +173,7 @@ public static void notify(@NonNull Context context, @NonNull PinSpec pin) {
 
         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/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 947d945..6e1f1d5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -26,6 +26,11 @@
     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

From 58c7de1b4701ef3d4fc2ba9bc5b276131a70ddd6 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Mon, 26 Dec 2022 18:30:15 +0100
Subject: [PATCH 10/35] Don't overlap drop down with the spinner it controls
 (requires API level 21)

---
 app/src/main/res/layout/dialog_main_content.xml |  7 ++-----
 app/src/main/res/values/styles.xml              | 12 ++++++++++--
 2 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/app/src/main/res/layout/dialog_main_content.xml b/app/src/main/res/layout/dialog_main_content.xml
index 73a4fb1..29d1a94 100644
--- a/app/src/main/res/layout/dialog_main_content.xml
+++ b/app/src/main/res/layout/dialog_main_content.xml
@@ -32,7 +32,6 @@
                 android:hint="@string/input_hint_content" />
         
 
-
         
 
             
 
             
         
 
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index f4531cd..2f4f96b 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,4 +1,4 @@
-
+
 
     
+
+    
 
     
 

From 03601b6ba1c9e6458b1b89d937baff5d2d5423cf Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Mon, 26 Dec 2022 20:05:12 +0100
Subject: [PATCH 12/35] Close activity when backing out of it

If this isn't done then pressing on a notification might unexpectedly resume the previous activity
---
 app/src/main/AndroidManifest.xml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ec6184b..154ca73 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,6 +27,7 @@
             android:label="@string/main_name"
             android:theme="@style/DialogTheme"
             android:launchMode="singleInstance"
+            android:noHistory="true"
             android:windowSoftInputMode="adjustResize"
             android:exported="true">
             

From 48b77c1a90c3a60a5e77513450d654c888847ebc Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Tue, 27 Dec 2022 03:12:59 +0100
Subject: [PATCH 13/35] Move package name into gradle file since having it in
 the manifest is deprecated

---
 app/build.gradle                 | 1 +
 app/src/main/AndroidManifest.xml | 3 +--
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/build.gradle b/app/build.gradle
index 5d98fe0..671f7b4 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -6,6 +6,7 @@ android {
 
     defaultConfig {
         applicationId "de.dotwee.micropinner"
+        namespace 'de.dotwee.micropinner'
         minSdkVersion 16
         targetSdkVersion 33
         versionCode 29
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 154ca73..bf8bd85 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,7 +1,6 @@
 
 
+    xmlns:tools="http://schemas.android.com/tools">
 
     
     

From dfc6678f95af343129b47ca8a6e98dbef60e09ec Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Tue, 27 Dec 2022 03:16:10 +0100
Subject: [PATCH 14/35] Remove label from activity, should fix the name mixup
 in issue #29

---
 app/src/main/AndroidManifest.xml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bf8bd85..0f457b2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -23,7 +23,6 @@
         
Date: Tue, 27 Dec 2022 04:01:17 +0100
Subject: [PATCH 15/35] Fix lint warnings

---
 .../dotwee/micropinner/database/PinSpec.java  |  3 +++
 .../presenter/MainPresenterImpl.java          | 23 ++++++++-----------
 .../micropinner/tools/NotificationTools.java  |  8 +++----
 .../view/custom/DialogHeaderView.java         |  7 +++---
 app/src/main/res/layout/dialog_main.xml       |  6 +++--
 .../main/res/layout/dialog_main_content.xml   |  6 +++--
 app/src/main/res/layout/dialog_main_head.xml  |  2 +-
 app/src/main/res/values/colors.xml            |  2 ++
 app/src/main/res/values/strings.xml           | 12 +++++-----
 build.gradle                                  |  4 ++--
 10 files changed, 39 insertions(+), 34 deletions(-)

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 3694a37..5aa4096 100644
--- a/app/src/main/java/de/dotwee/micropinner/database/PinSpec.java
+++ b/app/src/main/java/de/dotwee/micropinner/database/PinSpec.java
@@ -4,6 +4,7 @@
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 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/MainPresenterImpl.java b/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenterImpl.java
index e5949ed..4f4be32 100644
--- a/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenterImpl.java
+++ b/app/src/main/java/de/dotwee/micropinner/presenter/MainPresenterImpl.java
@@ -10,14 +10,15 @@
 import android.os.Build;
 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;
 
@@ -166,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);
@@ -223,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;
             }
@@ -308,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);
@@ -337,6 +335,7 @@ public void handleParentPriority(@NonNull PinSpec pin) {
             int priorityPosition;
 
             switch (parentPin.getPriority()) {
+                default:
                 case Notification.PRIORITY_DEFAULT:
                     priorityPosition = 0;
                     break;
@@ -352,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/tools/NotificationTools.java b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
index 913360d..79d6a13 100644
--- a/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
+++ b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
@@ -103,11 +103,11 @@ private static void createOrUpdateNotificationChannels(@NonNull Context context,
 
     private static String getChannelName(@NonNull PinSpec pin) {
         switch (pin.getVisibility()) {
-            case Notification.VISIBILITY_PUBLIC:
+            case NotificationCompat.VISIBILITY_PUBLIC:
                 return CHANNEL_NAME_PUBLIC;
-            case Notification.VISIBILITY_PRIVATE:
+            case NotificationCompat.VISIBILITY_PRIVATE:
                 return CHANNEL_NAME_PRIVATE;
-            case Notification.VISIBILITY_SECRET:
+            case NotificationCompat.VISIBILITY_SECRET:
                 return CHANNEL_NAME_SECRET;
             default:
                 throw new RuntimeException("Unknown visibility value");
@@ -141,7 +141,7 @@ public static void notify(@NonNull Context context, @NonNull PinSpec pin) {
                                         .putExtra(EXTRA_INTENT, pin), PendingIntent.FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE))
                         .setOngoing(pin.isPersistent());
 
-        if (pin.getVisibility() == Notification.VISIBILITY_PRIVATE && !pin.getContent().isEmpty()) {
+        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/
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 e05a5e0..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);
diff --git a/app/src/main/res/layout/dialog_main.xml b/app/src/main/res/layout/dialog_main.xml
index 2e3c8d4..0b80ff2 100644
--- a/app/src/main/res/layout/dialog_main.xml
+++ b/app/src/main/res/layout/dialog_main.xml
@@ -5,7 +5,8 @@
     android:layout_height="match_parent"
     android:layout_margin="16dp"
     android:background="@color/background"
-    tools:context=".view.MainDialog">
+    tools:context=".view.MainDialog"
+    tools:ignore="Overdraw">
 
     
+        android:layout_weight="1"
+        tools:ignore="InefficientWeight" />
 
     
+                    android:hint="@string/input_hint_title"
+                    android:importantForAutofill="no" />
 
                 
+                    android:hint="@string/input_hint_content"
+                    android:importantForAutofill="no" />
             
         
 
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 7e1b2eb..f5dc079 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -3,4 +3,6 @@
     #673AB7
     #512DA8
     #00FFFFFF
+    #FF5252
+    #FAFAFA
 
\ 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 6e1f1d5..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,15 +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.
+    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
+    Contents hidden
 
-    Public Pins
-    Private Pins
-    Secret Pins
+    Public Pins
+    Private Pins
+    Secret Pins
 
     public
     private
diff --git a/build.gradle b/build.gradle
index c75282e..798ca08 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,7 +2,7 @@
 
 buildscript {
     repositories {
-        jcenter()
+        mavenCentral()
         google()
     }
     dependencies {
@@ -12,7 +12,7 @@ buildscript {
 
 allprojects {
     repositories {
-        jcenter()
+        mavenCentral()
         google()
     }
 }

From 9470fa9e7e6f5938a75476d27dd9a525efcfb9d1 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Wed, 28 Dec 2022 04:10:56 +0100
Subject: [PATCH 16/35] actually restore notifications when app is opened and
 not just when device is rebooted, fixes issue #23

---
 .../micropinner/receiver/OnBootReceiver.java  | 25 +++++++++++--------
 1 file changed, 14 insertions(+), 11 deletions(-)

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 8065bab..ba3f246 100644
--- a/app/src/main/java/de/dotwee/micropinner/receiver/OnBootReceiver.java
+++ b/app/src/main/java/de/dotwee/micropinner/receiver/OnBootReceiver.java
@@ -1,11 +1,11 @@
 package de.dotwee.micropinner.receiver;
 
+import android.annotation.SuppressLint;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import android.util.Log;
 
 import java.util.Map;
 
@@ -16,20 +16,20 @@
 public class OnBootReceiver extends BroadcastReceiver {
     private final static String TAG = OnBootReceiver.class.getSimpleName();
 
+    /**
+     * Detect the first time the app is started. Can be 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;
+
+    @SuppressLint("UnsafeProtectedBroadcastReceiver")
     @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_BOOT_COMPLETED)) {
-            Log.w(TAG, "OnBootReceiver's intent actions is not "
-                    + Intent.ACTION_BOOT_COMPLETED
-                    + ", returning without work");
+        if (!JUST_STARTED) {
             return;
         }
+        JUST_STARTED = false;
 
         // get all pins
         final Map pinMap = PinDatabase.getInstance(context).getAllPinsMap();
@@ -38,6 +38,9 @@ public void onReceive(@NonNull Context context, @Nullable Intent intent) {
         for (Map.Entry entry : pinMap.entrySet()) {
             PinSpec pin = entry.getValue();
 
+            // TODO: on API level 23 and above we could double check that the notification doesn't already exists before restoring it, see:
+            // https://stackoverflow.com/questions/23831214/notificationmanager-get-notification-by-id
+
             // create a notification from the object and finally restore it
             NotificationTools.notify(context, pin);
         }

From df46fadd39f3af0e1563e7eacde5ad5cac77a4fb Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Wed, 28 Dec 2022 20:38:42 +0100
Subject: [PATCH 17/35] Restore notifications directly after an app update

---
 app/src/main/AndroidManifest.xml              | 21 ++++++----
 .../micropinner/receiver/OnBootReceiver.java  | 39 ++++++-------------
 .../receiver/OnUpdateReceiver.java            | 33 ++++++++++++++++
 .../micropinner/tools/NotificationTools.java  | 32 +++++++++++++++
 .../dotwee/micropinner/view/MainDialog.java   |  6 +--
 5 files changed, 94 insertions(+), 37 deletions(-)
 create mode 100644 app/src/main/java/de/dotwee/micropinner/receiver/OnUpdateReceiver.java

diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0f457b2..f9b893d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,10 +3,11 @@
     xmlns:tools="http://schemas.android.com/tools">
 
     
-    
+    
 
     
         
+
             
             
             
@@ -23,11 +24,11 @@
         
+            android:theme="@style/DialogTheme"
+            android:windowSoftInputMode="adjustResize">
             
                 
 
@@ -36,9 +37,7 @@
         
 
         
-
         
-
         
@@ -46,6 +45,14 @@
                 
             
         
+        
+            
+                
+            
+        
     
 
-
+
\ No newline at end of file
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 ba3f246..c6ff0dd 100644
--- a/app/src/main/java/de/dotwee/micropinner/receiver/OnBootReceiver.java
+++ b/app/src/main/java/de/dotwee/micropinner/receiver/OnBootReceiver.java
@@ -1,48 +1,33 @@
 package de.dotwee.micropinner.receiver;
 
-import android.annotation.SuppressLint;
 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 java.util.Map;
-
-import de.dotwee.micropinner.database.PinDatabase;
-import de.dotwee.micropinner.database.PinSpec;
 import de.dotwee.micropinner.tools.NotificationTools;
 
 public class OnBootReceiver extends BroadcastReceiver {
     private final static String TAG = OnBootReceiver.class.getSimpleName();
 
-    /**
-     * Detect the first time the app is started. Can be 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;
-
-    @SuppressLint("UnsafeProtectedBroadcastReceiver")
     @Override
     public void onReceive(@NonNull Context context, @Nullable Intent intent) {
-        if (!JUST_STARTED) {
+        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;
         }
-        JUST_STARTED = false;
-
-        // get all pins
-        final Map pinMap = PinDatabase.getInstance(context).getAllPinsMap();
 
-        // foreach through them all
-        for (Map.Entry entry : pinMap.entrySet()) {
-            PinSpec pin = entry.getValue();
-
-            // TODO: on API level 23 and above we could double check that the notification doesn't already exists before restoring it, see:
-            // https://stackoverflow.com/questions/23831214/notificationmanager-get-notification-by-id
-
-            // create a notification from the object and finally restore it
-            NotificationTools.notify(context, pin);
+        if (!intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
+            Log.w(TAG, "OnBootReceiver's intent actions is not "
+                    + Intent.ACTION_BOOT_COMPLETED
+                    + ", returning without work");
+            return;
         }
+
+        NotificationTools.restoreNotifications(context);
     }
 }
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..e07b192
--- /dev/null
+++ b/app/src/main/java/de/dotwee/micropinner/receiver/OnUpdateReceiver.java
@@ -0,0 +1,33 @@
+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;
+
+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 79d6a13..2b1e2c5 100644
--- a/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
+++ b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
@@ -18,7 +18,10 @@
 import android.text.style.StyleSpan;
 import android.util.Log;
 
+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;
@@ -44,6 +47,35 @@ public class NotificationTools {
      */
     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();
+
+        // foreach through them all
+        for (Map.Entry entry : pinMap.entrySet()) {
+            PinSpec pin = entry.getValue();
+
+            // TODO: on API level 23 and above we could double check that the notification doesn't already exists before restoring it, see:
+            // https://stackoverflow.com/questions/23831214/notificationmanager-get-notification-by-id
+
+            // create a notification from the object and finally restore it
+            NotificationTools.notify(context, pin);
+        }
+    }
+
+
     @NonNull
     private static PendingIntent getPinIntent(@NonNull Context context, @NonNull PinSpec pin) {
         Intent resultIntent = new Intent(context, MainDialog.class);
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 692b36c..7392b2e 100644
--- a/app/src/main/java/de/dotwee/micropinner/view/MainDialog.java
+++ b/app/src/main/java/de/dotwee/micropinner/view/MainDialog.java
@@ -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;
@@ -75,8 +75,8 @@ 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

From 42c4155b9a40dbbdabaafdf7a1d3ff07bc7b764d Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Wed, 28 Dec 2022 20:49:06 +0100
Subject: [PATCH 18/35] Documentation for OnUpdateReceiver

---
 .../de/dotwee/micropinner/receiver/OnUpdateReceiver.java     | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/app/src/main/java/de/dotwee/micropinner/receiver/OnUpdateReceiver.java b/app/src/main/java/de/dotwee/micropinner/receiver/OnUpdateReceiver.java
index e07b192..4d648e0 100644
--- a/app/src/main/java/de/dotwee/micropinner/receiver/OnUpdateReceiver.java
+++ b/app/src/main/java/de/dotwee/micropinner/receiver/OnUpdateReceiver.java
@@ -10,6 +10,11 @@
 
 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();
 

From 354b4b3ed58b952b18597a90567b79cdc4c7c8a9 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Wed, 28 Dec 2022 21:01:25 +0100
Subject: [PATCH 19/35] Don't make sounds when updating an existing
 notification

---
 .../main/java/de/dotwee/micropinner/tools/NotificationTools.java | 1 +
 1 file changed, 1 insertion(+)

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 2b1e2c5..87a8c83 100644
--- a/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
+++ b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
@@ -163,6 +163,7 @@ public static void notify(@NonNull Context context, @NonNull PinSpec pin) {
                         .setContentTitle(pin.getTitle())
                         .setContentText(pin.getContent())
                         .setSmallIcon(R.drawable.ic_notif_star)
+                        .setOnlyAlertOnce(true)
                         .setPriority(pin.getPriority())
                         .setVisibility(pin.getVisibility())
                         .setStyle(new NotificationCompat.BigTextStyle().bigText(pin.getContent()))

From 3fadbca4c6005a2ef1fc8a79f7ee676cb00f3b61 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Thu, 29 Dec 2022 04:39:46 +0100
Subject: [PATCH 20/35] Remove old notification channel

---
 .../de/dotwee/micropinner/tools/NotificationTools.java   | 9 +++++++++
 1 file changed, 9 insertions(+)

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 87a8c83..6070fdf 100644
--- a/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
+++ b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
@@ -36,6 +36,10 @@ public class NotificationTools {
      */
     public final static String EXTRA_INTENT = "IAMAPIN";
 
+    /**
+     * 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";
@@ -117,6 +121,10 @@ private static NotificationChannel getNotificationChannel(int pinPriority) {
 
     @RequiresApi(api = Build.VERSION_CODES.O)
     private static void createOrUpdateNotificationChannels(@NonNull Context context, @NonNull NotificationManager notificationManager) {
+        // 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),
                 NotificationManager.IMPORTANCE_DEFAULT);
@@ -164,6 +172,7 @@ public static void notify(@NonNull Context context, @NonNull PinSpec pin) {
                         .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()))

From 1dcaf5f14d04908d84bb8b579d5ff1c622712654 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Thu, 29 Dec 2022 04:43:22 +0100
Subject: [PATCH 21/35] Don't make sounds when creating notifications

---
 .../de/dotwee/micropinner/tools/NotificationTools.java | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

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 6070fdf..60c596a 100644
--- a/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
+++ b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
@@ -121,23 +121,27 @@ private static NotificationChannel getNotificationChannel(int pinPriority) {
 
     @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.
+        // 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),
-                NotificationManager.IMPORTANCE_DEFAULT);
+                importance);
         notificationManager.createNotificationChannel(public_channel);
 
         NotificationChannel private_channel = new NotificationChannel(CHANNEL_NAME_PRIVATE,
                 context.getResources().getString(R.string.notifications_channel_private),
-                NotificationManager.IMPORTANCE_DEFAULT);
+                importance);
         notificationManager.createNotificationChannel(private_channel);
 
         NotificationChannel secret_channel = new NotificationChannel(CHANNEL_NAME_SECRET,
                 context.getResources().getString(R.string.notifications_channel_secret),
-                NotificationManager.IMPORTANCE_DEFAULT);
+                importance);
         notificationManager.createNotificationChannel(secret_channel);
     }
 

From 2f620e814dbb9276de6943a8159ced0e5f1c668d Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Thu, 29 Dec 2022 16:45:36 +0100
Subject: [PATCH 22/35] Don't show notification count as application icon
 badges in Launcher by default

Also set some other settings for notification channels, note that lockscreen visibility is currently ignored
---
 .../dotwee/micropinner/tools/NotificationTools.java | 13 +++++++++++++
 1 file changed, 13 insertions(+)

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 60c596a..27e83a9 100644
--- a/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
+++ b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
@@ -122,6 +122,7 @@ private static NotificationChannel getNotificationChannel(int pinPriority) {
     @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;
 
@@ -132,16 +133,28 @@ private static void createOrUpdateNotificationChannels(@NonNull Context context,
         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);
     }
 

From b7beec2807d25e07f75a6007bc99e5fbefcd431f Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Thu, 29 Dec 2022 16:46:23 +0100
Subject: [PATCH 23/35] simpler return type of helper function

---
 .../java/de/dotwee/micropinner/tools/NotificationTools.java   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

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 27e83a9..b6f67c2 100644
--- a/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
+++ b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
@@ -171,7 +171,7 @@ private static String getChannelName(@NonNull PinSpec pin) {
         }
     }
 
-    private static Spannable styledText(CharSequence text, StyleSpan style) {
+    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);
@@ -207,7 +207,7 @@ public static void notify(@NonNull Context context, @NonNull PinSpec pin) {
 
             // Show "Contents hidden" placeholder as italic:
             // https://stackoverflow.com/questions/70698860/how-to-bold-title-in-notification
-            Spannable hiddenContent = styledText(
+            CharSequence hiddenContent = styledText(
                     context.getResources().getText(R.string.message_hidden_private_content),
                     new StyleSpan(Typeface.ITALIC)
             );

From c3025709a21ba17ad552f9ce94b60fd45979e475 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Fri, 30 Dec 2022 03:28:49 +0100
Subject: [PATCH 24/35] double check that notification doesn't exist before
 restoring it

---
 .../micropinner/tools/NotificationTools.java  | 38 ++++++++++++++++++-
 1 file changed, 36 insertions(+), 2 deletions(-)

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 b6f67c2..5d54e4b 100644
--- a/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
+++ b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
@@ -10,14 +10,17 @@
 import android.graphics.Typeface;
 import android.os.Build;
 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;
@@ -67,18 +70,49 @@ public static void restoreNotifications(@NonNull Context context) {
         // 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();
 
-            // TODO: on API level 23 and above we could double check that the notification doesn't already exists before restoring it, see:
-            // https://stackoverflow.com/questions/23831214/notificationmanager-get-notification-by-id
+            // 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) {

From c02ba56c88ce3ba12cc3e13bdc7663129579d650 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Fri, 30 Dec 2022 03:29:42 +0100
Subject: [PATCH 25/35] Use a different application id in debug builds

---
 app/build.gradle | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/build.gradle b/app/build.gradle
index 671f7b4..d48a265 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -19,6 +19,7 @@ android {
 
         debug {
             minifyEnabled false
+            applicationIdSuffix '.debug'
         }
 
         release {

From 661cc80eb7c9b4faa7e5cbde98d6283b291af442 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Fri, 30 Dec 2022 05:47:35 +0100
Subject: [PATCH 26/35] Move the namespace property to the right place in the
 build.gradle file

https://developer.android.com/studio/build/configure-app-module#set-namespace
---
 app/build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/build.gradle b/app/build.gradle
index d48a265..be794bc 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -3,10 +3,10 @@ apply plugin: 'com.android.application'
 android {
     compileSdkVersion 33
     buildToolsVersion "30.0.3"
+    namespace 'de.dotwee.micropinner'
 
     defaultConfig {
         applicationId "de.dotwee.micropinner"
-        namespace 'de.dotwee.micropinner'
         minSdkVersion 16
         targetSdkVersion 33
         versionCode 29

From 71dbc6b17429366be13dbeb628d81328fd194fd3 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Fri, 30 Dec 2022 15:11:53 +0100
Subject: [PATCH 27/35] Never make sounds when creating notifications

---
 .../main/java/de/dotwee/micropinner/tools/NotificationTools.java | 1 +
 1 file changed, 1 insertion(+)

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 5d54e4b..97fc2a4 100644
--- a/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
+++ b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
@@ -223,6 +223,7 @@ public static void notify(@NonNull Context context, @NonNull PinSpec pin) {
                         .setContentText(pin.getContent())
                         .setSmallIcon(R.drawable.ic_notif_star)
                         .setOnlyAlertOnce(true)
+                        .setSilent(true)
                         .setCategory(NotificationCompat.CATEGORY_REMINDER)
                         .setPriority(pin.getPriority())
                         .setVisibility(pin.getVisibility())

From f1a3d19a504f0bf61a2db50f8a023f0ab9595f63 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Sat, 31 Dec 2022 00:54:39 +0100
Subject: [PATCH 28/35] Revert "Never make sounds when creating notifications"

This reverts commit 71dbc6b17429366be13dbeb628d81328fd194fd3.
---
 .../main/java/de/dotwee/micropinner/tools/NotificationTools.java | 1 -
 1 file changed, 1 deletion(-)

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 97fc2a4..5d54e4b 100644
--- a/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
+++ b/app/src/main/java/de/dotwee/micropinner/tools/NotificationTools.java
@@ -223,7 +223,6 @@ public static void notify(@NonNull Context context, @NonNull PinSpec pin) {
                         .setContentText(pin.getContent())
                         .setSmallIcon(R.drawable.ic_notif_star)
                         .setOnlyAlertOnce(true)
-                        .setSilent(true)
                         .setCategory(NotificationCompat.CATEGORY_REMINDER)
                         .setPriority(pin.getPriority())
                         .setVisibility(pin.getVisibility())

From bf4a3902073898fb52c682d5d3ac7d45824b32a2 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Tue, 12 Aug 2025 07:03:52 +0200
Subject: [PATCH 29/35] Update android.yml (allow running manually)

---
 .github/workflows/android.yml | 1 +
 1 file changed, 1 insertion(+)

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:

From c4dc48392898c28fc7397a5510a8bd6568e17c55 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Tue, 12 Aug 2025 07:46:21 +0200
Subject: [PATCH 30/35] Create release.yml

---
 .github/workflows/release.yml | 68 +++++++++++++++++++++++++++++++++++
 1 file changed, 68 insertions(+)
 create mode 100644 .github/workflows/release.yml

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..19dfa16
--- /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 > my-release-key.keystore
+
+      - name: Build release APK with Gradle
+        run: ./gradlew assembleRelease
+        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/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/*

From 51d1545cc3f4d01e00c24ee8aa4f29cadb9cd839 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Tue, 12 Aug 2025 07:55:57 +0200
Subject: [PATCH 31/35] chore: signing config in "app/build.gradle"

---
 app/build.gradle | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/app/build.gradle b/app/build.gradle
index be794bc..0e21086 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -15,14 +15,23 @@ android {
         testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
     }
 
-    buildTypes {
+    signingConfigs {
+        release {
+            storeFile file(System.getenv("SIGNING_STORE_FILE") ?: "my-release-key.keystore")
+            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 {
+            signingConfig signingConfigs.release
             minifyEnabled true
             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
         }

From 095de48a20be82b8f9548b1439edc66c63efbea3 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Tue, 12 Aug 2025 08:09:09 +0200
Subject: [PATCH 32/35] chore: make signing config optional for release builds

---
 app/build.gradle | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

diff --git a/app/build.gradle b/app/build.gradle
index 0e21086..d9824bd 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -16,11 +16,15 @@ android {
     }
 
     signingConfigs {
-        release {
-            storeFile file(System.getenv("SIGNING_STORE_FILE") ?: "my-release-key.keystore")
-            storePassword System.getenv("SIGNING_STORE_PASSWORD")
-            keyAlias System.getenv("SIGNING_KEY_ALIAS")
-            keyPassword System.getenv("SIGNING_KEY_PASSWORD")
+        def keystoreFilePath = System.getenv("SIGNING_STORE_FILE") ?: "my-release-key.keystore"
+
+        if (file(keystoreFilePath).exists()) {
+            release {
+                storeFile file(keystoreFilePath)
+                storePassword System.getenv("SIGNING_STORE_PASSWORD")
+                keyAlias System.getenv("SIGNING_KEY_ALIAS")
+                keyPassword System.getenv("SIGNING_KEY_PASSWORD")
+            }
         }
     }
 
@@ -31,7 +35,10 @@ android {
         }
 
         release {
-            signingConfig signingConfigs.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'
         }

From 93f78a3d9ba38aa348a33f168c52a8a4265e9236 Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Tue, 12 Aug 2025 08:29:32 +0200
Subject: [PATCH 33/35] chore: fix release.yml so that the built apk is
 actually released

---
 .github/workflows/release.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 19dfa16..b747e5c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -29,7 +29,7 @@ jobs:
           echo $KEYSTORE_FILE | base64 -d > my-release-key.keystore
 
       - name: Build release APK with Gradle
-        run: ./gradlew assembleRelease
+        run: ./gradlew build
         env:
           SIGNING_KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
           SIGNING_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
@@ -39,7 +39,7 @@ jobs:
         uses: actions/upload-artifact@v4
         with:
           name: release-apk
-          path: app/release/*
+          path: app/build/outputs/apk/release
           retention-days: 3
           if-no-files-found: error
 

From 4dd97301e3e76e01692383ad3a0d675b53f43c7d Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Tue, 12 Aug 2025 08:41:15 +0200
Subject: [PATCH 34/35] chore: better gradle build

---
 .gitignore       | 6 ++++++
 app/build.gradle | 5 ++++-
 2 files changed, 10 insertions(+), 1 deletion(-)

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 d9824bd..dbe47d2 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -18,7 +18,10 @@ android {
     signingConfigs {
         def keystoreFilePath = System.getenv("SIGNING_STORE_FILE") ?: "my-release-key.keystore"
 
-        if (file(keystoreFilePath).exists()) {
+        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")

From f239f508d7cb89d5c37ecca30e425991fd4f2ffe Mon Sep 17 00:00:00 2001
From: Lej77 <31554212+Lej77@users.noreply.github.com>
Date: Tue, 12 Aug 2025 08:49:38 +0200
Subject: [PATCH 35/35] chore: Update release.yml with correct keystore path

---
 .github/workflows/release.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b747e5c..a00d75d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -26,7 +26,7 @@ jobs:
         env:
           KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }}
         run: |
-          echo $KEYSTORE_FILE | base64 -d > my-release-key.keystore
+          echo $KEYSTORE_FILE | base64 -d > app/my-release-key.keystore
 
       - name: Build release APK with Gradle
         run: ./gradlew build