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/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 0d2ad08..66928ca 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -16,25 +16,25 @@
 
 package android.app;
 
-import android.Manifest;
+import android.annotation.SystemApi;
+import android.app.usage.UsageStatsManager;
+import android.content.Context;
 import android.media.AudioAttributes.AttributeUsage;
 import android.os.Binder;
 import android.os.IBinder;
-import android.os.UserManager;
-import android.util.ArrayMap;
-
-import com.android.internal.app.IAppOpsService;
-import com.android.internal.app.IAppOpsCallback;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
-import android.content.Context;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.UserManager;
+import android.util.ArrayMap;
+
+import com.android.internal.app.IAppOpsCallback;
+import com.android.internal.app.IAppOpsService;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
 
 /**
  * API for interacting with "application operation" tracking.
@@ -203,8 +203,10 @@
     public static final int OP_TOAST_WINDOW = 45;
     /** @hide Capture the device's display contents and/or audio */
     public static final int OP_PROJECT_MEDIA = 46;
+    /** @hide Activate a VPN connection without user intervention. */
+    public static final int OP_ACTIVATE_VPN = 47;
     /** @hide */
-    public static final int _NUM_OP = 47;
+    public static final int _NUM_OP = 48;
 
     /** Access to coarse location information. */
     public static final String OPSTR_COARSE_LOCATION =
@@ -218,6 +220,9 @@
     /** Continually monitoring location data with a relatively high power request. */
     public static final String OPSTR_MONITOR_HIGH_POWER_LOCATION
             = "android:monitor_location_high_power";
+    /** Activate a VPN connection without user intervention. @hide */
+    @SystemApi
+    public static final String OPSTR_ACTIVATE_VPN = "android:activate_vpn";
 
     /**
      * This maps each operation to the operation that serves as the
@@ -275,6 +280,7 @@
             OP_MUTE_MICROPHONE,
             OP_TOAST_WINDOW,
             OP_PROJECT_MEDIA,
+            OP_ACTIVATE_VPN,
     };
 
     /**
@@ -329,6 +335,7 @@
             null,
             null,
             null,
+            OPSTR_ACTIVATE_VPN,
     };
 
     /**
@@ -383,6 +390,7 @@
             "MUTE_MICROPHONE",
             "TOAST_WINDOW",
             "PROJECT_MEDIA",
+            "ACTIVATE_VPN",
     };
 
     /**
@@ -437,6 +445,7 @@
             null, // no permission for muting/unmuting microphone
             null, // no permission for displaying toasts
             null, // no permission for projecting media
+            null, // no permission for activating vpn
     };
 
     /**
@@ -492,6 +501,7 @@
             UserManager.DISALLOW_UNMUTE_MICROPHONE, // MUTE_MICROPHONE
             UserManager.DISALLOW_CREATE_WINDOWS, // TOAST_WINDOW
             null, //PROJECT_MEDIA
+            UserManager.DISALLOW_CONFIG_VPN, // ACTIVATE_VPN
     };
 
     /**
@@ -546,6 +556,7 @@
             false, //MUTE_MICROPHONE
             true, //TOAST_WINDOW
             false, //PROJECT_MEDIA
+            false, //ACTIVATE_VPN
     };
 
     /**
@@ -599,6 +610,7 @@
             AppOpsManager.MODE_ALLOWED,
             AppOpsManager.MODE_ALLOWED,
             AppOpsManager.MODE_IGNORED, // OP_PROJECT_MEDIA
+            AppOpsManager.MODE_IGNORED, // OP_ACTIVATE_VPN
     };
 
     /**
@@ -656,6 +668,7 @@
             false,
             false,
             false,
+            false,
     };
 
     private static HashMap<String, Integer> sOpStrToOp = new HashMap<String, Integer>();
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index 0814e0f..b2fc3be 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -108,6 +108,8 @@
 
     boolean prepareVpn(String oldPackage, String newPackage);
 
+    void setVpnPackageAuthorization(boolean authorized);
+
     ParcelFileDescriptor establishVpn(in VpnConfig config);
 
     VpnConfig getVpnConfig();
diff --git a/core/java/com/android/internal/net/LegacyVpnInfo.java b/core/java/com/android/internal/net/LegacyVpnInfo.java
index d6f6d0b..f812ad6 100644
--- a/core/java/com/android/internal/net/LegacyVpnInfo.java
+++ b/core/java/com/android/internal/net/LegacyVpnInfo.java
@@ -40,7 +40,6 @@
 
     public String key;
     public int state = -1;
-    public PendingIntent intent;
 
     @Override
     public int describeContents() {
@@ -51,7 +50,6 @@
     public void writeToParcel(Parcel out, int flags) {
         out.writeString(key);
         out.writeInt(state);
-        out.writeParcelable(intent, flags);
     }
 
     public static final Parcelable.Creator<LegacyVpnInfo> CREATOR =
@@ -61,7 +59,6 @@
             LegacyVpnInfo info = new LegacyVpnInfo();
             info.key = in.readString();
             info.state = in.readInt();
-            info.intent = in.readParcelable(null);
             return info;
         }
 
diff --git a/core/java/com/android/internal/net/VpnConfig.java b/core/java/com/android/internal/net/VpnConfig.java
index 0099269..aa66d7d 100644
--- a/core/java/com/android/internal/net/VpnConfig.java
+++ b/core/java/com/android/internal/net/VpnConfig.java
@@ -20,17 +20,19 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
+import android.net.LinkAddress;
+import android.net.RouteInfo;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.os.UserHandle;
-import android.net.RouteInfo;
-import android.net.LinkAddress;
 
 import java.net.Inet4Address;
 import java.net.InetAddress;
-import java.util.List;
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * A simple container used to carry information in VpnBuilder, VpnDialogs,
@@ -55,12 +57,19 @@
         return intent;
     }
 
-    public static PendingIntent getIntentForStatusPanel(Context context) {
-        Intent intent = new Intent();
-        intent.setClassName(DIALOGS_PACKAGE, DIALOGS_PACKAGE + ".ManageDialog");
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_HISTORY |
-                Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
-        return PendingIntent.getActivityAsUser(context, 0, intent, 0, null, UserHandle.CURRENT);
+    public static CharSequence getVpnLabel(Context context, String packageName)
+            throws NameNotFoundException {
+        PackageManager pm = context.getPackageManager();
+        Intent intent = new Intent(SERVICE_INTERFACE);
+        intent.setPackage(packageName);
+        List<ResolveInfo> services = pm.queryIntentServices(intent, 0 /* flags */);
+        if (services != null && services.size() == 1) {
+            // This app contains exactly one VPN service. Call loadLabel, which will attempt to
+            // load the service's label, and fall back to the app label if none is present.
+            return services.get(0).loadLabel(pm);
+        } else {
+            return pm.getApplicationInfo(packageName, 0).loadLabel(pm);
+        }
     }
 
     public String user;
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index ed3fa58..8f05f7b 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -834,8 +834,8 @@
     <!-- Monitoring dialog title for normal devices  [CHAR LIMIT=35]-->
     <string name="monitoring_title">Network monitoring</string>
 
