1 /* 2 * Copyright (C) 2010 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.contacts.activities; 18 19 import android.animation.ValueAnimator; 20 import android.app.ActionBar; 21 import android.content.Context; 22 import android.content.SharedPreferences; 23 import android.content.res.TypedArray; 24 import android.os.Bundle; 25 import android.preference.PreferenceManager; 26 import android.text.Editable; 27 import android.text.TextUtils; 28 import android.text.TextWatcher; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.inputmethod.InputMethodManager; 33 import android.widget.SearchView.OnCloseListener; 34 import android.view.View.OnClickListener; 35 import android.widget.EditText; 36 import android.widget.Toolbar; 37 38 import com.android.contacts.R; 39 import com.android.contacts.activities.ActionBarAdapter.Listener.Action; 40 import com.android.contacts.list.ContactsRequest; 41 42 /** 43 * Adapter for the action bar at the top of the Contacts activity. 44 */ 45 public class ActionBarAdapter implements OnCloseListener { 46 47 public interface Listener { 48 public abstract class Action { 49 public static final int CHANGE_SEARCH_QUERY = 0; 50 public static final int START_SEARCH_MODE = 1; 51 public static final int STOP_SEARCH_MODE = 2; 52 } 53 onAction(int action)54 void onAction(int action); 55 56 /** 57 * Called when the user selects a tab. The new tab can be obtained using 58 * {@link #getCurrentTab}. 59 */ onSelectedTabChanged()60 void onSelectedTabChanged(); 61 onUpButtonPressed()62 void onUpButtonPressed(); 63 } 64 65 private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode"; 66 private static final String EXTRA_KEY_QUERY = "navBar.query"; 67 private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab"; 68 69 private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab"; 70 71 private boolean mSearchMode; 72 private String mQueryString; 73 74 private EditText mSearchView; 75 /** The view that represents tabs when we are in portrait mode **/ 76 private View mPortraitTabs; 77 /** The view that represents tabs when we are in landscape mode **/ 78 private View mLandscapeTabs; 79 private View mSearchContainer; 80 81 private int mMaxPortraitTabHeight; 82 private int mMaxToolbarContentInsetStart; 83 84 private final Context mContext; 85 private final SharedPreferences mPrefs; 86 87 private Listener mListener; 88 89 private final ActionBar mActionBar; 90 private final Toolbar mToolbar; 91 92 private boolean mShowHomeIcon; 93 94 public interface TabState { 95 public static int FAVORITES = 0; 96 public static int ALL = 1; 97 98 public static int COUNT = 2; 99 public static int DEFAULT = ALL; 100 } 101 102 private int mCurrentTab = TabState.DEFAULT; 103 ActionBarAdapter(Context context, Listener listener, ActionBar actionBar, View portraitTabs, View landscapeTabs, Toolbar toolbar)104 public ActionBarAdapter(Context context, Listener listener, ActionBar actionBar, 105 View portraitTabs, View landscapeTabs, Toolbar toolbar) { 106 mContext = context; 107 mListener = listener; 108 mActionBar = actionBar; 109 mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext); 110 mPortraitTabs = portraitTabs; 111 mLandscapeTabs = landscapeTabs; 112 mToolbar = toolbar; 113 mMaxToolbarContentInsetStart = mToolbar.getContentInsetStart(); 114 mShowHomeIcon = mContext.getResources().getBoolean(R.bool.show_home_icon); 115 116 setupSearchView(); 117 setupTabs(context); 118 } 119 setupTabs(Context context)120 private void setupTabs(Context context) { 121 final TypedArray attributeArray = context.obtainStyledAttributes( 122 new int[]{android.R.attr.actionBarSize}); 123 mMaxPortraitTabHeight = attributeArray.getDimensionPixelSize(0, 0); 124 // Hide tabs initially 125 setPortraitTabHeight(0); 126 } 127 setupSearchView()128 private void setupSearchView() { 129 final LayoutInflater inflater = (LayoutInflater) mToolbar.getContext().getSystemService( 130 Context.LAYOUT_INFLATER_SERVICE); 131 mSearchContainer = inflater.inflate(R.layout.search_bar_expanded, mToolbar, 132 /* attachToRoot = */ false); 133 mSearchContainer.setVisibility(View.VISIBLE); 134 mToolbar.addView(mSearchContainer); 135 136 mSearchContainer.setBackgroundColor(mContext.getResources().getColor( 137 R.color.searchbox_background_color)); 138 mSearchView = (EditText) mSearchContainer.findViewById(R.id.search_view); 139 mSearchView.setHint(mContext.getString(R.string.hint_findContacts)); 140 mSearchView.addTextChangedListener(new SearchTextWatcher()); 141 mSearchContainer.findViewById(R.id.search_close_button).setOnClickListener( 142 new OnClickListener() { 143 @Override 144 public void onClick(View v) { 145 setQueryString(null); 146 } 147 }); 148 mSearchContainer.findViewById(R.id.search_back_button).setOnClickListener( 149 new OnClickListener() { 150 @Override 151 public void onClick(View v) { 152 if (mListener != null) { 153 mListener.onUpButtonPressed(); 154 } 155 } 156 }); 157 } 158 initialize(Bundle savedState, ContactsRequest request)159 public void initialize(Bundle savedState, ContactsRequest request) { 160 if (savedState == null) { 161 mSearchMode = request.isSearchMode(); 162 mQueryString = request.getQueryString(); 163 mCurrentTab = loadLastTabPreference(); 164 } else { 165 mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE); 166 mQueryString = savedState.getString(EXTRA_KEY_QUERY); 167 168 // Just set to the field here. The listener will be notified by update(). 169 mCurrentTab = savedState.getInt(EXTRA_KEY_SELECTED_TAB); 170 } 171 if (mCurrentTab >= TabState.COUNT || mCurrentTab < 0) { 172 // Invalid tab index was saved (b/12938207). Restore the default. 173 mCurrentTab = TabState.DEFAULT; 174 } 175 // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in 176 // search mode. 177 update(true /* skipAnimation */); 178 // Expanding the {@link SearchView} clears the query, so set the query from the 179 // {@link ContactsRequest} after it has been expanded, if applicable. 180 if (mSearchMode && !TextUtils.isEmpty(mQueryString)) { 181 setQueryString(mQueryString); 182 } 183 } 184 setListener(Listener listener)185 public void setListener(Listener listener) { 186 mListener = listener; 187 } 188 189 private class SearchTextWatcher implements TextWatcher { 190 191 @Override onTextChanged(CharSequence queryString, int start, int before, int count)192 public void onTextChanged(CharSequence queryString, int start, int before, int count) { 193 if (queryString.equals(mQueryString)) { 194 return; 195 } 196 mQueryString = queryString.toString(); 197 if (!mSearchMode) { 198 if (!TextUtils.isEmpty(queryString)) { 199 setSearchMode(true); 200 } 201 } else if (mListener != null) { 202 mListener.onAction(Action.CHANGE_SEARCH_QUERY); 203 } 204 } 205 206 @Override afterTextChanged(Editable s)207 public void afterTextChanged(Editable s) {} 208 209 @Override beforeTextChanged(CharSequence s, int start, int count, int after)210 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 211 } 212 213 /** 214 * Save the current tab selection, and notify the listener. 215 */ setCurrentTab(int tab)216 public void setCurrentTab(int tab) { 217 setCurrentTab(tab, true); 218 } 219 220 /** 221 * Save the current tab selection. 222 */ setCurrentTab(int tab, boolean notifyListener)223 public void setCurrentTab(int tab, boolean notifyListener) { 224 if (tab == mCurrentTab) { 225 return; 226 } 227 mCurrentTab = tab; 228 229 if (notifyListener && mListener != null) mListener.onSelectedTabChanged(); 230 saveLastTabPreference(mCurrentTab); 231 } 232 getCurrentTab()233 public int getCurrentTab() { 234 return mCurrentTab; 235 } 236 237 /** 238 * @return Whether in search mode, i.e. if the search view is visible/expanded. 239 * 240 * Note even if the action bar is in search mode, if the query is empty, the search fragment 241 * will not be in search mode. 242 */ isSearchMode()243 public boolean isSearchMode() { 244 return mSearchMode; 245 } 246 setSearchMode(boolean flag)247 public void setSearchMode(boolean flag) { 248 if (mSearchMode != flag) { 249 mSearchMode = flag; 250 update(false /* skipAnimation */); 251 if (mSearchView == null) { 252 return; 253 } 254 if (mSearchMode) { 255 mSearchView.setEnabled(true); 256 setFocusOnSearchView(); 257 } else { 258 // Disable search view, so that it doesn't keep the IME visible. 259 mSearchView.setEnabled(false); 260 } 261 setQueryString(null); 262 } else if (flag) { 263 // Everything is already set up. Still make sure the keyboard is up 264 if (mSearchView != null) setFocusOnSearchView(); 265 } 266 } 267 getQueryString()268 public String getQueryString() { 269 return mSearchMode ? mQueryString : null; 270 } 271 setQueryString(String query)272 public void setQueryString(String query) { 273 mQueryString = query; 274 if (mSearchView != null) { 275 mSearchView.setText(query); 276 // When programmatically entering text into the search view, the most reasonable 277 // place for the cursor is after all the text. 278 mSearchView.setSelection(mSearchView.getText() == null ? 279 0 : mSearchView.getText().length()); 280 } 281 } 282 283 /** @return true if the "UP" icon is showing. */ isUpShowing()284 public boolean isUpShowing() { 285 return mSearchMode; // Only shown on the search mode. 286 } 287 updateDisplayOptionsInner()288 private void updateDisplayOptionsInner() { 289 // All the flags we may change in this method. 290 final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME 291 | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_CUSTOM; 292 293 // The current flags set to the action bar. (only the ones that we may change here) 294 final int current = mActionBar.getDisplayOptions() & MASK; 295 296 // Build the new flags... 297 int newFlags = 0; 298 if (mShowHomeIcon && !mSearchMode) { 299 newFlags |= ActionBar.DISPLAY_SHOW_HOME; 300 } 301 if (mSearchMode) { 302 newFlags |= ActionBar.DISPLAY_SHOW_CUSTOM; 303 mToolbar.setContentInsetsRelative(0, mToolbar.getContentInsetEnd()); 304 } else { 305 newFlags |= ActionBar.DISPLAY_SHOW_TITLE; 306 mToolbar.setContentInsetsRelative(mMaxToolbarContentInsetStart, 307 mToolbar.getContentInsetEnd()); 308 } 309 310 311 if (current != newFlags) { 312 // Pass the mask here to preserve other flags that we're not interested here. 313 mActionBar.setDisplayOptions(newFlags, MASK); 314 } 315 } 316 update(boolean skipAnimation)317 private void update(boolean skipAnimation) { 318 final boolean isIconifiedChanging 319 = (mSearchContainer.getParent() == null) == mSearchMode; 320 if (isIconifiedChanging && !skipAnimation) { 321 mToolbar.removeView(mLandscapeTabs); 322 if (mSearchMode) { 323 addSearchContainer(); 324 mSearchContainer.setAlpha(0); 325 mSearchContainer.animate().alpha(1); 326 animateTabHeightChange(mMaxPortraitTabHeight, 0); 327 updateDisplayOptions(isIconifiedChanging); 328 } else { 329 mSearchContainer.setAlpha(1); 330 animateTabHeightChange(0, mMaxPortraitTabHeight); 331 mSearchContainer.animate().alpha(0).withEndAction(new Runnable() { 332 @Override 333 public void run() { 334 updateDisplayOptionsInner(); 335 updateDisplayOptions(isIconifiedChanging); 336 addLandscapeViewPagerTabs(); 337 mToolbar.removeView(mSearchContainer); 338 } 339 }); 340 } 341 return; 342 } 343 if (isIconifiedChanging && skipAnimation) { 344 mToolbar.removeView(mLandscapeTabs); 345 if (mSearchMode) { 346 setPortraitTabHeight(0); 347 addSearchContainer(); 348 } else { 349 setPortraitTabHeight(mMaxPortraitTabHeight); 350 mToolbar.removeView(mSearchContainer); 351 addLandscapeViewPagerTabs(); 352 } 353 } 354 updateDisplayOptions(isIconifiedChanging); 355 } 356 addLandscapeViewPagerTabs()357 private void addLandscapeViewPagerTabs() { 358 if (mLandscapeTabs != null) { 359 mToolbar.removeView(mLandscapeTabs); 360 mToolbar.addView(mLandscapeTabs); 361 } 362 } 363 addSearchContainer()364 private void addSearchContainer() { 365 mToolbar.removeView(mSearchContainer); 366 mToolbar.addView(mSearchContainer); 367 } 368 updateDisplayOptions(boolean isIconifiedChanging)369 private void updateDisplayOptions(boolean isIconifiedChanging) { 370 if (mSearchMode) { 371 setFocusOnSearchView(); 372 // Since we have the {@link SearchView} in a custom action bar, we must manually handle 373 // expanding the {@link SearchView} when a search is initiated. Note that a side effect 374 // of this method is that the {@link SearchView} query text is set to empty string. 375 if (isIconifiedChanging) { 376 final CharSequence queryText = mSearchView.getText(); 377 if (!TextUtils.isEmpty(queryText)) { 378 mSearchView.setText(queryText); 379 } 380 } 381 if (mListener != null) { 382 mListener.onAction(Action.START_SEARCH_MODE); 383 } 384 } else { 385 if (mListener != null) { 386 mListener.onAction(Action.STOP_SEARCH_MODE); 387 mListener.onSelectedTabChanged(); 388 } 389 } 390 updateDisplayOptionsInner(); 391 } 392 393 @Override onClose()394 public boolean onClose() { 395 setSearchMode(false); 396 return false; 397 } 398 onSaveInstanceState(Bundle outState)399 public void onSaveInstanceState(Bundle outState) { 400 outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode); 401 outState.putString(EXTRA_KEY_QUERY, mQueryString); 402 outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab); 403 } 404 setFocusOnSearchView()405 public void setFocusOnSearchView() { 406 mSearchView.requestFocus(); 407 showInputMethod(mSearchView); // Workaround for the "IME not popping up" issue. 408 } 409 showInputMethod(View view)410 private void showInputMethod(View view) { 411 final InputMethodManager imm = (InputMethodManager) mContext.getSystemService( 412 Context.INPUT_METHOD_SERVICE); 413 if (imm != null) { 414 imm.showSoftInput(view, 0); 415 } 416 } 417 saveLastTabPreference(int tab)418 private void saveLastTabPreference(int tab) { 419 mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab).apply(); 420 } 421 loadLastTabPreference()422 private int loadLastTabPreference() { 423 try { 424 return mPrefs.getInt(PERSISTENT_LAST_TAB, TabState.DEFAULT); 425 } catch (IllegalArgumentException e) { 426 // Preference is corrupt? 427 return TabState.DEFAULT; 428 } 429 } 430 animateTabHeightChange(int start, int end)431 private void animateTabHeightChange(int start, int end) { 432 if (mPortraitTabs == null) { 433 return; 434 } 435 final ValueAnimator animator = ValueAnimator.ofInt(start, end); 436 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 437 @Override 438 public void onAnimationUpdate(ValueAnimator valueAnimator) { 439 int value = (Integer) valueAnimator.getAnimatedValue(); 440 setPortraitTabHeight(value); 441 } 442 }); 443 animator.setDuration(100).start(); 444 } 445 setPortraitTabHeight(int height)446 private void setPortraitTabHeight(int height) { 447 if (mPortraitTabs == null) { 448 return; 449 } 450 ViewGroup.LayoutParams layoutParams = mPortraitTabs.getLayoutParams(); 451 layoutParams.height = height; 452 mPortraitTabs.setLayoutParams(layoutParams); 453 } 454 } 455