summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjuncai <juncai@chromium.org>2016-03-25 13:40:35 -0700
committerCommit bot <commit-bot@chromium.org>2016-03-25 20:42:19 +0000
commit4cbad6ed5522e60cda2aa728c08e3aa80545e360 (patch)
treee6b54b14c22d141fc37623be9bfccdb4543e7bf4
parentcf4acce2eb7d4b7fc83a444af5d0a038151a1fa7 (diff)
downloadchromium_src-4cbad6ed5522e60cda2aa728c08e3aa80545e360.zip
chromium_src-4cbad6ed5522e60cda2aa728c08e3aa80545e360.tar.gz
chromium_src-4cbad6ed5522e60cda2aa728c08e3aa80545e360.tar.bz2
WebUsb Android chooser UI
This patch added code to display a chooser UI on Android for WebUsb. BUG=591735 Review URL: https://codereview.chromium.org/1739523002 Cr-Commit-Position: refs/heads/master@{#383351}
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/BluetoothChooserDialog.java56
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/ItemChooserDialog.java79
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/UsbChooserDialog.java141
-rw-r--r--chrome/android/java/strings/android_chrome_strings.grd17
-rw-r--r--chrome/android/javatests/src/org/chromium/chrome/browser/BluetoothChooserDialogTest.java12
-rw-r--r--chrome/android/javatests/src/org/chromium/chrome/browser/ItemChooserDialogTest.java99
-rw-r--r--chrome/browser/android/chrome_jni_registrar.cc2
-rw-r--r--chrome/browser/android/usb/web_usb_chooser_service_android.cc33
-rw-r--r--chrome/browser/android/usb/web_usb_chooser_service_android.h47
-rw-r--r--chrome/browser/chrome_content_browser_client.cc9
-rw-r--r--chrome/browser/ui/android/bluetooth_chooser_android.cc1
-rw-r--r--chrome/browser/ui/android/usb_chooser_dialog_android.cc226
-rw-r--r--chrome/browser/ui/android/usb_chooser_dialog_android.h87
-rw-r--r--chrome/browser/usb/usb_chooser_bubble_controller.cc71
-rw-r--r--chrome/browser/usb/usb_tab_helper.cc19
-rw-r--r--chrome/browser/usb/usb_tab_helper.h4
-rw-r--r--chrome/browser/usb/web_usb_histograms.cc12
-rw-r--r--chrome/browser/usb/web_usb_histograms.h29
-rw-r--r--chrome/chrome_browser.gypi5
-rw-r--r--chrome/chrome_browser_ui.gypi2
-rw-r--r--device/usb/webusb_descriptors.cc26
-rw-r--r--device/usb/webusb_descriptors.h5
-rw-r--r--ui/android/BUILD.gn1
-rw-r--r--ui/android/java/src/org/chromium/ui/text/NoUnderlineClickableSpan.java20
24 files changed, 821 insertions, 182 deletions
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/BluetoothChooserDialog.java b/chrome/android/java/src/org/chromium/chrome/browser/BluetoothChooserDialog.java
index 12f5837..e514465 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/BluetoothChooserDialog.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/BluetoothChooserDialog.java
@@ -8,11 +8,8 @@ import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
-import android.graphics.Color;
import android.text.SpannableString;
-import android.text.TextPaint;
import android.text.TextUtils;
-import android.text.style.ClickableSpan;
import android.view.View;
import org.chromium.base.VisibleForTesting;
@@ -21,12 +18,10 @@ import org.chromium.chrome.R;
import org.chromium.chrome.browser.omnibox.OmniboxUrlEmphasizer;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.ui.base.WindowAndroid;
+import org.chromium.ui.text.NoUnderlineClickableSpan;
import org.chromium.ui.text.SpanApplier;
import org.chromium.ui.text.SpanApplier.SpanInfo;
-import java.util.ArrayList;
-import java.util.List;
-
/**
* A dialog for picking available Bluetooth devices. This dialog is shown when a website requests to
* pair with a certain class of Bluetooth devices (e.g. through a bluetooth.requestDevice Javascript
@@ -76,9 +71,7 @@ public class BluetoothChooserDialog
}
/**
- * Creates the BluetoothChooserDialog and displays it (and starts waiting for data).
- *
- * @param context Context which is used for launching a dialog.
+ * Creates the BluetoothChooserDialog.
*/
@VisibleForTesting
BluetoothChooserDialog(WindowAndroid windowAndroid, String origin, int securityLevel,
@@ -110,34 +103,29 @@ public class BluetoothChooserDialog
String message = mContext.getString(R.string.bluetooth_not_found);
SpannableString noneFound = SpanApplier.applySpans(
message, new SpanInfo("<link>", "</link>",
- new NoUnderlineClickableSpan(LinkType.RESTART_SEARCH, mContext)));
+ new BluetoothClickableSpan(LinkType.RESTART_SEARCH, mContext)));
SpannableString searching = SpanApplier.applySpans(
mContext.getString(R.string.bluetooth_searching),
new SpanInfo("<link>", "</link>",
- new NoUnderlineClickableSpan(LinkType.EXPLAIN_BLUETOOTH, mContext)));
+ new BluetoothClickableSpan(LinkType.EXPLAIN_BLUETOOTH, mContext)));
String positiveButton = mContext.getString(R.string.bluetooth_confirm_button);
- SpannableString statusActive = SpanApplier.applySpans(
- mContext.getString(R.string.bluetooth_not_seeing_it),
- new SpanInfo("<link>", "</link>",
- new NoUnderlineClickableSpan(LinkType.EXPLAIN_BLUETOOTH, mContext)));
-
SpannableString statusIdleNoneFound = SpanApplier.applySpans(
mContext.getString(R.string.bluetooth_not_seeing_it_idle_none_found),
new SpanInfo("<link>", "</link>",
- new NoUnderlineClickableSpan(LinkType.EXPLAIN_BLUETOOTH, mContext)));
+ new BluetoothClickableSpan(LinkType.EXPLAIN_BLUETOOTH, mContext)));
SpannableString statusIdleSomeFound = SpanApplier.applySpans(
mContext.getString(R.string.bluetooth_not_seeing_it_idle_some_found),
new SpanInfo("<link1>", "</link1>",
- new NoUnderlineClickableSpan(LinkType.EXPLAIN_BLUETOOTH, mContext)),
+ new BluetoothClickableSpan(LinkType.EXPLAIN_BLUETOOTH, mContext)),
new SpanInfo("<link2>", "</link2>",
- new NoUnderlineClickableSpan(LinkType.RESTART_SEARCH, mContext)));
+ new BluetoothClickableSpan(LinkType.RESTART_SEARCH, mContext)));
ItemChooserDialog.ItemChooserLabels labels =
- new ItemChooserDialog.ItemChooserLabels(title, searching, noneFound, statusActive,
+ new ItemChooserDialog.ItemChooserLabels(title, searching, noneFound,
statusIdleNoneFound, statusIdleSomeFound, positiveButton);
mItemChooserDialog = new ItemChooserDialog(mContext, this, labels);
}
@@ -188,28 +176,25 @@ public class BluetoothChooserDialog
SpannableString needLocationMessage = SpanApplier.applySpans(
mContext.getString(R.string.bluetooth_need_location_permission),
new SpanInfo("<link>", "</link>",
- new NoUnderlineClickableSpan(
+ new BluetoothClickableSpan(
LinkType.REQUEST_LOCATION_PERMISSION, mContext)));
SpannableString needLocationStatus = SpanApplier.applySpans(
mContext.getString(R.string.bluetooth_need_location_permission_help),
new SpanInfo("<link>", "</link>",
- new NoUnderlineClickableSpan(
+ new BluetoothClickableSpan(
LinkType.NEED_LOCATION_PERMISSION_HELP, mContext)));
mItemChooserDialog.setErrorState(needLocationMessage, needLocationStatus);
}
- /**
- * A helper class to show a clickable link with underlines turned off.
- */
- private class NoUnderlineClickableSpan extends ClickableSpan {
+ private class BluetoothClickableSpan extends NoUnderlineClickableSpan {
// The type of link this span represents.
private LinkType mLinkType;
private Context mContext;
- NoUnderlineClickableSpan(LinkType linkType, Context context) {
+ BluetoothClickableSpan(LinkType linkType, Context context) {
mLinkType = linkType;
mContext = context;
}
@@ -264,13 +249,6 @@ public class BluetoothChooserDialog
// Get rid of the highlight background on selection.
view.invalidate();
}
-
- @Override
- public void updateDrawState(TextPaint textPaint) {
- super.updateDrawState(textPaint);
- textPaint.bgColor = Color.TRANSPARENT;
- textPaint.setUnderlineText(false);
- }
}
@CalledByNative
@@ -293,10 +271,8 @@ public class BluetoothChooserDialog
@VisibleForTesting
@CalledByNative
void addDevice(String deviceId, String deviceName) {
- List<ItemChooserDialog.ItemChooserRow> devices =
- new ArrayList<ItemChooserDialog.ItemChooserRow>();
- devices.add(new ItemChooserDialog.ItemChooserRow(deviceId, deviceName));
- mItemChooserDialog.addItemsToList(devices);
+ mItemChooserDialog.addItemToList(
+ new ItemChooserDialog.ItemChooserRow(deviceId, deviceName));
}
@VisibleForTesting
@@ -318,11 +294,11 @@ public class BluetoothChooserDialog
SpannableString adapterOffMessage = SpanApplier.applySpans(
mContext.getString(R.string.bluetooth_adapter_off),
new SpanInfo("<link>", "</link>",
- new NoUnderlineClickableSpan(LinkType.ADAPTER_OFF, mContext)));
+ new BluetoothClickableSpan(LinkType.ADAPTER_OFF, mContext)));
SpannableString adapterOffStatus = SpanApplier.applySpans(
mContext.getString(R.string.bluetooth_adapter_off_help),
new SpanInfo("<link>", "</link>",
- new NoUnderlineClickableSpan(LinkType.ADAPTER_OFF_HELP, mContext)));
+ new BluetoothClickableSpan(LinkType.ADAPTER_OFF_HELP, mContext)));
mItemChooserDialog.setErrorState(adapterOffMessage, adapterOffStatus);
}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ItemChooserDialog.java b/chrome/android/java/src/org/chromium/chrome/browser/ItemChooserDialog.java
index 89c697f..fd9b727 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ItemChooserDialog.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ItemChooserDialog.java
@@ -34,7 +34,6 @@ import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.widget.TextViewWithClickableSpans;
import java.util.HashSet;
-import java.util.List;
import java.util.Set;
/**
@@ -66,6 +65,19 @@ public class ItemChooserDialog {
mKey = key;
mDescription = description;
}
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ItemChooserRow)) return false;
+ if (this == obj) return true;
+ ItemChooserRow item = (ItemChooserRow) obj;
+ return mKey.equals(item.mKey) && mDescription.equals(item.mDescription);
+ }
+
+ @Override
+ public int hashCode() {
+ return mKey.hashCode() + mDescription.hashCode();
+ }
}
/**
@@ -73,31 +85,26 @@ public class ItemChooserDialog {
*/
public static class ItemChooserLabels {
// The title at the top of the dialog.
- public final SpannableString mTitle;
+ public final CharSequence mTitle;
// The message to show while there are no results.
- public final SpannableString mSearching;
+ public final CharSequence mSearching;
// The message to show when no results were produced.
- public final SpannableString mNoneFound;
- // A status message to show above the button row after an item has
- // been added and discovery is still ongoing.
- public final SpannableString mStatusActive;
+ public final CharSequence mNoneFound;
// A status message to show above the button row after discovery has
// stopped and no devices have been found.
- public final SpannableString mStatusIdleNoneFound;
+ public final CharSequence mStatusIdleNoneFound;
// A status message to show above the button row after an item has
// been added and discovery has stopped.
- public final SpannableString mStatusIdleSomeFound;
+ public final CharSequence mStatusIdleSomeFound;
// The label for the positive button (e.g. Select/Pair).
- public final String mPositiveButton;
+ public final CharSequence mPositiveButton;
- public ItemChooserLabels(SpannableString title, SpannableString searching,
- SpannableString noneFound, SpannableString statusActive,
- SpannableString statusIdleNoneFound, SpannableString statusIdleSomeFound,
- String positiveButton) {
+ public ItemChooserLabels(CharSequence title, CharSequence searching, CharSequence noneFound,
+ CharSequence statusIdleNoneFound, CharSequence statusIdleSomeFound,
+ CharSequence positiveButton) {
mTitle = title;
mSearching = searching;
mNoneFound = noneFound;
- mStatusActive = statusActive;
mStatusIdleNoneFound = statusIdleNoneFound;
mStatusIdleSomeFound = statusIdleSomeFound;
mPositiveButton = positiveButton;
@@ -107,7 +114,7 @@ public class ItemChooserDialog {
/**
* The various states the dialog can represent.
*/
- private enum State { STARTING, PROGRESS_UPDATE_AVAILABLE, DISCOVERY_IDLE }
+ private enum State { STARTING, DISCOVERY_IDLE }
/**
* An adapter for keeping track of which items to show in the dialog.
@@ -352,19 +359,24 @@ public class ItemChooserDialog {
}
/**
- * Add items to show in the dialog.
- *
- * @param list The list of items to add to the chooser. This function can be
- * called multiple times to add more items and new items will be appended to
- * the end of the list.
- */
- public void addItemsToList(List<ItemChooserRow> list) {
+ * Add an item to the end of the list to show in the dialog.
+ *
+ * @param item The item to be added to the end of the chooser.
+ */
+ public void addItemToList(ItemChooserRow item) {
mProgressBar.setVisibility(View.GONE);
+ mItemAdapter.add(item);
+ setState(State.DISCOVERY_IDLE);
+ }
- if (!list.isEmpty()) {
- mItemAdapter.addAll(list);
- }
- setState(State.PROGRESS_UPDATE_AVAILABLE);
+ /**
+ * Remove an item that is shown in the dialog.
+ *
+ * @param item The item to be removed in the chooser.
+ */
+ public void removeItemFromList(ItemChooserRow item) {
+ mItemAdapter.remove(item);
+ setState(State.DISCOVERY_IDLE);
}
/**
@@ -411,11 +423,6 @@ public class ItemChooserDialog {
mProgressBar.setVisibility(View.VISIBLE);
mEmptyMessage.setVisibility(View.GONE);
break;
- case PROGRESS_UPDATE_AVAILABLE:
- mStatus.setText(mLabels.mStatusActive);
- mProgressBar.setVisibility(View.GONE);
- mListView.setVisibility(View.VISIBLE);
- break;
case DISCOVERY_IDLE:
boolean showEmptyMessage = mItemAdapter.isEmpty();
mStatus.setText(showEmptyMessage
@@ -433,4 +440,12 @@ public class ItemChooserDialog {
public Dialog getDialogForTesting() {
return mDialog;
}
+
+ /**
+ * Returns the ItemAdapter associated with this class. For use with tests only.
+ */
+ @VisibleForTesting
+ public ItemAdapter getItemAdapterForTesting() {
+ return mItemAdapter;
+ }
}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/UsbChooserDialog.java b/chrome/android/java/src/org/chromium/chrome/browser/UsbChooserDialog.java
new file mode 100644
index 0000000..a291c51
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/UsbChooserDialog.java
@@ -0,0 +1,141 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser;
+
+import android.content.Context;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.view.View;
+
+import org.chromium.base.annotations.CalledByNative;
+import org.chromium.chrome.R;
+import org.chromium.chrome.browser.omnibox.OmniboxUrlEmphasizer;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.ui.base.WindowAndroid;
+import org.chromium.ui.text.NoUnderlineClickableSpan;
+import org.chromium.ui.text.SpanApplier;
+import org.chromium.ui.text.SpanApplier.SpanInfo;
+
+/**
+ * A dialog for showing available USB devices. This dialog is shown when a website requests to
+ * connect to a USB device (e.g. through a usb.requestDevice Javascript call).
+ */
+public class UsbChooserDialog implements ItemChooserDialog.ItemSelectedCallback {
+ /**
+ * The dialog to show to let the user pick a device.
+ */
+ ItemChooserDialog mItemChooserDialog;
+
+ /**
+ * A pointer back to the native part of the implementation for this dialog.
+ */
+ long mNativeUsbChooserDialogPtr;
+
+ /**
+ * Creates the UsbChooserDialog.
+ */
+ private UsbChooserDialog(long nativeUsbChooserDialogPtr) {
+ mNativeUsbChooserDialogPtr = nativeUsbChooserDialogPtr;
+ }
+
+ /**
+ * Shows the UsbChooserDialog.
+ *
+ * @param context Context which is used for launching a dialog.
+ * @param origin The origin for the site wanting to connect to the USB device.
+ * @param securityLevel The security level of the connection to the site wanting to connect to
+ * the USB device. For valid values see SecurityStateModel::SecurityLevel.
+ */
+ private void show(Context context, String origin, int securityLevel) {
+ // Emphasize the origin.
+ Profile profile = Profile.getLastUsedProfile();
+ SpannableString originSpannableString = new SpannableString(origin);
+ OmniboxUrlEmphasizer.emphasizeUrl(originSpannableString, context.getResources(), profile,
+ securityLevel, false /* isInternalPage */, true /* useDarkColors */,
+ true /* emphasizeHttpsScheme */);
+ // Construct a full string and replace the origin text with emphasized version.
+ SpannableString title =
+ new SpannableString(context.getString(R.string.usb_chooser_dialog_prompt, origin));
+ int start = title.toString().indexOf(origin);
+ TextUtils.copySpansFrom(originSpannableString, 0, originSpannableString.length(),
+ Object.class, title, start);
+
+ String searching = "";
+ String noneFound = context.getString(R.string.usb_chooser_dialog_no_devices_found_prompt);
+ SpannableString statusIdleNoneFound = SpanApplier.applySpans(
+ context.getString(R.string.usb_chooser_dialog_footnote_text),
+ new SpanInfo("<link>", "</link>", new NoUnderlineClickableSpan() {
+ @Override
+ public void onClick(View view) {
+ if (mNativeUsbChooserDialogPtr == 0) {
+ return;
+ }
+
+ nativeLoadUsbHelpPage(mNativeUsbChooserDialogPtr);
+
+ // Get rid of the highlight background on selection.
+ view.invalidate();
+ }
+ }));
+ SpannableString statusIdleSomeFound = statusIdleNoneFound;
+ String positiveButton = context.getString(R.string.usb_chooser_dialog_connect_button_text);
+
+ ItemChooserDialog.ItemChooserLabels labels =
+ new ItemChooserDialog.ItemChooserLabels(title, searching, noneFound,
+ statusIdleNoneFound, statusIdleSomeFound, positiveButton);
+ mItemChooserDialog = new ItemChooserDialog(context, this, labels);
+ }
+
+ @Override
+ public void onItemSelected(String id) {
+ if (mNativeUsbChooserDialogPtr != 0) {
+ if (id.isEmpty()) {
+ nativeOnDialogCancelled(mNativeUsbChooserDialogPtr);
+ } else {
+ nativeOnItemSelected(mNativeUsbChooserDialogPtr, id);
+ }
+ }
+ }
+
+ @CalledByNative
+ private static UsbChooserDialog create(WindowAndroid windowAndroid, String origin,
+ int securityLevel, long nativeUsbChooserDialogPtr) {
+ Context context = windowAndroid.getActivity().get();
+ if (context == null) {
+ return null;
+ }
+
+ UsbChooserDialog dialog = new UsbChooserDialog(nativeUsbChooserDialogPtr);
+ dialog.show(context, origin, securityLevel);
+ return dialog;
+ }
+
+ @CalledByNative
+ private void setIdleState() {
+ mItemChooserDialog.setIdleState();
+ }
+
+ @CalledByNative
+ private void addDevice(String deviceId, String deviceName) {
+ mItemChooserDialog.addItemToList(
+ new ItemChooserDialog.ItemChooserRow(deviceId, deviceName));
+ }
+
+ @CalledByNative
+ private void removeDevice(String deviceId, String deviceName) {
+ mItemChooserDialog.removeItemFromList(
+ new ItemChooserDialog.ItemChooserRow(deviceId, deviceName));
+ }
+
+ @CalledByNative
+ private void closeDialog() {
+ mNativeUsbChooserDialogPtr = 0;
+ mItemChooserDialog.dismiss();
+ }
+
+ private native void nativeOnItemSelected(long nativeUsbChooserDialogAndroid, String deviceId);
+ private native void nativeOnDialogCancelled(long nativeUsbChooserDialogAndroid);
+ private native void nativeLoadUsbHelpPage(long nativeUsbChooserDialogAndroid);
+}
diff --git a/chrome/android/java/strings/android_chrome_strings.grd b/chrome/android/java/strings/android_chrome_strings.grd
index 9075def..0ac3f6b2 100644
--- a/chrome/android/java/strings/android_chrome_strings.grd
+++ b/chrome/android/java/strings/android_chrome_strings.grd
@@ -1097,9 +1097,6 @@ To obtain new licenses, connect to the internet and play your downloaded content
<message name="IDS_BLUETOOTH_CONFIRM_BUTTON" desc="The button to confirm association of a website with a Bluetooth device. Use the same term as in 'Pair this Bluetooth headset with your phone.'">
Pair
</message>
- <message name="IDS_BLUETOOTH_NOT_SEEING_IT" desc="The message to show at the bottom of the dialog when Bluetooth scanning has uncovered some items but is still scanning for others.">
- Not seeing your device? <ph name="BEGIN_LINK">&lt;link&gt;</ph>Get help<ph name="END_LINK">&lt;/link&gt;</ph>.
- </message>
<message name="IDS_BLUETOOTH_NOT_SEEING_IT_IDLE_NONE_FOUND" desc="The message to show at the bottom of the dialog when Bluetooth scanning has not uncovered any devices and it is no longer discovering devices.">
<ph name="BEGIN_LINK">&lt;link&gt;</ph>Get help<ph name="END_LINK">&lt;/link&gt;</ph>
</message>
@@ -2406,6 +2403,20 @@ You can control the Physical Web in Chrome privacy settings.
<message name="IDS_UPDATE_PASSWORD_FOR_ACCOUNT" desc="A message shown to users to allow updating a saved password for a site, where user has multiple credentials saved, shown in an infobar that appears after user uses new password to sign in to the site or uses a password change form.">
Do you want <ph name="PASSWORD_MANAGER_BRAND">^1<ex>Google Chrome</ex></ph> to update the password for <ph name="USERNAME">^2<ex>don.john.lemon@example.com</ex></ph> for this site?
</message>
+
+ <!-- WebUsb Picker UI strings -->
+ <message name="IDS_USB_CHOOSER_DIALOG_PROMPT" desc="The text that is used to introduce the USB chooser dialog to the user.">
+ <ph name="SITE">%1$s<ex>https://www.google.com</ex></ph> wants to connect to:
+ </message>
+ <message name="IDS_USB_CHOOSER_DIALOG_NO_DEVICES_FOUND_PROMPT" desc="The text that is used to inform the user that no devices found in the USB chooser dialog.">
+ No devices found.
+ </message>
+ <message name="IDS_USB_CHOOSER_DIALOG_CONNECT_BUTTON_TEXT" desc="The text that is shown on the connect button in the USB chooser dialog.">
+ Connect
+ </message>
+ <message name="IDS_USB_CHOOSER_DIALOG_FOOTNOTE_TEXT" desc="This text is shown at the bottom of the USB chooser dialog with a link to part of its text.">
+ Not seeing your device? <ph name="BEGIN_LINK">&lt;link&gt;</ph>Get help<ph name="END_LINK">&lt;/link&gt;</ph>
+ </message>
</messages>
</release>
</grit>
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/BluetoothChooserDialogTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/BluetoothChooserDialogTest.java
index 16b307a..2e555f5 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/BluetoothChooserDialogTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/BluetoothChooserDialogTest.java
@@ -140,12 +140,13 @@ public class BluetoothChooserDialogTest extends ChromeActivityTestCaseBase<Chrom
}
/**
- * The messages include <link> ... </link> sections that are used to create
- * clickable spans. For testing the messages, this function returns the raw
- * string without the tags.
+ * The messages include <link> ... </link> or <link1> ... </link1>, <link2> ... </link2>
+ * sections that are used to create clickable spans. For testing the messages, this function
+ * returns the raw string without the tags.
*/
private static String removeLinkTags(String message) {
- return message.replaceAll("</?link>", "");
+ return message.replaceAll("</?link1>", "").replaceAll(
+ "</?link2>", "").replaceAll("</?link>", "");
}
@SmallTest
@@ -202,7 +203,8 @@ public class BluetoothChooserDialogTest extends ChromeActivityTestCaseBase<Chrom
// the progress spinner should disappear, the Commit button should still
// be disabled (since nothing's selected), and the list view should
// show.
- assertEquals(removeLinkTags(getActivity().getString(R.string.bluetooth_not_seeing_it)),
+ assertEquals(removeLinkTags(getActivity().getString(
+ R.string.bluetooth_not_seeing_it_idle_some_found)),
statusView.getText().toString());
assertFalse(button.isEnabled());
assertEquals(View.VISIBLE, items.getVisibility());
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/ItemChooserDialogTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/ItemChooserDialogTest.java
index 924be58..851423e 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/ItemChooserDialogTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/ItemChooserDialogTest.java
@@ -8,6 +8,7 @@ import android.app.Dialog;
import android.test.suitebuilder.annotation.SmallTest;
import android.text.SpannableString;
import android.view.View;
+import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
@@ -19,8 +20,6 @@ import org.chromium.content.browser.test.util.CriteriaHelper;
import org.chromium.content.browser.test.util.TouchCommon;
import org.chromium.ui.widget.TextViewWithClickableSpans;
-import java.util.ArrayList;
-import java.util.List;
import java.util.concurrent.Callable;
/**
@@ -61,12 +60,11 @@ public class ItemChooserDialogTest extends ChromeActivityTestCaseBase<ChromeActi
SpannableString title = new SpannableString("title");
SpannableString searching = new SpannableString("searching");
SpannableString noneFound = new SpannableString("noneFound");
- SpannableString statusActive = new SpannableString("statusActive");
SpannableString statusIdleNoneFound = new SpannableString("statusIdleNoneFound");
SpannableString statusIdleSomeFound = new SpannableString("statusIdleSomeFound");
String positiveButton = new String("positiveButton");
final ItemChooserDialog.ItemChooserLabels labels =
- new ItemChooserDialog.ItemChooserLabels(title, searching, noneFound, statusActive,
+ new ItemChooserDialog.ItemChooserLabels(title, searching, noneFound,
statusIdleNoneFound, statusIdleSomeFound, positiveButton);
ItemChooserDialog dialog = ThreadUtils.runOnUiThreadBlockingNoException(
new Callable<ItemChooserDialog>() {
@@ -132,21 +130,10 @@ public class ItemChooserDialogTest extends ChromeActivityTestCaseBase<ChromeActi
assertFalse(button.isEnabled());
assertEquals(View.GONE, items.getVisibility());
- List<ItemChooserDialog.ItemChooserRow> devices =
- new ArrayList<ItemChooserDialog.ItemChooserRow>();
- devices.add(new ItemChooserDialog.ItemChooserRow("key", "key"));
- devices.add(new ItemChooserDialog.ItemChooserRow("key2", "key2"));
- mChooserDialog.addItemsToList(devices);
+ mChooserDialog.addItemToList(new ItemChooserDialog.ItemChooserRow("key", "key"));
+ mChooserDialog.addItemToList(new ItemChooserDialog.ItemChooserRow("key2", "key2"));
- // Two items showing, the empty view should be no more and the button
- // should now be enabled.
- assertEquals(View.VISIBLE, items.getVisibility());
- assertEquals(View.GONE, items.getEmptyView().getVisibility());
- assertEquals("statusActive", statusView.getText().toString());
- assertFalse(button.isEnabled());
-
- mChooserDialog.setIdleState();
- // After discovery stops the list should still be visible,
+ // After discovery stops the list should be visible with two items,
// it should not show the empty view and the button should not be enabled.
// The chooser should show the status idle text.
assertEquals(View.VISIBLE, items.getVisibility());
@@ -193,11 +180,8 @@ public class ItemChooserDialogTest extends ChromeActivityTestCaseBase<ChromeActi
Dialog dialog = mChooserDialog.getDialogForTesting();
assertTrue(dialog.isShowing());
- List<ItemChooserDialog.ItemChooserRow> devices =
- new ArrayList<ItemChooserDialog.ItemChooserRow>();
- devices.add(new ItemChooserDialog.ItemChooserRow("key", "key"));
- devices.add(new ItemChooserDialog.ItemChooserRow("key2", "key2"));
- mChooserDialog.addItemsToList(devices);
+ mChooserDialog.addItemToList(new ItemChooserDialog.ItemChooserRow("key", "key"));
+ mChooserDialog.addItemToList(new ItemChooserDialog.ItemChooserRow("key2", "key2"));
// Disable one item and try to select it.
mChooserDialog.setEnabled("key", false);
@@ -207,4 +191,73 @@ public class ItemChooserDialogTest extends ChromeActivityTestCaseBase<ChromeActi
mChooserDialog.dismiss();
}
+
+ @SmallTest
+ public void testAddItemToListAndRemoveItemFromList() throws InterruptedException {
+ Dialog dialog = mChooserDialog.getDialogForTesting();
+ assertTrue(dialog.isShowing());
+
+ TextViewWithClickableSpans statusView = (TextViewWithClickableSpans)
+ dialog.findViewById(R.id.status);
+ final ListView items = (ListView) dialog.findViewById(R.id.items);
+ final Button button = (Button) dialog.findViewById(R.id.positive);
+
+ ArrayAdapter itemAdapter = mChooserDialog.getItemAdapterForTesting();
+ ItemChooserDialog.ItemChooserRow nonExistentItem =
+ new ItemChooserDialog.ItemChooserRow("key", "key");
+
+ // Initially the itemAdapter is empty.
+ assertTrue(itemAdapter.isEmpty());
+
+ // Try removing an item from an empty itemAdapter.
+ mChooserDialog.removeItemFromList(nonExistentItem);
+ assertTrue(itemAdapter.isEmpty());
+
+ // Add item 1.
+ ItemChooserDialog.ItemChooserRow item1 =
+ new ItemChooserDialog.ItemChooserRow("key1", "key1");
+ mChooserDialog.addItemToList(item1);
+ assertEquals(1, itemAdapter.getCount());
+ assertEquals(itemAdapter.getItem(0), item1);
+
+ // Add item 2.
+ ItemChooserDialog.ItemChooserRow item2 =
+ new ItemChooserDialog.ItemChooserRow("key2", "key2");
+ mChooserDialog.addItemToList(item2);
+ assertEquals(2, itemAdapter.getCount());
+ assertEquals(itemAdapter.getItem(0), item1);
+ assertEquals(itemAdapter.getItem(1), item2);
+
+ // Try removing an item that doesn't exist.
+ mChooserDialog.removeItemFromList(nonExistentItem);
+ assertEquals(2, itemAdapter.getCount());
+
+ // Remove item 2.
+ mChooserDialog.removeItemFromList(item2);
+ assertEquals(1, itemAdapter.getCount());
+ // Make sure the remaining item is item 1.
+ assertEquals(itemAdapter.getItem(0), item1);
+
+ // The list should be visible with one item, it should not show
+ // the empty view and the button should not be enabled.
+ // The chooser should show a status message at the bottom.
+ assertEquals(View.VISIBLE, items.getVisibility());
+ assertEquals(View.GONE, items.getEmptyView().getVisibility());
+ assertEquals("statusIdleSomeFound", statusView.getText().toString());
+ assertFalse(button.isEnabled());
+
+ // Remove item 1.
+ mChooserDialog.removeItemFromList(item1);
+ assertTrue(itemAdapter.isEmpty());
+
+ // Listview should now be showing empty, with an empty view visible
+ // and the button should not be enabled.
+ // The chooser should show a status message at the bottom.
+ assertEquals(View.GONE, items.getVisibility());
+ assertEquals(View.VISIBLE, items.getEmptyView().getVisibility());
+ assertEquals("statusIdleNoneFound", statusView.getText().toString());
+ assertFalse(button.isEnabled());
+
+ mChooserDialog.dismiss();
+ }
}
diff --git a/chrome/browser/android/chrome_jni_registrar.cc b/chrome/browser/android/chrome_jni_registrar.cc
index 3278139..a67dd52 100644
--- a/chrome/browser/android/chrome_jni_registrar.cc
+++ b/chrome/browser/android/chrome_jni_registrar.cc
@@ -157,6 +157,7 @@
#include "chrome/browser/ui/android/tab_model/single_tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_jni_bridge.h"
#include "chrome/browser/ui/android/toolbar/toolbar_model_android.h"
+#include "chrome/browser/ui/android/usb_chooser_dialog_android.h"
#include "chrome/browser/ui/android/website_settings_popup_android.h"
#include "components/bookmarks/common/android/component_jni_registrar.h"
#include "components/dom_distiller/android/component_jni_registrar.h"
@@ -375,6 +376,7 @@ static base::android::RegistrationMethod kChromeRegisteredMethods[] = {
{"UmaSessionStats", RegisterUmaSessionStats},
{"UpdatePasswordInfoBar", UpdatePasswordInfoBar::Register},
{"UrlUtilities", RegisterUrlUtilities},
+ {"UsbChooserDialogAndroid", UsbChooserDialogAndroid::Register},
{"Variations", variations::android::RegisterVariations},
{"VariationsSeedBridge", variations::android::RegisterVariationsSeedBridge},
{"VariationsSession", chrome::android::RegisterVariationsSession},
diff --git a/chrome/browser/android/usb/web_usb_chooser_service_android.cc b/chrome/browser/android/usb/web_usb_chooser_service_android.cc
new file mode 100644
index 0000000..d31b7b0
--- /dev/null
+++ b/chrome/browser/android/usb/web_usb_chooser_service_android.cc
@@ -0,0 +1,33 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/android/usb/web_usb_chooser_service_android.h"
+
+#include <utility>
+
+#include "chrome/browser/ui/android/usb_chooser_dialog_android.h"
+#include "content/public/browser/browser_thread.h"
+
+WebUsbChooserServiceAndroid::WebUsbChooserServiceAndroid(
+ content::RenderFrameHost* render_frame_host)
+ : render_frame_host_(render_frame_host) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ DCHECK(render_frame_host_);
+}
+
+WebUsbChooserServiceAndroid::~WebUsbChooserServiceAndroid() {}
+
+void WebUsbChooserServiceAndroid::GetPermission(
+ mojo::Array<device::usb::DeviceFilterPtr> device_filters,
+ const GetPermissionCallback& callback) {
+ usb_chooser_dialog_android_.push_back(
+ make_scoped_ptr(new UsbChooserDialogAndroid(
+ std::move(device_filters), render_frame_host_, callback)));
+}
+
+void WebUsbChooserServiceAndroid::Bind(
+ mojo::InterfaceRequest<device::usb::ChooserService> request) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ bindings_.AddBinding(this, std::move(request));
+}
diff --git a/chrome/browser/android/usb/web_usb_chooser_service_android.h b/chrome/browser/android/usb/web_usb_chooser_service_android.h
new file mode 100644
index 0000000..d83345a
--- /dev/null
+++ b/chrome/browser/android/usb/web_usb_chooser_service_android.h
@@ -0,0 +1,47 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_ANDROID_USB_WEB_USB_CHOOSER_SERVICE_ANDROID_H_
+#define CHROME_BROWSER_ANDROID_USB_WEB_USB_CHOOSER_SERVICE_ANDROID_H_
+
+#include <vector>
+
+#include "base/macros.h"
+#include "base/memory/scoped_ptr.h"
+#include "device/usb/public/interfaces/chooser_service.mojom.h"
+#include "mojo/public/cpp/bindings/array.h"
+#include "mojo/public/cpp/bindings/binding_set.h"
+#include "mojo/public/cpp/bindings/interface_request.h"
+
+class UsbChooserDialogAndroid;
+
+namespace content {
+class RenderFrameHost;
+}
+
+// Implementation of the public device::usb::ChooserService interface.
+// This interface can be used by a webpage to request permission from user
+// to access a certain device.
+class WebUsbChooserServiceAndroid : public device::usb::ChooserService {
+ public:
+ explicit WebUsbChooserServiceAndroid(
+ content::RenderFrameHost* render_frame_host);
+
+ ~WebUsbChooserServiceAndroid() override;
+
+ // device::usb::ChooserService:
+ void GetPermission(mojo::Array<device::usb::DeviceFilterPtr> device_filters,
+ const GetPermissionCallback& callback) override;
+
+ void Bind(mojo::InterfaceRequest<device::usb::ChooserService> request);
+
+ private:
+ content::RenderFrameHost* const render_frame_host_;
+ mojo::BindingSet<device::usb::ChooserService> bindings_;
+ std::vector<scoped_ptr<UsbChooserDialogAndroid>> usb_chooser_dialog_android_;
+
+ DISALLOW_COPY_AND_ASSIGN(WebUsbChooserServiceAndroid);
+};
+
+#endif // CHROME_BROWSER_ANDROID_USB_WEB_USB_CHOOSER_SERVICE_ANDROID_H_
diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index a98e46b..8fd472b 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -155,6 +155,7 @@
#include "content/public/common/service_registry.h"
#include "content/public/common/url_utils.h"
#include "content/public/common/web_preferences.h"
+#include "device/usb/public/interfaces/chooser_service.mojom.h"
#include "device/usb/public/interfaces/device_manager.mojom.h"
#include "gin/v8_initializer.h"
#include "mojo/shell/public/cpp/shell_client.h"
@@ -294,10 +295,6 @@
#include "chrome/browser/media/router/presentation_service_delegate_impl.h"
#endif
-#if !defined(OS_ANDROID)
-#include "device/usb/public/interfaces/chooser_service.mojom.h"
-#endif
-
#if defined(ENABLE_WAYLAND_SERVER)
#include "chrome/browser/chrome_browser_main_extra_parts_exo.h"
#endif
@@ -694,7 +691,6 @@ void CreateUsbDeviceManager(
tab_helper->CreateDeviceManager(render_frame_host, std::move(request));
}
-#if !defined(OS_ANDROID)
void CreateWebUsbChooserService(
RenderFrameHost* render_frame_host,
mojo::InterfaceRequest<device::usb::ChooserService> request) {
@@ -709,7 +705,6 @@ void CreateWebUsbChooserService(
UsbTabHelper::GetOrCreateForWebContents(web_contents);
tab_helper->CreateChooserService(render_frame_host, std::move(request));
}
-#endif // !defined(OS_ANDROID)
bool GetDataSaverEnabledPref(const PrefService* prefs) {
// Enable data saver only when data saver pref is enabled and not part of
@@ -2808,10 +2803,8 @@ void ChromeContentBrowserClient::RegisterRenderFrameMojoServices(
base::FeatureList::IsEnabled(features::kWebUsb)) {
registry->AddService(
base::Bind(&CreateUsbDeviceManager, render_frame_host));
-#if !defined(OS_ANDROID)
registry->AddService(
base::Bind(&CreateWebUsbChooserService, render_frame_host));
-#endif // !defined(OS_ANDROID)
}
}
diff --git a/chrome/browser/ui/android/bluetooth_chooser_android.cc b/chrome/browser/ui/android/bluetooth_chooser_android.cc
index a24ef89..6f4ce8b 100644
--- a/chrome/browser/ui/android/bluetooth_chooser_android.cc
+++ b/chrome/browser/ui/android/bluetooth_chooser_android.cc
@@ -9,7 +9,6 @@
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ssl/chrome_security_state_model_client.h"
-#include "chrome/browser/ui/android/view_android_helper.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "components/prefs/pref_service.h"
diff --git a/chrome/browser/ui/android/usb_chooser_dialog_android.cc b/chrome/browser/ui/android/usb_chooser_dialog_android.cc
new file mode 100644
index 0000000..10ef6d3
--- /dev/null
+++ b/chrome/browser/ui/android/usb_chooser_dialog_android.cc
@@ -0,0 +1,226 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/android/usb_chooser_dialog_android.h"
+
+#include <stddef.h>
+
+#include <algorithm>
+
+#include "base/android/jni_android.h"
+#include "base/android/jni_string.h"
+#include "base/bind.h"
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ssl/chrome_security_state_model_client.h"
+#include "chrome/browser/usb/usb_chooser_context.h"
+#include "chrome/browser/usb/usb_chooser_context_factory.h"
+#include "chrome/browser/usb/web_usb_histograms.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/url_constants.h"
+#include "components/prefs/pref_service.h"
+#include "components/url_formatter/elide_url.h"
+#include "content/public/browser/android/content_view_core.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
+#include "device/core/device_client.h"
+#include "device/usb/mojo/type_converters.h"
+#include "device/usb/usb_device.h"
+#include "device/usb/usb_device_filter.h"
+#include "device/usb/webusb_descriptors.h"
+#include "jni/UsbChooserDialog_jni.h"
+#include "ui/android/window_android.h"
+#include "url/gurl.h"
+
+UsbChooserDialogAndroid::UsbChooserDialogAndroid(
+ mojo::Array<device::usb::DeviceFilterPtr> device_filters,
+ content::RenderFrameHost* render_frame_host,
+ const device::usb::ChooserService::GetPermissionCallback& callback)
+ : render_frame_host_(render_frame_host),
+ callback_(callback),
+ usb_service_observer_(this),
+ weak_factory_(this) {
+ device::UsbService* usb_service =
+ device::DeviceClient::Get()->GetUsbService();
+ if (!usb_service)
+ return;
+
+ if (!usb_service_observer_.IsObserving(usb_service))
+ usb_service_observer_.Add(usb_service);
+
+ if (!device_filters.is_null())
+ filters_ = device_filters.To<std::vector<device::UsbDeviceFilter>>();
+
+ // Create (and show) the UsbChooser dialog.
+ content::WebContents* web_contents =
+ content::WebContents::FromRenderFrameHost(render_frame_host_);
+ base::android::ScopedJavaLocalRef<jobject> window_android =
+ content::ContentViewCore::FromWebContents(web_contents)
+ ->GetWindowAndroid()
+ ->GetJavaObject();
+ JNIEnv* env = base::android::AttachCurrentThread();
+ Profile* profile =
+ Profile::FromBrowserContext(web_contents->GetBrowserContext());
+ std::string languages =
+ profile->GetPrefs()->GetString(prefs::kAcceptLanguages);
+ base::android::ScopedJavaLocalRef<jstring> origin_string =
+ base::android::ConvertUTF16ToJavaString(
+ env, url_formatter::FormatUrlForSecurityDisplay(
+ render_frame_host->GetLastCommittedURL(), languages));
+ ChromeSecurityStateModelClient* security_model_client =
+ ChromeSecurityStateModelClient::FromWebContents(web_contents);
+ DCHECK(security_model_client);
+ java_dialog_.Reset(Java_UsbChooserDialog_create(
+ env, window_android.obj(), origin_string.obj(),
+ security_model_client->GetSecurityInfo().security_level,
+ reinterpret_cast<intptr_t>(this)));
+
+ if (!java_dialog_.is_null()) {
+ usb_service->GetDevices(
+ base::Bind(&UsbChooserDialogAndroid::GotUsbDeviceList,
+ weak_factory_.GetWeakPtr()));
+ }
+}
+
+UsbChooserDialogAndroid::~UsbChooserDialogAndroid() {
+ if (!callback_.is_null())
+ callback_.Run(nullptr);
+
+ if (!java_dialog_.is_null()) {
+ Java_UsbChooserDialog_closeDialog(base::android::AttachCurrentThread(),
+ java_dialog_.obj());
+ }
+}
+
+void UsbChooserDialogAndroid::OnDeviceAdded(
+ scoped_refptr<device::UsbDevice> device) {
+ if (device::UsbDeviceFilter::MatchesAny(device, filters_) &&
+ FindInWebUsbAllowedOrigins(
+ device->webusb_allowed_origins(),
+ render_frame_host_->GetLastCommittedURL().GetOrigin())) {
+ AddDeviceToChooserDialog(device);
+ devices_.push_back(device);
+ }
+}
+
+void UsbChooserDialogAndroid::OnDeviceRemoved(
+ scoped_refptr<device::UsbDevice> device) {
+ auto it = std::find(devices_.begin(), devices_.end(), device);
+ if (it != devices_.end()) {
+ RemoveDeviceFromChooserDialog(device);
+ devices_.erase(it);
+ }
+}
+
+void UsbChooserDialogAndroid::Select(const std::string& guid) {
+ for (size_t i = 0; i < devices_.size(); ++i) {
+ if (devices_[i]->guid() == guid) {
+ content::WebContents* web_contents =
+ content::WebContents::FromRenderFrameHost(render_frame_host_);
+ GURL embedding_origin =
+ web_contents->GetMainFrame()->GetLastCommittedURL().GetOrigin();
+ Profile* profile =
+ Profile::FromBrowserContext(web_contents->GetBrowserContext());
+ UsbChooserContext* chooser_context =
+ UsbChooserContextFactory::GetForProfile(profile);
+ chooser_context->GrantDevicePermission(
+ render_frame_host_->GetLastCommittedURL().GetOrigin(),
+ embedding_origin, devices_[i]->guid());
+ device::usb::DeviceInfoPtr device_info_ptr =
+ device::usb::DeviceInfo::From(*devices_[i]);
+ callback_.Run(std::move(device_info_ptr));
+ callback_.reset(); // Reset |callback_| so that it is only run once.
+ Java_UsbChooserDialog_closeDialog(base::android::AttachCurrentThread(),
+ java_dialog_.obj());
+
+ RecordWebUsbChooserClosure(
+ devices_[i]->serial_number().empty()
+ ? WEBUSB_CHOOSER_CLOSED_EPHEMERAL_PERMISSION_GRANTED
+ : WEBUSB_CHOOSER_CLOSED_PERMISSION_GRANTED);
+ }
+ }
+}
+
+void UsbChooserDialogAndroid::Cancel() {
+ callback_.Run(nullptr);
+ callback_.reset(); // Reset |callback_| so that it is only run once.
+ Java_UsbChooserDialog_closeDialog(base::android::AttachCurrentThread(),
+ java_dialog_.obj());
+
+ RecordWebUsbChooserClosure(devices_.size() == 0
+ ? WEBUSB_CHOOSER_CLOSED_CANCELLED_NO_DEVICES
+ : WEBUSB_CHOOSER_CLOSED_CANCELLED);
+}
+
+void UsbChooserDialogAndroid::OnItemSelected(
+ JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ const base::android::JavaParamRef<jstring>& device_id) {
+ Select(base::android::ConvertJavaStringToUTF8(env, device_id));
+}
+
+void UsbChooserDialogAndroid::OnDialogCancelled(
+ JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj) {
+ Cancel();
+}
+
+void UsbChooserDialogAndroid::LoadUsbHelpPage(
+ JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj) {
+ OpenUrl(chrome::kChooserUsbOverviewURL);
+}
+
+// Get a list of devices that can be shown in the chooser bubble UI for
+// user to grant permsssion.
+void UsbChooserDialogAndroid::GotUsbDeviceList(
+ const std::vector<scoped_refptr<device::UsbDevice>>& devices) {
+ for (const auto& device : devices) {
+ if (device::UsbDeviceFilter::MatchesAny(device, filters_) &&
+ FindInWebUsbAllowedOrigins(
+ device->webusb_allowed_origins(),
+ render_frame_host_->GetLastCommittedURL().GetOrigin())) {
+ AddDeviceToChooserDialog(device);
+ devices_.push_back(device);
+ }
+ }
+
+ JNIEnv* env = base::android::AttachCurrentThread();
+ Java_UsbChooserDialog_setIdleState(env, java_dialog_.obj());
+}
+
+void UsbChooserDialogAndroid::AddDeviceToChooserDialog(
+ scoped_refptr<device::UsbDevice> device) const {
+ JNIEnv* env = base::android::AttachCurrentThread();
+ base::android::ScopedJavaLocalRef<jstring> device_guid =
+ base::android::ConvertUTF8ToJavaString(env, device->guid());
+ base::android::ScopedJavaLocalRef<jstring> device_name =
+ base::android::ConvertUTF16ToJavaString(env, device->product_string());
+ Java_UsbChooserDialog_addDevice(env, java_dialog_.obj(), device_guid.obj(),
+ device_name.obj());
+}
+
+void UsbChooserDialogAndroid::RemoveDeviceFromChooserDialog(
+ scoped_refptr<device::UsbDevice> device) const {
+ JNIEnv* env = base::android::AttachCurrentThread();
+ base::android::ScopedJavaLocalRef<jstring> device_guid =
+ base::android::ConvertUTF8ToJavaString(env, device->guid());
+ base::android::ScopedJavaLocalRef<jstring> device_name =
+ base::android::ConvertUTF16ToJavaString(env, device->product_string());
+ Java_UsbChooserDialog_removeDevice(env, java_dialog_.obj(), device_guid.obj(),
+ device_name.obj());
+}
+
+void UsbChooserDialogAndroid::OpenUrl(const std::string& url) {
+ content::WebContents::FromRenderFrameHost(render_frame_host_)
+ ->OpenURL(content::OpenURLParams(GURL(url), content::Referrer(),
+ NEW_FOREGROUND_TAB,
+ ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
+ false)); // is_renderer_initiated
+}
+
+// static
+bool UsbChooserDialogAndroid::Register(JNIEnv* env) {
+ return RegisterNativesImpl(env);
+}
diff --git a/chrome/browser/ui/android/usb_chooser_dialog_android.h b/chrome/browser/ui/android/usb_chooser_dialog_android.h
new file mode 100644
index 0000000..1b244df
--- /dev/null
+++ b/chrome/browser/ui/android/usb_chooser_dialog_android.h
@@ -0,0 +1,87 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_ANDROID_USB_CHOOSER_DIALOG_ANDROID_H_
+#define CHROME_BROWSER_UI_ANDROID_USB_CHOOSER_DIALOG_ANDROID_H_
+
+#include <string>
+#include <vector>
+
+#include "base/android/scoped_java_ref.h"
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "base/scoped_observer.h"
+#include "base/strings/string16.h"
+#include "device/usb/public/interfaces/chooser_service.mojom.h"
+#include "device/usb/usb_service.h"
+#include "mojo/public/cpp/bindings/array.h"
+
+namespace content {
+class RenderFrameHost;
+class WebContents;
+}
+
+namespace device {
+class UsbDevice;
+class UsbDeviceFilter;
+}
+
+// Represents a way to ask the user to select a USB device from a list of
+// options.
+class UsbChooserDialogAndroid : public device::UsbService::Observer {
+ public:
+ UsbChooserDialogAndroid(
+ mojo::Array<device::usb::DeviceFilterPtr> device_filters,
+ content::RenderFrameHost* render_frame_host,
+ const device::usb::ChooserService::GetPermissionCallback& callback);
+ ~UsbChooserDialogAndroid() override;
+
+ // device::UsbService::Observer:
+ void OnDeviceAdded(scoped_refptr<device::UsbDevice> device) override;
+ void OnDeviceRemoved(scoped_refptr<device::UsbDevice> device) override;
+
+ // Report the dialog's result.
+ void OnItemSelected(JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj,
+ const base::android::JavaParamRef<jstring>& device_id);
+ void OnDialogCancelled(JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj);
+
+ void LoadUsbHelpPage(JNIEnv* env,
+ const base::android::JavaParamRef<jobject>& obj);
+
+ static bool Register(JNIEnv* env);
+
+ private:
+ void GotUsbDeviceList(
+ const std::vector<scoped_refptr<device::UsbDevice>>& devices);
+
+ // Called when the user selects the USB device with |guid| from the chooser
+ // dialog.
+ void Select(const std::string& guid);
+ // Called when the chooser dialog is closed.
+ void Cancel();
+
+ void AddDeviceToChooserDialog(scoped_refptr<device::UsbDevice> device) const;
+ void RemoveDeviceFromChooserDialog(
+ scoped_refptr<device::UsbDevice> device) const;
+
+ void OpenUrl(const std::string& url);
+
+ content::RenderFrameHost* const render_frame_host_;
+ device::usb::ChooserService::GetPermissionCallback callback_;
+ ScopedObserver<device::UsbService, device::UsbService::Observer>
+ usb_service_observer_;
+ std::vector<device::UsbDeviceFilter> filters_;
+
+ std::vector<scoped_refptr<device::UsbDevice>> devices_;
+
+ base::android::ScopedJavaGlobalRef<jobject> java_dialog_;
+ base::WeakPtrFactory<UsbChooserDialogAndroid> weak_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(UsbChooserDialogAndroid);
+};
+
+#endif // CHROME_BROWSER_UI_ANDROID_USB_CHOOSER_DIALOG_ANDROID_H_
diff --git a/chrome/browser/usb/usb_chooser_bubble_controller.cc b/chrome/browser/usb/usb_chooser_bubble_controller.cc
index 8d2b3f6..23d6f52 100644
--- a/chrome/browser/usb/usb_chooser_bubble_controller.cc
+++ b/chrome/browser/usb/usb_chooser_bubble_controller.cc
@@ -8,11 +8,10 @@
#include <utility>
#include "base/bind.h"
-#include "base/metrics/histogram_macros.h"
-#include "base/stl_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/usb/usb_chooser_context.h"
#include "chrome/browser/usb/usb_chooser_context_factory.h"
+#include "chrome/browser/usb/web_usb_histograms.h"
#include "chrome/common/url_constants.h"
#include "components/bubble/bubble_controller.h"
#include "content/public/browser/render_frame_host.h"
@@ -21,58 +20,9 @@
#include "device/usb/mojo/type_converters.h"
#include "device/usb/usb_device.h"
#include "device/usb/usb_device_filter.h"
+#include "device/usb/webusb_descriptors.h"
#include "url/gurl.h"
-namespace {
-
-// Reasons the chooser may be closed. These are used in histograms so do not
-// remove/reorder entries. Only add at the end just before
-// WEBUSB_CHOOSER_CLOSED_MAX. Also remember to update the enum listing in
-// tools/metrics/histograms/histograms.xml.
-enum WebUsbChooserClosed {
- // The user cancelled the permission prompt without selecting a device.
- WEBUSB_CHOOSER_CLOSED_CANCELLED = 0,
- // The user probably cancelled the permission prompt without selecting a
- // device because there were no devices to select.
- WEBUSB_CHOOSER_CLOSED_CANCELLED_NO_DEVICES,
- // The user granted permission to access a device.
- WEBUSB_CHOOSER_CLOSED_PERMISSION_GRANTED,
- // The user granted permission to access a device but that permission will be
- // revoked when the device is disconnected.
- WEBUSB_CHOOSER_CLOSED_EPHEMERAL_PERMISSION_GRANTED,
- // Maximum value for the enum.
- WEBUSB_CHOOSER_CLOSED_MAX
-};
-
-void RecordChooserClosure(WebUsbChooserClosed disposition) {
- UMA_HISTOGRAM_ENUMERATION("WebUsb.ChooserClosed", disposition,
- WEBUSB_CHOOSER_CLOSED_MAX);
-}
-
-// Check if the origin is allowed.
-bool FindInAllowedOrigins(const device::WebUsbAllowedOrigins* allowed_origins,
- const GURL& origin) {
- if (!allowed_origins)
- return false;
-
- if (ContainsValue(allowed_origins->origins, origin))
- return true;
-
- for (const auto& config : allowed_origins->configurations) {
- if (ContainsValue(config.origins, origin))
- return true;
-
- for (const auto& function : config.functions) {
- if (ContainsValue(function.origins, origin))
- return true;
- }
- }
-
- return false;
-}
-
-} // namespace
-
UsbChooserBubbleController::UsbChooserBubbleController(
content::RenderFrameHost* owner,
mojo::Array<device::usb::DeviceFilterPtr> device_filters,
@@ -133,18 +83,19 @@ void UsbChooserBubbleController::Select(size_t index) {
callback_.Run(std::move(device_info_ptr));
callback_.reset(); // Reset |callback_| so that it is only run once.
- RecordChooserClosure(devices_[index].first->serial_number().empty()
- ? WEBUSB_CHOOSER_CLOSED_EPHEMERAL_PERMISSION_GRANTED
- : WEBUSB_CHOOSER_CLOSED_PERMISSION_GRANTED);
+ RecordWebUsbChooserClosure(
+ devices_[index].first->serial_number().empty()
+ ? WEBUSB_CHOOSER_CLOSED_EPHEMERAL_PERMISSION_GRANTED
+ : WEBUSB_CHOOSER_CLOSED_PERMISSION_GRANTED);
if (bubble_reference_)
bubble_reference_->CloseBubble(BUBBLE_CLOSE_ACCEPTED);
}
void UsbChooserBubbleController::Cancel() {
- RecordChooserClosure(devices_.size() == 0
- ? WEBUSB_CHOOSER_CLOSED_CANCELLED_NO_DEVICES
- : WEBUSB_CHOOSER_CLOSED_CANCELLED);
+ RecordWebUsbChooserClosure(devices_.size() == 0
+ ? WEBUSB_CHOOSER_CLOSED_CANCELLED_NO_DEVICES
+ : WEBUSB_CHOOSER_CLOSED_CANCELLED);
if (bubble_reference_)
bubble_reference_->CloseBubble(BUBBLE_CLOSE_CANCELED);
@@ -155,7 +106,7 @@ void UsbChooserBubbleController::Close() {}
void UsbChooserBubbleController::OnDeviceAdded(
scoped_refptr<device::UsbDevice> device) {
if (device::UsbDeviceFilter::MatchesAny(device, filters_) &&
- FindInAllowedOrigins(
+ FindInWebUsbAllowedOrigins(
device->webusb_allowed_origins(),
render_frame_host_->GetLastCommittedURL().GetOrigin())) {
devices_.push_back(std::make_pair(device, device->product_string()));
@@ -187,7 +138,7 @@ void UsbChooserBubbleController::GotUsbDeviceList(
const std::vector<scoped_refptr<device::UsbDevice>>& devices) {
for (const auto& device : devices) {
if (device::UsbDeviceFilter::MatchesAny(device, filters_) &&
- FindInAllowedOrigins(
+ FindInWebUsbAllowedOrigins(
device->webusb_allowed_origins(),
render_frame_host_->GetLastCommittedURL().GetOrigin())) {
devices_.push_back(std::make_pair(device, device->product_string()));
diff --git a/chrome/browser/usb/usb_tab_helper.cc b/chrome/browser/usb/usb_tab_helper.cc
index a0f0998..0f7f989 100644
--- a/chrome/browser/usb/usb_tab_helper.cc
+++ b/chrome/browser/usb/usb_tab_helper.cc
@@ -7,10 +7,15 @@
#include <utility>
#include "base/memory/scoped_ptr.h"
-#include "chrome/browser/usb/web_usb_chooser_service.h"
#include "chrome/browser/usb/web_usb_permission_provider.h"
#include "device/usb/mojo/device_manager_impl.h"
+#if defined(OS_ANDROID)
+#include "chrome/browser/android/usb/web_usb_chooser_service_android.h"
+#else
+#include "chrome/browser/usb/web_usb_chooser_service.h"
+#endif // defined(OS_ANDROID)
+
using content::RenderFrameHost;
using content::WebContents;
@@ -18,7 +23,11 @@ DEFINE_WEB_CONTENTS_USER_DATA_KEY(UsbTabHelper);
struct FrameUsbServices {
scoped_ptr<WebUSBPermissionProvider> permission_provider;
+#if defined(OS_ANDROID)
+ scoped_ptr<WebUsbChooserServiceAndroid> chooser_service;
+#else
scoped_ptr<WebUsbChooserService> chooser_service;
+#endif // defined(OS_ANDROID)
};
// static
@@ -42,13 +51,11 @@ void UsbTabHelper::CreateDeviceManager(
GetPermissionProvider(render_frame_host), std::move(request));
}
-#if !defined(OS_ANDROID)
void UsbTabHelper::CreateChooserService(
content::RenderFrameHost* render_frame_host,
mojo::InterfaceRequest<device::usb::ChooserService> request) {
GetChooserService(render_frame_host, std::move(request));
}
-#endif // !defined(OS_ANDROID)
UsbTabHelper::UsbTabHelper(WebContents* web_contents)
: content::WebContentsObserver(web_contents) {}
@@ -80,15 +87,17 @@ UsbTabHelper::GetPermissionProvider(RenderFrameHost* render_frame_host) {
return frame_usb_services->permission_provider->GetWeakPtr();
}
-#if !defined(OS_ANDROID)
void UsbTabHelper::GetChooserService(
content::RenderFrameHost* render_frame_host,
mojo::InterfaceRequest<device::usb::ChooserService> request) {
FrameUsbServices* frame_usb_services = GetFrameUsbService(render_frame_host);
if (!frame_usb_services->chooser_service) {
frame_usb_services->chooser_service.reset(
+#if defined(OS_ANDROID)
+ new WebUsbChooserServiceAndroid(render_frame_host));
+#else
new WebUsbChooserService(render_frame_host));
+#endif // defined(OS_ANDROID)
}
frame_usb_services->chooser_service->Bind(std::move(request));
}
-#endif // !defined(OS_ANDROID)
diff --git a/chrome/browser/usb/usb_tab_helper.h b/chrome/browser/usb/usb_tab_helper.h
index 1771981..71eed59 100644
--- a/chrome/browser/usb/usb_tab_helper.h
+++ b/chrome/browser/usb/usb_tab_helper.h
@@ -38,11 +38,9 @@ class UsbTabHelper : public content::WebContentsObserver,
content::RenderFrameHost* render_frame_host,
mojo::InterfaceRequest<device::usb::DeviceManager> request);
-#if !defined(OS_ANDROID)
void CreateChooserService(
content::RenderFrameHost* render_frame_host,
mojo::InterfaceRequest<device::usb::ChooserService> request);
-#endif // !defined(OS_ANDROID)
private:
explicit UsbTabHelper(content::WebContents* web_contents);
@@ -57,11 +55,9 @@ class UsbTabHelper : public content::WebContentsObserver,
base::WeakPtr<device::usb::PermissionProvider> GetPermissionProvider(
content::RenderFrameHost* render_frame_host);
-#if !defined(OS_ANDROID)
void GetChooserService(
content::RenderFrameHost* render_frame_host,
mojo::InterfaceRequest<device::usb::ChooserService> request);
-#endif // !defined(OS_ANDROID)
FrameUsbServicesMap frame_usb_services_;
diff --git a/chrome/browser/usb/web_usb_histograms.cc b/chrome/browser/usb/web_usb_histograms.cc
new file mode 100644
index 0000000..a330c7a
--- /dev/null
+++ b/chrome/browser/usb/web_usb_histograms.cc
@@ -0,0 +1,12 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/usb/web_usb_histograms.h"
+
+#include "base/metrics/histogram_macros.h"
+
+void RecordWebUsbChooserClosure(WebUsbChooserClosed disposition) {
+ UMA_HISTOGRAM_ENUMERATION("WebUsb.ChooserClosed", disposition,
+ WEBUSB_CHOOSER_CLOSED_MAX);
+}
diff --git a/chrome/browser/usb/web_usb_histograms.h b/chrome/browser/usb/web_usb_histograms.h
new file mode 100644
index 0000000..84805b3
--- /dev/null
+++ b/chrome/browser/usb/web_usb_histograms.h
@@ -0,0 +1,29 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_USB_WEB_USB_HISTOGRAMS_H_
+#define CHROME_BROWSER_USB_WEB_USB_HISTOGRAMS_H_
+
+// Reasons the chooser may be closed. These are used in histograms so do not
+// remove/reorder entries. Only add at the end just before
+// WEBUSB_CHOOSER_CLOSED_MAX. Also remember to update the enum listing in
+// tools/metrics/histograms/histograms.xml.
+enum WebUsbChooserClosed {
+ // The user cancelled the permission prompt without selecting a device.
+ WEBUSB_CHOOSER_CLOSED_CANCELLED = 0,
+ // The user probably cancelled the permission prompt without selecting a
+ // device because there were no devices to select.
+ WEBUSB_CHOOSER_CLOSED_CANCELLED_NO_DEVICES,
+ // The user granted permission to access a device.
+ WEBUSB_CHOOSER_CLOSED_PERMISSION_GRANTED,
+ // The user granted permission to access a device but that permission will be
+ // revoked when the device is disconnected.
+ WEBUSB_CHOOSER_CLOSED_EPHEMERAL_PERMISSION_GRANTED,
+ // Maximum value for the enum.
+ WEBUSB_CHOOSER_CLOSED_MAX
+};
+
+void RecordWebUsbChooserClosure(WebUsbChooserClosed disposition);
+
+#endif // CHROME_BROWSER_USB_WEB_USB_HISTOGRAMS_H_
diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi
index e8e18c2..71ae3db 100644
--- a/chrome/chrome_browser.gypi
+++ b/chrome/chrome_browser.gypi
@@ -656,6 +656,8 @@
'browser/usb/usb_chooser_context_factory.h',
'browser/usb/usb_tab_helper.cc',
'browser/usb/usb_tab_helper.h',
+ 'browser/usb/web_usb_histograms.cc',
+ 'browser/usb/web_usb_histograms.h',
'browser/usb/web_usb_permission_provider.cc',
'browser/usb/web_usb_permission_provider.h',
'browser/web_data_service_factory.cc',
@@ -941,6 +943,8 @@
'browser/android/thumbnail/thumbnail_cache.h',
'browser/android/url_utilities.cc',
'browser/android/url_utilities.h',
+ 'browser/android/usb/web_usb_chooser_service_android.cc',
+ 'browser/android/usb/web_usb_chooser_service_android.h',
'browser/android/voice_search_tab_helper.cc',
'browser/android/voice_search_tab_helper.h',
'browser/android/warmup_manager.cc',
@@ -1990,6 +1994,7 @@
'android/java/src/org/chromium/chrome/browser/tabmodel/TabModelJniBridge.java',
'android/java/src/org/chromium/chrome/browser/TabState.java',
'android/java/src/org/chromium/chrome/browser/TtsPlatformImpl.java',
+ 'android/java/src/org/chromium/chrome/browser/UsbChooserDialog.java',
'android/java/src/org/chromium/chrome/browser/util/AccessibilityUtil.java',
'android/java/src/org/chromium/chrome/browser/util/FeatureUtilities.java',
'android/java/src/org/chromium/chrome/browser/util/PlatformUtil.java',
diff --git a/chrome/chrome_browser_ui.gypi b/chrome/chrome_browser_ui.gypi
index 75fa4985..2392f29 100644
--- a/chrome/chrome_browser_ui.gypi
+++ b/chrome/chrome_browser_ui.gypi
@@ -489,6 +489,8 @@
'browser/ui/android/tab_model/tab_model_list.h',
'browser/ui/android/toolbar/toolbar_model_android.cc',
'browser/ui/android/toolbar/toolbar_model_android.h',
+ 'browser/ui/android/usb_chooser_dialog_android.cc',
+ 'browser/ui/android/usb_chooser_dialog_android.h',
'browser/ui/android/view_android_helper.cc',
'browser/ui/android/view_android_helper.h',
'browser/ui/android/website_settings_popup_android.cc',
diff --git a/device/usb/webusb_descriptors.cc b/device/usb/webusb_descriptors.cc
index de2309e..bdd23a7 100644
--- a/device/usb/webusb_descriptors.cc
+++ b/device/usb/webusb_descriptors.cc
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include "device/usb/webusb_descriptors.h"
+
#include <stddef.h>
#include <iterator>
@@ -12,9 +14,9 @@
#include "base/bind.h"
#include "base/callback.h"
#include "base/logging.h"
+#include "base/stl_util.h"
#include "components/device_event_log/device_event_log.h"
#include "device/usb/usb_device_handle.h"
-#include "device/usb/webusb_descriptors.h"
#include "net/base/io_buffer.h"
using net::IOBufferWithSize;
@@ -567,4 +569,26 @@ void ReadWebUsbDescriptors(scoped_refptr<UsbDeviceHandle> device_handle,
base::Bind(&OnReadBosDescriptorHeader, device_handle, callback));
}
+bool FindInWebUsbAllowedOrigins(
+ const device::WebUsbAllowedOrigins* allowed_origins,
+ const GURL& origin) {
+ if (!allowed_origins)
+ return false;
+
+ if (ContainsValue(allowed_origins->origins, origin))
+ return true;
+
+ for (const auto& config : allowed_origins->configurations) {
+ if (ContainsValue(config.origins, origin))
+ return true;
+
+ for (const auto& function : config.functions) {
+ if (ContainsValue(function.origins, origin))
+ return true;
+ }
+ }
+
+ return false;
+}
+
} // namespace device
diff --git a/device/usb/webusb_descriptors.h b/device/usb/webusb_descriptors.h
index 516adfc..a13440c 100644
--- a/device/usb/webusb_descriptors.h
+++ b/device/usb/webusb_descriptors.h
@@ -67,6 +67,11 @@ void ReadWebUsbDescriptors(
const base::Callback<void(scoped_ptr<WebUsbAllowedOrigins> allowed_origins,
const GURL& landing_page)>& callback);
+// Check if the origin is allowed.
+bool FindInWebUsbAllowedOrigins(
+ const device::WebUsbAllowedOrigins* allowed_origins,
+ const GURL& origin);
+
} // device
#endif // DEVICE_USB_WEBUSB_DESCRIPTORS_H_
diff --git a/ui/android/BUILD.gn b/ui/android/BUILD.gn
index 3e356ce..fc4c550 100644
--- a/ui/android/BUILD.gn
+++ b/ui/android/BUILD.gn
@@ -194,6 +194,7 @@ android_library("ui_java") {
"java/src/org/chromium/ui/resources/statics/StaticResource.java",
"java/src/org/chromium/ui/resources/statics/StaticResourceLoader.java",
"java/src/org/chromium/ui/resources/system/SystemResourceLoader.java",
+ "java/src/org/chromium/ui/text/NoUnderlineClickableSpan.java",
"java/src/org/chromium/ui/text/SpanApplier.java",
"java/src/org/chromium/ui/widget/ButtonCompat.java",
"java/src/org/chromium/ui/widget/TextViewWithClickableSpans.java",
diff --git a/ui/android/java/src/org/chromium/ui/text/NoUnderlineClickableSpan.java b/ui/android/java/src/org/chromium/ui/text/NoUnderlineClickableSpan.java
new file mode 100644
index 0000000..ab8a74a
--- /dev/null
+++ b/ui/android/java/src/org/chromium/ui/text/NoUnderlineClickableSpan.java
@@ -0,0 +1,20 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.ui.text;
+
+import android.text.TextPaint;
+import android.text.style.ClickableSpan;
+
+/**
+* Show a clickable link with underlines turned off.
+*/
+public abstract class NoUnderlineClickableSpan extends ClickableSpan {
+ // Disable underline on the link text.
+ @Override
+ public void updateDrawState(TextPaint textPaint) {
+ super.updateDrawState(textPaint);
+ textPaint.setUnderlineText(false);
+ }
+}