Less intrusive VPN dialog and other UX tweaks.

-The ability to launch VPNs is now sticky; once approved by the user,
further approvals are not needed UNLESS the connection is revoked in
Quick Settings.

-The old persistent notification has been removed in favor of the new
Quick Settings UI.

-The name of the VPN app is now pulled from the label of the VPN
service rather than the app itself, if one is set.

Bug: 12878887
Bug: 16578022
Change-Id: I102a14c05db26ee3aef030cda971e5165f078a91
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 0f6b3ad..8466860 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -21,9 +21,7 @@
 import static android.system.OsConstants.AF_INET6;
 
 import android.app.AppGlobals;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
+import android.app.AppOpsManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -35,10 +33,6 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
 import android.content.pm.UserInfo;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.drawable.Drawable;
-import android.net.BaseNetworkStateTracker;
 import android.net.ConnectivityManager;
 import android.net.IConnectivityManager;
 import android.net.INetworkManagementEventObserver;
@@ -51,7 +45,6 @@
 import android.net.NetworkInfo;
 import android.net.NetworkInfo.DetailedState;
 import android.net.NetworkMisc;
-import android.net.NetworkUtils;
 import android.net.RouteInfo;
 import android.net.UidRange;
 import android.os.Binder;
@@ -70,15 +63,15 @@
 import android.security.Credentials;
 import android.security.KeyStore;
 import android.util.Log;
-import android.util.SparseBooleanArray;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.R;
 import com.android.internal.net.LegacyVpnInfo;
 import com.android.internal.net.VpnConfig;
 import com.android.internal.net.VpnProfile;
 import com.android.server.net.BaseNetworkObserver;
 
+import libcore.io.IoUtils;
+
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
@@ -92,8 +85,6 @@
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import libcore.io.IoUtils;
-
 /**
  * @hide
  */
@@ -114,8 +105,6 @@
     private boolean mAllowIPv6;
     private Connection mConnection;
     private LegacyVpnRunner mLegacyVpnRunner;
-    private PendingIntent mStatusIntent;
-    private volatile boolean mEnableNotif = true;
     private volatile boolean mEnableTeardown = true;
     private final IConnectivityManager mConnService;
     private final INetworkManagementService mNetd;
