1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 @file:JvmName("VpnStatusObserver")
18 
19 package com.android.systemui.statusbar.tv
20 
21 import android.app.Notification
22 import android.app.NotificationChannel
23 import android.app.NotificationManager
24 import android.content.Context
25 import com.android.internal.messages.nano.SystemMessageProto
26 import com.android.internal.net.VpnConfig
27 import com.android.systemui.CoreStartable
28 import com.android.systemui.R
29 import com.android.systemui.dagger.SysUISingleton
30 import com.android.systemui.statusbar.policy.SecurityController
31 import javax.inject.Inject
32 
33 /**
34  * Observes if a vpn connection is active and displays a notification to the user
35  */
36 @SysUISingleton
37 class VpnStatusObserver @Inject constructor(
38     private val context: Context,
39     private val securityController: SecurityController
40 ) : CoreStartable,
41         SecurityController.SecurityControllerCallback {
42 
43     private var vpnConnected = false
44     private val notificationManager = NotificationManager.from(context)
45     private val notificationChannel = createNotificationChannel()
46     private val vpnConnectedNotificationBuilder = createVpnConnectedNotificationBuilder()
47     private val vpnDisconnectedNotification = createVpnDisconnectedNotification()
48 
49     private val vpnIconId: Int
50         get() = if (securityController.isVpnBranded) {
51             R.drawable.stat_sys_branded_vpn
52         } else {
53             R.drawable.stat_sys_vpn_ic
54         }
55 
56     private val vpnName: String?
57         get() = securityController.primaryVpnName ?: securityController.workProfileVpnName
58 
59     override fun start() {
60         // register callback to vpn state changes
61         securityController.addCallback(this)
62     }
63 
64     override fun onStateChanged() {
65         securityController.isVpnEnabled.let { newVpnConnected ->
66             if (vpnConnected != newVpnConnected) {
67                 if (newVpnConnected) {
68                     notifyVpnConnected()
69                 } else {
70                     notifyVpnDisconnected()
71                 }
72                 vpnConnected = newVpnConnected
73             }
74         }
75     }
76 
77     private fun notifyVpnConnected() = notificationManager.notify(
78             NOTIFICATION_TAG,
79             SystemMessageProto.SystemMessage.NOTE_VPN_STATUS,
80             createVpnConnectedNotification()
81     )
82 
83     private fun notifyVpnDisconnected() = notificationManager.run {
84         // remove existing connected notification
85         cancel(NOTIFICATION_TAG, SystemMessageProto.SystemMessage.NOTE_VPN_STATUS)
86         // show the disconnected notification only for a short while
87         notify(NOTIFICATION_TAG, SystemMessageProto.SystemMessage.NOTE_VPN_DISCONNECTED,
88                 vpnDisconnectedNotification)
89     }
90 
91     private fun createNotificationChannel() =
92             NotificationChannel(
93                     NOTIFICATION_CHANNEL_TV_VPN,
94                     NOTIFICATION_CHANNEL_TV_VPN,
95                     NotificationManager.IMPORTANCE_HIGH
96             ).also {
97                 notificationManager.createNotificationChannel(it)
98             }
99 
100     private fun createVpnConnectedNotification() =
101             vpnConnectedNotificationBuilder
102                     .apply {
103                         vpnName?.let {
104                             setContentText(
105                                     context.getString(
106                                             R.string.notification_disclosure_vpn_text, it
107                                     )
108                             )
109                         }
110                     }
111                     .build()
112 
113     private fun createVpnConnectedNotificationBuilder() =
114             Notification.Builder(context, NOTIFICATION_CHANNEL_TV_VPN)
115                     .setSmallIcon(vpnIconId)
116                     .setVisibility(Notification.VISIBILITY_PUBLIC)
117                     .setCategory(Notification.CATEGORY_SYSTEM)
118                     .extend(Notification.TvExtender())
119                     .setOngoing(true)
120                     .setContentTitle(context.getString(R.string.notification_vpn_connected))
121                     .setContentIntent(VpnConfig.getIntentForStatusPanel(context))
122 
123     private fun createVpnDisconnectedNotification() =
124             Notification.Builder(context, NOTIFICATION_CHANNEL_TV_VPN)
125                     .setSmallIcon(vpnIconId)
126                     .setVisibility(Notification.VISIBILITY_PUBLIC)
127                     .setCategory(Notification.CATEGORY_SYSTEM)
128                     .extend(Notification.TvExtender())
129                     .setTimeoutAfter(VPN_DISCONNECTED_NOTIFICATION_TIMEOUT_MS)
130                     .setContentTitle(context.getString(R.string.notification_vpn_disconnected))
131                     .build()
132 
133     companion object {
134         const val NOTIFICATION_CHANNEL_TV_VPN = "VPN Status"
135         val NOTIFICATION_TAG: String = VpnStatusObserver::class.java.simpleName
136 
137         private const val TAG = "TvVpnNotification"
138         private const val VPN_DISCONNECTED_NOTIFICATION_TIMEOUT_MS = 5_000L
139     }
140 }
141