• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.dialer;
18 
19 import android.animation.ValueAnimator;
20 import android.app.Activity;
21 import android.app.SearchManager;
22 import android.content.Intent;
23 import android.net.Uri;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.support.annotation.Nullable;
27 import android.support.v4.app.Fragment;
28 import android.support.v4.app.FragmentActivity;
29 import android.support.v7.widget.LinearLayoutManager;
30 import android.support.v7.widget.RecyclerView;
31 import android.text.Editable;
32 import android.text.TextWatcher;
33 import android.view.View;
34 import android.view.inputmethod.InputMethodManager;
35 import android.widget.EditText;
36 
37 /**
38  * An activity that manages contact searching. This activity will display the result of a search
39  * as well as show the details of a contact when that contact is clicked.
40  */
41 public class ContactSearchActivity extends FragmentActivity {
42     private static final String CONTENT_FRAGMENT_TAG = "CONTENT_FRAGMENT_TAG";
43     private static final int ANIMATION_DURATION_MS = 100;
44 
45     /**
46      * A delay before actually starting a contact search. This ensures that there are not too many
47      * queries happening when the user is still typing.
48      */
49     private static final int CONTACT_SEARCH_DELAY = 400;
50 
51     private final Handler mHandler = new Handler();
52     private Runnable mCurrentSearch;
53 
54     private View mSearchContainer;
55     private EditText mSearchField;
56 
57     private float mContainerElevation;
58     private ValueAnimator mRemoveElevationAnimator;
59 
60     /**
61      * Whether or not it is safe to make transactions on the {@link android.app.FragmentManager}.
62      * This variable prevents a possible exception when calling commit() on the FragmentManager.
63      *
64      * <p>The default value is {@code true} because it is only after
65      * {@link #onSaveInstanceState(Bundle)} that fragment commits are not allowed.
66      */
67     private boolean mAllowFragmentCommits = true;
68 
69     @Override
onCreate(Bundle savedInstanceState)70     public void onCreate(Bundle savedInstanceState) {
71         super.onCreate(savedInstanceState);
72         setContentView(R.layout.contact_search_activity);
73 
74         mSearchContainer = findViewById(R.id.search_container);
75         mSearchField = findViewById(R.id.search_field);
76 
77         mSearchField.addTextChangedListener(new TextWatcher() {
78             @Override
79             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
80             }
81 
82             @Override
83             public void onTextChanged(CharSequence s, int start, int before, int count) {
84             }
85 
86             @Override
87             public void afterTextChanged(Editable s) {
88                 if (!(getCurrentFragment() instanceof ContactResultsFragment)) {
89                     showContactResultList(s.toString());
90                     return;
91                 }
92 
93                 // Cancel any pending searches.
94                 if (mCurrentSearch != null) {
95                     mHandler.removeCallbacks(mCurrentSearch);
96                 }
97 
98                 // Queue up a new search. This will be cancelled if the user types within the
99                 // time frame specified by CONTACT_SEARCH_DELAY.
100                 mCurrentSearch = new SearchRunnable(s.toString());
101                 mHandler.postDelayed(mCurrentSearch, CONTACT_SEARCH_DELAY);
102             }
103         });
104 
105         mContainerElevation = getResources()
106                 .getDimension(R.dimen.search_container_elevation);
107 
108         mRemoveElevationAnimator = ValueAnimator.ofFloat(mContainerElevation, 0.f);
109         mRemoveElevationAnimator
110                 .setDuration(ANIMATION_DURATION_MS)
111                 .addUpdateListener(animation -> mSearchContainer.setElevation(
112                         (float) animation.getAnimatedValue()));
113 
114         findViewById(R.id.back).setOnClickListener(v -> finish());
115         findViewById(R.id.clear).setOnClickListener(v -> {
116             mSearchField.getText().clear();
117 
118             Fragment currentFragment = getCurrentFragment();
119             if (currentFragment instanceof ContactResultsFragment) {
120                 ((ContactResultsFragment) currentFragment).clearResults();
121             }
122         });
123 
124         handleIntent(getIntent());
125     }
126 
127     @Override
onNewIntent(Intent intent)128     protected void onNewIntent(Intent intent) {
129         setIntent(intent);
130         handleIntent(intent);
131     }
132 
133     /**
134      * Inspects the Action within the given intent and loads up the appropriate fragment based on
135      * this.
136      */
handleIntent(Intent intent)137     private void handleIntent(Intent intent) {
138         if (intent == null || intent.getAction() == null) {
139             showContactResultList(null /* query */);
140             return;
141         }
142 
143         switch (intent.getAction()) {
144             case Intent.ACTION_SEARCH:
145                 showContactResultList(intent.getStringExtra(SearchManager.QUERY));
146                 break;
147 
148             case TelecomIntents.ACTION_SHOW_CONTACT_DETAILS:
149                 // Hide the keyboard so there's room on the screen for the detail view.
150                 InputMethodManager imm =
151                         (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE);
152                 imm.hideSoftInputFromWindow(mSearchField.getWindowToken(), 0);
153                 Uri contactUri = Uri.parse(intent.getStringExtra(
154                         TelecomIntents.CONTACT_LOOKUP_URI_EXTRA));
155                 setContentFragment(ContactDetailsFragment.newInstance(contactUri,
156                         new ContactScrollListener()));
157                 break;
158 
159             default:
160                 showContactResultList(null /* query */);
161         }
162     }
163 
164     /**
165      * Displays the fragment that will show the results of a search. The given query is used as
166      * the initial search to populate the list.
167      */
showContactResultList(@ullable String query)168     private void showContactResultList(@Nullable String query) {
169         // Check that the result list is not already being displayed. If it is, then simply set the
170         // search query.
171         Fragment currentFragment = getCurrentFragment();
172         if (currentFragment instanceof ContactResultsFragment) {
173             ((ContactResultsFragment) currentFragment).setSearchQuery(query);
174             return;
175         }
176 
177         setContentFragment(ContactResultsFragment.newInstance(new ContactScrollListener(), query));
178     }
179 
180     /**
181      * Sets the fragment that will be shown as the main content of this Activity.
182      */
setContentFragment(Fragment fragment)183     private void setContentFragment(Fragment fragment) {
184         if (!mAllowFragmentCommits) {
185             return;
186         }
187 
188         // The search panel might have elevation added to it, so remove it when the fragment
189         // changes since any lists in it will be reset to the top.
190         resetSearchPanelElevation();
191 
192         getSupportFragmentManager().beginTransaction()
193                 .setCustomAnimations(R.animator.fade_in, R.animator.fade_out)
194                 .replace(R.id.content_fragment_container, fragment, CONTENT_FRAGMENT_TAG)
195                 .commitNow();
196     }
197 
198     /**
199      * Returns the fragment that is currently being displayed as the content view.
200      */
201     @Nullable
getCurrentFragment()202     private Fragment getCurrentFragment() {
203         return getSupportFragmentManager().findFragmentByTag(CONTENT_FRAGMENT_TAG);
204     }
205 
206     @Override
onStart()207     protected void onStart() {
208         super.onStart();
209         // Fragment commits are not allowed once the Activity's state has been saved. Once
210         // onStart() has been called, the FragmentManager should now allow commits.
211         mAllowFragmentCommits = true;
212     }
213 
214     @Override
onSaveInstanceState(Bundle outState)215     public void onSaveInstanceState(Bundle outState) {
216         // A transaction can only be committed with this method prior to its containing activity
217         // saving its state.
218         mAllowFragmentCommits = false;
219         super.onSaveInstanceState(outState);
220     }
221 
222     /**
223      * Checks if {@link #mSearchContainer} has an elevation set on it and if it does, animates the
224      * removal of this elevation.
225      */
resetSearchPanelElevation()226     private void resetSearchPanelElevation() {
227         if (mSearchContainer.getElevation() != 0.f) {
228             mRemoveElevationAnimator.start();
229         }
230     }
231 
232     /**
233      * A {@link Runnable} that will execute a contact search with the given {@link #mSearchQuery}.
234      */
235     private class SearchRunnable implements Runnable {
236         private final String mSearchQuery;
237 
SearchRunnable(String searchQuery)238         public SearchRunnable(String searchQuery) {
239             mSearchQuery = searchQuery;
240         }
241 
242         @Override
run()243         public void run() {
244             Fragment currentFragment = getCurrentFragment();
245             if (currentFragment instanceof ContactResultsFragment) {
246                 ((ContactResultsFragment) currentFragment).setSearchQuery(mSearchQuery);
247             }
248         }
249     }
250 
251     /**
252      * Listener for scrolls in a fragment that has a list. It will will add elevation on the
253      * container holding the search field. This elevation will give the illusion of the list
254      * scrolling under that container.
255      */
256     public class ContactScrollListener extends RecyclerView.OnScrollListener {
257         @Override
onScrolled(RecyclerView recyclerView, int dx, int dy)258         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
259             // The default LayoutManager for PagedListView is a LinearLayoutManager. Dialer does
260             // not change this.
261             LinearLayoutManager layoutManager =
262                     (LinearLayoutManager) recyclerView.getLayoutManager();
263 
264             if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
265                 resetSearchPanelElevation();
266             } else {
267                 // No animation needed when adding the elevation because the scroll masks the adding
268                 // of the elevation.
269                 mSearchContainer.setElevation(mContainerElevation);
270             }
271         }
272     }
273 }
274