]> gitweb.stoutner.com Git - PrivacyCell.git/blobdiff - app/src/main/java/com/stoutner/privacycell/services/RealtimeMonitoringService.kt
Release 1.10.
[PrivacyCell.git] / app / src / main / java / com / stoutner / privacycell / services / RealtimeMonitoringService.kt
index a48757f987f6cc4fc45b28e3fae6604d91a17c55..72ace814d742aac8de7f08c6e4540ba2205b302e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2021-2022 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2021-2023 Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Cell <https://www.stoutner.com/privacy-cell>.
  *
@@ -35,6 +35,7 @@ import android.content.pm.PackageManager
 import android.os.Binder
 import android.os.IBinder
 import android.telephony.PhoneStateListener  // This can be replaced by `TelephonyCallback` once the minimum API >= 31.
+import android.telephony.ServiceState
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyManager
 
@@ -46,6 +47,7 @@ import androidx.work.WorkManager
 
 import com.stoutner.privacycell.R
 import com.stoutner.privacycell.activities.PrivacyCellActivity
+import com.stoutner.privacycell.helpers.ProtocolHelper
 import com.stoutner.privacycell.workers.RegisterRealtimeListenerWorker
 
 import java.util.concurrent.TimeUnit
@@ -65,7 +67,13 @@ class RealtimeMonitoringService : Service() {
 
     // Define the class variables.
     private var currentStatus = ""
+    private var voiceNetworkSecurityStatus = ProtocolHelper.UNPOPULATED
+    private var dataNetworkSecurityStatus = ProtocolHelper.UNPOPULATED
+
+    // Declare the class variables.
+    private lateinit var notificationManager: NotificationManager
     private lateinit var phoneStateListener: PhoneStateListener  // The `PhoneStateListener` can be replaced by `TelephonyCallback` once the minimum API >= 31.
+    private lateinit var privacyCellPendingIntent: PendingIntent
 
     inner class ServiceBinder : Binder() {
         // Get a copy of this service as a binder.
@@ -81,163 +89,131 @@ class RealtimeMonitoringService : Service() {
         // Get a handle for the shared preferences.
         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
 
-        // Get a handle for the notification manager.
-        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-
-        // Create a notification channel group.
-        notificationManager.createNotificationChannelGroup(NotificationChannelGroup(REALTIME_MONITORING, getString(R.string.realtime_monitoring)))
-
-        // Prepare the notification channels.
-        val secureNetworkChannel = NotificationChannel(SECURE_NETWORK, getString(R.string.secure_network_channel), NotificationManager.IMPORTANCE_HIGH)
-        val insecureNetworkChannel = NotificationChannel(INSECURE_NETWORK, getString(R.string.insecure_network_channel), NotificationManager.IMPORTANCE_HIGH)
-        val antiquatedNetworkChannel = NotificationChannel(ANTIQUATED_NETWORK, getString(R.string.antiquated_network_channel), NotificationManager.IMPORTANCE_HIGH)
-        val unknownNetworkChannel = NotificationChannel(UNKNOWN_NETWORK, getString(R.string.unknown_network_channel), NotificationManager.IMPORTANCE_LOW)
+        // Check to see if realtime monitoring is enabled.  Sometimes the shared preferences can't return a value in time, because Android sucks.
+        // So, the default value is set to true, which is the safest value if the shared preferences can't be queried.
+        if (sharedPreferences.getBoolean(applicationContext.getString(R.string.realtime_monitoring_key), true)) {  // Realtime monitoring is enabled.
+            // Get a handle for the notification manager.
+            notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
 
-        // Set the notification channel group.
-        secureNetworkChannel.group = REALTIME_MONITORING
-        insecureNetworkChannel.group = REALTIME_MONITORING
-        antiquatedNetworkChannel.group = REALTIME_MONITORING
-        unknownNetworkChannel.group = REALTIME_MONITORING
+            // Create a notification channel group.
+            notificationManager.createNotificationChannelGroup(NotificationChannelGroup(REALTIME_MONITORING, getString(R.string.realtime_monitoring)))
 
-        // Disable the notification dots.
-        secureNetworkChannel.setShowBadge(false)
-        insecureNetworkChannel.setShowBadge(false)
-        antiquatedNetworkChannel.setShowBadge(false)
-        unknownNetworkChannel.setShowBadge(false)
+            // Prepare the notification channels.
+            val secureNetworkChannel = NotificationChannel(SECURE_NETWORK, getString(R.string.secure_network_channel), NotificationManager.IMPORTANCE_HIGH)
+            val insecureNetworkChannel = NotificationChannel(INSECURE_NETWORK, getString(R.string.insecure_network_channel), NotificationManager.IMPORTANCE_HIGH)
+            val antiquatedNetworkChannel = NotificationChannel(ANTIQUATED_NETWORK, getString(R.string.antiquated_network_channel), NotificationManager.IMPORTANCE_HIGH)
+            val unknownNetworkChannel = NotificationChannel(UNKNOWN_NETWORK, getString(R.string.unknown_network_channel), NotificationManager.IMPORTANCE_LOW)
 
-        // Set the primary channel notifications to be public.
-        secureNetworkChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
-        insecureNetworkChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
-        antiquatedNetworkChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
+            // Set the notification channel group.
+            secureNetworkChannel.group = REALTIME_MONITORING
+            insecureNetworkChannel.group = REALTIME_MONITORING
+            antiquatedNetworkChannel.group = REALTIME_MONITORING
+            unknownNetworkChannel.group = REALTIME_MONITORING
 
-        // Create the notification channels.
-        notificationManager.createNotificationChannel(secureNetworkChannel)
-        notificationManager.createNotificationChannel(insecureNetworkChannel)
-        notificationManager.createNotificationChannel(antiquatedNetworkChannel)
-        notificationManager.createNotificationChannel(unknownNetworkChannel)
+            // Disable the notification dots.
+            secureNetworkChannel.setShowBadge(false)
+            insecureNetworkChannel.setShowBadge(false)
+            antiquatedNetworkChannel.setShowBadge(false)
+            unknownNetworkChannel.setShowBadge(false)
 
-        // Create a notification builder.
-        val notificationBuilder = Notification.Builder(this, UNKNOWN_NETWORK)
+            // Set the primary channel notifications to be public.
+            secureNetworkChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
+            insecureNetworkChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
+            antiquatedNetworkChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
 
-        // Create an intent to open Privacy Cell.
-        val privacyCellIntent = Intent(this, PrivacyCellActivity::class.java)
+            // Create the notification channels.
+            notificationManager.createNotificationChannel(secureNetworkChannel)
+            notificationManager.createNotificationChannel(insecureNetworkChannel)
+            notificationManager.createNotificationChannel(antiquatedNetworkChannel)
+            notificationManager.createNotificationChannel(unknownNetworkChannel)
 
-        // Create a pending intent from the Privacy Cell intent.
-        val privacyCellPendingIntent = PendingIntent.getActivity(this, 0, privacyCellIntent, PendingIntent.FLAG_IMMUTABLE)
+            // Create a notification builder.
+            val notificationBuilder = Notification.Builder(this, UNKNOWN_NETWORK)
 
-        // Set the notification to open Privacy Cell.
-        notificationBuilder.setContentIntent(privacyCellPendingIntent)
+            // Create an intent to open Privacy Cell.
+            val privacyCellIntent = Intent(this, PrivacyCellActivity::class.java)
 
-        // Set the notification text.
-        notificationBuilder.setContentText(getString(R.string.unknown_network))
+            // Create a pending intent from the Privacy Cell intent.
+            privacyCellPendingIntent = PendingIntent.getActivity(this, 0, privacyCellIntent, PendingIntent.FLAG_IMMUTABLE)
 
-        // Set the notification icon.
-        notificationBuilder.setSmallIcon(R.drawable.antiquated_notification_enabled)
+            // Set the notification to open Privacy Cell.
+            notificationBuilder.setContentIntent(privacyCellPendingIntent)
 
-        // Set the color.
-        notificationBuilder.setColor(getColor(R.color.red_notification_icon))
+            // Set the notification text.
+            notificationBuilder.setContentText(getString(R.string.unknown_network))
 
-        // Start the foreground notification.
-        startForeground(NOTIFICATION_ID, notificationBuilder.build())
+            // Set the notification icon.
+            notificationBuilder.setSmallIcon(R.drawable.antiquated_notification_enabled)
 
-        // Define the phone state listener.  The `PhoneStateListener` can be replaced by `TelephonyCallback` once the minimum API >= 31.
-        phoneStateListener = object : PhoneStateListener() {
-            override fun onDisplayInfoChanged(telephonyDisplayInfo: TelephonyDisplayInfo) {
-                // Get the consider 3G antiquated preference.
-                val consider3gAntiquated = sharedPreferences.getBoolean(getString(R.string.consider_3g_antiquated_key), false)
+            // Set the color.
+            notificationBuilder.setColor(getColor(R.color.red_notification_icon))
 
-                // Populate the notification according to the network type.
-                if ((telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_NR) || (telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_IWLAN) ||
-                    (telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_UNKNOWN)) {  // This is a secure network.
-                    // Only update the notification if the network status has changed.
-                    if (currentStatus != SECURE_NETWORK) {
-                        // Create a secure network notification builder.
-                        val secureNetworkNotificationBuilder = Notification.Builder(applicationContext, SECURE_NETWORK)
+            // Prevent swiping to dismiss the notification.  This no longer works (except on the lock screen) in API >= 34.
+            notificationBuilder.setOngoing(true)
 
-                        // Set the notification to open Privacy Cell.
-                        secureNetworkNotificationBuilder.setContentIntent(privacyCellPendingIntent)
+            // Start the foreground notification.
+            startForeground(NOTIFICATION_ID, notificationBuilder.build())
 
-                        // Set the notification text.
-                        secureNetworkNotificationBuilder.setContentText(getString(R.string.secure_network))
+            // Instantiate the protocol helper.
+            val protocolHelper = ProtocolHelper()
 
-                        // Set the notification icon.
-                        secureNetworkNotificationBuilder.setSmallIcon(R.drawable.secure_notification_enabled)
-
-                        // Set the color.
-                        secureNetworkNotificationBuilder.setColor(getColor(R.color.blue_icon))
-
-                        // Update the notification.
-                        notificationManager.notify(NOTIFICATION_ID, secureNetworkNotificationBuilder.build())
+            // Get a handle for the telephony manager.
+            val telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
 
-                        // Store the new network status.
-                        currentStatus = SECURE_NETWORK
-                    }
-                } else if ((telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_LTE) || (!consider3gAntiquated && (telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_1xRTT ||
-                            (telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_EVDO_0) || (telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_EVDO_A) ||
-                            (telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_EVDO_B) || (telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_EHRPD) ||
-                            (telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_UMTS) || (telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_TD_SCDMA) ||
-                            (telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_HSDPA) || (telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_HSUPA) ||
-                            (telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_HSPA) || (telephonyDisplayInfo.networkType == TelephonyManager.NETWORK_TYPE_HSPAP)))) {
-                            // This is an insecure network.
-                    // Only update the notification if the network status has changed.
-                    if (currentStatus != INSECURE_NETWORK) {
-                        // Create an insecure network notification builder.
-                        val insecureNetworkNotificationBuilder = Notification.Builder(applicationContext, INSECURE_NETWORK)
-
-                        // Set the notification to open Privacy Cell.
-                        insecureNetworkNotificationBuilder.setContentIntent(privacyCellPendingIntent)
-
-                        // Set the notification text.
-                        insecureNetworkNotificationBuilder.setContentText(getString(R.string.insecure_network))
-
-                        // Set the notification icon.
-                        insecureNetworkNotificationBuilder.setSmallIcon(R.drawable.insecure_notification_enabled)
-
-                        // Set the color.
-                        insecureNetworkNotificationBuilder.setColor(getColor(R.color.yellow_notification_icon))
-
-                        // Update the notification.
-                        notificationManager.notify(NOTIFICATION_ID, insecureNetworkNotificationBuilder.build())
-
-                        // Store the new network status.
-                        currentStatus = INSECURE_NETWORK
+            // Define the phone state listener.  The `PhoneStateListener` can be replaced by `TelephonyCallback` once the minimum API >= 31.
+            phoneStateListener = object : PhoneStateListener() {
+                @Deprecated("Deprecated in Java")
+                override fun onServiceStateChanged(serviceState: ServiceState) {  // Update the voice network status.
+                    // Check to see if realtime monitoring is enabled.  Sometimes the system keeps running the service even when it is supposed to shut down.
+                    if (sharedPreferences.getBoolean(applicationContext.getString(R.string.realtime_monitoring_key), true)) {  // Realtime monitoring is enabled.
+                        // Get the network registration info for the voice network, which is the second of the three entries (the first appears to be Wi-Fi and the third appears to be the cell data network).
+                        val networkRegistrationInfo = serviceState.networkRegistrationInfoList[1]
+
+                        // Get the consider 3G antiquated preference.
+                        val consider3gAntiquated = sharedPreferences.getBoolean(getString(R.string.consider_3g_antiquated_key), false)
+
+                        // Update the voice network security status.
+                        voiceNetworkSecurityStatus = protocolHelper.checkNetwork(networkRegistrationInfo.accessNetworkTechnology, consider3gAntiquated)
+
+                        // Populate the notification.
+                        populateNotification()
+                    } else {  // Realtime monitoring is disabled.
+                        // Cancel the current listener if it exists.  The `PhoneStateListener` can be replaced by `TelephonyCallback` once the minimum API >= 31.
+                        telephonyManager.listen(phoneStateListener, LISTEN_NONE)
                     }
-                } else {  // This is an antiquated network.
-                    // Only update the notification if the network status has changed.
-                    if (currentStatus != ANTIQUATED_NETWORK) {
-                        // Create an antiquated network notification builder.
-                        val antiquatedNetworkNotificationBuilder = Notification.Builder(applicationContext, ANTIQUATED_NETWORK)
-
-                        // Set the notification to open Privacy Cell.
-                        antiquatedNetworkNotificationBuilder.setContentIntent(privacyCellPendingIntent)
-
-                        // Set the notification text.
-                        antiquatedNetworkNotificationBuilder.setContentText(getString(R.string.antiquated_network))
-
-                        // Set the notification icon.
-                        antiquatedNetworkNotificationBuilder.setSmallIcon(R.drawable.antiquated_notification_enabled)
-
-                        // Set the color.
-                        antiquatedNetworkNotificationBuilder.setColor(getColor(R.color.red_notification_icon))
-
-                        // Update the notification.
-                        notificationManager.notify(NOTIFICATION_ID, antiquatedNetworkNotificationBuilder.build())
+                }
 
-                        // Store the new network status.
-                        currentStatus = ANTIQUATED_NETWORK
+                @Deprecated("Deprecated in Java")
+                override fun onDisplayInfoChanged(telephonyDisplayInfo: TelephonyDisplayInfo) {  // Update the data network status.
+                    // Check to see if realtime monitoring is enabled.  Sometimes the system keeps running the service even when it is supposed to shut down.
+                    if (sharedPreferences.getBoolean(applicationContext.getString(R.string.realtime_monitoring_key), true)) {  // Realtime monitoring is enabled.
+                        // Get the consider 3G antiquated preference.
+                        val consider3gAntiquated = sharedPreferences.getBoolean(getString(R.string.consider_3g_antiquated_key), false)
+
+                        // Update the data network security status.
+                        dataNetworkSecurityStatus = protocolHelper.checkNetwork(telephonyDisplayInfo.networkType, consider3gAntiquated)
+
+                        // Populate the notification.
+                        populateNotification()
+                    } else {  // Realtime monitoring is disabled.
+                        // Cancel the current listener if it exists.  The `PhoneStateListener` can be replaced by `TelephonyCallback` once the minimum API >= 31.
+                        telephonyManager.listen(phoneStateListener, LISTEN_NONE)
                     }
                 }
             }
-        }
 
-        // Check to see if the read phone state permission has been granted.
-        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
-            // Create a register realtime listener work request that fires every hour.
-            // This periodic request will fire shortly after being created (it fires about every hour near the beginning of the hour) and will reregister the listener if it gets garbage collected.
-            val registerRealtimeListenerWorkRequest = PeriodicWorkRequestBuilder<RegisterRealtimeListenerWorker>(1, TimeUnit.HOURS).build()
+            // Check to see if the read phone state permission has been granted.
+            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
+                // Create a register realtime listener work request that fires every fifteen minutes.
+                // This periodic request will fire shortly after being created and will reregister the listener if it gets garbage collected.
+                val registerRealtimeListenerWorkRequest = PeriodicWorkRequestBuilder<RegisterRealtimeListenerWorker>(15, TimeUnit.MINUTES).build()
 
-            // Register the realtime listener work request.
-            WorkManager.getInstance(this).enqueueUniquePeriodicWork(getString(R.string.register_listener_work_request), ExistingPeriodicWorkPolicy.REPLACE, registerRealtimeListenerWorkRequest)
+                // Register the realtime listener work request.
+                WorkManager.getInstance(this).enqueueUniquePeriodicWork(getString(R.string.register_listener_work_request), ExistingPeriodicWorkPolicy.REPLACE, registerRealtimeListenerWorkRequest)
+            }
+        } else {  // Realtime monitoring is disabled.  This can happen if the restart listener work request fires after realtime monitoring has been disabled.
+            // Cancel the realtime listener work request.
+            WorkManager.getInstance(applicationContext).cancelUniqueWork(applicationContext.getString(R.string.register_listener_work_request))
         }
 
         // Return a sticky service.
@@ -254,7 +230,99 @@ class RealtimeMonitoringService : Service() {
             telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)
 
             // Listen for changes to the phone state.  The `PhoneStateListener` can be replaced by `TelephonyCallback` once the minimum API >= 31.
-            telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_DISPLAY_INFO_CHANGED)
+            telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE or PhoneStateListener.LISTEN_DISPLAY_INFO_CHANGED)
+        }
+    }
+
+    fun populateNotification() {
+        // Get the list of current notifications.
+        val activeNotificationsArray = notificationManager.activeNotifications
+
+        // Check to see if there is a current notification.
+        val noCurrentNotification = activeNotificationsArray.isEmpty()
+
+        // Populate the notification according to the security status.
+        if ((voiceNetworkSecurityStatus == ProtocolHelper.ANTIQUATED) || (dataNetworkSecurityStatus == ProtocolHelper.ANTIQUATED)) {  // This is an antiquated network.
+            // Only update the notification if the network status has changed.
+            if ((currentStatus != ANTIQUATED_NETWORK) || noCurrentNotification) {
+                // Create an antiquated network notification builder.
+                val antiquatedNetworkNotificationBuilder = Notification.Builder(applicationContext, ANTIQUATED_NETWORK)
+
+                // Set the notification to open Privacy Cell.
+                antiquatedNetworkNotificationBuilder.setContentIntent(privacyCellPendingIntent)
+
+                // Set the notification text.
+                antiquatedNetworkNotificationBuilder.setContentText(getString(R.string.antiquated_network))
+
+                // Set the notification icon.
+                antiquatedNetworkNotificationBuilder.setSmallIcon(R.drawable.antiquated_notification_enabled)
+
+                // Set the color.
+                antiquatedNetworkNotificationBuilder.setColor(getColor(R.color.red_notification_icon))
+
+                // Prevent swiping to dismiss the notification.  This no longer works (except on the lock screen) in API >= 34.
+                antiquatedNetworkNotificationBuilder.setOngoing(true)
+
+                // Update the notification.
+                notificationManager.notify(NOTIFICATION_ID, antiquatedNetworkNotificationBuilder.build())
+
+                // Store the new network status.
+                currentStatus = ANTIQUATED_NETWORK
+            }
+        } else if ((voiceNetworkSecurityStatus == ProtocolHelper.INSECURE) || (dataNetworkSecurityStatus == ProtocolHelper.INSECURE)) {  // This is an insecure network.
+            // Only update the notification if the network status has changed.
+            if ((currentStatus != INSECURE_NETWORK) || noCurrentNotification) {
+                // Create an insecure network notification builder.
+                val insecureNetworkNotificationBuilder = Notification.Builder(applicationContext, INSECURE_NETWORK)
+
+                // Set the notification to open Privacy Cell.
+                insecureNetworkNotificationBuilder.setContentIntent(privacyCellPendingIntent)
+
+                // Set the notification text.
+                insecureNetworkNotificationBuilder.setContentText(getString(R.string.insecure_network))
+
+                // Set the notification icon.
+                insecureNetworkNotificationBuilder.setSmallIcon(R.drawable.insecure_notification_enabled)
+
+                // Set the color.
+                insecureNetworkNotificationBuilder.setColor(getColor(R.color.yellow_notification_icon))
+
+                // Prevent swiping to dismiss the notification.  This no longer works (except on the lock screen) in API >= 34.
+                insecureNetworkNotificationBuilder.setOngoing(true)
+
+                // Update the notification.
+                notificationManager.notify(NOTIFICATION_ID, insecureNetworkNotificationBuilder.build())
+
+                // Store the new network status.
+                currentStatus = INSECURE_NETWORK
+            }
+        } else {  // This is a secure network.
+            // Only update the notification if the network status has changed.
+            if ((currentStatus != SECURE_NETWORK) || noCurrentNotification) {
+                // Create a secure network notification builder.
+                val secureNetworkNotificationBuilder = Notification.Builder(applicationContext, SECURE_NETWORK)
+
+                // Set the notification to open Privacy Cell.
+                secureNetworkNotificationBuilder.setContentIntent(privacyCellPendingIntent)
+
+                // Set the notification text.
+                secureNetworkNotificationBuilder.setContentText(getString(R.string.secure_network))
+
+                // Set the notification icon.
+                secureNetworkNotificationBuilder.setSmallIcon(R.drawable.secure_notification_enabled)
+
+                // Set the color.
+                secureNetworkNotificationBuilder.setColor(getColor(R.color.blue_icon))
+
+                // Prevent swiping to dismiss the notification.  This no longer works (except on the lock screen) in API >= 34.
+                secureNetworkNotificationBuilder.setOngoing(true)
+
+                // Update the notification.
+                notificationManager.notify(NOTIFICATION_ID, secureNetworkNotificationBuilder.build())
+
+                // Store the new network status.
+                currentStatus = SECURE_NETWORK
+            }
         }
     }
-}
\ No newline at end of file
+}