| // Copyright 2020 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.ntp.search; |
| |
| import android.content.Context; |
| import android.content.res.ColorStateList; |
| import android.graphics.Bitmap; |
| import android.graphics.drawable.Drawable; |
| import android.text.TextUtils; |
| import android.util.Pair; |
| import android.view.View.OnClickListener; |
| import android.view.ViewGroup; |
| |
| import androidx.annotation.ColorInt; |
| import androidx.core.graphics.drawable.RoundedBitmapDrawable; |
| |
| import org.chromium.chrome.browser.gsa.GSAState; |
| import org.chromium.chrome.browser.lens.LensController; |
| import org.chromium.chrome.browser.lens.LensEntryPoint; |
| import org.chromium.chrome.browser.lens.LensIntentParams; |
| import org.chromium.chrome.browser.lens.LensQueryParams; |
| import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher; |
| import org.chromium.chrome.browser.lifecycle.DestroyObserver; |
| import org.chromium.chrome.browser.lifecycle.NativeInitObserver; |
| import org.chromium.chrome.browser.omnibox.voice.AssistantVoiceSearchService; |
| import org.chromium.chrome.browser.preferences.SharedPreferencesManager; |
| import org.chromium.chrome.browser.profiles.Profile; |
| import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory; |
| import org.chromium.chrome.browser.signin.services.IdentityServicesProvider; |
| import org.chromium.components.browser_ui.styles.ChromeColors; |
| import org.chromium.components.externalauth.ExternalAuthUtils; |
| import org.chromium.components.signin.AccountManagerFacadeProvider; |
| import org.chromium.ui.base.ViewUtils; |
| import org.chromium.ui.base.WindowAndroid; |
| import org.chromium.ui.modelutil.PropertyModel; |
| import org.chromium.ui.modelutil.PropertyModelChangeProcessor; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| class SearchBoxMediator |
| implements DestroyObserver, NativeInitObserver, AssistantVoiceSearchService.Observer { |
| private final Context mContext; |
| private final PropertyModel mModel; |
| private final ViewGroup mView; |
| private final List<OnClickListener> mVoiceSearchClickListeners = new ArrayList<>(); |
| private final List<OnClickListener> mLensClickListeners = new ArrayList<>(); |
| private ActivityLifecycleDispatcher mActivityLifecycleDispatcher; |
| private AssistantVoiceSearchService mAssistantVoiceSearchService; |
| private SearchBoxChipDelegate mChipDelegate; |
| |
| /** Constructor. */ |
| SearchBoxMediator(Context context, PropertyModel model, ViewGroup view) { |
| mContext = context; |
| mModel = model; |
| mView = view; |
| PropertyModelChangeProcessor.create(mModel, mView, new SearchBoxViewBinder()); |
| } |
| |
| /** |
| * Initializes the SearchBoxContainerView with the given params. This must be called for |
| * classes that use the SearchBoxContainerView. |
| * |
| * @param activityLifecycleDispatcher Used to register for lifecycle events. |
| */ |
| void initialize(ActivityLifecycleDispatcher activityLifecycleDispatcher) { |
| assert mActivityLifecycleDispatcher == null; |
| mActivityLifecycleDispatcher = activityLifecycleDispatcher; |
| mActivityLifecycleDispatcher.register(this); |
| if (mActivityLifecycleDispatcher.isNativeInitializationFinished()) { |
| onFinishNativeInitialization(); |
| } |
| } |
| |
| @Override |
| public void onDestroy() { |
| if (mAssistantVoiceSearchService != null) { |
| mAssistantVoiceSearchService.destroy(); |
| mAssistantVoiceSearchService = null; |
| } |
| |
| if (mActivityLifecycleDispatcher != null) { |
| mActivityLifecycleDispatcher.unregister(this); |
| mActivityLifecycleDispatcher = null; |
| } |
| } |
| |
| @Override |
| public void onFinishNativeInitialization() { |
| mAssistantVoiceSearchService = new AssistantVoiceSearchService(mContext, |
| ExternalAuthUtils.getInstance(), TemplateUrlServiceFactory.get(), |
| GSAState.getInstance(mContext), this, SharedPreferencesManager.getInstance(), |
| IdentityServicesProvider.get().getIdentityManager( |
| Profile.getLastUsedRegularProfile()), |
| AccountManagerFacadeProvider.getInstance()); |
| onAssistantVoiceSearchServiceChanged(); |
| } |
| |
| @Override |
| public void onAssistantVoiceSearchServiceChanged() { |
| // Potential race condition between destroy and the observer, see crbug.com/1055274. |
| if (mAssistantVoiceSearchService == null) return; |
| |
| Drawable drawable = mAssistantVoiceSearchService.getCurrentMicDrawable(); |
| mModel.set(SearchBoxProperties.VOICE_SEARCH_DRAWABLE, drawable); |
| |
| final @ColorInt int primaryColor = ChromeColors.getDefaultThemeColor( |
| mContext.getResources(), false /* forceDarkBgColor= */); |
| ColorStateList colorStateList = |
| mAssistantVoiceSearchService.getButtonColorStateList(primaryColor, mContext); |
| mModel.set(SearchBoxProperties.VOICE_SEARCH_COLOR_STATE_LIST, colorStateList); |
| } |
| |
| /** Called to set a click listener for the search box. */ |
| void setSearchBoxClickListener(OnClickListener listener) { |
| mModel.set(SearchBoxProperties.SEARCH_BOX_CLICK_CALLBACK, v -> { |
| boolean isChipVisible = mModel.get(SearchBoxProperties.CHIP_VISIBILITY); |
| if (isChipVisible) { |
| String chipText = mModel.get(SearchBoxProperties.CHIP_TEXT); |
| mModel.set(SearchBoxProperties.SEARCH_TEXT, Pair.create(chipText, true)); |
| mChipDelegate.onCancelClicked(); |
| } |
| listener.onClick(v); |
| }); |
| } |
| |
| /** |
| * Called to add a click listener for the voice search button. |
| */ |
| void addVoiceSearchButtonClickListener(OnClickListener listener) { |
| boolean hasExistingListeners = !mVoiceSearchClickListeners.isEmpty(); |
| mVoiceSearchClickListeners.add(listener); |
| if (hasExistingListeners) return; |
| mModel.set(SearchBoxProperties.VOICE_SEARCH_CLICK_CALLBACK, v -> { |
| for (OnClickListener clickListener : mVoiceSearchClickListeners) { |
| clickListener.onClick(v); |
| } |
| }); |
| } |
| |
| /** |
| * Called to add a click listener for the voice search button. |
| */ |
| void addLensButtonClickListener(OnClickListener listener) { |
| boolean hasExistingListeners = !mLensClickListeners.isEmpty(); |
| mLensClickListeners.add(listener); |
| if (hasExistingListeners) return; |
| mModel.set(SearchBoxProperties.LENS_CLICK_CALLBACK, v -> { |
| for (OnClickListener clickListener : mLensClickListeners) { |
| clickListener.onClick(v); |
| } |
| }); |
| } |
| |
| /** |
| * Called to set or clear a chip on the search box. |
| * @param chipText The text to be shown on the chip. |
| */ |
| void setChipText(String chipText) { |
| boolean chipVisible = !TextUtils.isEmpty(chipText); |
| mModel.set(SearchBoxProperties.CHIP_VISIBILITY, chipVisible); |
| mModel.set(SearchBoxProperties.SEARCH_HINT_VISIBILITY, !chipVisible); |
| mModel.set(SearchBoxProperties.CHIP_TEXT, chipText); |
| } |
| |
| /** |
| * Called to set a delegate for handling the interactions between the chip and the embedder. |
| * @param chipDelegate A {@link SearchBoxChipDelegate}. |
| */ |
| void setChipDelegate(SearchBoxChipDelegate chipDelegate) { |
| mChipDelegate = chipDelegate; |
| mModel.set(SearchBoxProperties.CHIP_CLICK_CALLBACK, v -> chipDelegate.onChipClicked()); |
| mModel.set(SearchBoxProperties.CHIP_CANCEL_CALLBACK, v -> chipDelegate.onCancelClicked()); |
| mChipDelegate.getChipIcon(bitmap -> { |
| mModel.set(SearchBoxProperties.CHIP_DRAWABLE, getRoundedDrawable(bitmap)); |
| }); |
| } |
| |
| /** |
| * Launch the Lens app. |
| * @param lensEntryPoint A {@link LensEntryPoint}. |
| * @param windowAndroid A {@link WindowAndroid} instance. |
| * @param isIncognito Whether the request is from a Incognito tab. |
| */ |
| void startLens( |
| @LensEntryPoint int lensEntryPoint, WindowAndroid windowAndroid, boolean isIncognito) { |
| LensController.getInstance().startLens( |
| windowAndroid, new LensIntentParams.Builder(lensEntryPoint, isIncognito).build()); |
| } |
| |
| /** |
| * Check whether the Lens is enabled for an entry point. |
| * @param lensEntryPoint A {@link LensEntryPoint}. |
| * @param isIncognito Whether the request is from a Incognito tab. |
| * @param isTablet Whether the request is from a tablet. |
| * @return Whether the Lens is currently enabled. |
| */ |
| boolean isLensEnabled( |
| @LensEntryPoint int lensEntryPoint, boolean isIncognito, boolean isTablet) { |
| return LensController.getInstance().isLensEnabled( |
| new LensQueryParams.Builder(lensEntryPoint, isIncognito, isTablet).build()); |
| } |
| |
| private Drawable getRoundedDrawable(Bitmap bitmap) { |
| if (bitmap == null) return null; |
| RoundedBitmapDrawable roundedBitmapDrawable = |
| ViewUtils.createRoundedBitmapDrawable(mContext.getResources(), bitmap, 0); |
| roundedBitmapDrawable.setCircular(true); |
| return roundedBitmapDrawable; |
| } |
| } |