/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.toolbar; import android.app.Activity; import android.content.Intent; import android.graphics.drawable.Drawable; import android.speech.RecognizerIntent; import android.widget.Button; import android.widget.ImageButton; import org.mozilla.gecko.ActivityHandlerHelper; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoSharedPrefs; import org.mozilla.gecko.R; import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.animation.PropertyAnimator; import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener; import org.mozilla.gecko.preferences.GeckoPreferences; import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener; import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener; import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener; import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState; import org.mozilla.gecko.util.ActivityResultHandler; import org.mozilla.gecko.util.DrawableUtil; import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.util.StringUtils; import org.mozilla.gecko.util.InputOptionsUtils; import org.mozilla.gecko.widget.themed.ThemedLinearLayout; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.ImageView; import java.util.List; /** * {@code ToolbarEditLayout} is the UI for when the toolbar is in * edit state. It controls a text entry ({@code ToolbarEditText}) * and its matching 'go' button which changes depending on the * current type of text in the entry. */ public class ToolbarEditLayout extends ThemedLinearLayout { public interface OnSearchStateChangeListener { public void onSearchStateChange(boolean isActive); } private final ImageView mSearchIcon; private final ToolbarEditText mEditText; private final ImageButton mVoiceInput; private final ImageButton mQrCode; private OnFocusChangeListener mFocusChangeListener; private boolean showKeyboardOnFocus = false; // Indicates if we need to show the keyboard after the app resumes public ToolbarEditLayout(Context context, AttributeSet attrs) { super(context, attrs); setOrientation(HORIZONTAL); LayoutInflater.from(context).inflate(R.layout.toolbar_edit_layout, this); mSearchIcon = (ImageView) findViewById(R.id.search_icon); mEditText = (ToolbarEditText) findViewById(R.id.url_edit_text); mVoiceInput = (ImageButton) findViewById(R.id.mic); mQrCode = (ImageButton) findViewById(R.id.qrcode); } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); if (HardwareUtils.isTablet()) { mSearchIcon.setVisibility(View.VISIBLE); } mEditText.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (mFocusChangeListener != null) { mFocusChangeListener.onFocusChange(ToolbarEditLayout.this, hasFocus); // Checking if voice and QR code input are enabled each time the user taps on the URL bar if (hasFocus) { if (voiceIsEnabled(getContext(), getResources().getString(R.string.voicesearch_prompt))) { mVoiceInput.setVisibility(View.VISIBLE); } else { mVoiceInput.setVisibility(View.GONE); } if (qrCodeIsEnabled(getContext())) { mQrCode.setVisibility(View.VISIBLE); } else { mQrCode.setVisibility(View.GONE); } } } } }); mEditText.setOnSearchStateChangeListener(new OnSearchStateChangeListener() { @Override public void onSearchStateChange(boolean isActive) { updateSearchIcon(isActive); } }); mVoiceInput.setOnClickListener(new Button.OnClickListener() { @Override public void onClick(View v) { launchVoiceRecognizer(); } }); mQrCode.setOnClickListener(new Button.OnClickListener() { @Override public void onClick(View v) { launchQRCodeReader(); } }); // Set an inactive search icon on tablet devices when in editing mode updateSearchIcon(false); } /** * Update the search icon at the left of the edittext based * on its state. * * @param isActive The state of the edittext. Active is when the initialized * text has changed and is not empty. */ void updateSearchIcon(boolean isActive) { if (!HardwareUtils.isTablet()) { return; } // When on tablet show a magnifying glass in editing mode final int searchDrawableId = R.drawable.search_icon_active; final Drawable searchDrawable; if (!isActive) { searchDrawable = DrawableUtil.tintDrawableWithColorRes(getContext(), searchDrawableId, R.color.placeholder_grey); } else { if (isPrivateMode()) { searchDrawable = DrawableUtil.tintDrawableWithColorRes(getContext(), searchDrawableId, R.color.tabs_tray_icon_grey); } else { searchDrawable = getResources().getDrawable(searchDrawableId); } } mSearchIcon.setImageDrawable(searchDrawable); } @Override public void setOnFocusChangeListener(OnFocusChangeListener listener) { mFocusChangeListener = listener; } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); mEditText.setEnabled(enabled); } @Override public void setPrivateMode(boolean isPrivate) { super.setPrivateMode(isPrivate); mEditText.setPrivateMode(isPrivate); } /** * Called when the parent gains focus (on app launch and resume) */ public void onParentFocus() { if (showKeyboardOnFocus) { showKeyboardOnFocus = false; Activity activity = GeckoAppShell.getGeckoInterface().getActivity(); activity.runOnUiThread(new Runnable() { public void run() { mEditText.requestFocus(); showSoftInput(); } }); } // Checking if qr code is supported after resuming the app if (qrCodeIsEnabled(getContext())) { mQrCode.setVisibility(View.VISIBLE); } else { mQrCode.setVisibility(View.GONE); } } void setToolbarPrefs(final ToolbarPrefs prefs) { mEditText.setToolbarPrefs(prefs); } private void showSoftInput() { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT); } void prepareShowAnimation(final PropertyAnimator animator) { if (animator == null) { mEditText.requestFocus(); showSoftInput(); return; } animator.addPropertyAnimationListener(new PropertyAnimationListener() { @Override public void onPropertyAnimationStart() { mEditText.requestFocus(); } @Override public void onPropertyAnimationEnd() { showSoftInput(); } }); } void setOnCommitListener(OnCommitListener listener) { mEditText.setOnCommitListener(listener); } void setOnDismissListener(OnDismissListener listener) { mEditText.setOnDismissListener(listener); } void setOnFilterListener(OnFilterListener listener) { mEditText.setOnFilterListener(listener); } void onEditSuggestion(String suggestion) { mEditText.setText(suggestion); mEditText.setSelection(mEditText.getText().length()); mEditText.requestFocus(); showSoftInput(); } void setText(String text) { mEditText.setText(text); } String getText() { return mEditText.getText().toString(); } protected void saveTabEditingState(final TabEditingState editingState) { editingState.lastEditingText = mEditText.getNonAutocompleteText(); editingState.selectionStart = mEditText.getSelectionStart(); editingState.selectionEnd = mEditText.getSelectionEnd(); } protected void restoreTabEditingState(final TabEditingState editingState) { mEditText.setText(editingState.lastEditingText); mEditText.setSelection(editingState.selectionStart, editingState.selectionEnd); } private boolean voiceIsEnabled(Context context, String prompt) { final boolean voiceIsSupported = InputOptionsUtils.supportsVoiceRecognizer(context, prompt); if (!voiceIsSupported) { return false; } return GeckoSharedPrefs.forApp(context) .getBoolean(GeckoPreferences.PREFS_VOICE_INPUT_ENABLED, true); } private void launchVoiceRecognizer() { Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "voice_input_launch"); final Intent intent = InputOptionsUtils.createVoiceRecognizerIntent(getResources().getString(R.string.voicesearch_prompt)); Activity activity = GeckoAppShell.getGeckoInterface().getActivity(); ActivityHandlerHelper.startIntentForActivity(activity, intent, new ActivityResultHandler() { @Override public void onActivityResult(int resultCode, Intent data) { if (resultCode != Activity.RESULT_OK) { return; } Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "voice_input_success"); // We have RESULT_OK, not RESULT_NO_MATCH so it should be safe to assume that // we have at least one match. We only need one: this will be // used for showing the user search engines with this search term in it. List voiceStrings = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); String text = voiceStrings.get(0); mEditText.setText(text); mEditText.setSelection(0, text.length()); final InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT); } }); } private boolean qrCodeIsEnabled(Context context) { final boolean qrCodeIsSupported = InputOptionsUtils.supportsQrCodeReader(context); if (!qrCodeIsSupported) { return false; } return GeckoSharedPrefs.forApp(context) .getBoolean(GeckoPreferences.PREFS_QRCODE_ENABLED, true); } private void launchQRCodeReader() { Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "qrcode_input_launch"); final Intent intent = InputOptionsUtils.createQRCodeReaderIntent(); Activity activity = GeckoAppShell.getGeckoInterface().getActivity(); ActivityHandlerHelper.startIntentForActivity(activity, intent, new ActivityResultHandler() { @Override public void onActivityResult(int resultCode, Intent intent) { if (resultCode == Activity.RESULT_OK) { String text = intent.getStringExtra("SCAN_RESULT"); if (!StringUtils.isSearchQuery(text, false)) { Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "qrcode_input_success"); mEditText.setText(text); mEditText.selectAll(); // Queuing up the keyboard show action. // At this point the app has not resumed yet, and trying to show // the keyboard will fail. showKeyboardOnFocus = true; } } // We can get the SCAN_RESULT_FORMAT, SCAN_RESULT_BYTES, // SCAN_RESULT_ORIENTATION and SCAN_RESULT_ERROR_CORRECTION_LEVEL // as well as the actual result, which may hold a URL. } }); } }