com.amazon.android.tv.tenfoot.ui.fragments.ContentSearchFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.amazon.android.tv.tenfoot.ui.fragments.ContentSearchFragment.java

Source

/**
 * This file was modified by Amazon:
 * Copyright 2015-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *     http://aws.amazon.com/apache2.0/
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.amazon.android.tv.tenfoot.ui.fragments;

import com.amazon.android.contentbrowser.ContentBrowser;
import com.amazon.android.contentbrowser.helper.AnalyticsHelper;
import com.amazon.android.model.content.Content;
import com.amazon.android.search.SearchManager;
import com.amazon.android.utils.Helpers;
import com.amazon.android.tv.tenfoot.BuildConfig;
import com.amazon.android.tv.tenfoot.R;
import com.amazon.android.tv.tenfoot.presenter.CardPresenter;
import com.amazon.android.tv.tenfoot.presenter.CustomListRowPresenter;
import com.amazon.android.tv.tenfoot.ui.activities.ContentDetailsActivity;

import android.Manifest;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ObjectAdapter;
import android.support.v17.leanback.widget.OnItemViewClickedListener;
import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.Row;
import android.support.v17.leanback.widget.RowHeaderPresenter;
import android.support.v17.leanback.widget.RowPresenter;
import android.support.v17.leanback.widget.SearchBar;
import android.support.v17.leanback.widget.SearchEditText;
import android.support.v17.leanback.widget.SpeechOrbView;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.RelativeLayout;
import android.widget.Toast;

/**
 * This class provides search capabilities by extending the android ContentSearchFragment.
 * The primary function of this class is to take user input and pass that data into {@link
 * SearchManager}.
 * This class will populate the search results row when the search is complete.
 */