-    <!-- Monitoring dialog open app button [CHAR LIMIT=30] -->
-    <string name="open_app">Open app</string>
+    <!-- Monitoring dialog disable vpn button [CHAR LIMIT=30] -->
+    <string name="disable_vpn">Disable VPN</string>
 
     <!-- Monitoring dialog disconnect vpn button [CHAR LIMIT=30] -->
     <string name="disconnect_vpn">Disconnect VPN</string>
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
index bfbc56c..a8199fa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java
@@ -119,11 +119,7 @@
     @Override
     public void onClick(DialogInterface dialog, int which) {
         if (which == DialogInterface.BUTTON_NEGATIVE) {
-            if (mSecurityController.isLegacyVpn()) {
-                mSecurityController.disconnectFromLegacyVpn();
-            } else {
-                mSecurityController.openVpnApp();
-            }
+            mSecurityController.disconnectFromVpn();
         }
     }
 
@@ -142,7 +138,7 @@
         if (mSecurityController.isLegacyVpn()) {
             return mContext.getString(R.string.disconnect_vpn);
         } else {
-            return mContext.getString(R.string.open_app);
+            return mContext.getString(R.string.disable_vpn);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
index ede8129..3a5a53b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityController.java
@@ -23,8 +23,7 @@
     String getVpnApp();
     boolean isLegacyVpn();
     String getLegacyVpnName();
-    void openVpnApp();
-    void disconnectFromLegacyVpn();
+    void disconnectFromVpn();
 
     void addCallback(VpnCallback callback);
     void removeCallback(VpnCallback callback);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
index 8e04e5e..ae0291b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -18,7 +18,6 @@
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
@@ -109,18 +108,17 @@
     }
 
     @Override
