Merge branch 'notificationImprovements' into 'master'

Improvements to notifications

Closes #307 and #306

See merge request pixeldroid/PixelDroid!418
This commit is contained in:
Matthieu 2022-02-06 15:04:34 +00:00
commit b72b78619e
9 changed files with 192 additions and 21 deletions

View File

@ -34,9 +34,6 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
lintOptions{
disable 'MissingTranslation'
}
sourceSets {
main.java.srcDirs += 'src/main/java'
test.java.srcDirs += 'src/test/java'
@ -96,6 +93,9 @@ android {
}
apply plugin: 'kotlin-kapt'
lint {
disable 'MissingTranslation'
}
}
dependencies {
@ -108,15 +108,15 @@ dependencies {
*/
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.4.0'
implementation "androidx.browser:browser:1.4.0"
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.4.0'
implementation 'androidx.paging:paging-runtime-ktx:3.1.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
@ -126,18 +126,18 @@ dependencies {
implementation "androidx.annotation:annotation:1.3.0"
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation "androidx.activity:activity-ktx:1.4.0"
implementation 'androidx.fragment:fragment-ktx:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
// Use the most recent version of CameraX
def cameraX_version = '1.0.2'
implementation "androidx.camera:camera-core:${cameraX_version}"
implementation "androidx.camera:camera-camera2:${cameraX_version}"
def cameraX_version = '1.1.0-beta01'
implementation "androidx.camera:camera-core:$cameraX_version"
implementation "androidx.camera:camera-camera2:$cameraX_version"
// CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:$cameraX_version"
// CameraX View class
implementation 'androidx.camera:camera-view:1.0.0-alpha32'
implementation "androidx.camera:camera-view:$cameraX_version"
def room_version = "2.4.1"
implementation "androidx.room:room-runtime:$room_version"

View File

@ -852,4 +852,70 @@
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/room#2.4.1
url: https://developer.android.com/jetpack/androidx/releases/room#2.4.1
- artifact: androidx.window:window:+
name: window
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/window#1.0.0
- artifact: com.android.support:animated-vector-drawable:+
name: animated-vector-drawable
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:appcompat-v7:+
name: appcompat-v7
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-annotations:+
name: support-annotations
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-compat:+
name: support-compat
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-core-ui:+
name: support-core-ui
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-core-utils:+
name: support-core-utils
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-fragment:+
name: support-fragment
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-media-compat:+
name: support-media-compat
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-v4:+
name: support-v4
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-vector-drawable:+
name: support-vector-drawable
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html

File diff suppressed because one or more lines are too long

View File

@ -9,6 +9,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ViewModelProvider
@ -31,6 +32,7 @@ import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.Notification
import org.pixeldroid.app.utils.api.objects.Status
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import org.pixeldroid.app.utils.notificationsWorker.makeChannelGroupId
/**
@ -64,6 +66,16 @@ class NotificationsFragment : CachedFeedFragment<Notification>() {
}
override fun onResume() {
super.onResume()
with(NotificationManagerCompat.from(requireContext())) {
// Cancel account notification group
db.userDao().getActiveUser()?.let {
cancel( makeChannelGroupId(it).hashCode())
}
}
}
/**
* View Holder for a [Notification] RecyclerView list item.
*/

View File

@ -4,6 +4,7 @@ import android.content.Intent
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import org.pixeldroid.app.MainActivity
@ -83,7 +84,9 @@ class SettingsActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceCha
//Hide Notification setting for Android versions where it doesn't work
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
preferenceScreen.removePreference(preferenceManager.findPreference("notification"))
preferenceManager.findPreference<Preference?>("notification")?.let {
preferenceScreen.removePreference(it)
}
}
}
}

View File

