Skip to content

Commit

Permalink
Add ability for local push notifications on Android (#1000)
Browse files Browse the repository at this point in the history
* Add inbox message notifications

* Improve appearance of notifications
 - Show more text in expanded body
 - Show post name and community
 - Render markdown/html

* Do not show unformatted markdown

* Show notification experimental warning

* added release build configuration

* Remove multiDexEnabled due to SDK >= 21

* Refactor some code

* Make setState more clear

---------

Co-authored-by: Hamlet Jiang Su <hamlet.jiangsu@outlook.com>
  • Loading branch information
micahmo and hjiangsu authored Jan 17, 2024
1 parent 226a382 commit f2b6f1a
Show file tree
Hide file tree
Showing 20 changed files with 546 additions and 31 deletions.
21 changes: 15 additions & 6 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@ if (keystorePropertiesFile.exists()) {

android {
namespace "com.hjiangsu.thunder"
compileSdkVersion flutter.compileSdkVersion
// Use variable for version requested by background_fetch and flutter_local_notifications
compileSdkVersion rootProject.ext.compileSdkVersion
ndkVersion flutter.ndkVersion

compileOptions {
// The following line is required by flutter_local_notifications
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
Expand All @@ -52,7 +55,8 @@ android {
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion
// Use variable for version requested by background_fetch and flutter_local_notifications
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
Expand All @@ -68,9 +72,9 @@ android {

buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
// signingConfig signingConfigs.debug
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
signingConfig signingConfigs.release
}
}
Expand All @@ -80,4 +84,9 @@ flutter {
source '../..'
}

dependencies {}
dependencies {
// The following 3 dependencies are required by flutter_local_notifications
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
implementation 'androidx.window:window:1.0.0'
implementation 'androidx.window:window-java:1.0.0'
}
32 changes: 32 additions & 0 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
##---------------Begin: proguard configuration for Gson ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature

# For using GSON @Expose annotation
-keepattributes *Annotation*

# Gson specific classes
-dontwarn sun.misc.**
#-keep class com.google.gson.stream.** { *; }

# Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model.** { <fields>; }

# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * extends com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer

# Prevent R8 from leaving Data object members always null
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName <fields>;
}

# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken

##---------------End: proguard configuration for Gson ----------
4 changes: 4 additions & 0 deletions android/app/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>

<!-- In Debug mode, request the EXACT_ALARM permission. -->
<!-- This allows us to run background_fetch at lower intervals for the sake of quicker testing. -->
<uses-permission android:minSdkVersion="34" android:name="android.permission.USE_EXACT_ALARM" />
</manifest>
6 changes: 5 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
<!-- The tools namespace is needed by background_fetch -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.baseflow.permissionhandler">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- The tools:replace line is needed by background_fetch -->
<!-- requestLegacyExternalStorage is required for saving media to an album in API 29 -->
<application
tools:replace="android:label"
android:label="Thunder"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon"
Expand Down
2 changes: 2 additions & 0 deletions android/app/src/main/res/raw/keep.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@drawable/*" />
14 changes: 13 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
buildscript {
ext.kotlin_version = '1.7.10'
// Versions recommended/needed by flutter_local_notifications and background_fetch
// These can be upgraded over time.
ext {
compileSdkVersion = 33
targetSdkVersion = 33
appCompatVersion = "1.6.1"
}
repositories {
google()
mavenCentral()
}

dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
// flutter_local_notifications requires gradle version 7.3.1
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
Expand All @@ -15,6 +23,10 @@ allprojects {
repositories {
google()
mavenCentral()
// The following section is required by background_fetch
maven {
url "${project(':background_fetch').projectDir}/libs"
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
PODS:
- background_fetch (1.2.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
Expand All @@ -10,6 +12,8 @@ PODS:
- Flutter
- flutter_keyboard_visibility (0.0.1):
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (0.0.1):
- Flutter
- FMDB (2.7.5):
Expand Down Expand Up @@ -45,12 +49,14 @@ PODS:
- Flutter

DEPENDENCIES:
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_custom_tabs_ios (from `.symlinks/plugins/flutter_custom_tabs_ios/ios`)
- flutter_file_dialog (from `.symlinks/plugins/flutter_file_dialog/ios`)
- flutter_icmp_ping (from `.symlinks/plugins/flutter_icmp_ping/ios`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- gal (from `.symlinks/plugins/gal/darwin`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
Expand All @@ -70,6 +76,8 @@ SPEC REPOS:
- FMDB

EXTERNAL SOURCES:
background_fetch:
:path: ".symlinks/plugins/background_fetch/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
Expand All @@ -82,6 +90,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_icmp_ping/ios"
flutter_keyboard_visibility:
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
gal:
Expand Down Expand Up @@ -110,12 +120,14 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"

SPEC CHECKSUMS:
background_fetch: 896944864b038d2837fc750d470e9841e1e6a363
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_custom_tabs_ios: 62439c843b2691aae516fd50119a01eb9755fff7
flutter_file_dialog: 4c014a45b105709a27391e266c277d7e588e9299
flutter_icmp_ping: 2b159955eee0c487c766ad83fec224ae35e7c935
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
Expand Down
21 changes: 12 additions & 9 deletions lib/core/enums/full_name_separator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,28 @@ enum FullNameSeparator {
const FullNameSeparator(this.label);
}

String generateUserFullName(BuildContext context, name, instance) {
final ThunderState thunderState = context.read<ThunderBloc>().state;
return switch (thunderState.userSeparator) {
String generateUserFullName(BuildContext? context, name, instance, {FullNameSeparator? userSeparator}) {
assert(context != null || userSeparator != null);
userSeparator ??= context!.read<ThunderBloc>().state.userSeparator;
return switch (userSeparator) {
FullNameSeparator.dot => '$name · $instance',
FullNameSeparator.at => '$name@$instance',
};
}

String generateUserFullNameSuffix(BuildContext context, instance) {
final ThunderState thunderState = context.read<ThunderBloc>().state;
return switch (thunderState.userSeparator) {
String generateUserFullNameSuffix(BuildContext? context, instance, {FullNameSeparator? userSeparator}) {
assert(context != null || userSeparator != null);
userSeparator ??= context!.read<ThunderBloc>().state.userSeparator;
return switch (userSeparator) {
FullNameSeparator.dot => ' · $instance',
FullNameSeparator.at => '@$instance',
};
}

String generateCommunityFullName(BuildContext context, name, instance) {
final ThunderState thunderState = context.read<ThunderBloc>().state;
return switch (thunderState.communitySeparator) {
String generateCommunityFullName(BuildContext? context, name, instance, {FullNameSeparator? communitySeparator}) {
assert(context != null || communitySeparator != null);
communitySeparator ??= context!.read<ThunderBloc>().state.communitySeparator;
return switch (communitySeparator) {
FullNameSeparator.dot => '$name · $instance',
FullNameSeparator.at => '$name@$instance',
};
Expand Down
2 changes: 2 additions & 0 deletions lib/core/enums/local_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ enum LocalSettings {
useDisplayNamesForUsers(name: 'setting_use_display_names_for_users', key: 'showUserDisplayNames'),
markPostAsReadOnMediaView(name: 'setting_general_mark_post_read_on_media_view', key: 'markPostAsReadOnMediaView'),
showInAppUpdateNotification(name: 'setting_notifications_show_inapp_update', key: 'showInAppUpdateNotifications'),
enableInboxNotifications(name: 'setting_enable_inbox_notifications', key: 'enableInboxNotifications'),
scoreCounters(name: 'setting_score_counters', key: "showScoreCounters"),
appLanguageCode(name: 'setting_app_language_code', key: 'appLanguage'),

Expand Down Expand Up @@ -159,6 +160,7 @@ extension LocalizationExt on AppLocalizations {
'showUserDisplayNames': showUserDisplayNames,
'markPostAsReadOnMediaView': markPostAsReadOnMediaView,
'showInAppUpdateNotifications': showInAppUpdateNotifications,
'enableInboxNotifications': enableInboxNotifications,
'showScoreCounters': showScoreCounters,
'appLanguage': appLanguage,
'compactView': compactView,
Expand Down
28 changes: 28 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@
"@back": {},
"backButton": "Back button",
"@backButton": {},
"backgroundCheckWarning": "Note that notification checks will consume additional battery",
"@backgroundCheckWarning": {
"description": "Warning for enabling notifications"
},
"backToTop": "Back To Top",
"@backToTop": {},
"base": "Base",
Expand Down Expand Up @@ -357,6 +361,10 @@
"@dimReadPosts": {
"description": "Description of the effect on read posts."
},
"disable": "Disable",
"@disable": {
"description": "Action for disabling something"
},
"dismissRead": "Dismiss Read",
"@dismissRead": {},
"displayUserScore": "Display User Scores (Karma).",
Expand Down Expand Up @@ -393,6 +401,10 @@
"@enableFeedFab": {
"description": "Enable the Floating Action Button for the feed"
},
"enableInboxNotifications": "Enable Inbox Notifications (Experimental)",
"@enableInboxNotifications": {
"description": "Setting name for inbox notifications"
},
"enableFloatingButtonOnFeeds": "Enable Floating Button On Feeds",
"@enableFloatingButtonOnFeeds": {
"description": "Setting for enable floating button on feeds"
Expand Down Expand Up @@ -739,6 +751,14 @@
"@notificationsBehaviourSettings": {
"description": "Subcategory in Setting -> General"
},
"notificationsNotAllowed": "Notifications are not allowed for Thunder in system settings",
"@notificationsNotAllowed": {
"description": "Description for when notifications are now allowed for app"
},
"notificationsWarningDialog": "Notifications are an experimental feature which may not function correctly on all devices.\n\n· Checks will occur every ~15 minutes and will consume additional battery.\n\n· Disable battery optimizations for a higher likelihood of successful notifications.\n\nSee the following page for more information.",
"@notificationsWarningDialog": {
"description": "The content of the warning dialog for the notifications feature"
},
"off": "off",
"@off": {},
"ok": "OK",
Expand Down Expand Up @@ -1265,6 +1285,10 @@
"@unblockInstance": {
"description": "Tooltip for unblocking an instance"
},
"understandEnable": "I Understand, Enable",
"@understandEnable": {
"description": "Action for acknowledging and enabling something"
},
"unexpectedError": "Unexpected Error",
"@unexpectedError": {},
"unsubscribe": "Unsubscribe",
Expand Down Expand Up @@ -1333,6 +1357,10 @@
"@visitInstance": {},
"visitUserProfile": "Visit User Profile",
"@visitUserProfile": {},
"warning": "Warning",
"@warning": {
"description": "Heading for warning dialogs"
},
"xDownvotes": "{x} downvotes",
"@xDownvotes": {},
"xScore": "{x} score",
Expand Down
Loading

0 comments on commit f2b6f1a

Please sign in to comment.