Bug 1351739 - Part 2 - Convert CustomTabsActivity to SafeIntents. r?sebastian,walkingice
These are potentially untrusted external intents, so we should use SafeIntents for interacting with them.
MozReview-Commit-ID: 3nmjg85wbr1
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -38,16 +38,17 @@ import org.mozilla.gecko.SnackbarBuilder
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.GeckoMenuInflater;
+import org.mozilla.gecko.mozglue.SafeIntent;
import org.mozilla.gecko.util.Clipboard;
import org.mozilla.gecko.util.ColorUtil;
import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.util.IntentUtils;
import org.mozilla.gecko.widget.ActionModePresenter;
import org.mozilla.gecko.widget.GeckoPopupMenu;
import java.util.List;
@@ -63,29 +64,30 @@ public class CustomTabsActivity extends
private View doorhangerOverlay;
private ActionBarPresenter actionBarPresenter;
private ProgressBar mProgressView;
// A state to indicate whether this activity is finishing with customize animation
private boolean usingCustomAnimation = false;
// Bug 1351605 - getIntent() not always returns the intent which started this activity.
// Therefore we make a copy in case of this Activity is re-created.
- private Intent startIntent;
+ private SafeIntent startIntent;
private MenuItem menuItemControl;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
- startIntent = savedInstanceState.getParcelable(SAVED_START_INTENT);
+ final Intent restoredIntent = savedInstanceState.getParcelable(SAVED_START_INTENT);
+ startIntent = new SafeIntent(restoredIntent);
} else {
sendTelemetry();
- startIntent = getIntent();
+ startIntent = new SafeIntent(getIntent());
final String host = getReferrerHost();
recordCustomTabUsage(host);
}
setThemeFromToolbarColor();
doorhangerOverlay = findViewById(R.id.custom_tabs_doorhanger_overlay);
@@ -217,17 +219,17 @@ public class CustomTabsActivity extends
}
updateMenuItemForward();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
- outState.putParcelable(SAVED_START_INTENT, startIntent);
+ outState.putParcelable(SAVED_START_INTENT, startIntent.getUnsafe());
}
@Override
public void onResume() {
if (lastSelectedTabId >= 0) {
final Tabs tabs = Tabs.getInstance();
final Tab tab = tabs.getTab(lastSelectedTabId);
if (tab == null) {
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/IntentUtil.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/IntentUtil.java
@@ -1,25 +1,26 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.customtabs;
import android.app.PendingIntent;
-import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.customtabs.CustomTabsIntent;
+import org.mozilla.gecko.mozglue.SafeIntent;
+
import java.util.ArrayList;
import java.util.List;
/**
* A utility class for CustomTabsActivity to extract information from intent.
* For example, this class helps to extract exit-animation resource id.
*/
class IntentUtil {
@@ -39,225 +40,225 @@ class IntentUtil {
private static final String KEY_ANIM_EXIT_RES_ID = PREFIX + "animExitRes";
/**
* To determine whether the intent has necessary information to build an Action-Button.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return true, if intent has all necessary information.
*/
- static boolean hasActionButton(@NonNull Intent intent) {
+ static boolean hasActionButton(@NonNull SafeIntent intent) {
return (getActionButtonBundle(intent) != null)
&& (getActionButtonIcon(intent) != null)
&& (getActionButtonDescription(intent) != null)
&& (getActionButtonPendingIntent(intent) != null);
}
/**
* To determine whether the intent requires to add share action to menu item.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return true, if intent requires to add share action to menu item.
*/
- static boolean hasShareItem(@NonNull Intent intent) {
+ static boolean hasShareItem(@NonNull SafeIntent intent) {
return intent.getBooleanExtra(CustomTabsIntent.EXTRA_DEFAULT_SHARE_MENU_ITEM, false);
}
/**
* To extract bitmap icon from intent for Action-Button.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return bitmap icon, if any. Otherwise, null.
*/
- static Bitmap getActionButtonIcon(@NonNull Intent intent) {
+ static Bitmap getActionButtonIcon(@NonNull SafeIntent intent) {
final Bundle bundle = getActionButtonBundle(intent);
return (bundle == null) ? null : (Bitmap) bundle.getParcelable(CustomTabsIntent.KEY_ICON);
}
/**
* Only for telemetry to understand caller app's customization
* This method should only be called once during one usage.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return true, if the caller customized the color.
*/
- static boolean hasToolbarColor(@NonNull Intent intent) {
+ static boolean hasToolbarColor(@NonNull SafeIntent intent) {
return intent.hasExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR);
}
/**
* To extract color code from intent for top toolbar.
* It also ensure the color is not translucent.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return color code in integer type.
*/
@ColorInt
- static int getToolbarColor(@NonNull Intent intent) {
+ static int getToolbarColor(@NonNull SafeIntent intent) {
@ColorInt int toolbarColor = intent.getIntExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR,
DEFAULT_ACTION_BAR_COLOR);
// Translucent color does not make sense for toolbar color. Ensure it is 0xFF.
toolbarColor = 0xFF000000 | toolbarColor;
return toolbarColor;
}
/**
* To extract description from intent for Action-Button. This description is used for
* accessibility.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return description, if any. Otherwise, null.
*/
- static String getActionButtonDescription(@NonNull Intent intent) {
+ static String getActionButtonDescription(@NonNull SafeIntent intent) {
final Bundle bundle = getActionButtonBundle(intent);
return (bundle == null) ? null : bundle.getString(CustomTabsIntent.KEY_DESCRIPTION);
}
/**
* To extract pending-intent from intent for Action-Button.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return PendingIntent, if any. Otherwise, null.
*/
- static PendingIntent getActionButtonPendingIntent(@NonNull Intent intent) {
+ static PendingIntent getActionButtonPendingIntent(@NonNull SafeIntent intent) {
final Bundle bundle = getActionButtonBundle(intent);
return (bundle == null)
? null
: (PendingIntent) bundle.getParcelable(CustomTabsIntent.KEY_PENDING_INTENT);
}
/**
* To know whether the Action-Button should be tinted.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return true, if Action-Button should be tinted. Default value is false.
*/
- static boolean isActionButtonTinted(@NonNull Intent intent) {
+ static boolean isActionButtonTinted(@NonNull SafeIntent intent) {
return intent.getBooleanExtra(CustomTabsIntent.EXTRA_TINT_ACTION_BUTTON, false);
}
/**
* To extract extra Action-button bundle from an intent.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return bundle for Action-Button, if any. Otherwise, null.
*/
- private static Bundle getActionButtonBundle(@NonNull Intent intent) {
+ private static Bundle getActionButtonBundle(@NonNull SafeIntent intent) {
return intent.getBundleExtra(CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE);
}
/**
* To get package name of 3rd-party-app from an intent.
* If the app defined extra exit-animation to use, it should also provide its package name
* to get correct animation resource.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return package name, if the intent defined extra exit-animation bundle. Otherwise, null.
*/
- static String getAnimationPackageName(@NonNull Intent intent) {
+ static String getAnimationPackageName(@NonNull SafeIntent intent) {
final Bundle bundle = getAnimationBundle(intent);
return (bundle == null) ? null : bundle.getString(KEY_PACKAGE_NAME);
}
/**
* To get titles for Menu Items from an intent.
* 3rd-party-app is able to add and customize up to five menu items. This method helps to
* get titles for each menu items.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return A list of string as title for each menu items
*/
- static List<String> getMenuItemsTitle(@NonNull Intent intent) {
+ static List<String> getMenuItemsTitle(@NonNull SafeIntent intent) {
final List<Bundle> bundles = getMenuItemsBundle(intent);
final List<String> titles = new ArrayList<>();
for (Bundle b : bundles) {
titles.add(b.getString(CustomTabsIntent.KEY_MENU_ITEM_TITLE));
}
return titles;
}
/**
* To get pending-intent for Menu Items from an intent.
* 3rd-party-app is able to add and customize up to five menu items. This method helps to
* get pending-intent for each menu items.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return A list of pending-intent for each menu items
*/
- static List<PendingIntent> getMenuItemsPendingIntent(@NonNull Intent intent) {
+ static List<PendingIntent> getMenuItemsPendingIntent(@NonNull SafeIntent intent) {
final List<Bundle> bundles = getMenuItemsBundle(intent);
final List<PendingIntent> intents = new ArrayList<>();
for (Bundle b : bundles) {
PendingIntent p = b.getParcelable(CustomTabsIntent.KEY_PENDING_INTENT);
intents.add(p);
}
return intents;
}
/**
* To check whether the intent has necessary information to apply customize exit-animation.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return true, if the intent has necessary information.
*/
- static boolean hasExitAnimation(@NonNull Intent intent) {
+ static boolean hasExitAnimation(@NonNull SafeIntent intent) {
final Bundle bundle = getAnimationBundle(intent);
return (bundle != null)
&& (getAnimationPackageName(intent) != null)
&& (getEnterAnimationRes(intent) != NO_ANIMATION_RESOURCE)
&& (getExitAnimationRes(intent) != NO_ANIMATION_RESOURCE);
}
/**
* To get enter-animation resource id of 3rd-party-app from an intent.
* If the app defined extra exit-animation to use, it should also provide its animation resource
* id.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return animation resource id if any; otherwise, NO_ANIMATION_RESOURCE;
*/
- static int getEnterAnimationRes(@NonNull Intent intent) {
+ static int getEnterAnimationRes(@NonNull SafeIntent intent) {
final Bundle bundle = getAnimationBundle(intent);
return (bundle == null)
? NO_ANIMATION_RESOURCE
: bundle.getInt(KEY_ANIM_ENTER_RES_ID, NO_ANIMATION_RESOURCE);
}
/**
* To get exit-animation resource id of 3rd-party-app from an intent.
* If the app defined extra exit-animation to use, it should also provide its animation resource
* id.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return animation resource id if any; otherwise, NO_ANIMATION_RESOURCE.
*/
- static int getExitAnimationRes(@NonNull Intent intent) {
+ static int getExitAnimationRes(@NonNull SafeIntent intent) {
final Bundle bundle = getAnimationBundle(intent);
return (bundle == null)
? NO_ANIMATION_RESOURCE
: bundle.getInt(KEY_ANIM_EXIT_RES_ID, NO_ANIMATION_RESOURCE);
}
/**
* To extract extra exit-animation bundle from an intent.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return bundle for extra exit-animation, if any. Otherwise, null.
*/
- private static Bundle getAnimationBundle(@NonNull Intent intent) {
+ private static Bundle getAnimationBundle(@NonNull SafeIntent intent) {
return intent.getBundleExtra(CustomTabsIntent.EXTRA_EXIT_ANIMATION_BUNDLE);
}
/**
* To extract bundles for Menu Items from an intent.
* 3rd-party-app is able to add and customize up to five menu items. This method helps to
* extract bundles to build menu items.
*
* @param intent which to launch a Custom-Tabs-Activity
* @return bundle for menu items, if any. Otherwise, an empty list.
*/
- private static List<Bundle> getMenuItemsBundle(@NonNull Intent intent) {
- ArrayList<Bundle> extra = intent.getParcelableArrayListExtra(
+ private static List<Bundle> getMenuItemsBundle(@NonNull SafeIntent intent) {
+ ArrayList<Bundle> extra = intent.getUnsafe().getParcelableArrayListExtra(
CustomTabsIntent.EXTRA_MENU_ITEMS);
return (extra == null) ? new ArrayList<Bundle>() : extra;
}
}
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/customtabs/TestCustomTabsActivity.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/customtabs/TestCustomTabsActivity.java
@@ -17,16 +17,17 @@ import android.view.MenuItem;
import junit.framework.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.internal.util.reflection.Whitebox;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.mozglue.SafeIntent;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.fakes.RoboMenu;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -58,46 +59,46 @@ public class TestCustomTabsActivity {
}
/**
* Activity should not call overridePendingTransition if custom animation does not exist.
*/
@Test
public void testFinishWithoutCustomAnimation() {
final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
- final Intent i = builder.build().intent;
+ final SafeIntent i = new SafeIntent(builder.build().intent);
Whitebox.setInternalState(spyActivity, "startIntent", i);
spyActivity.finish();
verify(spyActivity, times(0)).overridePendingTransition(anyInt(), anyInt());
}
/**
* Activity should call overridePendingTransition if custom animation exists.
*/
@Test
public void testFinish() {
final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setExitAnimations(spyContext, enterRes, exitRes);
- final Intent i = builder.build().intent;
+ final SafeIntent i = new SafeIntent(builder.build().intent);
Whitebox.setInternalState(spyActivity, "startIntent", i);
spyActivity.finish();
verify(spyActivity, times(1)).overridePendingTransition(eq(enterRes), eq(exitRes));
}
/**
* To get 3rd party app's package name, if custom animation exists.
*/
@Test
public void testGetPackageName() {
final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setExitAnimations(spyContext, enterRes, exitRes);
- final Intent i = builder.build().intent;
+ final SafeIntent i = new SafeIntent(builder.build().intent);
Whitebox.setInternalState(spyActivity, "usingCustomAnimation", true);
Whitebox.setInternalState(spyActivity, "startIntent", i);
Assert.assertEquals(THIRD_PARTY_PACKAGE_NAME, spyActivity.getPackageName());
}
}
\ No newline at end of file
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/customtabs/TestIntentUtil.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/customtabs/TestIntentUtil.java
@@ -15,16 +15,17 @@ import android.support.customtabs.Custom
import android.text.TextUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.mozglue.SafeIntent;
import org.robolectric.RuntimeEnvironment;
import java.util.List;
import java.util.Objects;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
@@ -49,56 +50,56 @@ public class TestIntentUtil {
final Bitmap bitmap = BitmapFactory.decodeResource(
spyContext.getResources(),
R.drawable.ic_action_settings); // arbitrary icon resource
final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setActionButton(bitmap, description, pendingIntent, tinted);
- Intent intent = builder.build().intent;
+ SafeIntent intent = new SafeIntent(builder.build().intent);
Assert.assertTrue(IntentUtil.hasActionButton(intent));
Assert.assertEquals(tinted, IntentUtil.isActionButtonTinted(intent));
Assert.assertEquals(bitmap, IntentUtil.getActionButtonIcon(intent));
Assert.assertEquals(description, IntentUtil.getActionButtonDescription(intent));
Assert.assertTrue(
Objects.equals(pendingIntent, IntentUtil.getActionButtonPendingIntent(intent)));
}
@Test
public void testIntentWithoutActionButton() {
final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
- Intent intent = builder.build().intent;
+ SafeIntent intent = new SafeIntent(builder.build().intent);
Assert.assertFalse(IntentUtil.hasActionButton(intent));
Assert.assertFalse(IntentUtil.isActionButtonTinted(intent));
Assert.assertNull(IntentUtil.getActionButtonIcon(intent));
Assert.assertNull(IntentUtil.getActionButtonDescription(intent));
Assert.assertNull(IntentUtil.getActionButtonPendingIntent(intent));
}
@Test
public void testIntentWithCustomAnimation() {
@AnimRes final int enterRes = 0x123; // arbitrary number as animation resource id
@AnimRes final int exitRes = 0x456; // arbitrary number as animation resource id
final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setExitAnimations(spyContext, enterRes, exitRes);
- final Intent i = builder.build().intent;
+ final SafeIntent i = new SafeIntent(builder.build().intent);
Assert.assertEquals(true, IntentUtil.hasExitAnimation(i));
Assert.assertEquals(THIRD_PARTY_PACKAGE_NAME, IntentUtil.getAnimationPackageName(i));
Assert.assertEquals(enterRes, IntentUtil.getEnterAnimationRes(i));
Assert.assertEquals(exitRes, IntentUtil.getExitAnimationRes(i));
}
@Test
public void testIntentWithoutCustomAnimation() {
final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
- final Intent i = builder.build().intent;
+ final SafeIntent i = new SafeIntent(builder.build().intent);
Assert.assertEquals(false, IntentUtil.hasExitAnimation(i));
Assert.assertEquals(null, IntentUtil.getAnimationPackageName(i));
Assert.assertEquals(IntentUtil.NO_ANIMATION_RESOURCE,
IntentUtil.getEnterAnimationRes(i));
Assert.assertEquals(IntentUtil.NO_ANIMATION_RESOURCE,
IntentUtil.getExitAnimationRes(i));
}
@@ -109,55 +110,55 @@ public class TestIntentUtil {
builder.addDefaultShareMenuItem(); // This should not effect menu-item method
PendingIntent intent0 = createPendingIntent(0x100, "http://mozilla.com/0");
PendingIntent intent1 = createPendingIntent(0x100, "http://mozilla.com/1");
PendingIntent intent2 = createPendingIntent(0x100, "http://mozilla.com/2");
builder.addMenuItem("Label 0", intent0);
builder.addMenuItem("Label 1", intent1);
builder.addMenuItem("Label 2", intent2);
- final Intent intent = builder.build().intent;
+ final SafeIntent intent = new SafeIntent(builder.build().intent);
List<String> titles = IntentUtil.getMenuItemsTitle(intent);
List<PendingIntent> intents = IntentUtil.getMenuItemsPendingIntent(intent);
Assert.assertEquals(3, titles.size());
Assert.assertEquals(3, intents.size());
Assert.assertEquals("Label 0", titles.get(0));
Assert.assertEquals("Label 1", titles.get(1));
Assert.assertEquals("Label 2", titles.get(2));
Assert.assertTrue(Objects.equals(intent0, intents.get(0)));
Assert.assertTrue(Objects.equals(intent1, intents.get(1)));
Assert.assertTrue(Objects.equals(intent2, intents.get(2)));
}
@Test
public void testToolbarColor() {
final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
- Assert.assertEquals(IntentUtil.getToolbarColor(builder.build().intent),
+ Assert.assertEquals(IntentUtil.getToolbarColor(new SafeIntent(builder.build().intent)),
IntentUtil.DEFAULT_ACTION_BAR_COLOR);
// Test red color
builder.setToolbarColor(0xFF0000);
- Assert.assertEquals(IntentUtil.getToolbarColor(builder.build().intent), 0xFFFF0000);
+ Assert.assertEquals(IntentUtil.getToolbarColor(new SafeIntent(builder.build().intent)), 0xFFFF0000);
builder.setToolbarColor(0xFFFF0000);
- Assert.assertEquals(IntentUtil.getToolbarColor(builder.build().intent), 0xFFFF0000);
+ Assert.assertEquals(IntentUtil.getToolbarColor(new SafeIntent(builder.build().intent)), 0xFFFF0000);
// Test translucent green color, it should force alpha value to be 0xFF
builder.setToolbarColor(0x0000FF00);
- Assert.assertEquals(IntentUtil.getToolbarColor(builder.build().intent), 0xFF00FF00);
+ Assert.assertEquals(IntentUtil.getToolbarColor(new SafeIntent(builder.build().intent)), 0xFF00FF00);
}
@Test
public void testMenuShareItem() {
final CustomTabsIntent.Builder builderNoShareItem = new CustomTabsIntent.Builder();
- Assert.assertFalse(IntentUtil.hasShareItem(builderNoShareItem.build().intent));
+ Assert.assertFalse(IntentUtil.hasShareItem(new SafeIntent(builderNoShareItem.build().intent)));
final CustomTabsIntent.Builder builderHasShareItem = new CustomTabsIntent.Builder();
builderHasShareItem.addDefaultShareMenuItem();
- Assert.assertTrue(IntentUtil.hasShareItem(builderHasShareItem.build().intent));
+ Assert.assertTrue(IntentUtil.hasShareItem(new SafeIntent(builderHasShareItem.build().intent)));
}
private PendingIntent createPendingIntent(int reqCode, @Nullable String uri) {
final Intent actionIntent = new Intent(Intent.ACTION_VIEW);
if (!TextUtils.isEmpty(uri)) {
actionIntent.setData(Uri.parse(uri));
}
return PendingIntent.getActivity(spyContext, reqCode, actionIntent,