diff options
author | juncai <juncai@chromium.org> | 2016-03-25 13:40:35 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2016-03-25 20:42:19 +0000 |
commit | 4cbad6ed5522e60cda2aa728c08e3aa80545e360 (patch) | |
tree | e6b54b14c22d141fc37623be9bfccdb4543e7bf4 | |
parent | cf4acce2eb7d4b7fc83a444af5d0a038151a1fa7 (diff) | |
download | chromium_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}
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"><link></ph>Get help<ph name="END_LINK"></link></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"><link></ph>Get help<ph name="END_LINK"></link></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"><link></ph>Get help<ph name="END_LINK"></link></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); + } +} |