Skip to content
GitLab
Menu
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
The Tor Project
Applications
android-components
Commits
19d55909
Commit
19d55909
authored
Oct 16, 2019
by
Roger Yang
Browse files
Closes #2295, #4549: Implement Web Notification Feature
parent
43be615a
Changes
16
Hide whitespace changes
Inline
Side-by-side
components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.kt
View file @
19d55909
...
...
@@ -10,6 +10,7 @@ import mozilla.components.browser.engine.gecko.integration.LocaleSettingUpdater
import
mozilla.components.browser.engine.gecko.mediaquery.from
import
mozilla.components.browser.engine.gecko.mediaquery.toGeckoValue
import
mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension
import
mozilla.components.browser.engine.gecko.webnotifications.GeckoWebNotificationDelegate
import
mozilla.components.concept.engine.Engine
import
mozilla.components.concept.engine.EngineSession
import
mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
...
...
@@ -25,6 +26,7 @@ import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import
mozilla.components.concept.engine.utils.EngineVersion
import
mozilla.components.concept.engine.webextension.WebExtension
import
mozilla.components.concept.engine.webextension.WebExtensionDelegate
import
mozilla.components.concept.engine.webnotifications.WebNotificationDelegate
import
org.json.JSONObject
import
org.mozilla.geckoview.ContentBlocking
import
org.mozilla.geckoview.ContentBlockingController
...
...
@@ -173,6 +175,15 @@ class GeckoEngine(
runtime
.
webExtensionController
.
tabDelegate
=
tabsDelegate
}
/**
* See [Engine.registerWebNotificationDelegate].
*/
override
fun
registerWebNotificationDelegate
(
webNotificationDelegate
:
WebNotificationDelegate
)
{
runtime
.
webNotificationDelegate
=
GeckoWebNotificationDelegate
(
webNotificationDelegate
)
}
/**
* See [Engine.clearData].
*/
...
...
components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/webnotifications/GeckoWebNotificationDelegate.kt
0 → 100644
View file @
19d55909
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package
mozilla.components.browser.engine.gecko.webnotifications
import
mozilla.components.concept.engine.webnotifications.WebNotification
import
mozilla.components.concept.engine.webnotifications.WebNotificationDelegate
import
org.mozilla.geckoview.WebNotification
as
GeckoViewWebNotification
import
org.mozilla.geckoview.WebNotificationDelegate
as
GeckoViewWebNotificationDelegate
internal
class
GeckoWebNotificationDelegate
(
private
val
webNotificationDelegate
:
WebNotificationDelegate
)
:
GeckoViewWebNotificationDelegate
{
override
fun
onShowNotification
(
webNotification
:
GeckoViewWebNotification
)
{
webNotificationDelegate
.
onShowNotification
(
webNotification
.
toWebNotification
())
}
override
fun
onCloseNotification
(
webNotification
:
GeckoViewWebNotification
)
{
webNotificationDelegate
.
onCloseNotification
(
webNotification
.
toWebNotification
())
}
private
fun
GeckoViewWebNotification
.
toWebNotification
():
WebNotification
{
return
WebNotification
(
title
,
tag
,
text
,
imageUrl
,
textDirection
,
lang
,
requireInteraction
)
}
}
components/browser/engine-gecko-nightly/src/test/java/mozilla/components/browser/engine/gecko/webnotifications/GeckoWebNotificationDelegateTest.kt
0 → 100644
View file @
19d55909
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package
mozilla.components.browser.engine.gecko.webnotifications
import
androidx.test.ext.junit.runners.AndroidJUnit4
import
mozilla.components.concept.engine.webnotifications.WebNotificationDelegate
import
mozilla.components.support.test.any
import
mozilla.components.support.test.mock
import
org.junit.Assert.assertEquals
import
org.junit.Test
import
org.junit.runner.RunWith
import
org.mockito.Mockito.doNothing
import
java.lang.IllegalStateException
import
org.mozilla.geckoview.WebNotification
as
GeckoViewWebNotification
@RunWith
(
AndroidJUnit4
::
class
)
class
GeckoWebNotificationDelegateTest
{
@Test
fun
`register
background
message
handler`
()
{
val
webNotificationDelegate
:
WebNotificationDelegate
=
mock
()
val
geckoViewWebNotification
:
GeckoViewWebNotification
=
mock
()
val
geckoWebNotificationDelegate
=
GeckoWebNotificationDelegate
(
webNotificationDelegate
)
var
message
:
String
?
=
null
doNothing
().
`when`
(
webNotificationDelegate
).
onShowNotification
(
any
())
try
{
geckoWebNotificationDelegate
.
onShowNotification
(
geckoViewWebNotification
)
}
catch
(
e
:
IllegalStateException
)
{
message
=
e
.
localizedMessage
}
assertEquals
(
message
,
"tag must not be null"
)
message
=
null
doNothing
().
`when`
(
webNotificationDelegate
).
onCloseNotification
(
any
())
try
{
geckoWebNotificationDelegate
.
onCloseNotification
(
geckoViewWebNotification
)
}
catch
(
e
:
IllegalStateException
)
{
message
=
e
.
localizedMessage
}
assertEquals
(
message
,
"tag must not be null"
)
}
}
\ No newline at end of file
components/concept/engine/src/main/java/mozilla/components/concept/engine/Engine.kt
View file @
19d55909
...
...
@@ -12,12 +12,14 @@ import mozilla.components.concept.engine.content.blocking.TrackerLog
import
mozilla.components.concept.engine.utils.EngineVersion
import
mozilla.components.concept.engine.webextension.WebExtension
import
mozilla.components.concept.engine.webextension.WebExtensionDelegate
import
mozilla.components.concept.engine.webnotifications.WebNotificationDelegate
import
org.json.JSONObject
import
java.lang.UnsupportedOperationException
/**
* Entry point for interacting with the engine implementation.
*/
@Suppress
(
"TooManyFunctions"
)
interface
Engine
{
/**
...
...
@@ -140,6 +142,16 @@ interface Engine {
webExtensionDelegate
:
WebExtensionDelegate
):
Unit
=
throw
UnsupportedOperationException
(
"Web extension support is not available in this engine"
)
/**
* Registers a [WebNotificationDelegate] to be notified of engine events
* related to web notifications
*
* @param webNotificationDelegate callback to be invoked for web notification events.
*/
fun
registerWebNotificationDelegate
(
webNotificationDelegate
:
WebNotificationDelegate
):
Unit
=
throw
UnsupportedOperationException
(
"Web notification support is not available in this engine"
)
/**
* Clears browsing data stored by the engine.
*
...
...
components/concept/engine/src/main/java/mozilla/components/concept/engine/webnotifications/WebNotification.kt
View file @
19d55909
...
...
@@ -7,31 +7,23 @@ package mozilla.components.concept.engine.webnotifications
/**
* A notification sent by the Web Notifications API.
*
* @property origin The website that fired this notification.
* @property title Title of the notification to be displayed in the first row.
* @property body Body of the notification to be displayed in the second row.
* @property tag Tag used to identify the notification.
* @property iconUrl Medium image to display in the notification.
* @property body Body of the notification to be displayed in the second row.
* @property iconUrl Large icon url to display in the notification.
* Corresponds to [android.app.Notification.Builder.setLargeIcon].
* @property vibrate Vibration pattern felt when the notification is displayed.
* @property direction Preference for text direction.
* @property lang language of the notification.
* @property requireInteraction Preference flag that indicates the notification should remain.
* @property timestamp Time when the notification was created.
* @property requireInteraction Preference flag that indicates the notification should remain
* active until the user clicks or dismisses it.
* @property silent Preference flag that indicates no sounds or vibrations should be made.
* @property onClick Callback called with the selected action, or null if the main body of the
* notification was clicked.
* @property onClose Callback called when the notification is dismissed.
*/
data class
WebNotification
(
val
origin
:
String
,
val
title
:
String
?
=
null
,
val
body
:
String
?
=
null
,
val
tag
:
String
?
=
null
,
val
iconUrl
:
String
?
=
null
,
val
vibrate
:
LongArray
=
longArrayOf
(),
val
timestamp
:
Long
?
=
null
,
val
requireInteraction
:
Boolean
=
false
,
val
silent
:
Boolean
=
false
,
val
onClick
:
()
->
Unit
,
val
onClose
:
()
->
Unit
val
title
:
String
?,
val
tag
:
String
,
val
body
:
String
?,
val
iconUrl
:
String
?,
val
direction
:
String
?,
val
lang
:
String
?,
val
requireInteraction
:
Boolean
,
val
timestamp
:
Long
=
System
.
currentTimeMillis
()
)
components/concept/engine/src/main/java/mozilla/components/concept/engine/webnotifications/WebNotificationDelegate.kt
0 → 100644
View file @
19d55909
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package
mozilla.components.concept.engine.webnotifications
/**
* Notifies applications or other components of engine events related to web
* notifications e.g. an notification is to be shown or is to be closed
*/
interface
WebNotificationDelegate
{
/**
* Invoked when a web notification is to be shown.
*
* @param webNotification The web notification intended to be shown.
*/
fun
onShowNotification
(
webNotification
:
WebNotification
)
=
Unit
/**
* Invoked when a web notification is to be closed.
*
* @param webNotification The web notification intended to be closed.
*/
fun
onCloseNotification
(
webNotification
:
WebNotification
)
=
Unit
}
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadNotification.kt
View file @
19d55909
...
...
@@ -87,12 +87,7 @@ internal object DownloadNotification {
val
channel
=
notificationManager
.
getNotificationChannel
(
channelId
)
if
(
channel
.
importance
==
IMPORTANCE_NONE
)
return
false
if
(
SDK_INT
>=
Build
.
VERSION_CODES
.
P
)
{
val
group
=
notificationManager
.
getNotificationChannelGroup
(
channel
.
group
)
group
?.
isBlocked
!=
true
}
else
{
true
}
true
}
else
{
NotificationManagerCompat
.
from
(
context
).
areNotificationsEnabled
()
}
...
...
components/feature/webnotifications/src/main/java/mozilla/components/feature/webnotifications/NativeNotificationBridge.kt
View file @
19d55909
...
...
@@ -4,12 +4,16 @@
package
mozilla.components.feature.webnotifications
import
android.app.Activity
import
android.app.Notification
import
android.app.PendingIntent
import
android.content.Context
import
android.content.Intent
import
android.graphics.Bitmap
import
android.net.Uri
import
android.os.Build
import
android.os.Build.VERSION.SDK_INT
import
androidx.annotation.DrawableRes
import
androidx.core.net.toUri
import
mozilla.components.browser.icons.BrowserIcons
import
mozilla.components.browser.icons.Icon.Source
...
...
@@ -18,16 +22,23 @@ import mozilla.components.browser.icons.IconRequest.Size
import
mozilla.components.concept.engine.webnotifications.WebNotification
internal
class
NativeNotificationBridge
(
private
val
icons
:
BrowserIcons
private
val
icons
:
BrowserIcons
,
@DrawableRes
private
val
smallIcon
:
Int
)
{
companion
object
{
private
const
val
EXTRA_ON_CLICK
=
"mozac.feature.webnotifications.generic.onclick"
}
/**
* Create a system [Notification] from this [WebNotification].
*/
@Suppress
(
"LongParameterList"
)
suspend
fun
convertToAndroidNotification
(
notification
:
WebNotification
,
context
:
Context
,
channelId
:
String
channelId
:
String
,
activityClass
:
Class
<
out
Activity
>?,
requestId
:
Int
):
Notification
{
val
builder
=
if
(
SDK_INT
>=
Build
.
VERSION_CODES
.
O
)
{
Notification
.
Builder
(
context
,
channelId
)
...
...
@@ -37,22 +48,25 @@ internal class NativeNotificationBridge(
}
with
(
notification
)
{
loadIcon
(
iconUrl
?.
toUri
(),
origin
,
Size
.
DEFAULT
)
?.
let
{
icon
->
builder
.
setLargeIcon
(
icon
)
}
builder
.
setContentTitle
(
title
).
setContentText
(
body
)
activityClass
?.
let
{
val
intent
=
Intent
(
context
,
activityClass
).
apply
{
putExtra
(
EXTRA_ON_CLICK
,
tag
)
}
@Suppress
(
"Deprecation"
)
builder
.
setVibrate
(
vibrate
)
timestamp
?.
let
{
builder
.
setShowWhen
(
true
).
setWhen
(
it
)
PendingIntent
.
getActivity
(
context
,
requestId
,
intent
,
0
).
apply
{
builder
.
setContentIntent
(
this
)
}
}
if
(
silent
)
{
@Suppress
(
"Deprecation"
)
builder
.
setDefaults
(
0
)
builder
.
setSmallIcon
(
smallIcon
)
.
setContentTitle
(
title
)
.
setContentText
(
body
)
.
setShowWhen
(
true
)
.
setWhen
(
timestamp
)
.
setAutoCancel
(
true
)
loadIcon
(
iconUrl
?.
toUri
(),
Size
.
DEFAULT
)
?.
let
{
iconBitmap
->
builder
.
setLargeIcon
(
iconBitmap
)
}
}
...
...
@@ -62,10 +76,10 @@ internal class NativeNotificationBridge(
/**
* Load an icon for a notification.
*/
private
suspend
fun
loadIcon
(
url
:
Uri
?,
origin
:
String
,
size
:
Size
):
Bitmap
?
{
private
suspend
fun
loadIcon
(
url
:
Uri
?,
size
:
Size
):
Bitmap
?
{
url
?:
return
null
val
icon
=
icons
.
loadIcon
(
IconRequest
(
url
=
origin
,
url
=
url
.
toString
()
,
size
=
size
,
resources
=
listOf
(
IconRequest
.
Resource
(
url
=
url
.
toString
(),
...
...
components/feature/webnotifications/src/main/java/mozilla/components/feature/webnotifications/WebNotificationFeature.kt
0 → 100644
View file @
19d55909
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package
mozilla.components.feature.webnotifications
import
android.app.Activity
import
android.app.NotificationChannel
import
android.app.NotificationManager
import
android.content.Context
import
android.os.Build
import
androidx.annotation.DrawableRes
import
androidx.core.app.NotificationCompat
import
androidx.core.content.getSystemService
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.GlobalScope
import
kotlinx.coroutines.launch
import
mozilla.components.browser.icons.BrowserIcons
import
mozilla.components.concept.engine.Engine
import
mozilla.components.concept.engine.webnotifications.WebNotification
import
mozilla.components.concept.engine.webnotifications.WebNotificationDelegate
import
mozilla.components.support.base.log.logger.Logger
import
java.lang.UnsupportedOperationException
private
const
val
NOTIFICATION_CHANNEL_ID
=
"mozac.feature.webnotifications.generic.channel"
/**
* Feature implementation for configuring and displaying web notifications to the user.
*
* Initialize this feature globally once on app start
* ```Kotlin
* WebNotificationFeature(
* applicationContext, engine, icons, R.mipmap.ic_launcher, BrowserActivity::class.java
* )
* ```
*
* @param context The application Context.
* @param engine The browser engine.
* @param browserIcons The entry point for loading the large icon for the notification.
* @param smallIcon The small icon for the notification.
* @param activityClass The Activity that the notification will launch if user taps on it
*/
class
WebNotificationFeature
(
private
val
context
:
Context
,
private
val
engine
:
Engine
,
private
val
browserIcons
:
BrowserIcons
,
@DrawableRes
private
val
smallIcon
:
Int
,
private
val
activityClass
:
Class
<
out
Activity
>?
)
:
WebNotificationDelegate
{
private
val
logger
=
Logger
(
"WebNotificationFeature"
)
private
var
pendingRequestId
=
0
private
var
notificationId
=
0
private
val
notificationIdMap
=
HashMap
<
String
,
Int
>()
private
val
notificationManager
=
context
.
getSystemService
<
NotificationManager
>()
private
val
nativeNotificationBridge
=
NativeNotificationBridge
(
browserIcons
,
smallIcon
)
init
{
try
{
engine
.
registerWebNotificationDelegate
(
this
)
}
catch
(
e
:
UnsupportedOperationException
)
{
logger
.
error
(
"failed to register for web notification delegate"
,
e
)
}
}
override
fun
onShowNotification
(
webNotification
:
WebNotification
)
{
ensureNotificationGroupAndChannelExists
()
notificationIdMap
[
webNotification
.
tag
]
?.
let
{
notificationManager
?.
cancel
(
it
)
}
pendingRequestId
++
notificationId
++
notificationIdMap
[
webNotification
.
tag
]
=
notificationId
GlobalScope
.
launch
(
Dispatchers
.
IO
)
{
val
notification
=
nativeNotificationBridge
.
convertToAndroidNotification
(
webNotification
,
context
,
NOTIFICATION_CHANNEL_ID
,
activityClass
,
pendingRequestId
)
notificationManager
?.
notify
(
notificationId
,
notification
)
}
}
override
fun
onCloseNotification
(
webNotification
:
WebNotification
)
{
notificationIdMap
[
webNotification
.
tag
]
?.
let
{
notificationManager
?.
cancel
(
it
)
}
}
private
fun
ensureNotificationGroupAndChannelExists
()
{
if
(
Build
.
VERSION
.
SDK_INT
>=
Build
.
VERSION_CODES
.
O
)
{
val
channel
=
NotificationChannel
(
NOTIFICATION_CHANNEL_ID
,
context
.
getString
(
R
.
string
.
mozac_feature_notification_channel_name
),
NotificationManager
.
IMPORTANCE_LOW
)
channel
.
setShowBadge
(
true
)
channel
.
lockscreenVisibility
=
NotificationCompat
.
VISIBILITY_PRIVATE
notificationManager
?.
createNotificationChannel
(
channel
)
}
}
}
components/feature/webnotifications/src/main/res/values/strings.xml
0 → 100644
View file @
19d55909
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this file,
- You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources>
<!-- Default Web Notification Channel Name. -->
<string
name=
"mozac_feature_notification_channel_name"
>
Site notifications
</string>
</resources>
components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/NativeNotificationBridgeTest.kt
View file @
19d55909
...
...
@@ -15,8 +15,8 @@ import mozilla.components.concept.engine.webnotifications.WebNotification
import
mozilla.components.support.test.any
import
mozilla.components.support.test.mock
import
mozilla.components.support.test.robolectric.testContext
import
org.junit.Assert.assertArrayEquals
import
org.junit.Assert.assertEquals
import
org.junit.Assert.assertNotNull
import
org.junit.Assert.assertNull
import
org.junit.Before
import
org.junit.Test
...
...
@@ -24,15 +24,16 @@ import org.junit.runner.RunWith
import
org.mockito.Mockito.doReturn
import
org.mockito.Mockito.verify
private
const
val
TEST_TITLE
=
"test title"
private
const
val
TEST_TAG
=
"test tag"
private
const
val
TEST_TEXT
=
"test text"
private
const
val
TEST_CHANNEL
=
"testChannel"
@RunWith
(
AndroidJUnit4
::
class
)
@ExperimentalCoroutinesApi
class
NativeNotificationBridgeTest
{
private
val
blankNotificaiton
=
WebNotification
(
origin
=
"https://example.com"
,
onClick
=
{},
onClose
=
{}
)
private
val
blankNotification
=
WebNotification
(
TEST_TITLE
,
TEST_TAG
,
TEST_TEXT
,
null
,
null
,
null
,
true
,
0
)
private
lateinit
var
icons
:
BrowserIcons
private
lateinit
var
bridge
:
NativeNotificationBridge
...
...
@@ -40,7 +41,7 @@ class NativeNotificationBridgeTest {
@Before
fun
setup
()
{
icons
=
mock
()
bridge
=
NativeNotificationBridge
(
icons
)
bridge
=
NativeNotificationBridge
(
icons
,
android
.
R
.
drawable
.
ic_dialog_alert
)
val
mockIcon
=
Icon
(
mock
(),
source
=
Icon
.
Source
.
GENERATOR
)
doReturn
(
CompletableDeferred
(
mockIcon
)).
`when`
(
icons
).
loadIcon
(
any
())
...
...
@@ -49,38 +50,28 @@ class NativeNotificationBridgeTest {
@Test
fun
`create
blank
notification`
()
=
runBlockingTest
{
val
notification
=
bridge
.
convertToAndroidNotification
(
blankNotifica
i
ton
,
blankNotificat
i
on
,
testContext
,
"channel"
TEST_CHANNEL
,
null
,
0
)
assertNull
(
notification
.
actions
)
@Suppress
(
"Deprecation"
)
assertArrayEquals
(
longArrayOf
(),
notification
.
vibrate
)
assertEquals
(
TEST_CHANNEL
,
notification
.
channelId
)
assertEquals
(
0
,
notification
.
`when`
)
assert
Equals
(
"channel"
,
notification
.
channelId
)
assert
NotNull
(
notification
.
smallIcon
)
assertNull
(
notification
.
getLargeIcon
())
assertNull
(
notification
.
smallIcon
)
}
@Test
fun
`set
vibration
pattern`
()
=
runBlockingTest
{
val
notification
=
bridge
.
convertToAndroidNotification
(
blankNotificaiton
.
copy
(
vibrate
=
longArrayOf
(
1
,
2
,
3
)),
testContext
,
"channel"
)
@Suppress
(
"Deprecation"
)
assertArrayEquals
(
longArrayOf
(
1
,
2
,
3
),
notification
.
vibrate
)
}
@Test
fun
`set
when
`
()
=
runBlockingTest
{
val
notification
=
bridge
.
convertToAndroidNotification
(
blankNotifica
i
ton
.
copy
(
timestamp
=
1234567890
),
blankNotificat
i
on
.
copy
(
timestamp
=
1234567890
),
testContext
,
"channel"
TEST_CHANNEL
,
null
,
0
)
assertEquals
(
1234567890
,
notification
.
`when`
)
...
...
@@ -89,14 +80,16 @@ class NativeNotificationBridgeTest {
@Test
fun
`icon
is
loaded
from
BrowserIcons`
()
=
runBlockingTest
{
bridge
.
convertToAndroidNotification
(
blankNotifica
i
ton
.
copy
(
iconUrl
=
"https://example.com/large.png"
),
blankNotificat
i
on
.
copy
(
iconUrl
=
"https://example.com/large.png"
),
testContext
,
"channel"
TEST_CHANNEL
,
null
,
0
)
verify
(
icons
).
loadIcon
(
IconRequest
(
url
=
"https://example.com"
,
url
=
"https://example.com
/large.png
"
,
size
=
IconRequest
.
Size
.
DEFAULT
,
resources
=
listOf
(
IconRequest
.
Resource
(
url
=
"https://example.com/large.png"
,
...
...
components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/WebNotificationFeatureTest.kt
0 → 100644
View file @
19d55909
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package
mozilla.components.feature.webnotifications