diff --git a/packages/SystemUI/res/values-television/config.xml b/packages/SystemUI/res/values-television/config.xml
index 015ac90e2521c..cebe1e83ceeda 100644
--- a/packages/SystemUI/res/values-television/config.xml
+++ b/packages/SystemUI/res/values-television/config.xml
@@ -30,6 +30,7 @@
- com.android.systemui.volume.VolumeUI
- com.android.systemui.statusbar.tv.TvStatusBar
- com.android.systemui.statusbar.tv.TvNotificationPanel
+ - com.android.systemui.statusbar.tv.VpnStatusObserver
- com.android.systemui.usb.StorageNotification
- com.android.systemui.power.PowerUI
- com.android.systemui.media.RingtonePlayer
diff --git a/packages/SystemUI/res/values/strings_tv.xml b/packages/SystemUI/res/values/strings_tv.xml
index 5d7f08eef0a10..13271d6f7f2e3 100644
--- a/packages/SystemUI/res/values/strings_tv.xml
+++ b/packages/SystemUI/res/values/strings_tv.xml
@@ -20,4 +20,10 @@
Microphone Active
%1$s accessed your microphone
+
+ VPN is connected
+ VPN is disconnected
+
+ Via %1$s
+
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt b/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt
new file mode 100644
index 0000000000000..7c6f86aceeb0d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("VpnStatusObserver")
+
+package com.android.systemui.statusbar.tv
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import com.android.internal.messages.nano.SystemMessageProto
+import com.android.internal.net.VpnConfig
+import com.android.systemui.Dependency
+import com.android.systemui.R
+import com.android.systemui.SystemUI
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.policy.SecurityController
+import javax.inject.Inject
+
+/**
+ * Observes if a vpn connection is active and displays a notification to the user
+ */
+@SysUISingleton
+class VpnStatusObserver @Inject constructor(context: Context) : SystemUI(context),
+ SecurityController.SecurityControllerCallback {
+
+ private var vpnConnected = false
+ private val securityController: SecurityController =
+ Dependency.get(SecurityController::class.java)
+ private val notificationManager = NotificationManager.from(context)
+ private val notificationChannel = createNotificationChannel()
+ private val vpnConnectedNotificationBuilder = createVpnConnectedNotificationBuilder()
+ private val vpnDisconnectedNotification = createVpnDisconnectedNotification()
+
+ private val vpnIconId: Int
+ get() = if (securityController.isVpnBranded) {
+ R.drawable.stat_sys_branded_vpn
+ } else {
+ R.drawable.stat_sys_vpn_ic
+ }
+
+ private val vpnName: String?
+ get() = securityController.primaryVpnName ?: securityController.workProfileVpnName
+
+ override fun start() {
+ // register callback to vpn state changes
+ securityController.addCallback(this)
+ }
+
+ override fun onStateChanged() {
+ securityController.isVpnEnabled.let { newVpnConnected ->
+ if (vpnConnected != newVpnConnected) {
+ if (newVpnConnected) {
+ notifyVpnConnected()
+ } else {
+ notifyVpnDisconnected()
+ }
+ vpnConnected = newVpnConnected
+ }
+ }
+ }
+
+ private fun notifyVpnConnected() = notificationManager.notify(
+ NOTIFICATION_TAG,
+ SystemMessageProto.SystemMessage.NOTE_VPN_STATUS,
+ createVpnConnectedNotification()
+ )
+
+ private fun notifyVpnDisconnected() = notificationManager.run {
+ // remove existing connected notification
+ cancel(NOTIFICATION_TAG, SystemMessageProto.SystemMessage.NOTE_VPN_STATUS)
+ // show the disconnected notification only for a short while
+ notify(NOTIFICATION_TAG, SystemMessageProto.SystemMessage.NOTE_VPN_DISCONNECTED,
+ vpnDisconnectedNotification)
+ }
+
+ private fun createNotificationChannel() =
+ NotificationChannel(
+ NOTIFICATION_CHANNEL_TV_VPN,
+ NOTIFICATION_CHANNEL_TV_VPN,
+ NotificationManager.IMPORTANCE_HIGH
+ ).also {
+ notificationManager.createNotificationChannel(it)
+ }
+
+ private fun createVpnConnectedNotification() =
+ vpnConnectedNotificationBuilder
+ .apply {
+ vpnName?.let {
+ setContentText(
+ mContext.getString(
+ R.string.notification_disclosure_vpn_text, it
+ )
+ )
+ }
+ }
+ .build()
+
+ private fun createVpnConnectedNotificationBuilder() =
+ Notification.Builder(mContext, NOTIFICATION_CHANNEL_TV_VPN)
+ .setSmallIcon(vpnIconId)
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .setCategory(Notification.CATEGORY_SYSTEM)
+ .extend(Notification.TvExtender())
+ .setOngoing(true)
+ .setContentTitle(mContext.getString(R.string.notification_vpn_connected))
+ .setContentIntent(VpnConfig.getIntentForStatusPanel(mContext))
+
+ private fun createVpnDisconnectedNotification() =
+ Notification.Builder(mContext, NOTIFICATION_CHANNEL_TV_VPN)
+ .setSmallIcon(vpnIconId)
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .setCategory(Notification.CATEGORY_SYSTEM)
+ .extend(Notification.TvExtender())
+ .setTimeoutAfter(VPN_DISCONNECTED_NOTIFICATION_TIMEOUT_MS)
+ .setContentTitle(mContext.getString(R.string.notification_vpn_disconnected))
+ .build()
+
+ companion object {
+ const val NOTIFICATION_CHANNEL_TV_VPN = "VPN Status"
+ val NOTIFICATION_TAG: String = VpnStatusObserver::class.java.simpleName
+
+ private const val TAG = "TvVpnNotification"
+ private const val VPN_DISCONNECTED_NOTIFICATION_TIMEOUT_MS = 5_000L
+ }
+}
\ No newline at end of file