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