public class ContentSearchFragment extends android.support.v17.leanback.app.SearchFragment
        implements android.support.v17.leanback.app.SearchFragment.SearchResultProvider {

    private static final String TAG = ContentSearchFragment.class.getSimpleName();
    private static final boolean DEBUG = BuildConfig.DEBUG;
    private static final long SEARCH_DELAY_MS = 1000L;

    private final Handler mHandler = new Handler();
    private final Runnable mDelayedLoad = new Runnable() {
        @Override
        public void run() {

            loadRows();
            //TODO this will change to update any listeners of query Changes.
        }
    };
    private ArrayObjectAdapter mRowsAdapter;
    private String mQuery;
    private boolean mHasResults = false;
    private SpeechOrbView mSpeechOrbView = null;
    private SearchEditText mSearchEditText = null;

    // A local list row Adapter
    private ArrayObjectAdapter mListRowAdapter;

    /**
     * This handler will be used to give the focus to search textview.
     * Needs to be attached to this fragment
     */
    private Handler mAutoTextViewFocusHandler = new Handler();

    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        CustomListRowPresenter presenter = new CustomListRowPresenter();
        presenter.setHeaderPresenter(new RowHeaderPresenter());
        mRowsAdapter = new ArrayObjectAdapter(presenter);

        setSearchResultProvider(this);
        setOnItemViewClickedListener(new ItemViewClickedListener());

        // TODO: Set up speech recognizer for AndroidTV only.
        if (!hasPermission(Manifest.permission.RECORD_AUDIO)) {
            // SpeechRecognitionCallback is not required and if not provided recognition will be
            // handled using internal speech recognizer, in which case you must have RECORD_AUDIO
            // permission.
            setSpeechRecognitionCallback(() -> {
                if (DEBUG)
                    Log.v(TAG, "recognizeSpeech");
                try {
                    // Disabling speech recognizer so the fragment works on Fire TV.
                    //startActivityForResult(getRecognizerIntent(), REQUEST_SPEECH);
                } catch (ActivityNotFoundException e) {
                    Log.e(TAG, "Cannot find activity for speech recognizer", e);
                }
            });
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        final View view = super.onCreateView(inflater, container, savedInstanceState);

        if (view != null) {
            // Set background color and drawable.
            view.setBackgroundColor(ContextCompat.getColor(getActivity(), android.R.color.transparent));
            view.setBackground(ContextCompat.getDrawable(getActivity(), R.drawable.search_background));

            final SearchBar searchBar = (SearchBar) view.findViewById(R.id.lb_search_bar);
            if (searchBar != null) {

                // Set the left margin of the search bar.
                ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) searchBar
                        .getLayoutParams();

                layoutParams.setMarginStart((int) getResources().getDimension(R.dimen.search_bar_margin_left));

                searchBar.setLayoutParams(layoutParams);

                // Move the search bar items next to the search icon.
                RelativeLayout searchBarItems = (RelativeLayout) searchBar.findViewById(R.id.lb_search_bar_items);

                if (searchBarItems != null) {

                    RelativeLayout.LayoutParams searchBarItemsLayoutParams = (RelativeLayout.LayoutParams) searchBarItems
                            .getLayoutParams();

                    searchBarItemsLayoutParams.setMarginStart(
                            (int) getResources().getDimension(R.dimen.search_bar_items_margin_left));

                    searchBarItems.setLayoutParams(searchBarItemsLayoutParams);

                    // Set the search bar items background selector.
                    searchBarItems.setBackground(ContextCompat.getDrawable(getActivity(),
                            R.drawable.search_edit_text_bg_color_selector));
                }

                // Set speech orb icon.
                mSpeechOrbView = (SpeechOrbView) searchBar.findViewById(R.id.lb_search_bar_speech_orb);

                if (mSpeechOrbView != null) {
                    mSpeechOrbView.setOrbIcon(ContextCompat.getDrawable(getActivity(), R.drawable.search_icon));
                }

                final SearchEditText searchEditText = (SearchEditText) searchBar
                        .findViewById(R.id.lb_search_text_editor);

                if (searchEditText != null) {

                    mSearchEditText = searchEditText;

                    // Handle keyboard being dismissed to prevent focus going to SearchOrb
                    // If user presses back from keyboard, you don't get KeyboardDismissListener
                    // so handle that here.
                    searchEditText.setOnEditorActionListener((textView, actionId, keyEvent) -> {

                        // Track search if keyboard is closed with IME_ACTION_PREVIOUS or
                        // if IME_ACTION_SEARCH occurs.
                        if (actionId == EditorInfo.IME_ACTION_SEARCH
                                || actionId == EditorInfo.IME_ACTION_PREVIOUS) {

                            if (mQuery != null) {
                                AnalyticsHelper.trackSearchQuery(mQuery);
                            }
                        }

                        if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {

                            // Prevent highlighting SearchOrb
                            mSpeechOrbView.setFocusable(false);
                            mSpeechOrbView.clearFocus();
                            // If there are results allow first result to be selected
                            if (mHasResults) {
                                mSearchEditText.clearFocus();
                            }

                            // Hide keyboard since we are handling the action
                            if (isAdded()) {
                                // Ensure we are added before calling getActivity
                                InputMethodManager inputManager = (InputMethodManager) getActivity()
                                        .getSystemService(Context.INPUT_METHOD_SERVICE);
                                if (inputManager != null) {
                                    inputManager.hideSoftInputFromWindow(
                                            getActivity().getCurrentFocus().getWindowToken(),
                                            InputMethodManager.HIDE_NOT_ALWAYS);
                                }
                            } else {
                                Log.e(TAG, "Cannot find activity, can't dismiss keyboard");
                                // Couldn't handle action.
                                // Will expose other focus issues potentially.
                                return false;
                            }
                            // No more processing of this action.
                            return true;
                        }
                        // Someone else needs to handle this action.
                        return false;
                    });

                    // Override the dismiss listener to get around keyboard issue where dismissing
                    // keyboard takes user into first search result's
                    // content_details_activity_layout page.
                    searchEditText.setOnKeyboardDismissListener(() -> {
                        // If search returns results, focus on the first item in the result list.
                        // If search doesn't have results, this will focus on searchEditText again.
                        mSpeechOrbView.setFocusable(false);
                        mSpeechOrbView.clearFocus();
                        // We don't need to clearFocus on SearchEditText here, the first
                        // result will be selected already.
                    });
                }
            }
        }
        return view;
    }

    @Override
    public void onResume() {

        super.onResume();

        if (!mHasResults) {
            mAutoTextViewFocusHandler.postDelayed(() -> {

                if (mSearchEditText != null) {

                    // Select search edit text, bring up keyboard.
                    // Always make SpeechOrb not focusable, leanback always tries to bring it back.
                    mSearchEditText.setFocusable(true);
                    mSearchEditText.requestFocus();
                    mSpeechOrbView.setFocusable(false);
                    InputMethodManager imm = (InputMethodManager) getActivity()
                            .getSystemService(Context.INPUT_METHOD_SERVICE);
                    if (imm != null) {
                        imm.showSoftInput(mSearchEditText, 0);
                    }
                }
                // There must be a delay to allow SearchOrb to initialize, otherwise no search
                // results will come back from leanback.
            }, 1000);
        }
    }

    @Override
    public void onPause() {

        mAutoTextViewFocusHandler.removeCallbacksAndMessages(null);
        mHandler.removeCallbacksAndMessages(null);
        super.onPause();
    }

    @Override
    public ObjectAdapter getResultsAdapter() {

        return mRowsAdapter;
    }

    @Override
    public boolean onQueryTextChange(String newQuery) {

        Log.i(TAG, String.format("Search Query Text Change %s", newQuery));
        loadQuery(newQuery);
        return true;
    }

    @Override
    public boolean onQueryTextSubmit(String query) {

        Log.i(TAG, String.format("Search Query Text Submit %s", query));

        // Focus on the SearchEditText when "Next" is pressed and has no results.
        if ((TextUtils.isEmpty(query) && query.equals("nil")) || !mHasResults) {
            mSearchEditText.setFocusable(true);
            mSearchEditText.requestFocus();
        }
        // Focus on the result list when "Next" is pressed and has results.
        else {
            if (getView() != null) {
                if (getView().findViewById(R.id.row_content) != null) {
                    getView().findViewById(R.id.row_content).requestFocus();
                }
            }
        }

        return true;
    }

    /**
     * Returns if the search resulted in any matches.
     *
     * @return True if there are search results; false otherwise.
     */
    public boolean hasResults() {

        return mRowsAdapter.size() > 0;
    }

    private boolean hasPermission(final String permission) {

        final Context context = getActivity();
        return PackageManager.PERMISSION_GRANTED == context.getPackageManager().checkPermission(permission,
                context.getPackageName());
    }

    private void loadQuery(String query) {

        mHandler.removeCallbacks(mDelayedLoad);
        if (!TextUtils.isEmpty(query) && !query.equals("nil")) {
            mQuery = query;
            mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS);
        }
        // If search query is empty, clear the previous results.
        else {
            mRowsAdapter.clear();
            mHasResults = false;
        }
    }

    private void loadRows() {

        mListRowAdapter = new ArrayObjectAdapter(new CardPresenter());

        ContentBrowser.getInstance(getActivity()).search(mQuery, (t, done) -> updateResults(t, done));
    }

    /**
     * This method is the entry point for new content to be added to the view.
     * If there is no more new content coming in the current in flight query the boolean is set to
     * true and we send a new {@link ListRow} to the {@link #mRowsAdapter}
     *
     * @param inputContent the new content to be added
     * @param done         a boolean that tells us that there are no new items coming in the
     *                     current
     *                     in flight query
     */
    private void updateResults(Object inputContent, boolean done) {

        // If done then add the content to the mRowsAdapter.
        if (done) {
            mRowsAdapter.clear();
            HeaderItem header = new HeaderItem(getString(R.string.search_results, mQuery));
            mRowsAdapter.add(new ListRow(header, mListRowAdapter));
        }
        // Add the found content to the mlistRowAdapter
        else {
            // Only add the content if the adapter does not already contain it.
            if (mListRowAdapter.indexOf(inputContent) == -1) {
                mListRowAdapter.add(inputContent);
            }
        }
    }

    private final class ItemViewClickedListener implements OnItemViewClickedListener {

        @Override
        public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
                RowPresenter.ViewHolder rowViewHolder, Row row) {

            if (item instanceof Content) {
                Content content = (Content) item;
                if (Helpers.DEBUG) {
                    Log.d(TAG, "Content: " + content.toString());
                }

                Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(),
                        ((ImageCardView) itemViewHolder.view).getMainImageView(),
                        ContentDetailsActivity.SHARED_ELEMENT_NAME).toBundle();

                ContentBrowser.getInstance(getActivity()).setLastSelectedContent(content)
                        .switchToScreen(ContentBrowser.CONTENT_DETAILS_SCREEN, bundle);
            } else {
                Toast.makeText(getActivity(), ((String) item), Toast.LENGTH_SHORT).show();
            }
        }
    }
}