/*
* Copyright 2015 Google Inc. 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.
* 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.google.samples.apps.iosched.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.TargetApi;
import android.app.LoaderManager;
import android.app.SearchManager;
import android.content.CursorLoader;
import android.content.Intent;
import android.content.Loader;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.widget.SimpleCursorAdapter;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.AnimationUtils;
import android.widget.AdapterView;
import android.widget.ListView;
import com.google.samples.apps.iosched.R;
import com.google.samples.apps.iosched.explore.ExploreSessionsActivity;
import com.google.samples.apps.iosched.provider.ScheduleContract;
import com.google.samples.apps.iosched.session.SessionDetailActivity;
import com.google.samples.apps.iosched.util.AnalyticsHelper;
import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag;
public class SearchActivity extends BaseActivity implements
LoaderManager.LoaderCallbacks<Cursor>, AdapterView.OnItemClickListener {
private static final String TAG = makeLogTag("SearchActivity");
private static final String SCREEN_LABEL = "Search";
private static final String ARG_QUERY = "query";
private SearchView mSearchView;
private String mQuery = "";
private ListView mSearchResults;
private SimpleCursorAdapter mResultsAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
mSearchView = (SearchView) findViewById(R.id.search_view);
setupSearchView();
mSearchResults = (ListView) findViewById(R.id.search_results);
mResultsAdapter = new SimpleCursorAdapter(this,
R.layout.list_item_search_result, null,
new String[]{ScheduleContract.SearchTopicSessionsColumns.SEARCH_SNIPPET},
new int[]{R.id.search_result}, 0);
mSearchResults.setAdapter(mResultsAdapter);
mSearchResults.setOnItemClickListener(this);
Toolbar toolbar = getToolbar();
Drawable up = DrawableCompat.wrap(
VectorDrawableCompat.create(getResources(), R.drawable.ic_up, getTheme()));
DrawableCompat.setTint(up, getResources().getColor(R.color.app_body_text_2));
toolbar.setNavigationIcon(up);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
navigateUpOrBack(SearchActivity.this, null);
}
});
String query = getIntent().getStringExtra(SearchManager.QUERY);
query = query == null ? "" : query;
mQuery = query;
if (mSearchView != null) {
mSearchView.setQuery(query, false);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
doEnterAnim();
}
overridePendingTransition(0, 0);
}
/**
* As we only ever want one instance of this screen, we set a launchMode of singleTop. This
* means that instead of re-creating this Activity, a new intent is delivered via this callback.
* This prevents multiple instances of the search dialog 'stacking up' e.g. if you perform a
* voice search.
*
* See: http://developer.android.com/guide/topics/manifest/activity-element.html#lmode
*/
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (intent.hasExtra(SearchManager.QUERY)) {
String query = intent.getStringExtra(SearchManager.QUERY);
if (!TextUtils.isEmpty(query)) {
searchFor(query);
mSearchView.setQuery(query, false);
}
}
}
private void setupSearchView() {
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
mSearchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
mSearchView.setIconified(false);
// Set the query hint.
mSearchView.setQueryHint(getString(R.string.search_hint));
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String s) {
mSearchView.clearFocus();
return true;
}
@Override
public boolean onQueryTextChange(String s) {
searchFor(s);
return true;
}
});
mSearchView.setOnCloseListener(new SearchView.OnCloseListener() {
@Override
public boolean onClose() {
dismiss(null);
return false;
}
});
if (!TextUtils.isEmpty(mQuery)) {
mSearchView.setQuery(mQuery, false);
}
}
@Override
public void onBackPressed() {
dismiss(null);
}
public void dismiss(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
doExitAnim();
} else {
ActivityCompat.finishAfterTransition(this);
}
}
/**
* On Lollipop+ perform a circular reveal animation (an expanding circular mask) when showing
* the search panel.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void doEnterAnim() {
// Fade in a background scrim as this is a floating window. We could have used a
// translucent window background but this approach allows us to turn off window animation &
// overlap the fade with the reveal animation – making it feel snappier.
View scrim = findViewById(R.id.scrim);
scrim.animate()
.alpha(1f)
.setDuration(500L)
.setInterpolator(
AnimationUtils.loadInterpolator(this, android.R.interpolator.fast_out_slow_in))
.start();
// Next perform the circular reveal on the search panel
final View searchPanel = findViewById(R.id.search_panel);
if (searchPanel != null) {
// We use a view tree observer to set this up once the view is measured & laid out
searchPanel.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
searchPanel.getViewTreeObserver().removeOnPreDrawListener(this);
// As the height will change once the initial suggestions are delivered by the
// loader, we can't use the search panels height to calculate the final radius
// so we fall back to it's parent to be safe
final ViewGroup searchPanelParent = (ViewGroup) searchPanel.getParent();
final int revealRadius = (int) Math.hypot(
searchPanelParent.getWidth(), searchPanelParent.getHeight());
// Center the animation on the top right of the panel i.e. near to the
// search button which launched this screen.
Animator show = ViewAnimationUtils.createCircularReveal(searchPanel,
searchPanel.getRight(), searchPanel.getTop(), 0f, revealRadius);
show.setDuration(250L);
show.setInterpolator(AnimationUtils.loadInterpolator(SearchActivity.this,
android.R.interpolator.fast_out_slow_in));
show.start();
return false;
}
});
}
}
/**
* On Lollipop+ perform a circular animation (a contracting circular mask) when hiding the
* search panel.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void doExitAnim() {
final View searchPanel = findViewById(R.id.search_panel);
// Center the animation on the top right of the panel i.e. near to the search button which
// launched this screen. The starting radius therefore is the diagonal distance from the top
// right to the bottom left
final int revealRadius = (int) Math.hypot(searchPanel.getWidth(), searchPanel.getHeight());
// Animating the radius to 0 produces the contracting effect
Animator shrink = ViewAnimationUtils.createCircularReveal(searchPanel,
searchPanel.getRight(), searchPanel.getTop(), revealRadius, 0f);
shrink.setDuration(200L);
shrink.setInterpolator(AnimationUtils.loadInterpolator(SearchActivity.this,
android.R.interpolator.fast_out_slow_in));
shrink.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
searchPanel.setVisibility(View.INVISIBLE);
ActivityCompat.finishAfterTransition(SearchActivity.this);
}
});
shrink.start();
// We also animate out the translucent background at the same time.
findViewById(R.id.scrim).animate()
.alpha(0f)
.setDuration(200L)
.setInterpolator(
AnimationUtils.loadInterpolator(SearchActivity.this,
android.R.interpolator.fast_out_slow_in))
.start();
}
private void searchFor(String query) {
// ANALYTICS EVENT: Start a search on the Search activity
// Contains: Nothing (Event params are constant: Search query not included)
AnalyticsHelper.sendEvent(SCREEN_LABEL, "Search", "");
Bundle args = new Bundle(1);
if (query == null) {
query = "";
}
args.putString(ARG_QUERY, query);
if (TextUtils.equals(query, mQuery)) {
getLoaderManager().initLoader(SearchTopicsSessionsQuery.TOKEN, args, this);
} else {
getLoaderManager().restartLoader(SearchTopicsSessionsQuery.TOKEN, args, this);
}
mQuery = query;
}
@Override
protected void onPause() {
super.onPause();
if (isFinishing()) {
overridePendingTransition(0, 0);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.menu_search) {
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(this,
ScheduleContract.SearchTopicsSessions.CONTENT_URI,
SearchTopicsSessionsQuery.PROJECTION,
null, new String[] {args.getString(ARG_QUERY)}, null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mResultsAdapter.swapCursor(data);
mSearchResults.setVisibility(data.getCount() > 0 ? View.VISIBLE : View.GONE);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Cursor c = mResultsAdapter.getCursor();
c.moveToPosition(position);
boolean isTopicTag = c.getInt(SearchTopicsSessionsQuery.IS_TOPIC_TAG) == 1;
String tagOrSessionId = c.getString(SearchTopicsSessionsQuery.TAG_OR_SESSION_ID);
if (isTopicTag) {
Intent intent = new Intent(this, ExploreSessionsActivity.class);
intent.putExtra(ExploreSessionsActivity.EXTRA_FILTER_TAG, tagOrSessionId);
startActivity(intent);
} else if (tagOrSessionId != null) {
Intent intent = new Intent(this, SessionDetailActivity.class);
intent.setData(ScheduleContract.Sessions.buildSessionUri(tagOrSessionId));
startActivity(intent);
}
}
private interface SearchTopicsSessionsQuery {
int TOKEN = 0x4;
String[] PROJECTION = ScheduleContract.SearchTopicsSessions.DEFAULT_PROJECTION;
int _ID = 0;
int TAG_OR_SESSION_ID = 1;
int SEARCH_SNIPPET = 2;
int IS_TOPIC_TAG = 3;
}
}