Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3382392f authored by Dorin Drimus's avatar Dorin Drimus
Browse files

Notify VPN status from SystemUI as notifications

A `VPN is connected.` ongoing notification is sent when connected to a
VPN and a `VPN is disconnected.` short lived notification is send when
disconnected from a VPN.

Test: Manual tests. Connecting and disconnecting to VPNs (from a VPN
client app) show the (undismissable, ongoing) connected and
(dismissable, short lived) disconnected notifications. A CTS test will
be added in a separate CL.
Bug: 157500504

Change-Id: I321dec45127104b5139b9e868bac76b9d882e9f2
parent a6bccfc4
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@
        <item>com.android.systemui.volume.VolumeUI</item>
        <item>com.android.systemui.statusbar.tv.TvStatusBar</item>
        <item>com.android.systemui.statusbar.tv.TvNotificationPanel</item>
        <item>com.android.systemui.statusbar.tv.VpnStatusObserver</item>
        <item>com.android.systemui.usb.StorageNotification</item>
        <item>com.android.systemui.power.PowerUI</item>
        <item>com.android.systemui.media.RingtonePlayer</item>
+6 −0
Original line number Diff line number Diff line
@@ -20,4 +20,10 @@
    <!-- Title and subtitle for AudioRecordingIndicator -->
    <string name="mic_active">Microphone Active</string>
    <string name="app_accessed_mic">%1$s accessed your microphone</string>

    <string name="notification_vpn_connected">VPN is connected</string>
    <string name="notification_vpn_disconnected">VPN is disconnected</string>
    <!-- Disclosure text in the connected notification that indicates that the device is connected to a VPN. The placeholder is the VPN name. [CHAR LIMIT=40] -->
    <string name="notification_disclosure_vpn_text">Via <xliff:g id="vpn_app" example="Foo VPN App">%1$s</xliff:g></string>

</resources>
+140 −0
Original line number Diff line number Diff line
/*
 * 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