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 com.android.contacts.R; 20 import com.android.contacts.activities.ActionBarAdapter.Listener.Action; 21 import com.android.contacts.list.ContactsRequest; 22 23 import android.app.ActionBar; 24 import android.app.ActionBar.LayoutParams; 25 import android.app.ActionBar.Tab; 26 import android.app.FragmentTransaction; 27 import android.content.Context; 28 import android.content.SharedPreferences; 29 import android.os.Bundle; 30 import android.preference.PreferenceManager; 31 import android.text.TextUtils; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.inputmethod.InputMethodManager; 35 import android.widget.SearchView; 36 import android.widget.SearchView.OnCloseListener; 37 import android.widget.SearchView.OnQueryTextListener; 38 39 /** 40 * Adapter for the action bar at the top of the Contacts activity. 41 */ 42 public class ActionBarAdapter implements OnQueryTextListener, OnCloseListener { 43 44 public interface Listener { 45 public enum Action { 46 CHANGE_SEARCH_QUERY, START_SEARCH_MODE, STOP_SEARCH_MODE 47 } 48 onAction(Action action)49 void onAction(Action action); 50 51 /** 52 * Called when the user selects a tab. The new tab can be obtained using 53 * {@link #getCurrentTab}. 54 */ onSelectedTabChanged()55 void onSelectedTabChanged(); 56 } 57 58 private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode"; 59 private static final String EXTRA_KEY_QUERY = "navBar.query"; 60 private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab"; 61 62 private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab"; 63 64 private boolean mSearchMode; 65 private String mQueryString; 66 67 private SearchView mSearchView; 68 69 private final Context mContext; 70 private final SharedPreferences mPrefs; 71 72 private Listener mListener; 73 74 private final ActionBar mActionBar; 75 private final MyTabListener mTabListener = new MyTabListener(); 76 77 private boolean mShowHomeIcon; 78 79 public enum TabState { 80 GROUPS, 81 ALL, 82 FAVORITES; 83 fromInt(int value)84 public static TabState fromInt(int value) { 85 if (GROUPS.ordinal() == value) { 86 return GROUPS; 87 } 88 if (ALL.ordinal() == value) { 89 return ALL; 90 } 91 if (FAVORITES.ordinal() == value) { 92 return FAVORITES; 93 } 94 throw new IllegalArgumentException("Invalid value: " + value); 95 } 96 } 97 98 private static final TabState DEFAULT_TAB = TabState.ALL; 99 private TabState mCurrentTab = DEFAULT_TAB; 100 ActionBarAdapter(Context context, Listener listener, ActionBar actionBar)101 public ActionBarAdapter(Context context, Listener listener, ActionBar actionBar) { 102 mContext = context; 103 mListener = listener; 104 mActionBar = actionBar; 105 mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext); 106 107 mShowHomeIcon = mContext.getResources().getBoolean(R.bool.show_home_icon); 108 109 // Set up search view. 110 View customSearchView = LayoutInflater.from(mActionBar.getThemedContext()).inflate( 111 R.layout.custom_action_bar, null); 112 int searchViewWidth = mContext.getResources().getDimensionPixelSize( 113 R.dimen.search_view_width); 114 if (searchViewWidth == 0) { 115 searchViewWidth = LayoutParams.MATCH_PARENT; 116 } 117 LayoutParams layoutParams = new LayoutParams(searchViewWidth, LayoutParams.WRAP_CONTENT); 118 mSearchView = (SearchView) customSearchView.findViewById(R.id.search_view); 119 // Since the {@link SearchView} in this app is "click-to-expand", set the below mode on the 120 // {@link SearchView} so that the magnifying glass icon appears inside the editable text 121 // field. (In the "click-to-expand" search pattern, the user must explicitly expand the 122 // search field and already knows a search is being conducted, so the icon is redundant 123 // and can go away once the user starts typing.) 124 mSearchView.setIconifiedByDefault(true); 125 mSearchView.setQueryHint(mContext.getString(R.string.hint_findContacts)); 126 mSearchView.setOnQueryTextListener(this); 127 mSearchView.setOnCloseListener(this); 128 mSearchView.setQuery(mQueryString, false); 129 mActionBar.setCustomView(customSearchView, layoutParams); 130 131 // Set up tabs 132 addTab(TabState.GROUPS, R.drawable.ic_tab_groups, R.string.contactsGroupsLabel); 133 addTab(TabState.ALL, R.drawable.ic_tab_all, R.string.contactsAllLabel); 134 addTab(TabState.FAVORITES, R.drawable.ic_tab_starred, R.string.contactsFavoritesLabel); 135 } 136 initialize(Bundle savedState, ContactsRequest request)137 public void initialize(Bundle savedState, ContactsRequest request) { 138 if (savedState == null) { 139 mSearchMode = request.isSearchMode(); 140 mQueryString = request.getQueryString(); 141 mCurrentTab = loadLastTabPreference(); 142 } else { 143 mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE); 144 mQueryString = savedState.getString(EXTRA_KEY_QUERY); 145 146 // Just set to the field here. The listener will be notified by update(). 147 mCurrentTab = TabState.fromInt(savedState.getInt(EXTRA_KEY_SELECTED_TAB)); 148 } 149 update(); 150 } 151 setListener(Listener listener)152 public void setListener(Listener listener) { 153 mListener = listener; 154 } 155 addTab(TabState tabState, int icon, int contentDescription)156 private void addTab(TabState tabState, int icon, int contentDescription) { 157 final Tab tab = mActionBar.newTab(); 158 tab.setTag(tabState); 159 tab.setIcon(icon); 160 tab.setContentDescription(contentDescription); 161 tab.setTabListener(mTabListener); 162 mActionBar.addTab(tab); 163 } 164 165 private class MyTabListener implements ActionBar.TabListener { 166 /** 167 * If true, it won't call {@link #setCurrentTab} in {@link #onTabSelected}. 168 * This flag is used when we want to programmatically update the current tab without 169 * {@link #onTabSelected} getting called. 170 */ 171 public boolean mIgnoreTabSelected; 172 onTabReselected(Tab tab, FragmentTransaction ft)173 @Override public void onTabReselected(Tab tab, FragmentTransaction ft) { } onTabUnselected(Tab tab, FragmentTransaction ft)174 @Override public void onTabUnselected(Tab tab, FragmentTransaction ft) { } 175 onTabSelected(Tab tab, FragmentTransaction ft)176 @Override public void onTabSelected(Tab tab, FragmentTransaction ft) { 177 if (!mIgnoreTabSelected) { 178 setCurrentTab((TabState)tab.getTag()); 179 } 180 } 181 } 182 183 /** 184 * Change the current tab, and notify the listener. 185 */ setCurrentTab(TabState tab)186 public void setCurrentTab(TabState tab) { 187 setCurrentTab(tab, true); 188 } 189 190 /** 191 * Change the current tab 192 */ setCurrentTab(TabState tab, boolean notifyListener)193 public void setCurrentTab(TabState tab, boolean notifyListener) { 194 if (tab == null) throw new NullPointerException(); 195 if (tab == mCurrentTab) { 196 return; 197 } 198 mCurrentTab = tab; 199 200 int index = mCurrentTab.ordinal(); 201 if ((mActionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_TABS) 202 && (index != mActionBar.getSelectedNavigationIndex())) { 203 mActionBar.setSelectedNavigationItem(index); 204 } 205 206 if (notifyListener && mListener != null) mListener.onSelectedTabChanged(); 207 saveLastTabPreference(mCurrentTab); 208 } 209 getCurrentTab()210 public TabState getCurrentTab() { 211 return mCurrentTab; 212 } 213 isSearchMode()214 public boolean isSearchMode() { 215 return mSearchMode; 216 } 217 shouldShowSearchResult()218 public boolean shouldShowSearchResult() { 219 return mSearchMode && !TextUtils.isEmpty(mQueryString); 220 } 221 setSearchMode(boolean flag)222 public void setSearchMode(boolean flag) { 223 if (mSearchMode != flag) { 224 mSearchMode = flag; 225 update(); 226 if (mSearchView == null) { 227 return; 228 } 229 if (mSearchMode) { 230 setFocusOnSearchView(); 231 } else { 232 mSearchView.setQuery(null, false); 233 } 234 } 235 } 236 getQueryString()237 public String getQueryString() { 238 return mQueryString; 239 } 240 setQueryString(String query)241 public void setQueryString(String query) { 242 mQueryString = query; 243 if (mSearchView != null) { 244 mSearchView.setQuery(query, false); 245 } 246 } 247 248 /** @return true if the "UP" icon is showing. */ isUpShowing()249 public boolean isUpShowing() { 250 return mSearchMode; // Only shown on the search mode. 251 } 252 updateDisplayOptions()253 private void updateDisplayOptions() { 254 // All the flags we may change in this method. 255 final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME 256 | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_CUSTOM; 257 258 // The current flags set to the action bar. (only the ones that we may change here) 259 final int current = mActionBar.getDisplayOptions() & MASK; 260 261 // Build the new flags... 262 int newFlags = 0; 263 newFlags |= ActionBar.DISPLAY_SHOW_TITLE; 264 if (mShowHomeIcon) { 265 newFlags |= ActionBar.DISPLAY_SHOW_HOME; 266 } 267 if (mSearchMode) { 268 newFlags |= ActionBar.DISPLAY_SHOW_HOME; 269 newFlags |= ActionBar.DISPLAY_HOME_AS_UP; 270 newFlags |= ActionBar.DISPLAY_SHOW_CUSTOM; 271 } 272 mActionBar.setHomeButtonEnabled(mSearchMode); 273 274 if (current != newFlags) { 275 // Pass the mask here to preserve other flags that we're not interested here. 276 mActionBar.setDisplayOptions(newFlags, MASK); 277 } 278 } 279 update()280 private void update() { 281 if (mSearchMode) { 282 setFocusOnSearchView(); 283 // Since we have the {@link SearchView} in a custom action bar, we must manually handle 284 // expanding the {@link SearchView} when a search is initiated. 285 mSearchView.onActionViewExpanded(); 286 if (mActionBar.getNavigationMode() != ActionBar.NAVIGATION_MODE_STANDARD) { 287 mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); 288 } 289 if (mListener != null) { 290 mListener.onAction(Action.START_SEARCH_MODE); 291 } 292 } else { 293 if (mActionBar.getNavigationMode() != ActionBar.NAVIGATION_MODE_TABS) { 294 // setNavigationMode will trigger onTabSelected() with the tab which was previously 295 // selected. 296 // The issue is that when we're first switching to the tab navigation mode after 297 // screen orientation changes, onTabSelected() will get called with the first tab 298 // (i.e. favorite), which would results in mCurrentTab getting set to FAVORITES and 299 // we'd lose restored tab. 300 // So let's just disable the callback here temporarily. We'll notify the listener 301 // after this anyway. 302 mTabListener.mIgnoreTabSelected = true; 303 mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); 304 mActionBar.setSelectedNavigationItem(mCurrentTab.ordinal()); 305 mTabListener.mIgnoreTabSelected = false; 306 } 307 mActionBar.setTitle(null); 308 // Since we have the {@link SearchView} in a custom action bar, we must manually handle 309 // collapsing the {@link SearchView} when search mode is exited. 310 mSearchView.onActionViewCollapsed(); 311 if (mListener != null) { 312 mListener.onAction(Action.STOP_SEARCH_MODE); 313 mListener.onSelectedTabChanged(); 314 } 315 } 316 updateDisplayOptions(); 317 } 318 319 @Override onQueryTextChange(String queryString)320 public boolean onQueryTextChange(String queryString) { 321 // TODO: Clean up SearchView code because it keeps setting the SearchView query, 322 // invoking onQueryChanged, setting up the fragment again, invalidating the options menu, 323 // storing the SearchView again, and etc... unless we add in the early return statements. 324 if (queryString.equals(mQueryString)) { 325 return false; 326 } 327 mQueryString = queryString; 328 if (!mSearchMode) { 329 if (!TextUtils.isEmpty(queryString)) { 330 setSearchMode(true); 331 } 332 } else if (mListener != null) { 333 mListener.onAction(Action.CHANGE_SEARCH_QUERY); 334 } 335 336 return true; 337 } 338 339 @Override onQueryTextSubmit(String query)340 public boolean onQueryTextSubmit(String query) { 341 // When the search is "committed" by the user, then hide the keyboard so the user can 342 // more easily browse the list of results. 343 if (mSearchView != null) { 344 InputMethodManager imm = (InputMethodManager) mContext.getSystemService( 345 Context.INPUT_METHOD_SERVICE); 346 if (imm != null) { 347 imm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0); 348 } 349 mSearchView.clearFocus(); 350 } 351 return true; 352 } 353 354 @Override onClose()355 public boolean onClose() { 356 setSearchMode(false); 357 return false; 358 } 359 onSaveInstanceState(Bundle outState)360 public void onSaveInstanceState(Bundle outState) { 361 outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode); 362 outState.putString(EXTRA_KEY_QUERY, mQueryString); 363 outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab.ordinal()); 364 } 365 setFocusOnSearchView()366 private void setFocusOnSearchView() { 367 mSearchView.requestFocus(); 368 mSearchView.setIconified(false); // Workaround for the "IME not popping up" issue. 369 } 370 saveLastTabPreference(TabState tab)371 private void saveLastTabPreference(TabState tab) { 372 mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab.ordinal()).apply(); 373 } 374 loadLastTabPreference()375 private TabState loadLastTabPreference() { 376 try { 377 return TabState.fromInt(mPrefs.getInt(PERSISTENT_LAST_TAB, DEFAULT_TAB.ordinal())); 378 } catch (IllegalArgumentException e) { 379 // Preference is corrupt? 380 return DEFAULT_TAB; 381 } 382 } 383 } 384