blob: 4dcc87761b0e26b3957c444c8347625c6db4dc6c [file] [log] [blame]
/*
* Copyright (C) 2017 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.android.car.dialer;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.app.SearchManager;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
/**
* An activity that manages contact searching. This activity will display the result of a search
* as well as show the details of a contact when that contact is clicked.
*/
public class ContactSearchActivity extends FragmentActivity {
private static final String CONTENT_FRAGMENT_TAG = "CONTENT_FRAGMENT_TAG";
private static final int ANIMATION_DURATION_MS = 100;
/**
* A delay before actually starting a contact search. This ensures that there are not too many
* queries happening when the user is still typing.
*/
private static final int CONTACT_SEARCH_DELAY = 400;
private final Handler mHandler = new Handler();
private Runnable mCurrentSearch;
private View mSearchContainer;
private EditText mSearchField;
private float mContainerElevation;
private ValueAnimator mRemoveElevationAnimator;
/**
* Whether or not it is safe to make transactions on the {@link android.app.FragmentManager}.
* This variable prevents a possible exception when calling commit() on the FragmentManager.
*
* <p>The default value is {@code true} because it is only after
* {@link #onSaveInstanceState(Bundle)} that fragment commits are not allowed.
*/
private boolean mAllowFragmentCommits = true;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.contact_search_activity);
mSearchContainer = findViewById(R.id.search_container);
mSearchField = findViewById(R.id.search_field);
mSearchField.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (!(getCurrentFragment() instanceof ContactResultsFragment)) {
showContactResultList(s.toString());
return;
}
// Cancel any pending searches.
if (mCurrentSearch != null) {
mHandler.removeCallbacks(mCurrentSearch);
}
// Queue up a new search. This will be cancelled if the user types within the
// time frame specified by CONTACT_SEARCH_DELAY.
mCurrentSearch = new SearchRunnable(s.toString());
mHandler.postDelayed(mCurrentSearch, CONTACT_SEARCH_DELAY);
}
});
mContainerElevation = getResources()
.getDimension(R.dimen.search_container_elevation);
mRemoveElevationAnimator = ValueAnimator.ofFloat(mContainerElevation, 0.f);
mRemoveElevationAnimator
.setDuration(ANIMATION_DURATION_MS)
.addUpdateListener(animation -> mSearchContainer.setElevation(
(float) animation.getAnimatedValue()));
findViewById(R.id.back).setOnClickListener(v -> finish());
findViewById(R.id.clear).setOnClickListener(v -> {
mSearchField.getText().clear();
Fragment currentFragment = getCurrentFragment();
if (currentFragment instanceof ContactResultsFragment) {
((ContactResultsFragment) currentFragment).clearResults();
}
});
handleIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
setIntent(intent);
handleIntent(intent);
}
/**
* Inspects the Action within the given intent and loads up the appropriate fragment based on
* this.
*/
private void handleIntent(Intent intent) {
if (intent == null || intent.getAction() == null) {
showContactResultList(null /* query */);
return;
}
switch (intent.getAction()) {
case Intent.ACTION_SEARCH:
showContactResultList(intent.getStringExtra(SearchManager.QUERY));
break;
case TelecomIntents.ACTION_SHOW_CONTACT_DETAILS:
// Hide the keyboard so there's room on the screen for the detail view.
InputMethodManager imm =
(InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mSearchField.getWindowToken(), 0);
Uri contactUri = Uri.parse(intent.getStringExtra(
TelecomIntents.CONTACT_LOOKUP_URI_EXTRA));
setContentFragment(ContactDetailsFragment.newInstance(contactUri,
new ContactScrollListener()));
break;
default:
showContactResultList(null /* query */);
}
}
/**
* Displays the fragment that will show the results of a search. The given query is used as
* the initial search to populate the list.
*/
private void showContactResultList(@Nullable String query) {
// Check that the result list is not already being displayed. If it is, then simply set the
// search query.
Fragment currentFragment = getCurrentFragment();
if (currentFragment instanceof ContactResultsFragment) {
((ContactResultsFragment) currentFragment).setSearchQuery(query);
return;
}
setContentFragment(ContactResultsFragment.newInstance(new ContactScrollListener(), query));
}
/**
* Sets the fragment that will be shown as the main content of this Activity.
*/
private void setContentFragment(Fragment fragment) {
if (!mAllowFragmentCommits) {
return;
}
// The search panel might have elevation added to it, so remove it when the fragment
// changes since any lists in it will be reset to the top.
resetSearchPanelElevation();
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.animator.fade_in, R.animator.fade_out)
.replace(R.id.content_fragment_container, fragment, CONTENT_FRAGMENT_TAG)
.commitNow();
}
/**
* Returns the fragment that is currently being displayed as the content view.
*/
@Nullable
private Fragment getCurrentFragment() {
return getSupportFragmentManager().findFragmentByTag(CONTENT_FRAGMENT_TAG);
}
@Override
protected void onStart() {
super.onStart();
// Fragment commits are not allowed once the Activity's state has been saved. Once
// onStart() has been called, the FragmentManager should now allow commits.
mAllowFragmentCommits = true;
}
@Override
public void onSaveInstanceState(Bundle outState) {
// A transaction can only be committed with this method prior to its containing activity
// saving its state.
mAllowFragmentCommits = false;
super.onSaveInstanceState(outState);
}
/**
* Checks if {@link #mSearchContainer} has an elevation set on it and if it does, animates the
* removal of this elevation.
*/
private void resetSearchPanelElevation() {
if (mSearchContainer.getElevation() != 0.f) {
mRemoveElevationAnimator.start();
}
}
/**
* A {@link Runnable} that will execute a contact search with the given {@link #mSearchQuery}.
*/
private class SearchRunnable implements Runnable {
private final String mSearchQuery;
public SearchRunnable(String searchQuery) {
mSearchQuery = searchQuery;
}
@Override
public void run() {
Fragment currentFragment = getCurrentFragment();
if (currentFragment instanceof ContactResultsFragment) {
((ContactResultsFragment) currentFragment).setSearchQuery(mSearchQuery);
}
}
}
/**
* Listener for scrolls in a fragment that has a list. It will will add elevation on the
* container holding the search field. This elevation will give the illusion of the list
* scrolling under that container.
*/
public class ContactScrollListener extends RecyclerView.OnScrollListener {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// The default LayoutManager for PagedListView is a LinearLayoutManager. Dialer does
// not change this.
LinearLayoutManager layoutManager =
(LinearLayoutManager) recyclerView.getLayoutManager();
if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
resetSearchPanelElevation();
} else {
// No animation needed when adding the elevation because the scroll masks the adding
// of the elevation.
mSearchContainer.setElevation(mContainerElevation);
}
}
}
}