blob: df15c1638c115883800da2309259c17a7c314a7d [file] [log] [blame]
// 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.
package org.chromium.chrome.browser;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.text.SpannableString;
import android.text.TextUtils;
import android.view.View;
import org.chromium.base.VisibleForTesting;
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.components.location.LocationUtils;
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 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
* call).
*/
public class BluetoothChooserDialog
implements ItemChooserDialog.ItemSelectedCallback, WindowAndroid.PermissionCallback {
// These constants match BluetoothChooserAndroid::ShowDiscoveryState, and are used in
// notifyDiscoveryState().
static final int DISCOVERY_FAILED_TO_START = 0;
static final int DISCOVERING = 1;
static final int DISCOVERY_IDLE = 2;
// Values passed to nativeOnDialogFinished:eventType, and only used in the native function.
static final int DIALOG_FINISHED_DENIED_PERMISSION = 0;
static final int DIALOG_FINISHED_CANCELLED = 1;
static final int DIALOG_FINISHED_SELECTED = 2;
// The window that owns this dialog.
final WindowAndroid mWindowAndroid;
// Always equal to mWindowAndroid.getActivity().get(), but stored separately to make sure it's
// not GC'ed.
final Activity mActivity;
// The dialog to show to let the user pick a device.
ItemChooserDialog mItemChooserDialog;
// The origin for the site wanting to pair with the bluetooth devices.
String mOrigin;
// The security level of the connection to the site wanting to pair with the
// bluetooth devices. For valid values see SecurityStateModel::SecurityLevel.
int mSecurityLevel;
// A pointer back to the native part of the implementation for this dialog.
long mNativeBluetoothChooserDialogPtr;
// The type of link that is shown within the dialog.
private enum LinkType {
EXPLAIN_BLUETOOTH,
ADAPTER_OFF,
ADAPTER_OFF_HELP,
REQUEST_LOCATION_PERMISSION,
NEED_LOCATION_PERMISSION_HELP,
RESTART_SEARCH,
}
/**
* Creates the BluetoothChooserDialog.
*/
@VisibleForTesting
BluetoothChooserDialog(WindowAndroid windowAndroid, String origin, int securityLevel,
long nativeBluetoothChooserDialogPtr) {
mWindowAndroid = windowAndroid;
mActivity = windowAndroid.getActivity().get();
assert mActivity != null;
mOrigin = origin;
mSecurityLevel = securityLevel;
mNativeBluetoothChooserDialogPtr = nativeBluetoothChooserDialogPtr;
}
/**
* Show the BluetoothChooserDialog.
*/
@VisibleForTesting
void show() {
// Emphasize the origin.
Profile profile = Profile.getLastUsedProfile();
SpannableString origin = new SpannableString(mOrigin);
OmniboxUrlEmphasizer.emphasizeUrl(
origin, mActivity.getResources(), profile, mSecurityLevel, false, true, true);
// Construct a full string and replace the origin text with emphasized version.
SpannableString title =
new SpannableString(mActivity.getString(R.string.bluetooth_dialog_title, mOrigin));
int start = title.toString().indexOf(mOrigin);
TextUtils.copySpansFrom(origin, 0, origin.length(), Object.class, title, start);
String message = mActivity.getString(R.string.bluetooth_not_found);
SpannableString noneFound = SpanApplier.applySpans(
message, new SpanInfo("<link>", "</link>",
new BluetoothClickableSpan(LinkType.RESTART_SEARCH, mActivity)));
SpannableString searching = SpanApplier.applySpans(
mActivity.getString(R.string.bluetooth_searching),
new SpanInfo("<link>", "</link>",
new BluetoothClickableSpan(LinkType.EXPLAIN_BLUETOOTH, mActivity)));
String positiveButton = mActivity.getString(R.string.bluetooth_confirm_button);
SpannableString statusIdleNoneFound = SpanApplier.applySpans(
mActivity.getString(R.string.bluetooth_not_seeing_it_idle_none_found),
new SpanInfo("<link>", "</link>",
new BluetoothClickableSpan(LinkType.EXPLAIN_BLUETOOTH, mActivity)));
SpannableString statusIdleSomeFound = SpanApplier.applySpans(
mActivity.getString(R.string.bluetooth_not_seeing_it_idle_some_found),
new SpanInfo("<link1>", "</link1>",
new BluetoothClickableSpan(LinkType.EXPLAIN_BLUETOOTH, mActivity)),
new SpanInfo("<link2>", "</link2>",
new BluetoothClickableSpan(LinkType.RESTART_SEARCH, mActivity)));
ItemChooserDialog.ItemChooserLabels labels =
new ItemChooserDialog.ItemChooserLabels(title, searching, noneFound,
statusIdleNoneFound, statusIdleSomeFound, positiveButton);
mItemChooserDialog = new ItemChooserDialog(mActivity, this, labels);
}
@Override
public void onItemSelected(String id) {
if (mNativeBluetoothChooserDialogPtr != 0) {
if (id.isEmpty()) {
nativeOnDialogFinished(
mNativeBluetoothChooserDialogPtr, DIALOG_FINISHED_CANCELLED, "");
} else {
nativeOnDialogFinished(
mNativeBluetoothChooserDialogPtr, DIALOG_FINISHED_SELECTED, id);
}
}
}
@Override
public void onRequestPermissionsResult(String[] permissions, int[] grantResults) {
for (int i = 0; i < grantResults.length; i++) {
if (permissions[i].equals(Manifest.permission.ACCESS_COARSE_LOCATION)) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
mItemChooserDialog.clear();
nativeRestartSearch(mNativeBluetoothChooserDialogPtr);
} else {
checkLocationPermission();
}
return;
}
}
// If the location permission is not present, leave the currently-shown message in place.
}
private void checkLocationPermission() {
if (LocationUtils.getInstance().hasAndroidLocationPermission(mActivity)) {
return;
}
if (!mWindowAndroid.canRequestPermission(Manifest.permission.ACCESS_COARSE_LOCATION)) {
if (mNativeBluetoothChooserDialogPtr != 0) {
nativeOnDialogFinished(
mNativeBluetoothChooserDialogPtr, DIALOG_FINISHED_DENIED_PERMISSION, "");
return;
}
}
SpannableString needLocationMessage = SpanApplier.applySpans(
mActivity.getString(R.string.bluetooth_need_location_permission),
new SpanInfo("<link>", "</link>",
new BluetoothClickableSpan(
LinkType.REQUEST_LOCATION_PERMISSION, mActivity)));
SpannableString needLocationStatus = SpanApplier.applySpans(
mActivity.getString(R.string.bluetooth_need_location_permission_help),
new SpanInfo("<link>", "</link>",
new BluetoothClickableSpan(
LinkType.NEED_LOCATION_PERMISSION_HELP, mActivity)));
mItemChooserDialog.setErrorState(needLocationMessage, needLocationStatus);
}
private class BluetoothClickableSpan extends NoUnderlineClickableSpan {
// The type of link this span represents.
private LinkType mLinkType;
private Context mContext;
BluetoothClickableSpan(LinkType linkType, Context context) {
mLinkType = linkType;
mContext = context;
}
@Override
public void onClick(View view) {
if (mNativeBluetoothChooserDialogPtr == 0) {
return;
}
switch (mLinkType) {
case EXPLAIN_BLUETOOTH: {
// No need to close the dialog here because
// ShowBluetoothOverviewLink will close it.
// TODO(ortuno): The BluetoothChooserDialog should dismiss
// itself when a new tab is opened or the current tab navigates.
// https://crbug.com/588127
nativeShowBluetoothOverviewLink(mNativeBluetoothChooserDialogPtr);
break;
}
case ADAPTER_OFF: {
Intent intent = new Intent();
intent.setAction(android.provider.Settings.ACTION_BLUETOOTH_SETTINGS);
mContext.startActivity(intent);
break;
}
case ADAPTER_OFF_HELP: {
nativeShowBluetoothAdapterOffLink(mNativeBluetoothChooserDialogPtr);
closeDialog();
break;
}
case REQUEST_LOCATION_PERMISSION: {
mWindowAndroid.requestPermissions(
new String[] {Manifest.permission.ACCESS_COARSE_LOCATION},
BluetoothChooserDialog.this);
break;
}
case NEED_LOCATION_PERMISSION_HELP: {
nativeShowNeedLocationPermissionLink(mNativeBluetoothChooserDialogPtr);
closeDialog();
break;
}
case RESTART_SEARCH: {
mItemChooserDialog.clear();
nativeRestartSearch(mNativeBluetoothChooserDialogPtr);
break;
}
default:
assert false;
}
// Get rid of the highlight background on selection.
view.invalidate();
}
}
@CalledByNative
private static BluetoothChooserDialog create(WindowAndroid windowAndroid, String origin,
int securityLevel, long nativeBluetoothChooserDialogPtr) {
if (!LocationUtils.getInstance().hasAndroidLocationPermission(
windowAndroid.getActivity().get())
&& !windowAndroid.canRequestPermission(
Manifest.permission.ACCESS_COARSE_LOCATION)) {
// If we can't even ask for enough permission to scan for Bluetooth devices, don't open
// the dialog.
return null;
}
BluetoothChooserDialog dialog = new BluetoothChooserDialog(
windowAndroid, origin, securityLevel, nativeBluetoothChooserDialogPtr);
dialog.show();
return dialog;
}
@VisibleForTesting
@CalledByNative
void addDevice(String deviceId, String deviceName) {
mItemChooserDialog.addItemToList(
new ItemChooserDialog.ItemChooserRow(deviceId, deviceName));
}
@VisibleForTesting
@CalledByNative
void closeDialog() {
mNativeBluetoothChooserDialogPtr = 0;
mItemChooserDialog.dismiss();
}
@VisibleForTesting
@CalledByNative
void removeDevice(String deviceId) {
mItemChooserDialog.setEnabled(deviceId, false);
}
@VisibleForTesting
@CalledByNative
void notifyAdapterTurnedOff() {
SpannableString adapterOffMessage = SpanApplier.applySpans(
mActivity.getString(R.string.bluetooth_adapter_off),
new SpanInfo("<link>", "</link>",
new BluetoothClickableSpan(LinkType.ADAPTER_OFF, mActivity)));
SpannableString adapterOffStatus = SpanApplier.applySpans(
mActivity.getString(R.string.bluetooth_adapter_off_help),
new SpanInfo("<link>", "</link>",
new BluetoothClickableSpan(LinkType.ADAPTER_OFF_HELP, mActivity)));
mItemChooserDialog.setErrorState(adapterOffMessage, adapterOffStatus);
}
@CalledByNative
private void notifyAdapterTurnedOn() {
mItemChooserDialog.clear();
}
@VisibleForTesting
@CalledByNative
void notifyDiscoveryState(int discoveryState) {
switch (discoveryState) {
case DISCOVERY_FAILED_TO_START: {
// FAILED_TO_START might be caused by a missing Location permission.
// Check, and show a request if so.
checkLocationPermission();
break;
}
case DISCOVERY_IDLE: {
mItemChooserDialog.setIdleState();
break;
}
default: {
// TODO(jyasskin): Report the new state to the user.
break;
}
}
}
@VisibleForTesting
native void nativeOnDialogFinished(
long nativeBluetoothChooserAndroid, int eventType, String deviceId);
@VisibleForTesting
native void nativeRestartSearch(long nativeBluetoothChooserAndroid);
// Help links.
@VisibleForTesting
native void nativeShowBluetoothOverviewLink(long nativeBluetoothChooserAndroid);
@VisibleForTesting
native void nativeShowBluetoothAdapterOffLink(long nativeBluetoothChooserAndroid);
@VisibleForTesting
native void nativeShowNeedLocationPermissionLink(long nativeBluetoothChooserAndroid);
}