diff --git a/packages/VpnDialogs/Android.bp b/packages/VpnDialogs/Android.bp index 05135b2bebf94..e4f80e22694b8 100644 --- a/packages/VpnDialogs/Android.bp +++ b/packages/VpnDialogs/Android.bp @@ -23,10 +23,15 @@ package { default_applicable_licenses: ["frameworks_base_license"], } +android_library { + name: "VpnDialogsLib", + srcs: ["src/**/*.java"], +} + android_app { name: "VpnDialogs", certificate: "platform", privileged: true, - srcs: ["src/**/*.java"], + static_libs: ["VpnDialogsLib"], platform_apis: true, } diff --git a/packages/VpnDialogs/res/values/strings.xml b/packages/VpnDialogs/res/values/strings.xml index f971a09168377..28e7272853c49 100644 --- a/packages/VpnDialogs/res/values/strings.xml +++ b/packages/VpnDialogs/res/values/strings.xml @@ -100,4 +100,33 @@ without any consequences. [CHAR LIMIT=20] --> Dismiss + + + %1$s… ( + %2$s) + + + + + %1$s ( + %2$s) + + diff --git a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java index fb2367843fc18..a98d6d8e02172 100644 --- a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java +++ b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java @@ -33,6 +33,7 @@ import android.view.View; import android.widget.Button; import android.widget.TextView; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.AlertActivity; import com.android.internal.net.VpnConfig; @@ -40,12 +41,19 @@ public class ConfirmDialog extends AlertActivity implements DialogInterface.OnClickListener, ImageGetter { private static final String TAG = "VpnConfirm"; + // Usually the label represents the app name, 150 code points might be enough to display the app + // name, and 150 code points won't cover the warning message from VpnDialog. + @VisibleForTesting + static final int MAX_VPN_LABEL_LENGTH = 150; + @VpnManager.VpnType private final int mVpnType; private String mPackage; private VpnManager mVm; + private View mView; + public ConfirmDialog() { this(VpnManager.TYPE_VPN_SERVICE); } @@ -54,6 +62,43 @@ public class ConfirmDialog extends AlertActivity mVpnType = vpnType; } + /** + * This function will use the string resource to combine the VPN label and the package name. + * + * If the VPN label violates the length restriction, the first 30 code points of VPN label and + * the package name will be returned. Or return the VPN label and the package name directly if + * the VPN label doesn't violate the length restriction. + * + * The result will be something like, + * - ThisIsAVeryLongVpnAppNameWhich... (com.vpn.app) + * if the VPN label violates the length restriction. + * or + * - VpnLabelWith<br>HtmlTag (com.vpn.app) + * if the VPN label doesn't violate the length restriction. + * + */ + private String getSimplifiedLabel(String vpnLabel, String packageName) { + if (vpnLabel.codePointCount(0, vpnLabel.length()) > 30) { + return getString(R.string.sanitized_vpn_label_with_ellipsis, + vpnLabel.substring(0, vpnLabel.offsetByCodePoints(0, 30)), + packageName); + } + + return getString(R.string.sanitized_vpn_label, vpnLabel, packageName); + } + + @VisibleForTesting + protected String getSanitizedVpnLabel(String vpnLabel, String packageName) { + final String sanitizedVpnLabel = Html.escapeHtml(vpnLabel); + final boolean exceedMaxVpnLabelLength = sanitizedVpnLabel.codePointCount(0, + sanitizedVpnLabel.length()) > MAX_VPN_LABEL_LENGTH; + if (exceedMaxVpnLabelLength || !vpnLabel.equals(sanitizedVpnLabel)) { + return getSimplifiedLabel(sanitizedVpnLabel, packageName); + } + + return sanitizedVpnLabel; + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -75,15 +120,16 @@ public class ConfirmDialog extends AlertActivity finish(); return; } - View view = View.inflate(this, R.layout.confirm, null); - ((TextView) view.findViewById(R.id.warning)).setText( - Html.fromHtml(getString(R.string.warning, getVpnLabel()), - this, null /* tagHandler */)); + mView = View.inflate(this, R.layout.confirm, null); + ((TextView) mView.findViewById(R.id.warning)).setText( + Html.fromHtml(getString(R.string.warning, getSanitizedVpnLabel( + getVpnLabel().toString(), mPackage)), + this /* imageGetter */, 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.mView = view; + mAlertParams.mView = mView; setupAlert(); getWindow().setCloseOnTouchOutside(false); @@ -92,6 +138,11 @@ public class ConfirmDialog extends AlertActivity button.setFilterTouchesWhenObscured(true); } + @VisibleForTesting + public CharSequence getWarningText() { + return ((TextView) mView.findViewById(R.id.warning)).getText(); + } + private CharSequence getVpnLabel() { try { return VpnConfig.getVpnLabel(this, mPackage); diff --git a/packages/VpnDialogs/tests/Android.bp b/packages/VpnDialogs/tests/Android.bp new file mode 100644 index 0000000000000..68639bd1c4fe1 --- /dev/null +++ b/packages/VpnDialogs/tests/Android.bp @@ -0,0 +1,36 @@ +// Copyright 2022, 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 { + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "VpnDialogsTests", + // Use platform certificate because the test will invoke a hidden API. + // (e.g. VpnManager#prepareVpn()). + certificate: "platform", + libs: [ + "android.test.runner", + "android.test.base", + ], + static_libs: [ + "androidx.test.core", + "androidx.test.rules", + "androidx.test.ext.junit", + "mockito-target-minus-junit4", + "VpnDialogsLib", + ], + srcs: ["src/**/*.java"], +} diff --git a/packages/VpnDialogs/tests/AndroidManifest.xml b/packages/VpnDialogs/tests/AndroidManifest.xml new file mode 100644 index 0000000000000..f26c1fecb4e30 --- /dev/null +++ b/packages/VpnDialogs/tests/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java b/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java new file mode 100644 index 0000000000000..7cfa466ac961f --- /dev/null +++ b/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2022 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 static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.VpnManager; + +import androidx.test.core.app.ActivityScenario; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class VpnDialogTest { + private ActivityScenario mActivityScenario; + + @SuppressWarnings("StaticMockMember") + @Mock + private static PackageManager sPm; + + @SuppressWarnings("StaticMockMember") + @Mock + private static VpnManager sVm; + + @Mock + private ApplicationInfo mAi; + + private static final String VPN_APP_NAME = "VpnApp"; + private static final String VPN_APP_PACKAGE_NAME = "com.android.vpndialogs.VpnDialogTest"; + private static final String VPN_LABEL_CONTAINS_HTML_TAG = + "Google Play"; + private static final String VPN_LABEL_CONTAINS_HTML_TAG_AND_VIOLATE_LENGTH_RESTRICTION = + "Google Play" + + " Wants to connect the network.









" + + "















Deny it?"; + private static final String VPN_LABEL_VIOLATES_LENGTH_RESTRICTION = "This is a VPN label" + + " which violates the length restriction. The length restriction here are 150 code" + + " points. So the VPN label should be sanitized, and shows the package name to the" + + " user."; + + public static class InstrumentedConfirmDialog extends ConfirmDialog { + @Override + public PackageManager getPackageManager() { + return sPm; + } + + @Override + public @Nullable Object getSystemService(@ServiceName @NonNull String name) { + switch (name) { + case Context.VPN_MANAGEMENT_SERVICE: + return sVm; + default: + return super.getSystemService(name); + } + } + + @Override + public String getCallingPackage() { + return VPN_APP_PACKAGE_NAME; + } + } + + private void launchActivity() { + final Context context = getInstrumentation().getContext(); + mActivityScenario = ActivityScenario.launch( + new Intent(context, InstrumentedConfirmDialog.class)); + } + + @Test + public void testGetSanitizedVpnLabel_withNormalCase() throws Exception { + // Test the normal case that the VPN label showed in the VpnDialog is the app name. + doReturn(VPN_APP_NAME).when(mAi).loadLabel(sPm); + launchActivity(); + mActivityScenario.onActivity(activity -> { + assertTrue(activity.getWarningText().toString().contains(VPN_APP_NAME)); + }); + } + + private void verifySanitizedVpnLabel(String originalLabel) { + doReturn(originalLabel).when(mAi).loadLabel(sPm); + launchActivity(); + mActivityScenario.onActivity(activity -> { + // The VPN label was sanitized because violating length restriction or having a html + // tag, so the warning message will contain the package name. + assertTrue(activity.getWarningText().toString().contains(activity.getCallingPackage())); + // Also, the length of sanitized VPN label shouldn't longer than MAX_VPN_LABEL_LENGTH + // and it shouldn't contain html tag. + final String sanitizedVpnLabel = + activity.getSanitizedVpnLabel(originalLabel, VPN_APP_PACKAGE_NAME); + assertTrue(sanitizedVpnLabel.codePointCount(0, sanitizedVpnLabel.length()) + < ConfirmDialog.MAX_VPN_LABEL_LENGTH); + assertFalse(sanitizedVpnLabel.contains("")); + }); + } + + @Test + public void testGetSanitizedVpnLabel_withHtmlTag() throws Exception { + // Test the case that the VPN label was sanitized because there is a html tag. + verifySanitizedVpnLabel(VPN_LABEL_CONTAINS_HTML_TAG); + } + + @Test + public void testGetSanitizedVpnLabel_withHtmlTagAndViolateLengthRestriction() throws Exception { + // Test the case that the VPN label was sanitized because there is a html tag. + verifySanitizedVpnLabel(VPN_LABEL_CONTAINS_HTML_TAG_AND_VIOLATE_LENGTH_RESTRICTION); + } + + @Test + public void testGetSanitizedVpnLabel_withLengthRestriction() throws Exception { + // Test the case that the VPN label was sanitized because hitting the length restriction. + verifySanitizedVpnLabel(VPN_LABEL_VIOLATES_LENGTH_RESTRICTION); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + doReturn(false).when(sVm).prepareVpn(anyString(), anyString(), anyInt()); + doReturn(null).when(sPm).queryIntentServices(any(), anyInt()); + doReturn(mAi).when(sPm).getApplicationInfo(anyString(), anyInt()); + } +}