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