@ -15,6 +15,7 @@ import androidx.work.WorkerParameters
import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R
import org.pixeldroid.app.posts.PostActivity
import org.pixeldroid.app.posts.fromHtml
import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.api.PixelfedAPI.Companion.apiForUser
import org.pixeldroid.app.utils.api.objects.Notification
@ -71,8 +72,7 @@ class NotificationsWorker(
)
while (!newNotifications.isNullOrEmpty()
&& newNotifications.map { it.created_at ?: Instant.MIN }
.maxOrNull()!! > previouslyLatestNotification?.created_at ?: Instant.MIN
&& newNotifications.maxOf { it.created_at ?: Instant.MIN } > previouslyLatestNotification?.created_at ?: Instant.MIN
) {
// Add to db
val filteredNewNotifications: List<Notification> = newNotifications.filter {
@ -83,6 +83,12 @@ class NotificationsWorker(
db.notificationDao().insertAll(filteredNewNotifications)
//If multiple notifications, show summary of them
if(filteredNewNotifications.size > 1){
showNotificationSummary(filteredNewNotifications, uniqueUserId)
}
// Launch new notifications
filteredNewNotifications.forEach {
showNotification(it, user, uniqueUserId)
@ -106,6 +112,39 @@ class NotificationsWorker(
return Result.success()
}
private fun showNotificationSummary(notifications: List<Notification>, uniqueUserId: String) {
val content = joinNames(
applicationContext,
notifications.mapNotNull { it.account?.getDisplayName() }
)
val title: String = applicationContext.resources.getQuantityString(
R.plurals.notification_title_summary,
notifications.size,
notifications.size
)
val groupBuilder = NotificationCompat.Builder(applicationContext, makeChannelId(uniqueUserId, null))
.setContentTitle(title)
.setContentText(content)
.setGroupSummary(true)
.setAutoCancel(true)
.setGroup(uniqueUserId)
.setSmallIcon(R.drawable.notification_icon)
.setStyle(NotificationCompat.BigTextStyle().bigText(content))
.setContentIntent(
PendingIntent.getActivity(applicationContext, 0, Intent(applicationContext, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(SHOW_NOTIFICATION_TAG, true)
}, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
)
with(NotificationManagerCompat.from(applicationContext)) {
notify(uniqueUserId.hashCode(), groupBuilder.build())
}
}
private fun showNotification(
notification: Notification,
user: UserDatabaseEntity,
@ -166,7 +205,7 @@ class NotificationsWorker(
.setAutoCancel(true)
if (notification.type == mention || notification.type == comment || notification.type == poll){
builder.setContentText(notification.status?.content)
builder.setContentText(notification.status?.content?.let { fromHtml(it) })
}
builder.setGroup(uniqueUserId)
@ -237,7 +276,8 @@ fun makeNotificationChannels(context: Context, handle: String, channelGroupId: S
/**
* [channelGroupId] is the id used to uniquely identify the group: for us it is a unique id
* identifying a user consisting of the concatenation of the instance uri and user id.
* identifying a user consisting of the concatenation of the instance uri and user id
* (see [makeChannelGroupId]).
*/
private fun makeChannelId(channelGroupId: String, type: Notification.NotificationType?): String =
(channelGroupId + (type ?: NotificationsWorker.otherNotificationType)).hashCode().toString()
@ -264,4 +304,27 @@ fun removeNotificationChannelsFromAccount(context: Context, user: UserDatabaseEn
}
}
}
}
/**
* BidiFormatter.unicodeWrap is insufficient in some cases (see Tusky#1921)
* So we force isolation manually
* https://unicode.org/reports/tr9/#Explicit_Directional_Isolates
*/
fun CharSequence.unicodeWrap(): String = "\u2068${this}\u2069"
private fun joinNames(context: Context, notifications: List<String>): String {
return when {
notifications.size > 3 -> {
context.getString(R.string.notification_summary_large).format(
*notifications.subList(0, 3).map { it.unicodeWrap() }.toTypedArray(),
notifications.size - 3
)
}
else -> context.getString( when(notifications.size) {
2 -> R.string.notification_summary_small
else /* ==3 */-> R.string.notification_summary_medium
}).format(*notifications.map { it.unicodeWrap() }.toTypedArray())
}
}

View File

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:scaleX="4"
android:scaleY="4"
android:translateX="-53"
android:translateY="-53">
<path
android:pathData="M23.0332,30.2725L27.5781,30.2725C31.8595,30.2725 35.3302,26.9088 35.3302,22.7596C35.3302,18.6103 31.8595,15.2467 27.5781,15.2467L21.0185,15.2467C18.5485,15.2467 16.5461,17.1872 16.5461,19.581L16.5461,36.451L23.0332,30.2725Z"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</group>
</vector>

View File

@ -64,6 +64,15 @@
<string name="poll_notification_channel">"Polls"</string>
<string name="other_notification_channel">"Other"</string>
<plurals name="notification_title_summary" >
<item quantity="one">"%d new notification"</item>
<item quantity="other">"%d new notifications"</item>
</plurals>
<string name="notification_summary_large">%1$s, %2$s, %3$s and %4$d others</string>
<string name="notification_summary_medium">%1$s, %2$s, and %3$s</string>
<string name="notification_summary_small">%1$s and %2$s</string>
<!-- Login page -->

View File

@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.4'
classpath 'com.android.tools.build:gradle:7.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong