org.mozilla.search.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.mozilla.search.MainActivity.java

Source

/* 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.search;

import android.content.AsyncQueryHandler;
import android.content.ContentValues;
import android.content.Intent;
import android.graphics.Rect;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.text.TextUtils;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.TextView;

import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorSet;
import com.nineoldandroids.animation.ObjectAnimator;

import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract.SearchHistory;
import org.mozilla.search.autocomplete.ClearableEditText;
import org.mozilla.search.autocomplete.SuggestionsFragment;

/**
 * The main entrance for the Android search intent.
 * <p/>
 * State management is delegated to child fragments. Fragments communicate
 * with each other by passing messages through this activity.
 */
public class MainActivity extends FragmentActivity implements AcceptsSearchQuery {

    private static final String KEY_SEARCH_STATE = "search_state";
    private static final String KEY_EDIT_STATE = "edit_state";
    private static final String KEY_QUERY = "query";

    static enum SearchState {
        PRESEARCH, POSTSEARCH
    }

    static enum EditState {
        WAITING, EDITING
    }

    // Default states when activity is created.
    private SearchState searchState = SearchState.PRESEARCH;
    private EditState editState = EditState.WAITING;

    private AsyncQueryHandler queryHandler;

    // Main views in layout.
    private ClearableEditText editText;
    private View preSearch;
    private View postSearch;

    private View suggestions;
    private SuggestionsFragment suggestionsFragment;

    private static final int SUGGESTION_TRANSITION_DURATION = 300;
    private static final Interpolator SUGGESTION_TRANSITION_INTERPOLATOR = new AccelerateDecelerateInterpolator();

    // Views used for suggestion animation.
    private TextView animationText;
    private View animationCard;

    // Suggestion card background padding.
    private int cardPaddingX;
    private int cardPaddingY;

    // Vertical translation of suggestion animation text to align with the search bar.
    private int textEndY;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.search_activity_main);

        queryHandler = new AsyncQueryHandler(getContentResolver()) {
        };

        editText = (ClearableEditText) findViewById(R.id.search_edit_text);
        editText.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                setEditState(EditState.EDITING);
            }
        });

        editText.setTextListener(new ClearableEditText.TextListener() {
            @Override
            public void onChange(String text) {
                // Only load suggestions if we're in edit mode.
                if (editState == EditState.EDITING) {
                    suggestionsFragment.loadSuggestions(text);
                }
            }

            @Override
            public void onSubmit(String text) {
                // Don't submit an empty query.
                final String trimmedQuery = text.trim();
                if (!TextUtils.isEmpty(trimmedQuery)) {
                    onSearch(trimmedQuery);
                }
            }

            @Override
            public void onFocusChange(boolean hasFocus) {
                setEditState(hasFocus ? EditState.EDITING : EditState.WAITING);
            }
        });

        preSearch = findViewById(R.id.presearch);
        postSearch = findViewById(R.id.postsearch);

        suggestions = findViewById(R.id.suggestions);
        suggestionsFragment = (SuggestionsFragment) getSupportFragmentManager().findFragmentById(R.id.suggestions);

        animationText = (TextView) findViewById(R.id.animation_text);
        animationCard = findViewById(R.id.animation_card);

        cardPaddingX = getResources().getDimensionPixelSize(R.dimen.card_background_padding_x);
        cardPaddingY = getResources().getDimensionPixelSize(R.dimen.card_background_padding_y);
        textEndY = getResources().getDimensionPixelSize(R.dimen.animation_text_translation_y);

        if (savedInstanceState != null) {
            setSearchState(SearchState.valueOf(savedInstanceState.getString(KEY_SEARCH_STATE)));
            setEditState(EditState.valueOf(savedInstanceState.getString(KEY_EDIT_STATE)));

            final String query = savedInstanceState.getString(KEY_QUERY);
            editText.setText(query);

            // If we're in the postsearch state, we need to re-do the query.
            if (searchState == SearchState.POSTSEARCH) {
                ((PostSearchFragment) getSupportFragmentManager().findFragmentById(R.id.postsearch))
                        .startSearch(query);
            }
        } else {
            // If there isn't a state to restore, the activity will start in the presearch state,
            // and we should enter editing mode to bring up the keyboard.
            setEditState(EditState.EDITING);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        queryHandler = null;
        editText = null;
        preSearch = null;
        postSearch = null;
        suggestionsFragment = null;
        suggestions = null;
        animationText = null;
        animationCard = null;
    }

    @Override
    protected void onStart() {
        super.onStart();
        Telemetry.startUISession(TelemetryContract.Session.SEARCH_ACTIVITY);
    }

    @Override
    protected void onStop() {
        super.onStop();
        Telemetry.stopUISession(TelemetryContract.Session.SEARCH_ACTIVITY);
    }

    @Override
    public void onNewIntent(Intent intent) {
        // Reset the activity in the presearch state if it was launched from a new intent.
        setSearchState(SearchState.PRESEARCH);

        // Also clear any existing search term and enter editing mode.
        editText.setText("");
        setEditState(EditState.EDITING);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        outState.putString(KEY_SEARCH_STATE, searchState.toString());
        outState.putString(KEY_EDIT_STATE, editState.toString());
        outState.putString(KEY_QUERY, editText.getText());
    }

    @Override
    public void onSuggest(String query) {
        editText.setText(query);
    }

    @Override
    public void onSearch(String query) {
        onSearch(query, null);
    }

    @Override
    public void onSearch(String query, SuggestionAnimation suggestionAnimation) {
        storeQuery(query);

        ((PostSearchFragment) getSupportFragmentManager().findFragmentById(R.id.postsearch)).startSearch(query);

        if (suggestionAnimation != null) {
            // Animate the suggestion card if start bounds are specified.
            animateSuggestion(query, suggestionAnimation);
        } else {
            // Otherwise immediately switch to the results view.
            setEditState(EditState.WAITING);
            setSearchState(SearchState.POSTSEARCH);
        }
    }

    /**
     * Animates search suggestion to search bar. This animation has 2 main parts:
     *
     *   1) Vertically translate query text from suggestion card to search bar.
     *   2) Expand suggestion card to fill the results view area.
     *
     * @param query
     * @param suggestionAnimation
     */
    private void animateSuggestion(final String query, final SuggestionAnimation suggestionAnimation) {
        animationText.setText(query);

        final Rect startBounds = suggestionAnimation.getStartBounds();
        final Rect endBounds = new Rect();
        animationCard.getGlobalVisibleRect(endBounds, null);

        // Vertically translate the animated card to align with the start bounds.
        final float cardStartY = startBounds.centerY() - endBounds.centerY();

        // Account for card background padding when calculating start scale.
        final float startScaleX = (float) (startBounds.width() - cardPaddingX * 2) / endBounds.width();
        final float startScaleY = (float) (startBounds.height() - cardPaddingY * 2) / endBounds.height();

        animationText.setVisibility(View.VISIBLE);
        animationCard.setVisibility(View.VISIBLE);

        final AnimatorSet set = new AnimatorSet();
        set.playTogether(ObjectAnimator.ofFloat(animationText, "translationY", startBounds.top, textEndY),
                ObjectAnimator.ofFloat(animationCard, "translationY", cardStartY, 0),
                ObjectAnimator.ofFloat(animationCard, "alpha", 0.5f, 1),
                ObjectAnimator.ofFloat(animationCard, "scaleX", startScaleX, 1f),
                ObjectAnimator.ofFloat(animationCard, "scaleY", startScaleY, 1f));

        set.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                setEditState(EditState.WAITING);
                setSearchState(SearchState.POSTSEARCH);

                editText.setText(query);

                // We need to manually clear the animation for the views to be hidden on gingerbread.
                animationText.clearAnimation();
                animationCard.clearAnimation();

                animationText.setVisibility(View.INVISIBLE);
                animationCard.setVisibility(View.INVISIBLE);
            }

            @Override
            public void onAnimationCancel(Animator animation) {
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
            }
        });

        set.setDuration(SUGGESTION_TRANSITION_DURATION);
        set.setInterpolator(SUGGESTION_TRANSITION_INTERPOLATOR);

        set.start();
    }

    private void setEditState(EditState editState) {
        if (this.editState == editState) {
            return;
        }
        this.editState = editState;

        editText.setActive(editState == EditState.EDITING);
        suggestions.setVisibility(editState == EditState.EDITING ? View.VISIBLE : View.INVISIBLE);
    }

    private void setSearchState(SearchState searchState) {
        if (this.searchState == searchState) {
            return;
        }
        this.searchState = searchState;

        preSearch.setVisibility(searchState == SearchState.PRESEARCH ? View.VISIBLE : View.INVISIBLE);
        postSearch.setVisibility(searchState == SearchState.POSTSEARCH ? View.VISIBLE : View.INVISIBLE);
    }

    @Override
    public void onBackPressed() {
        if (editState == EditState.EDITING) {
            setEditState(EditState.WAITING);
        } else if (searchState == SearchState.POSTSEARCH) {
            setSearchState(SearchState.PRESEARCH);
        } else {
            super.onBackPressed();
        }
    }

    /**
     * Store the search query in Fennec's search history database.
     */
    private void storeQuery(String query) {
        final ContentValues cv = new ContentValues();
        cv.put(SearchHistory.QUERY, query);
        // Setting 0 for the token, since we only have one type of insert.
        // Setting null for the cookie, since we don't handle the result of the insert.
        queryHandler.startInsert(0, null, SearchHistory.CONTENT_URI, cv);
    }
}