-    public void openVpnApp() {
-        Intent i = mContext.getPackageManager().getLaunchIntentForPackage(mVpnConfig.user);
-        if (i != null) {
-            i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            mContext.startActivity(i);
-        }
-    }
-
-    @Override
-    public void disconnectFromLegacyVpn() {
+    public void disconnectFromVpn() {
         try {
-            mConnectivityService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN);
+            if (isLegacyVpn()) {
+                mConnectivityService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN);
+            } else {
+                // Prevent this app from initiating VPN connections in the future without user
+                // intervention.
+                mConnectivityService.setVpnPackageAuthorization(false);
+
+                mConnectivityService.prepareVpn(mVpnConfig.user, VpnConfig.LEGACY_VPN);
+            }
         } catch (Exception e) {
             Log.e(TAG, "Unable to disconnect from VPN", e);
         }
@@ -154,9 +152,7 @@
             mIsVpnEnabled = mVpnConfig != null;
 
             if (mVpnConfig != null && !mVpnConfig.legacy) {
-                ApplicationInfo info =
-                        mContext.getPackageManager().getApplicationInfo(mVpnConfig.user, 0);
-                mVpnName = mContext.getPackageManager().getApplicationLabel(info).toString();
+                mVpnName = VpnConfig.getVpnLabel(mContext, mVpnConfig.user).toString();
             }
         } catch (RemoteException | NameNotFoundException e) {
             Log.w(TAG, "Unable to get current VPN", e);
diff --git a/packages/VpnDialogs/AndroidManifest.xml b/packages/VpnDialogs/AndroidManifest.xml
index 03d920a..1768400 100644
--- a/packages/VpnDialogs/AndroidManifest.xml
+++ b/packages/VpnDialogs/AndroidManifest.xml
@@ -28,14 +28,5 @@
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
-
-        <activity android:name=".ManageDialog"
-                android:theme="@*android:style/Theme.DeviceDefault.Light.Dialog.Alert"
-                android:noHistory="true">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.DEFAULT"/>
-            </intent-filter>
-        </activity>
     </application>
 </manifest>
diff --git a/packages/VpnDialogs/res/drawable-hdpi/ic_vpn_dialog.png b/packages/VpnDialogs/res/drawable-hdpi/ic_vpn_dialog.png
new file mode 100644
index 0000000..a0b4b61
--- /dev/null
+++ b/packages/VpnDialogs/res/drawable-hdpi/ic_vpn_dialog.png
Binary files differ
diff --git a/packages/VpnDialogs/res/drawable-mdpi/ic_vpn_dialog.png b/packages/VpnDialogs/res/drawable-mdpi/ic_vpn_dialog.png
new file mode 100644
index 0000000..df5dfe8
--- /dev/null
+++ b/packages/VpnDialogs/res/drawable-mdpi/ic_vpn_dialog.png
Binary files differ
diff --git a/packages/VpnDialogs/res/drawable-xhdpi/ic_vpn_dialog.png b/packages/VpnDialogs/res/drawable-xhdpi/ic_vpn_dialog.png
new file mode 100644
index 0000000..18d5a3a
--- /dev/null
+++ b/packages/VpnDialogs/res/drawable-xhdpi/ic_vpn_dialog.png
Binary files differ
diff --git a/packages/VpnDialogs/res/drawable-xxhdpi/ic_vpn_dialog.png b/packages/VpnDialogs/res/drawable-xxhdpi/ic_vpn_dialog.png
new file mode 100644
index 0000000..4d475dc
--- /dev/null
+++ b/packages/VpnDialogs/res/drawable-xxhdpi/ic_vpn_dialog.png
Binary files differ
diff --git a/packages/VpnDialogs/res/drawable-xxxhdpi/ic_vpn_dialog.png b/packages/VpnDialogs/res/drawable-xxxhdpi/ic_vpn_dialog.png
new file mode 100644
index 0000000..9d458b4
--- /dev/null
+++ b/packages/VpnDialogs/res/drawable-xxxhdpi/ic_vpn_dialog.png
Binary files differ
diff --git a/packages/VpnDialogs/res/layout/confirm.xml b/packages/VpnDialogs/res/layout/confirm.xml
index ee7f4b8..66fec59 100644
--- a/packages/VpnDialogs/res/layout/confirm.xml
+++ b/packages/VpnDialogs/res/layout/confirm.xml
@@ -18,41 +18,12 @@
 <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
         android:layout_width="match_parent"
         android:layout_height="wrap_content">
-    <LinearLayout android:layout_width="match_parent"
+    <TextView android:id="@+id/warning"
+            android:layout_width="fill_parent"
             android:layout_height="wrap_content"
-            android:orientation="vertical"
-            android:padding="3mm">
-
-        <LinearLayout android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:gravity="center_vertical">
-
-            <ImageView android:id="@+id/icon"
-                    android:layout_width="@android:dimen/app_icon_size"
-                    android:layout_height="@android:dimen/app_icon_size"
-                    android:paddingRight="1mm"/>
-
-            <TextView android:id="@+id/prompt"
-                    android:layout_width="fill_parent"
-                    android:layout_height="wrap_content"
-                    android:textSize="18sp"/>
-        </LinearLayout>
-
-        <TextView android:id="@+id/warning"
-                android:layout_width="fill_parent"
-                android:layout_height="wrap_content"
-                android:paddingTop="1mm"
-                android:paddingBottom="1mm"
-                android:text="@string/warning"
-                android:textSize="18sp"/>
-
-        <CheckBox android:id="@+id/check"
-                android:layout_width="fill_parent"
-                android:layout_height="wrap_content"
-                android:text="@string/accept"
-                android:textSize="20sp"
-                android:filterTouchesWhenObscured="true"
-                android:checked="false"/>
-    </LinearLayout>
+            android:textSize="18sp"
+            android:paddingTop="4mm"
+            android:paddingLeft="3mm"
+            android:paddingRight="3mm"
+            android:paddingBottom="4mm"/>
 </ScrollView>
diff --git a/packages/VpnDialogs/res/layout/manage.xml b/packages/VpnDialogs/res/layout/manage.xml
deleted file mode 100644
index 56332c3..0000000
--- a/packages/VpnDialogs/res/layout/manage.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2011 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.
--->
-
-<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:padding="3mm"
-        android:stretchColumns="0,1"
-        android:shrinkColumns="1">
-
-    <TableRow>
-        <TextView android:text="@string/session" style="@style/label"/>
-        <TextView android:id="@+id/session" style="@style/value"/>
-    </TableRow>
-
-    <TableRow>
-        <TextView android:text="@string/duration" style="@style/label"/>
-        <TextView android:id="@+id/duration" style="@style/value"/>
-    </TableRow>
-
-    <TableRow android:id="@+id/data_transmitted_row" android:visibility="gone">
-        <TextView android:text="@string/data_transmitted" style="@style/label"/>
-        <TextView android:id="@+id/data_transmitted" style="@style/value"/>
-    </TableRow>
-
-    <TableRow android:id="@+id/data_received_row" android:visibility="gone">
-        <TextView android:text="@string/data_received" style="@style/label"/>
-        <TextView android:id="@+id/data_received" style="@style/value"/>
-    </TableRow>
-
-</TableLayout>
diff --git a/packages/VpnDialogs/res/values/strings.xml b/packages/VpnDialogs/res/values/strings.xml
index 3ff767a..84206a1 100644
--- a/packages/VpnDialogs/res/values/strings.xml
+++ b/packages/VpnDialogs/res/values/strings.xml
@@ -17,40 +17,15 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
 
     <!-- Dialog title to identify the request from a VPN application. [CHAR LIMIT=60] -->
-    <string name="prompt"><xliff:g id="app">%s</xliff:g>
-        attempts to create a VPN connection.
-    </string>
+    <string name="prompt">Connection request</string>
 
     <!-- Dialog message to warn about the risk of using a VPN application. [CHAR LIMIT=NONE] -->
-    <string name="warning">By proceeding, you are giving the application
-        permission to intercept all network traffic.
-        <b>Do NOT accept unless you trust the application.</b> Otherwise,
-        you run the risk of having your data compromised by a malicious
-        software.
-    </string>
-
-    <!-- Checkbox label to accept the request from a VPN application. [CHAR LIMIT=60] -->
-    <string name="accept">I trust this application.</string>
-
-    <!-- Dialog title for built-in VPN. [CHAR LIMIT=40]  -->
-    <string name="legacy_title">VPN is connected</string>
-    <!-- Button label to configure the current VPN session. [CHAR LIMIT=20] -->
-    <string name="configure">Configure</string>
-    <!-- Button label to disconnect the current VPN session. [CHAR LIMIT=20] -->
-    <string name="disconnect">Disconnect</string>
-
-    <!-- Label for the name of the current VPN session. [CHAR LIMIT=20] -->
-    <string name="session">Session:</string>
-    <!-- Label for the duration of the current VPN session. [CHAR LIMIT=20] -->
-    <string name="duration">Duration:</string>
-    <!-- Label for the network usage of data transmitted over VPN. [CHAR LIMIT=20] -->
-    <string name="data_transmitted">Sent:</string>
-    <!-- Label for the network usage of data received over VPN. [CHAR LIMIT=20] -->
-    <string name="data_received">Received:</string>
-
-    <!-- Formatted string for the network usage over VPN. [CHAR LIMIT=40] -->
-    <string name="data_value_format">
-        <xliff:g id="number">%1$s</xliff:g> bytes /
-        <xliff:g id="number">%2$s</xliff:g> packets
+    <string name="warning"><xliff:g id="app">%s</xliff:g> wants to set up a VPN connection
+        that allows it to monitor network traffic. Only accept if you trust the source.
+        <![CDATA[
+        <br />
+        <br />
+        <img src="vpn_icon" />
+        ]]> appears at the top of your screen when VPN is active.
     </string>
 </resources>
diff --git a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
index ddafc66..897c96cf 100644
--- a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
+++ b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
@@ -18,21 +18,28 @@
 
 import android.content.Context;
 import android.content.DialogInterface;
+import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
 import android.net.IConnectivityManager;
+import android.net.VpnService;
 import android.os.ServiceManager;
+import android.text.Html;
+import android.text.Html.ImageGetter;
 import android.util.Log;
 import android.view.View;
 import android.widget.Button;
-import android.widget.CompoundButton;
-import android.widget.ImageView;
 import android.widget.TextView;
 
 import com.android.internal.app.AlertActivity;
+import com.android.internal.net.VpnConfig;
 
-public class ConfirmDialog extends AlertActivity implements
-        CompoundButton.OnCheckedChangeListener, DialogInterface.OnClickListener {
+import java.util.List;
+
+public class ConfirmDialog extends AlertActivity
+        implements DialogInterface.OnClickListener, ImageGetter {
     private static final String TAG = "VpnConfirm";
 
     private String mPackage;
@@ -56,27 +63,22 @@
                 return;
             }
 
-            PackageManager pm = getPackageManager();
-            ApplicationInfo app = pm.getApplicationInfo(mPackage, 0);
-
             View view = View.inflate(this, R.layout.confirm, null);
-            ((ImageView) view.findViewById(R.id.icon)).setImageDrawable(app.loadIcon(pm));
-            ((TextView) view.findViewById(R.id.prompt)).setText(
-                    getString(R.string.prompt, app.loadLabel(pm)));
-            ((CompoundButton) view.findViewById(R.id.check)).setOnCheckedChangeListener(this);
 
-            mAlertParams.mIconAttrId = android.R.attr.alertDialogIcon;
-            mAlertParams.mTitle = getText(android.R.string.dialog_alert_title);
+            ((TextView) view.findViewById(R.id.warning)).setText(
+                    Html.fromHtml(
+                            getString(R.string.warning, VpnConfig.getVpnLabel(this, mPackage)),
+                    this, null /* tagHandler */));
+
+            mAlertParams.mTitle = getText(R.string.prompt);
             mAlertParams.mPositiveButtonText = getText(android.R.string.ok);
             mAlertParams.mPositiveButtonListener = this;
             mAlertParams.mNegativeButtonText = getText(android.R.string.cancel);
-            mAlertParams.mNegativeButtonListener = this;
             mAlertParams.mView = view;
             setupAlert();
 
             getWindow().setCloseOnTouchOutside(false);
             mButton = mAlert.getButton(DialogInterface.BUTTON_POSITIVE);
-            mButton.setEnabled(false);
             mButton.setFilterTouchesWhenObscured(true);
         } catch (Exception e) {
             Log.e(TAG, "onResume", e);
@@ -85,18 +87,24 @@
     }
 
     @Override
-    public void onBackPressed() {
+    public Drawable getDrawable(String source) {
+        // Should only reach this when fetching the VPN icon for the warning string.
+        Drawable icon = getDrawable(R.drawable.ic_vpn_dialog);
+        icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
+        return icon;
     }
 
     @Override
-    public void onCheckedChanged(CompoundButton button, boolean checked) {
-        mButton.setEnabled(checked);
+    public void onBackPressed() {
     }
 
     @Override
     public void onClick(DialogInterface dialog, int which) {
         try {
-            if (which == DialogInterface.BUTTON_POSITIVE && mService.prepareVpn(null, mPackage)) {
+            if (mService.prepareVpn(null, mPackage)) {
+                // Authorize this app to initiate VPN connections in the future without user
+                // intervention.
+                mService.setVpnPackageAuthorization(true);
                 setResult(RESULT_OK);
             }
         } catch (Exception e) {
diff --git a/packages/VpnDialogs/src/com/android/vpndialogs/ManageDialog.java b/packages/VpnDialogs/src/com/android/vpndialogs/ManageDialog.java
deleted file mode 100644
index eb20995..0000000
--- a/packages/VpnDialogs/src/com/android/vpndialogs/ManageDialog.java
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * Copyright (C) 2011 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.
- */
-
-package com.android.vpndialogs;
-
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.net.IConnectivityManager;
-import android.os.Handler;
-import android.os.Message;
-import android.os.ServiceManager;
-import android.os.SystemClock;
-import android.util.Log;
-import android.view.View;
-import android.widget.TextView;
-
-import com.android.internal.app.AlertActivity;
-import com.android.internal.net.VpnConfig;
-
-import java.io.DataInputStream;
-import java.io.FileInputStream;
-
-public class ManageDialog extends AlertActivity implements
-        DialogInterface.OnClickListener, Handler.Callback {
-    private static final String TAG = "VpnManage";
-
-    private VpnConfig mConfig;
-
-    private IConnectivityManager mService;
-
-    private TextView mDuration;
-    private TextView mDataTransmitted;
-    private TextView mDataReceived;
-    private boolean mDataRowsHidden;
-
-    private Handler mHandler;
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-
-        if (getCallingPackage() != null) {
-            Log.e(TAG, getCallingPackage() + " cannot start this activity");
-            finish();
-            return;
-        }
-
-        try {
-
-            mService = IConnectivityManager.Stub.asInterface(
-                    ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
-
-            mConfig = mService.getVpnConfig();
-
-            // mConfig can be null if we are a restricted user, in that case don't show this dialog
-            if (mConfig == null) {
-                finish();
-                return;
-            }
-
-            View view = View.inflate(this, R.layout.manage, null);
-            if (mConfig.session != null) {
-                ((TextView) view.findViewById(R.id.session)).setText(mConfig.session);
-            }
-            mDuration = (TextView) view.findViewById(R.id.duration);
-            mDataTransmitted = (TextView) view.findViewById(R.id.data_transmitted);
-            mDataReceived = (TextView) view.findViewById(R.id.data_received);
-            mDataRowsHidden = true;
-
-            if (mConfig.legacy) {
-                mAlertParams.mIconId = android.R.drawable.ic_dialog_info;
-                mAlertParams.mTitle = getText(R.string.legacy_title);
-            } else {
-                PackageManager pm = getPackageManager();
-                ApplicationInfo app = pm.getApplicationInfo(mConfig.user, 0);
-                mAlertParams.mIcon = app.loadIcon(pm);
-                mAlertParams.mTitle = app.loadLabel(pm);
-            }
-            if (mConfig.configureIntent != null) {
-                mAlertParams.mPositiveButtonText = getText(R.string.configure);
-                mAlertParams.mPositiveButtonListener = this;
-            }
-            mAlertParams.mNeutralButtonText = getText(R.string.disconnect);
-            mAlertParams.mNeutralButtonListener = this;
-            mAlertParams.mNegativeButtonText = getText(android.R.string.cancel);
-            mAlertParams.mNegativeButtonListener = this;
-            mAlertParams.mView = view;
-            setupAlert();
-
-            if (mHandler == null) {
-                mHandler = new Handler(this);
-            }
-            mHandler.sendEmptyMessage(0);
-        } catch (Exception e) {
-            Log.e(TAG, "onResume", e);
-            finish();
-        }
-    }
-
-    @Override
-    protected void onPause() {
-        super.onPause();
-        if (!isFinishing()) {
-            finish();
-        }
-    }
-
-    @Override
-    public void onClick(DialogInterface dialog, int which) {
-        try {
-            if (which == DialogInterface.BUTTON_POSITIVE) {
-                mConfig.configureIntent.send();
-            } else if (which == DialogInterface.BUTTON_NEUTRAL) {
-                if (mConfig.legacy) {
-                    mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN);
-                } else {
-                    mService.prepareVpn(mConfig.user, VpnConfig.LEGACY_VPN);
-                }
-            }
-        } catch (Exception e) {
-            Log.e(TAG, "onClick", e);
-            finish();
-        }
-    }
-
-    @Override
-    public boolean handleMessage(Message message) {
-        mHandler.removeMessages(0);
-
-        if (!isFinishing()) {
-            if (mConfig.startTime != -1) {
-                long seconds = (SystemClock.elapsedRealtime() - mConfig.startTime) / 1000;
-                mDuration.setText(String.format("%02d:%02d:%02d",
-                        seconds / 3600, seconds / 60 % 60, seconds % 60));
-            }
-
-            String[] numbers = getNumbers();
-            if (numbers != null) {
-                // First unhide the related data rows.
-                if (mDataRowsHidden) {
-                    findViewById(R.id.data_transmitted_row).setVisibility(View.VISIBLE);
-                    findViewById(R.id.data_received_row).setVisibility(View.VISIBLE);
-                    mDataRowsHidden = false;
-                }
-
-                // [1] and [2] are received data in bytes and packets.
-                mDataReceived.setText(getString(R.string.data_value_format,
-                        numbers[1], numbers[2]));
-
-                // [9] and [10] are transmitted data in bytes and packets.
-                mDataTransmitted.setText(getString(R.string.data_value_format,
-                        numbers[9], numbers[10]));
-            }
-            mHandler.sendEmptyMessageDelayed(0, 1000);
-        }
-        return true;
-    }
-
-    private String[] getNumbers() {
-        DataInputStream in = null;
-        try {
-            // See dev_seq_printf_stats() in net/core/dev.c.
-            in = new DataInputStream(new FileInputStream("/proc/net/dev"));
-            String prefix = mConfig.interfaze + ':';
-
-            while (true) {
-                String line = in.readLine().trim();
-                if (line.startsWith(prefix)) {
-                    String[] numbers = line.substring(prefix.length()).split(" +");
-                    for (int i = 1; i < 17; ++i) {
-                        if (!numbers[i].equals("0")) {
-                            return numbers;
-                        }
-                    }
-                    break;
-                }
-            }
-        } catch (Exception e) {
-            // ignore
-        } finally {
-            try {
-                in.close();
-            } catch (Exception e) {
-                // ignore
-            }
-        }
-        return null;
-    }
-}
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 67c01e5..1d3daab 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -2663,6 +2663,20 @@
     }
 
     /**
+     * Set whether the current VPN package has the ability to launch VPNs without
+     * user intervention. This method is used by system UIs and not available
+     * in ConnectivityManager. Permissions are checked in Vpn class.
+     * @hide
+     */
+    @Override
+    public void setVpnPackageAuthorization(boolean authorized) {
+        int user = UserHandle.getUserId(Binder.getCallingUid());
+        synchronized(mVpns) {
+            mVpns.get(user).setPackageAuthorization(authorized);
+        }
+    }
+
+    /**
      * Configure a TUN interface and return its file descriptor. Parameters
      * are encoded and opaque to this class. This method is used by VpnBuilder
      * and not available in ConnectivityManager. Permissions are checked in
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;
     }
 
diff --git a/services/core/java/com/android/server/net/LockdownVpnTracker.java b/services/core/java/com/android/server/net/LockdownVpnTracker.java
index 52e741b..04df3e7 100644
--- a/services/core/java/com/android/server/net/LockdownVpnTracker.java
+++ b/services/core/java/com/android/server/net/LockdownVpnTracker.java
@@ -205,7 +205,6 @@
     private void initLocked() {
         Slog.d(TAG, "initLocked()");
 
-        mVpn.setEnableNotifications(false);
         mVpn.setEnableTeardown(false);
 
         final IntentFilter resetFilter = new IntentFilter(ACTION_LOCKDOWN_RESET);
@@ -249,7 +248,6 @@
         hideNotification();
 
         mContext.unregisterReceiver(mResetReceiver);
-        mVpn.setEnableNotifications(true);
         mVpn.setEnableTeardown(true);
     }