@@ -180,14 +169,6 @@
     }
 
     /**
-     * Set if this object is responsible for showing its own notifications. When
-     * {@code false}, notifications are handled externally by someone else.
-     */
-    public void setEnableNotifications(boolean enableNotif) {
-        mEnableNotif = enableNotif;
-    }
-
-    /**
      * Set if this object is responsible for watching for {@link NetworkInfo}
      * teardown. When {@code false}, teardown is handled externally by someone
      * else.
@@ -228,6 +209,20 @@
     public synchronized boolean prepare(String oldPackage, String newPackage) {
         // Return false if the package does not match.
         if (oldPackage != null && !oldPackage.equals(mPackage)) {
+            // The package doesn't match. If this VPN was not previously authorized, return false
+            // to force user authorization. Otherwise, revoke the VPN anyway.
+            if (!oldPackage.equals(VpnConfig.LEGACY_VPN) && isVpnUserPreConsented(oldPackage)) {
+                long token = Binder.clearCallingIdentity();
+                try {
+                    // This looks bizarre, but it is what ConfirmDialog in VpnDialogs is doing when
+                    // the user clicks through to allow the VPN to consent. So we are emulating the
+                    // action of the dialog without actually showing it.
+                    prepare(null, oldPackage);
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                }
+                return true;
+            }
             return false;
         }
 
@@ -240,11 +235,8 @@
         // Check if the caller is authorized.
         enforceControlPermission();
 
-        // Reset the interface and hide the notification.
+        // Reset the interface.
         if (mInterface != null) {
-            for (UidRange uidRange : mVpnUsers) {
-                hideNotification(uidRange.getStartUser());
-            }
             agentDisconnect();
             jniReset(mInterface);
             mInterface = null;
@@ -287,12 +279,46 @@
             Binder.restoreCallingIdentity(token);
         }
         mConfig = null;
+
         updateState(DetailedState.IDLE, "prepare");
         return true;
     }
 
+    /**
+     * Set whether the current package has the ability to launch VPNs without user intervention.
+     */
+    public void setPackageAuthorization(boolean authorized) {
+        // Check if the caller is authorized.
+        enforceControlPermission();
+
+        if (mPackage == null || VpnConfig.LEGACY_VPN.equals(mPackage)) {
+            return;
+        }
+
+        long token = Binder.clearCallingIdentity();
+        try {
+            AppOpsManager appOps =
+                    (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+            appOps.setMode(AppOpsManager.OP_ACTIVATE_VPN, mOwnerUID, mPackage,
+                    authorized ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED);
+        } catch (Exception e) {
+            Log.wtf(TAG, "Failed to set app ops for package " + mPackage, e);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    private boolean isVpnUserPreConsented(String packageName) {
+        AppOpsManager appOps =
+                (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+
+        // Verify that the caller matches the given package and has permission to activate VPNs.
+        return appOps.noteOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN, Binder.getCallingUid(),
+                packageName) == AppOpsManager.MODE_ALLOWED;
+    }
+
     private int getAppUid(String app) {
-        if (app == VpnConfig.LEGACY_VPN) {
+        if (VpnConfig.LEGACY_VPN.equals(app)) {
             return Process.myUid();
         }
         PackageManager pm = mContext.getPackageManager();
@@ -355,9 +381,10 @@
         try {
             mNetworkAgent = new NetworkAgent(mLooper, mContext, NETWORKTYPE,
                     mNetworkInfo, mNetworkCapabilities, lp, 0, networkMisc) {
+                            @Override
                             public void unwanted() {
                                 // We are user controlled, not driven by NetworkRequest.
-                            };
+                            }
                         };
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -540,39 +567,6 @@
 
         // add the user
         mVpnUsers.add(UidRange.createForUser(user));
-
-        // show the notification
-        if (!mPackage.equals(VpnConfig.LEGACY_VPN)) {
-            // Load everything for the user's notification
-            PackageManager pm = mContext.getPackageManager();
-            ApplicationInfo app = null;
-            final long token = Binder.clearCallingIdentity();
-            try {
-                app = AppGlobals.getPackageManager().getApplicationInfo(mPackage, 0, mUserId);
-            } catch (RemoteException e) {
-                throw new IllegalStateException("Invalid application");
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
-            String label = app.loadLabel(pm).toString();
-            // Load the icon and convert it into a bitmap.
-            Drawable icon = app.loadIcon(pm);
-            Bitmap bitmap = null;
-            if (icon.getIntrinsicWidth() > 0 && icon.getIntrinsicHeight() > 0) {
-                int width = mContext.getResources().getDimensionPixelSize(
-                        android.R.dimen.notification_large_icon_width);
-                int height = mContext.getResources().getDimensionPixelSize(
-                        android.R.dimen.notification_large_icon_height);
-                icon.setBounds(0, 0, width, height);
-                bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
-                Canvas c = new Canvas(bitmap);
-                icon.draw(c);
-                c.setBitmap(null);
-            }
-            showNotification(label, bitmap, user);
-        } else {
-            showNotification(null, null, user);
-        }
     }
 
     private void removeVpnUserLocked(int user) {
@@ -584,7 +578,6 @@
                 mNetworkAgent.removeUidRanges(new UidRange[] { uidRange });
             }
             mVpnUsers.remove(uidRange);
-            hideNotification(user);
     }
 
     private void onUserAdded(int userId) {
@@ -652,9 +645,6 @@
         public void interfaceRemoved(String interfaze) {
             synchronized (Vpn.this) {
                 if (interfaze.equals(mInterface) && jniCheck(interfaze) == 0) {
-                    for (UidRange uidRange : mVpnUsers) {
-                        hideNotification(uidRange.getStartUser());
-                    }
                     mVpnUsers = null;
                     mInterface = null;
                     if (mConnection != null) {
@@ -712,56 +702,6 @@
         }
     }
 
-    private void showNotification(String label, Bitmap icon, int user) {
-        if (!mEnableNotif) return;
-        final long token = Binder.clearCallingIdentity();
-        try {
-            mStatusIntent = VpnConfig.getIntentForStatusPanel(mContext);
-
-            NotificationManager nm = (NotificationManager)
-                    mContext.getSystemService(Context.NOTIFICATION_SERVICE);
-
-            if (nm != null) {
-                String title = (label == null) ? mContext.getString(R.string.vpn_title) :
-                        mContext.getString(R.string.vpn_title_long, label);
-                String text = (mConfig.session == null) ? mContext.getString(R.string.vpn_text) :
-                        mContext.getString(R.string.vpn_text_long, mConfig.session);
-
-                Notification notification = new Notification.Builder(mContext)
-                        .setSmallIcon(R.drawable.vpn_connected)
-                        .setLargeIcon(icon)
-                        .setContentTitle(title)
-                        .setContentText(text)
-                        .setContentIntent(mStatusIntent)
-                        .setDefaults(0)
-                        .setOngoing(true)
-                        .setColor(mContext.getResources().getColor(
-                                com.android.internal.R.color.system_notification_accent_color))
-                        .build();
-                nm.notifyAsUser(null, R.drawable.vpn_connected, notification, new UserHandle(user));
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    private void hideNotification(int user) {
-        if (!mEnableNotif) return;
-        mStatusIntent = null;
-
-        NotificationManager nm = (NotificationManager)
-                mContext.getSystemService(Context.NOTIFICATION_SERVICE);
-
-        if (nm != null) {
-            final long token = Binder.clearCallingIdentity();
-            try {
-                nm.cancelAsUser(null, R.drawable.vpn_connected, new UserHandle(user));
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
-        }
-    }
-
     public synchronized boolean addAddress(String address, int prefixLength) {
         if (Binder.getCallingUid() != mOwnerUID || mInterface == null || mNetworkAgent == null) {
             return false;
@@ -971,9 +911,6 @@
         final LegacyVpnInfo info = new LegacyVpnInfo();
         info.key = mConfig.user;
         info.state = LegacyVpnInfo.stateFromNetworkInfo(mNetworkInfo);
-        if (mNetworkInfo.isConnected()) {
-            info.intent = mStatusIntent;
-        }
         return info;
     }