diff --git a/APG/android-libs/ActionBarSherlock/AndroidManifest.xml b/APG/android-libs/ActionBarSherlock/AndroidManifest.xml index c4a75f32c..7b8a84824 100644 --- a/APG/android-libs/ActionBarSherlock/AndroidManifest.xml +++ b/APG/android-libs/ActionBarSherlock/AndroidManifest.xml @@ -1,6 +1,6 @@ - + - + diff --git a/APG/android-libs/ActionBarSherlock/build.xml b/APG/android-libs/ActionBarSherlock/build.xml index 3948e7c6a..56bc65293 100644 --- a/APG/android-libs/ActionBarSherlock/build.xml +++ b/APG/android-libs/ActionBarSherlock/build.xml @@ -1,5 +1,5 @@ - + + + + + + + - - - + + + + + + + diff --git a/APG/android-libs/ActionBarSherlock/res/drawable/abs__ic_clear_holo_light.xml b/APG/android-libs/ActionBarSherlock/res/drawable/abs__ic_clear_holo_light.xml new file mode 100644 index 000000000..256de80fb --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/res/drawable/abs__ic_clear_holo_light.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/APG/android-libs/ActionBarSherlock/res/drawable/abs__search_dropdown_dark.xml b/APG/android-libs/ActionBarSherlock/res/drawable/abs__search_dropdown_dark.xml new file mode 100644 index 000000000..26284187a --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/res/drawable/abs__search_dropdown_dark.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/APG/android-libs/ActionBarSherlock/res/drawable/abs__search_dropdown_light.xml b/APG/android-libs/ActionBarSherlock/res/drawable/abs__search_dropdown_light.xml new file mode 100644 index 000000000..0d00c5878 --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/res/drawable/abs__search_dropdown_light.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/APG/android-libs/ActionBarSherlock/res/drawable/abs__textfield_searchview_holo_dark.xml b/APG/android-libs/ActionBarSherlock/res/drawable/abs__textfield_searchview_holo_dark.xml new file mode 100644 index 000000000..b6d58c040 --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/res/drawable/abs__textfield_searchview_holo_dark.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/APG/android-libs/ActionBarSherlock/res/drawable/abs__textfield_searchview_holo_light.xml b/APG/android-libs/ActionBarSherlock/res/drawable/abs__textfield_searchview_holo_light.xml new file mode 100644 index 000000000..3d6acf808 --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/res/drawable/abs__textfield_searchview_holo_light.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/APG/android-libs/ActionBarSherlock/res/drawable/abs__textfield_searchview_right_holo_dark.xml b/APG/android-libs/ActionBarSherlock/res/drawable/abs__textfield_searchview_right_holo_dark.xml new file mode 100644 index 000000000..05ff4eda5 --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/res/drawable/abs__textfield_searchview_right_holo_dark.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/APG/android-libs/ActionBarSherlock/res/drawable/abs__textfield_searchview_right_holo_light.xml b/APG/android-libs/ActionBarSherlock/res/drawable/abs__textfield_searchview_right_holo_light.xml new file mode 100644 index 000000000..f6d61e57a --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/res/drawable/abs__textfield_searchview_right_holo_light.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/APG/android-libs/ActionBarSherlock/res/layout/abs__activity_chooser_view.xml b/APG/android-libs/ActionBarSherlock/res/layout/abs__activity_chooser_view.xml index 019d14ef4..6a0ac9ece 100644 --- a/APG/android-libs/ActionBarSherlock/res/layout/abs__activity_chooser_view.xml +++ b/APG/android-libs/ActionBarSherlock/res/layout/abs__activity_chooser_view.xml @@ -19,7 +19,7 @@ diff --git a/APG/android-libs/ActionBarSherlock/res/layout/abs__dialog_title_holo.xml b/APG/android-libs/ActionBarSherlock/res/layout/abs__dialog_title_holo.xml index 6402f28be..ab2b0ee6c 100644 --- a/APG/android-libs/ActionBarSherlock/res/layout/abs__dialog_title_holo.xml +++ b/APG/android-libs/ActionBarSherlock/res/layout/abs__dialog_title_holo.xml @@ -35,7 +35,7 @@ enabled. android:layout_height="2dip" android:background="@color/abs__holo_blue_light" /> diff --git a/APG/android-libs/ActionBarSherlock/res/layout/abs__search_dropdown_item_icons_2line.xml b/APG/android-libs/ActionBarSherlock/res/layout/abs__search_dropdown_item_icons_2line.xml new file mode 100644 index 000000000..e1d3dc49c --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/res/layout/abs__search_dropdown_item_icons_2line.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/APG/android-libs/ActionBarSherlock/res/layout/abs__search_view.xml b/APG/android-libs/ActionBarSherlock/res/layout/abs__search_view.xml new file mode 100644 index 000000000..6ba319121 --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/res/layout/abs__search_view.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/APG/android-libs/ActionBarSherlock/res/layout/abs__simple_dropdown_hint.xml b/APG/android-libs/ActionBarSherlock/res/layout/abs__simple_dropdown_hint.xml new file mode 100644 index 000000000..8fc0eb12c --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/res/layout/abs__simple_dropdown_hint.xml @@ -0,0 +1,29 @@ + + + diff --git a/APG/android-libs/ActionBarSherlock/res/values-v14/abs__styles.xml b/APG/android-libs/ActionBarSherlock/res/values-v14/abs__styles.xml index f2aa64d2d..88a60dd92 100644 --- a/APG/android-libs/ActionBarSherlock/res/values-v14/abs__styles.xml +++ b/APG/android-libs/ActionBarSherlock/res/values-v14/abs__styles.xml @@ -85,6 +85,11 @@ + + + diff --git a/APG/android-libs/ActionBarSherlock/res/values-v14/abs__themes.xml b/APG/android-libs/ActionBarSherlock/res/values-v14/abs__themes.xml index ceb960737..5fac1ab58 100644 --- a/APG/android-libs/ActionBarSherlock/res/values-v14/abs__themes.xml +++ b/APG/android-libs/ActionBarSherlock/res/values-v14/abs__themes.xml @@ -10,10 +10,12 @@ ?android:attr/actionBarSize 48dp - + @style/Theme.Sherlock ?android:attr/dropDownListViewStyle + + ?android:attr/selectableItemBackground @@ -279,6 +280,28 @@ + + + + + + + + diff --git a/APG/android-libs/ActionBarSherlock/res/values/abs__themes.xml b/APG/android-libs/ActionBarSherlock/res/values/abs__themes.xml index 5300dedd6..634fa798b 100644 --- a/APG/android-libs/ActionBarSherlock/res/values/abs__themes.xml +++ b/APG/android-libs/ActionBarSherlock/res/values/abs__themes.xml @@ -11,7 +11,7 @@ - + - - + + - - - - - - - + + @@ -205,21 +218,21 @@ @null @android:style/Animation.Dialog stateUnspecified|adjustPan - + false true true true 0.6 - + @null - + @color/abs__primary_text_holo_light @color/abs__primary_text_holo_dark - + @dimen/abs__dialog_min_width_major @dimen/abs__dialog_min_width_minor - + false @null diff --git a/APG/android-libs/ActionBarSherlock/src/android/support/v4/app/Watson.java b/APG/android-libs/ActionBarSherlock/src/android/support/v4/app/Watson.java new file mode 100644 index 000000000..d93de4c6a --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/src/android/support/v4/app/Watson.java @@ -0,0 +1,144 @@ +package android.support.v4.app; + +import android.util.Log; +import android.view.View; +import android.view.Window; +import com.actionbarsherlock.ActionBarSherlock.OnCreatePanelMenuListener; +import com.actionbarsherlock.ActionBarSherlock.OnMenuItemSelectedListener; +import com.actionbarsherlock.ActionBarSherlock.OnPreparePanelListener; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuInflater; +import com.actionbarsherlock.view.MenuItem; + +import java.util.ArrayList; + +/** I'm in ur package. Stealing ur variables. */ +public abstract class Watson extends FragmentActivity implements OnCreatePanelMenuListener, OnPreparePanelListener, OnMenuItemSelectedListener { + private static final boolean DEBUG = false; + private static final String TAG = "Watson"; + + /** Fragment interface for menu creation callback. */ + public interface OnCreateOptionsMenuListener { + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater); + } + /** Fragment interface for menu preparation callback. */ + public interface OnPrepareOptionsMenuListener { + public void onPrepareOptionsMenu(Menu menu); + } + /** Fragment interface for menu item selection callback. */ + public interface OnOptionsItemSelectedListener { + public boolean onOptionsItemSelected(MenuItem item); + } + + private ArrayList mCreatedMenus; + + + /////////////////////////////////////////////////////////////////////////// + // Sherlock menu handling + /////////////////////////////////////////////////////////////////////////// + + @Override + public boolean onCreatePanelMenu(int featureId, Menu menu) { + if (DEBUG) Log.d(TAG, "[onCreatePanelMenu] featureId: " + featureId + ", menu: " + menu); + + if (featureId == Window.FEATURE_OPTIONS_PANEL) { + boolean result = onCreateOptionsMenu(menu); + if (DEBUG) Log.d(TAG, "[onCreatePanelMenu] activity create result: " + result); + + MenuInflater inflater = getSupportMenuInflater(); + boolean show = false; + ArrayList newMenus = null; + if (mFragments.mAdded != null) { + for (int i = 0; i < mFragments.mAdded.size(); i++) { + Fragment f = mFragments.mAdded.get(i); + if (f != null && !f.mHidden && f.mHasMenu && f.mMenuVisible && f instanceof OnCreateOptionsMenuListener) { + show = true; + ((OnCreateOptionsMenuListener)f).onCreateOptionsMenu(menu, inflater); + if (newMenus == null) { + newMenus = new ArrayList(); + } + newMenus.add(f); + } + } + } + + if (mCreatedMenus != null) { + for (int i = 0; i < mCreatedMenus.size(); i++) { + Fragment f = mCreatedMenus.get(i); + if (newMenus == null || !newMenus.contains(f)) { + f.onDestroyOptionsMenu(); + } + } + } + + mCreatedMenus = newMenus; + + if (DEBUG) Log.d(TAG, "[onCreatePanelMenu] fragments create result: " + show); + result |= show; + + if (DEBUG) Log.d(TAG, "[onCreatePanelMenu] returning " + result); + return result; + } + return false; + } + + @Override + public boolean onPreparePanel(int featureId, View view, Menu menu) { + if (DEBUG) Log.d(TAG, "[onPreparePanel] featureId: " + featureId + ", view: " + view + " menu: " + menu); + + if (featureId == Window.FEATURE_OPTIONS_PANEL) { + boolean result = onPrepareOptionsMenu(menu); + if (DEBUG) Log.d(TAG, "[onPreparePanel] activity prepare result: " + result); + + boolean show = false; + if (mFragments.mAdded != null) { + for (int i = 0; i < mFragments.mAdded.size(); i++) { + Fragment f = mFragments.mAdded.get(i); + if (f != null && !f.mHidden && f.mHasMenu && f.mMenuVisible && f instanceof OnPrepareOptionsMenuListener) { + show = true; + ((OnPrepareOptionsMenuListener)f).onPrepareOptionsMenu(menu); + } + } + } + + if (DEBUG) Log.d(TAG, "[onPreparePanel] fragments prepare result: " + show); + result |= show; + + result &= menu.hasVisibleItems(); + if (DEBUG) Log.d(TAG, "[onPreparePanel] returning " + result); + return result; + } + return false; + } + + @Override + public boolean onMenuItemSelected(int featureId, MenuItem item) { + if (DEBUG) Log.d(TAG, "[onMenuItemSelected] featureId: " + featureId + ", item: " + item); + + if (featureId == Window.FEATURE_OPTIONS_PANEL) { + if (onOptionsItemSelected(item)) { + return true; + } + + if (mFragments.mAdded != null) { + for (int i = 0; i < mFragments.mAdded.size(); i++) { + Fragment f = mFragments.mAdded.get(i); + if (f != null && !f.mHidden && f.mHasMenu && f.mMenuVisible && f instanceof OnOptionsItemSelectedListener) { + if (((OnOptionsItemSelectedListener)f).onOptionsItemSelected(item)) { + return true; + } + } + } + } + } + return false; + } + + public abstract boolean onCreateOptionsMenu(Menu menu); + + public abstract boolean onPrepareOptionsMenu(Menu menu); + + public abstract boolean onOptionsItemSelected(MenuItem item); + + public abstract MenuInflater getSupportMenuInflater(); +} diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/ActionBarSherlock.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/ActionBarSherlock.java index 8340fb591..ab160f836 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/ActionBarSherlock.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/ActionBarSherlock.java @@ -537,6 +537,9 @@ public abstract class ActionBarSherlock { */ public void dispatchDestroy() {} + public void dispatchSaveInstanceState(Bundle outState) {} + + public void dispatchRestoreInstanceState(Bundle savedInstanceState) {} /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// @@ -769,7 +772,7 @@ public abstract class ActionBarSherlock { // Make sure that action views can get an appropriate theme. if (mMenuInflater == null) { if (getActionBar() != null) { - mMenuInflater = new MenuInflater(getThemedContext()); + mMenuInflater = new MenuInflater(getThemedContext(), mActivity); } else { mMenuInflater = new MenuInflater(mActivity); } diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/ActionBar.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/ActionBar.java index 2497d24ff..03755be2b 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/ActionBar.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/ActionBar.java @@ -17,6 +17,7 @@ package com.actionbarsherlock.app; import android.content.Context; +import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.support.v4.app.FragmentTransaction; import android.util.AttributeSet; @@ -895,6 +896,10 @@ public abstract class ActionBar { * @attr ref android.R.styleable#ActionBar_LayoutParams_layout_gravity */ public static class LayoutParams extends MarginLayoutParams { + private static final int[] ATTRS = new int[] { + android.R.attr.layout_gravity + }; + /** * Gravity for the view associated with these LayoutParams. * @@ -918,6 +923,10 @@ public abstract class ActionBar { public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); + + TypedArray a = c.obtainStyledAttributes(attrs, ATTRS); + gravity = a.getInt(0, -1); + a.recycle(); } public LayoutParams(int width, int height) { diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockActivity.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockActivity.java index 9cb57e95a..7b4543640 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockActivity.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockActivity.java @@ -116,6 +116,17 @@ public abstract class SherlockActivity extends Activity implements OnCreatePanel return super.dispatchKeyEvent(event); } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + getSherlock().dispatchSaveInstanceState(outState); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + getSherlock().dispatchRestoreInstanceState(savedInstanceState); + } /////////////////////////////////////////////////////////////////////////// // Native menu handling diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockFragmentActivity.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockFragmentActivity.java index 5cd13ba7c..3d092f033 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockFragmentActivity.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockFragmentActivity.java @@ -2,7 +2,7 @@ package com.actionbarsherlock.app; import android.content.res.Configuration; import android.os.Bundle; -import android.support.v4.app._ActionBarSherlockTrojanHorse; +import android.support.v4.app.Watson; import android.util.Log; import android.view.KeyEvent; import android.view.View; @@ -17,8 +17,8 @@ import com.actionbarsherlock.view.MenuItem; import static com.actionbarsherlock.ActionBarSherlock.OnActionModeFinishedListener; import static com.actionbarsherlock.ActionBarSherlock.OnActionModeStartedListener; -/** @see {@link _ActionBarSherlockTrojanHorse} */ -public class SherlockFragmentActivity extends _ActionBarSherlockTrojanHorse implements OnActionModeStartedListener, OnActionModeFinishedListener { +/** @see {@link android.support.v4.app.Watson} */ +public class SherlockFragmentActivity extends Watson implements OnActionModeStartedListener, OnActionModeFinishedListener { private static final boolean DEBUG = false; private static final String TAG = "SherlockFragmentActivity"; @@ -122,6 +122,17 @@ public class SherlockFragmentActivity extends _ActionBarSherlockTrojanHorse impl return super.dispatchKeyEvent(event); } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + getSherlock().dispatchSaveInstanceState(outState); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + getSherlock().dispatchRestoreInstanceState(savedInstanceState); + } /////////////////////////////////////////////////////////////////////////// // Native menu handling diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockListActivity.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockListActivity.java index 00c00fee5..aba6d85e8 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockListActivity.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockListActivity.java @@ -116,6 +116,17 @@ public abstract class SherlockListActivity extends ListActivity implements OnCre return super.dispatchKeyEvent(event); } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + getSherlock().dispatchSaveInstanceState(outState); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + getSherlock().dispatchRestoreInstanceState(savedInstanceState); + } /////////////////////////////////////////////////////////////////////////// // Native menu handling diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockPreferenceActivity.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockPreferenceActivity.java index 4f80be515..bee72cb25 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockPreferenceActivity.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/app/SherlockPreferenceActivity.java @@ -116,6 +116,17 @@ public abstract class SherlockPreferenceActivity extends PreferenceActivity impl return super.dispatchKeyEvent(event); } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + getSherlock().dispatchSaveInstanceState(outState); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + getSherlock().dispatchRestoreInstanceState(savedInstanceState); + } /////////////////////////////////////////////////////////////////////////// // Native menu handling diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/ActionBarSherlockCompat.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/ActionBarSherlockCompat.java index 05353d28c..5e69275c7 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/ActionBarSherlockCompat.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/ActionBarSherlockCompat.java @@ -52,6 +52,7 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu /** Window features which are enabled by default. */ protected static final int DEFAULT_FEATURES = 0; + static private final String PANELS_TAG = "sherlock:Panels"; public ActionBarSherlockCompat(Activity activity, int flags) { super(activity, flags); @@ -71,8 +72,6 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu private MenuBuilder mMenu; /** Map between native options items and sherlock items. */ protected HashMap mNativeItemMap; - /** Indication of a long-press on the hardware menu key. */ - private boolean mMenuKeyIsLongPress = false; /** Parent view of the window decoration (action bar, mode, etc.). */ private ViewGroup mDecor; @@ -293,7 +292,10 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu return false; } - return wActionBar.hideOverflowMenu(); + if (wActionBar != null) { + return wActionBar.hideOverflowMenu(); + } + return false; } @Override @@ -424,27 +426,8 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu } } - boolean result = false; - if (keyCode == KeyEvent.KEYCODE_MENU && isReservingOverflow()) { - if (event.getAction() == KeyEvent.ACTION_DOWN && event.isLongPress()) { - mMenuKeyIsLongPress = true; - } else if (event.getAction() == KeyEvent.ACTION_UP) { - if (!mMenuKeyIsLongPress) { - if (mActionMode == null && wActionBar != null) { - if (wActionBar.isOverflowMenuShowing()) { - wActionBar.hideOverflowMenu(); - } else { - wActionBar.showOverflowMenu(); - } - } - result = true; - } - mMenuKeyIsLongPress = false; - } - } - - if (DEBUG) Log.d(TAG, "[dispatchKeyEvent] returning " + result); - return result; + if (DEBUG) Log.d(TAG, "[dispatchKeyEvent] returning false"); + return false; } @Override @@ -452,6 +435,19 @@ public class ActionBarSherlockCompat extends ActionBarSherlock implements MenuBu mIsDestroyed = true; } + @Override + public void dispatchSaveInstanceState(Bundle outState) { + if (mMenu != null) { + mMenuFrozenActionViewState = new Bundle(); + mMenu.saveActionViewStates(mMenuFrozenActionViewState); + } + outState.putParcelable(PANELS_TAG, mMenuFrozenActionViewState); + } + + @Override + public void dispatchRestoreInstanceState(Bundle savedInstanceState) { + mMenuFrozenActionViewState = savedInstanceState.getParcelable(PANELS_TAG); + } /////////////////////////////////////////////////////////////////////////// // Menu callback lifecycle and creation diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/ActionBarSherlockNative.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/ActionBarSherlockNative.java index 9afca185a..0824d3848 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/ActionBarSherlockNative.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/ActionBarSherlockNative.java @@ -208,7 +208,12 @@ public class ActionBarSherlockNative extends ActionBarSherlock { //is where we will set the new instance to mActionMode since we need //to pass it through to the sherlock callbacks and the call below //will not have returned yet to store its value. - mActivity.startActionMode(wrapped); + if (mActivity.startActionMode(wrapped) == null) { + mActionMode = null; + } + if (mActivity instanceof OnActionModeStartedListener && mActionMode != null) { + ((OnActionModeStartedListener)mActivity).onActionModeStarted(mActionMode); + } return mActionMode; } @@ -241,6 +246,9 @@ public class ActionBarSherlockNative extends ActionBarSherlock { @Override public void onDestroyActionMode(android.view.ActionMode mode) { mCallback.onDestroyActionMode(mActionMode); + if (mActivity instanceof OnActionModeFinishedListener) { + ((OnActionModeFinishedListener)mActivity).onActionModeFinished(mActionMode); + } } } diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/app/ActionBarImpl.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/app/ActionBarImpl.java index 6ae0402c0..d022a2465 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/app/ActionBarImpl.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/app/ActionBarImpl.java @@ -26,6 +26,7 @@ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Handler; +import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentTransaction; import android.util.TypedValue; import android.view.ContextThemeWrapper; @@ -36,7 +37,6 @@ import android.view.accessibility.AccessibilityEvent; import android.widget.SpinnerAdapter; import com.actionbarsherlock.R; import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.SherlockFragmentActivity; import com.actionbarsherlock.internal.nineoldandroids.animation.Animator; import com.actionbarsherlock.internal.nineoldandroids.animation.AnimatorListenerAdapter; import com.actionbarsherlock.internal.nineoldandroids.animation.AnimatorSet; @@ -506,8 +506,8 @@ public class ActionBarImpl extends ActionBar { } FragmentTransaction trans = null; - if (mActivity instanceof SherlockFragmentActivity) { - trans = ((SherlockFragmentActivity)mActivity).getSupportFragmentManager().beginTransaction() + if (mActivity instanceof FragmentActivity) { + trans = ((FragmentActivity)mActivity).getSupportFragmentManager().beginTransaction() .disallowAddToBackStack(); } diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/app/ActionBarWrapper.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/app/ActionBarWrapper.java index e390ea428..840cb3d27 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/app/ActionBarWrapper.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/app/ActionBarWrapper.java @@ -6,12 +6,12 @@ import java.util.Set; import android.app.Activity; import android.content.Context; import android.graphics.drawable.Drawable; +import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentTransaction; import android.view.View; import android.widget.SpinnerAdapter; import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.SherlockFragmentActivity; public class ActionBarWrapper extends ActionBar implements android.app.ActionBar.OnNavigationListener, android.app.ActionBar.OnMenuVisibilityListener { private final Activity mActivity; @@ -319,8 +319,8 @@ public class ActionBarWrapper extends ActionBar implements android.app.ActionBar public void onTabReselected(android.app.ActionBar.Tab tab, android.app.FragmentTransaction ft) { if (mListener != null) { FragmentTransaction trans = null; - if (mActivity instanceof SherlockFragmentActivity) { - trans = ((SherlockFragmentActivity)mActivity).getSupportFragmentManager().beginTransaction() + if (mActivity instanceof FragmentActivity) { + trans = ((FragmentActivity)mActivity).getSupportFragmentManager().beginTransaction() .disallowAddToBackStack(); } @@ -336,8 +336,8 @@ public class ActionBarWrapper extends ActionBar implements android.app.ActionBar public void onTabSelected(android.app.ActionBar.Tab tab, android.app.FragmentTransaction ft) { if (mListener != null) { - if (mFragmentTransaction == null && mActivity instanceof SherlockFragmentActivity) { - mFragmentTransaction = ((SherlockFragmentActivity)mActivity).getSupportFragmentManager().beginTransaction() + if (mFragmentTransaction == null && mActivity instanceof FragmentActivity) { + mFragmentTransaction = ((FragmentActivity)mActivity).getSupportFragmentManager().beginTransaction() .disallowAddToBackStack(); } @@ -356,8 +356,8 @@ public class ActionBarWrapper extends ActionBar implements android.app.ActionBar public void onTabUnselected(android.app.ActionBar.Tab tab, android.app.FragmentTransaction ft) { if (mListener != null) { FragmentTransaction trans = null; - if (mActivity instanceof SherlockFragmentActivity) { - trans = ((SherlockFragmentActivity)mActivity).getSupportFragmentManager().beginTransaction() + if (mActivity instanceof FragmentActivity) { + trans = ((FragmentActivity)mActivity).getSupportFragmentManager().beginTransaction() .disallowAddToBackStack(); mFragmentTransaction = trans; } diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/nineoldandroids/widget/NineFrameLayout.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/nineoldandroids/widget/NineFrameLayout.java index 2c428e907..953e3e844 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/nineoldandroids/widget/NineFrameLayout.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/nineoldandroids/widget/NineFrameLayout.java @@ -9,18 +9,10 @@ import com.actionbarsherlock.internal.nineoldandroids.view.animation.AnimatorPro public class NineFrameLayout extends FrameLayout { private final AnimatorProxy mProxy; - public NineFrameLayout(Context context) { - super(context); - mProxy = AnimatorProxy.NEEDS_PROXY ? AnimatorProxy.wrap(this) : null; - } public NineFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); mProxy = AnimatorProxy.NEEDS_PROXY ? AnimatorProxy.wrap(this) : null; } - public NineFrameLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - mProxy = AnimatorProxy.NEEDS_PROXY ? AnimatorProxy.wrap(this) : null; - } @Override public void setVisibility(int visibility) { diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/nineoldandroids/widget/NineLinearLayout.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/nineoldandroids/widget/NineLinearLayout.java index a670b1f64..1f381013a 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/nineoldandroids/widget/NineLinearLayout.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/nineoldandroids/widget/NineLinearLayout.java @@ -9,18 +9,10 @@ import com.actionbarsherlock.internal.nineoldandroids.view.animation.AnimatorPro public class NineLinearLayout extends LinearLayout { private final AnimatorProxy mProxy; - public NineLinearLayout(Context context) { - super(context); - mProxy = AnimatorProxy.NEEDS_PROXY ? AnimatorProxy.wrap(this) : null; - } public NineLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); mProxy = AnimatorProxy.NEEDS_PROXY ? AnimatorProxy.wrap(this) : null; } - public NineLinearLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - mProxy = AnimatorProxy.NEEDS_PROXY ? AnimatorProxy.wrap(this) : null; - } @Override public void setVisibility(int visibility) { diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/ActionMenuPresenter.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/ActionMenuPresenter.java index 6f568c698..876a22c58 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/ActionMenuPresenter.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/ActionMenuPresenter.java @@ -23,7 +23,6 @@ import java.util.Set; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; -import android.content.res.TypedArray; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; @@ -119,14 +118,6 @@ public class ActionMenuPresenter extends BaseMenuPresenter } public static boolean reserveOverflow(Context context) { - //Check for theme-forced overflow action item - TypedArray a = context.getTheme().obtainStyledAttributes(R.styleable.SherlockTheme); - boolean result = a.getBoolean(R.styleable.SherlockTheme_absForceOverflow, false); - a.recycle(); - if (result) { - return true; - } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB); } else { @@ -621,6 +612,8 @@ public class ActionMenuPresenter extends BaseMenuPresenter for (View_OnAttachStateChangeListener listener : mListeners) { listener.onViewDetachedFromWindow(this); } + + if (mOverflowPopup != null) mOverflowPopup.dismiss(); } @Override diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/ActionMenuView.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/ActionMenuView.java index e090677a1..0e3b1ae0d 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/ActionMenuView.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/ActionMenuView.java @@ -520,6 +520,9 @@ public class ActionMenuView extends IcsLinearLayout implements MenuBuilder.ItemI //@Override protected boolean hasDividerBeforeChildAt(int childIndex) { + if (childIndex == 0) { + return false; + } final View childBefore = getChildAt(childIndex - 1); final View child = getChildAt(childIndex); boolean result = false; diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/MenuItemWrapper.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/MenuItemWrapper.java index 907a7aa04..aaf2997b7 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/MenuItemWrapper.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/MenuItemWrapper.java @@ -2,10 +2,12 @@ package com.actionbarsherlock.internal.view.menu; import android.content.Intent; import android.graphics.drawable.Drawable; -import android.view.View; import android.view.ContextMenu.ContextMenuInfo; +import android.view.View; import com.actionbarsherlock.internal.view.ActionProviderWrapper; +import com.actionbarsherlock.internal.widget.CollapsibleActionViewWrapper; import com.actionbarsherlock.view.ActionProvider; +import com.actionbarsherlock.view.CollapsibleActionView; import com.actionbarsherlock.view.MenuItem; import com.actionbarsherlock.view.SubMenu; @@ -215,19 +217,35 @@ public class MenuItemWrapper implements MenuItem, android.view.MenuItem.OnMenuIt @Override public MenuItem setActionView(View view) { + if (view != null && view instanceof CollapsibleActionView) { + view = new CollapsibleActionViewWrapper(view); + } mNativeItem.setActionView(view); return this; } @Override public MenuItem setActionView(int resId) { + //Allow the native menu to inflate the resource mNativeItem.setActionView(resId); + if (resId != 0) { + //Get newly created view + View view = mNativeItem.getActionView(); + if (view instanceof CollapsibleActionView) { + //Wrap it and re-set it + mNativeItem.setActionView(new CollapsibleActionViewWrapper(view)); + } + } return this; } @Override public View getActionView() { - return mNativeItem.getActionView(); + View actionView = mNativeItem.getActionView(); + if (actionView instanceof CollapsibleActionViewWrapper) { + return ((CollapsibleActionViewWrapper)actionView).unwrap(); + } + return actionView; } @Override diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/MenuWrapper.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/MenuWrapper.java index 64fc4aeaa..3d4dd42fd 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/MenuWrapper.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/view/menu/MenuWrapper.java @@ -79,10 +79,15 @@ public class MenuWrapper implements Menu { @Override public int addIntentOptions(int groupId, int itemId, int order, ComponentName caller, Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) { - android.view.MenuItem[] nativeOutItems = new android.view.MenuItem[outSpecificItems.length]; - int result = mNativeMenu.addIntentOptions(groupId, itemId, order, caller, specifics, intent, flags, nativeOutItems); - for (int i = 0, length = outSpecificItems.length; i < length; i++) { - outSpecificItems[i] = new MenuItemWrapper(nativeOutItems[i]); + int result; + if (outSpecificItems != null) { + android.view.MenuItem[] nativeOutItems = new android.view.MenuItem[outSpecificItems.length]; + result = mNativeMenu.addIntentOptions(groupId, itemId, order, caller, specifics, intent, flags, nativeOutItems); + for (int i = 0, length = outSpecificItems.length; i < length; i++) { + outSpecificItems[i] = new MenuItemWrapper(nativeOutItems[i]); + } + } else { + result = mNativeMenu.addIntentOptions(groupId, itemId, order, caller, specifics, intent, flags, null); } return result; } diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/ActionBarContainer.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/ActionBarContainer.java index 5e5aa2867..1d9c68b37 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/ActionBarContainer.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/ActionBarContainer.java @@ -18,8 +18,11 @@ package com.actionbarsherlock.internal.widget; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; @@ -60,6 +63,16 @@ public class ActionBarContainer extends NineFrameLayout { mStackedBackground = a.getDrawable( R.styleable.SherlockActionBar_backgroundStacked); + //Fix for issue #379 + if (mStackedBackground instanceof ColorDrawable && Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(bitmap); + mStackedBackground.draw(c); + int color = bitmap.getPixel(0, 0); + bitmap.recycle(); + mStackedBackground = new IcsColorDrawable(color); + } + if (getId() == R.id.abs__split_action_bar) { mIsSplit = true; mSplitBackground = a.getDrawable( diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/CapitalizingTextView.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/CapitalizingTextView.java index 673ec554f..cae8b8aed 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/CapitalizingTextView.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/CapitalizingTextView.java @@ -1,12 +1,13 @@ package com.actionbarsherlock.internal.widget; -import java.util.Locale; import android.content.Context; import android.content.res.TypedArray; import android.os.Build; import android.util.AttributeSet; import android.widget.TextView; +import java.util.Locale; + public class CapitalizingTextView extends TextView { private static final boolean SANS_ICE_CREAM = Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH; private static final boolean IS_GINGERBREAD = Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD; @@ -33,7 +34,12 @@ public class CapitalizingTextView extends TextView { public void setTextCompat(CharSequence text) { if (SANS_ICE_CREAM && mAllCaps && text != null) { if (IS_GINGERBREAD) { - setText(text.toString().toUpperCase(Locale.ROOT)); + try { + setText(text.toString().toUpperCase(Locale.ROOT)); + } catch (NoSuchFieldError e) { + //Some manufacturer broke Locale.ROOT. See #572. + setText(text.toString().toUpperCase()); + } } else { setText(text.toString().toUpperCase()); } diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/CollapsibleActionViewWrapper.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/CollapsibleActionViewWrapper.java new file mode 100644 index 000000000..14f092c81 --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/CollapsibleActionViewWrapper.java @@ -0,0 +1,30 @@ +package com.actionbarsherlock.internal.widget; + +import android.view.View; +import android.widget.FrameLayout; +import com.actionbarsherlock.view.CollapsibleActionView; + +/** + * Wraps an ABS collapsible action view in a native container that delegates the calls. + */ +public class CollapsibleActionViewWrapper extends FrameLayout implements android.view.CollapsibleActionView { + private final CollapsibleActionView child; + + public CollapsibleActionViewWrapper(View child) { + super(child.getContext()); + this.child = (CollapsibleActionView) child; + addView(child); + } + + @Override public void onActionViewExpanded() { + child.onActionViewExpanded(); + } + + @Override public void onActionViewCollapsed() { + child.onActionViewCollapsed(); + } + + public View unwrap() { + return getChildAt(0); + } +} diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/IcsColorDrawable.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/IcsColorDrawable.java new file mode 100644 index 000000000..a78b3f71b --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/IcsColorDrawable.java @@ -0,0 +1,41 @@ +package com.actionbarsherlock.internal.widget; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; + +/** + * A version of {@link android.graphics.drawable.ColorDrawable} that respects bounds. + */ +public class IcsColorDrawable extends Drawable { + private int color; + private final Paint paint = new Paint(); + + public IcsColorDrawable(int color) { + this.color = color; + } + + @Override public void draw(Canvas canvas) { + if ((color >>> 24) != 0) { + paint.setColor(color); + canvas.drawRect(getBounds(), paint); + } + } + + @Override + public void setAlpha(int alpha) { + if (alpha != (color >>> 24)) { + color = (color & 0x00FFFFFF) & (alpha << 24); + invalidateSelf(); + } + } + + @Override public void setColorFilter(ColorFilter colorFilter) { + //Ignored + } + + @Override public int getOpacity() { + return color >>> 24; + } +} diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/IcsLinearLayout.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/IcsLinearLayout.java index 1b4463a59..4947c41df 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/IcsLinearLayout.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/IcsLinearLayout.java @@ -6,6 +6,8 @@ import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; +import android.widget.LinearLayout; + import com.actionbarsherlock.internal.nineoldandroids.widget.NineLinearLayout; /** @@ -16,14 +18,16 @@ import com.actionbarsherlock.internal.nineoldandroids.widget.NineLinearLayout; * {@link android.widget.FrameLayout} so it can receive the margin. */ public class IcsLinearLayout extends NineLinearLayout { - private static final int[] LinearLayout = new int[] { + private static final int[] R_styleable_LinearLayout = new int[] { /* 0 */ android.R.attr.divider, - /* 1 */ android.R.attr.showDividers, - /* 2 */ android.R.attr.dividerPadding, + /* 1 */ android.R.attr.measureWithLargestChild, + /* 2 */ android.R.attr.showDividers, + /* 3 */ android.R.attr.dividerPadding, }; private static final int LinearLayout_divider = 0; - private static final int LinearLayout_showDividers = 1; - private static final int LinearLayout_dividerPadding = 2; + private static final int LinearLayout_measureWithLargestChild = 1; + private static final int LinearLayout_showDividers = 2; + private static final int LinearLayout_dividerPadding = 3; /** * Don't show any dividers. @@ -49,15 +53,17 @@ public class IcsLinearLayout extends NineLinearLayout { private int mShowDividers; private int mDividerPadding; + private boolean mUseLargestChild; public IcsLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); - TypedArray a = context.obtainStyledAttributes(attrs, /*com.android.internal.R.styleable.*/LinearLayout); + TypedArray a = context.obtainStyledAttributes(attrs, /*com.android.internal.R.styleable.*/R_styleable_LinearLayout); setDividerDrawable(a.getDrawable(/*com.android.internal.R.styleable.*/LinearLayout_divider)); mShowDividers = a.getInt(/*com.android.internal.R.styleable.*/LinearLayout_showDividers, SHOW_DIVIDER_NONE); mDividerPadding = a.getDimensionPixelSize(/*com.android.internal.R.styleable.*/LinearLayout_dividerPadding, 0); + mUseLargestChild = a.getBoolean(/*com.android.internal.R.styleable.*/LinearLayout_measureWithLargestChild, false); a.recycle(); } @@ -199,7 +205,7 @@ public class IcsLinearLayout extends NineLinearLayout { if (child == null) { bottom = getHeight() - getPaddingBottom() - mDividerHeight; } else { - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + //final LayoutParams lp = (LayoutParams) child.getLayoutParams(); bottom = child.getBottom()/* + lp.bottomMargin*/; } drawHorizontalDivider(canvas, bottom); @@ -226,7 +232,7 @@ public class IcsLinearLayout extends NineLinearLayout { if (child == null) { right = getWidth() - getPaddingRight() - mDividerWidth; } else { - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + //final LayoutParams lp = (LayoutParams) child.getLayoutParams(); right = child.getRight()/* + lp.rightMargin*/; } drawVerticalDivider(canvas, right); @@ -269,4 +275,136 @@ public class IcsLinearLayout extends NineLinearLayout { } return false; } + + /** + * When true, all children with a weight will be considered having + * the minimum size of the largest child. If false, all children are + * measured normally. + * + * @return True to measure children with a weight using the minimum + * size of the largest child, false otherwise. + * + * @attr ref android.R.styleable#LinearLayout_measureWithLargestChild + */ + public boolean isMeasureWithLargestChildEnabled() { + return mUseLargestChild; + } + + /** + * When set to true, all children with a weight will be considered having + * the minimum size of the largest child. If false, all children are + * measured normally. + * + * Disabled by default. + * + * @param enabled True to measure children with a weight using the + * minimum size of the largest child, false otherwise. + * + * @attr ref android.R.styleable#LinearLayout_measureWithLargestChild + */ + public void setMeasureWithLargestChildEnabled(boolean enabled) { + mUseLargestChild = enabled; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (mUseLargestChild) { + final int orientation = getOrientation(); + switch (orientation) { + case HORIZONTAL: + useLargestChildHorizontal(); + break; + + case VERTICAL: + useLargestChildVertical(); + break; + } + } + } + + private void useLargestChildHorizontal() { + final int childCount = getChildCount(); + + // Find largest child width + int largestChildWidth = 0; + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + largestChildWidth = Math.max(child.getMeasuredWidth(), largestChildWidth); + } + + int totalWidth = 0; + // Re-measure childs + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child == null || child.getVisibility() == View.GONE) { + continue; + } + + final LinearLayout.LayoutParams lp = + (LinearLayout.LayoutParams) child.getLayoutParams(); + + float childExtra = lp.weight; + if (childExtra > 0) { + child.measure( + MeasureSpec.makeMeasureSpec(largestChildWidth, + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(), + MeasureSpec.EXACTLY)); + totalWidth += largestChildWidth; + + } else { + totalWidth += child.getMeasuredWidth(); + } + + totalWidth += lp.leftMargin + lp.rightMargin; + } + + totalWidth += getPaddingLeft() + getPaddingRight(); + setMeasuredDimension(totalWidth, getMeasuredHeight()); + } + + private void useLargestChildVertical() { + final int childCount = getChildCount(); + + // Find largest child width + int largestChildHeight = 0; + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + largestChildHeight = Math.max(child.getMeasuredHeight(), largestChildHeight); + } + + int totalHeight = 0; + // Re-measure childs + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child == null || child.getVisibility() == View.GONE) { + continue; + } + + final LinearLayout.LayoutParams lp = + (LinearLayout.LayoutParams) child.getLayoutParams(); + + float childExtra = lp.weight; + if (childExtra > 0) { + child.measure( + MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(largestChildHeight, + MeasureSpec.EXACTLY)); + totalHeight += largestChildHeight; + + } else { + totalHeight += child.getMeasuredHeight(); + } + + totalHeight += lp.leftMargin + lp.rightMargin; + } + + totalHeight += getPaddingLeft() + getPaddingRight(); + setMeasuredDimension(getMeasuredWidth(), totalHeight); + } } diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/ScrollingTabContainerView.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/ScrollingTabContainerView.java index 1a532e06c..48fb5d8b4 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/ScrollingTabContainerView.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/internal/widget/ScrollingTabContainerView.java @@ -188,6 +188,7 @@ public class ScrollingTabContainerView extends NineHorizontalScrollView private IcsLinearLayout createTabLayout() { final IcsLinearLayout tabLayout = (IcsLinearLayout) LayoutInflater.from(getContext()) .inflate(R.layout.abs__action_bar_tab_bar_view, null); + tabLayout.setMeasureWithLargestChildEnabled(true); tabLayout.setLayoutParams(new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT)); return tabLayout; diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/view/MenuInflater.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/view/MenuInflater.java index 969459749..5a0f40859 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/view/MenuInflater.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/view/MenuInflater.java @@ -67,6 +67,7 @@ public class MenuInflater { private final Object[] mActionProviderConstructorArguments; private Context mContext; + private Object mRealOwner; /** * Constructs a menu inflater. @@ -75,6 +76,20 @@ public class MenuInflater { */ public MenuInflater(Context context) { mContext = context; + mRealOwner = context; + mActionViewConstructorArguments = new Object[] {context}; + mActionProviderConstructorArguments = mActionViewConstructorArguments; + } + + /** + * Constructs a menu inflater. + * + * @see Activity#getMenuInflater() + * @hide + */ + public MenuInflater(Context context, Object realOwner) { + mContext = context; + mRealOwner = realOwner; mActionViewConstructorArguments = new Object[] {context}; mActionProviderConstructorArguments = mActionViewConstructorArguments; } @@ -192,12 +207,12 @@ public class MenuInflater { implements MenuItem.OnMenuItemClickListener { private static final Class[] PARAM_TYPES = new Class[] { MenuItem.class }; - private Context mContext; + private Object mRealOwner; private Method mMethod; - public InflatedOnMenuItemClickListener(Context context, String methodName) { - mContext = context; - Class c = context.getClass(); + public InflatedOnMenuItemClickListener(Object realOwner, String methodName) { + mRealOwner = realOwner; + Class c = realOwner.getClass(); try { mMethod = c.getMethod(methodName, PARAM_TYPES); } catch (Exception e) { @@ -212,9 +227,9 @@ public class MenuInflater { public boolean onMenuItemClick(MenuItem item) { try { if (mMethod.getReturnType() == Boolean.TYPE) { - return (Boolean) mMethod.invoke(mContext, item); + return (Boolean) mMethod.invoke(mRealOwner, item); } else { - mMethod.invoke(mContext, item); + mMethod.invoke(mRealOwner, item); return true; } } catch (Exception e) { @@ -358,8 +373,16 @@ public class MenuInflater { itemListenerMethodName = a.getString(R.styleable.SherlockMenuItem_android_onClick); itemActionViewLayout = a.getResourceId(R.styleable.SherlockMenuItem_android_actionLayout, 0); - itemActionViewClassName = a.getString(R.styleable.SherlockMenuItem_android_actionViewClass); - itemActionProviderClassName = a.getString(R.styleable.SherlockMenuItem_android_actionProviderClass); + + // itemActionViewClassName = a.getString(R.styleable.SherlockMenuItem_android_actionViewClass); + value = new TypedValue(); + a.getValue(R.styleable.SherlockMenuItem_android_actionViewClass, value); + itemActionViewClassName = value.type == TypedValue.TYPE_STRING ? value.string.toString() : null; + + // itemActionProviderClassName = a.getString(R.styleable.SherlockMenuItem_android_actionProviderClass); + value = new TypedValue(); + a.getValue(R.styleable.SherlockMenuItem_android_actionProviderClass, value); + itemActionProviderClassName = value.type == TypedValue.TYPE_STRING ? value.string.toString() : null; final boolean hasActionProvider = itemActionProviderClassName != null; if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) { @@ -407,7 +430,7 @@ public class MenuInflater { + "be used within a restricted context"); } item.setOnMenuItemClickListener( - new InflatedOnMenuItemClickListener(mContext, itemListenerMethodName)); + new InflatedOnMenuItemClickListener(mRealOwner, itemListenerMethodName)); } if (itemCheckable >= 2) { diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/widget/ActivityChooserModel.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/widget/ActivityChooserModel.java index 379207471..d7f110fc6 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/widget/ActivityChooserModel.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/widget/ActivityChooserModel.java @@ -25,7 +25,6 @@ import android.os.Handler; import android.text.TextUtils; import android.util.Log; import android.util.Xml; - import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; @@ -39,11 +38,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; +import java.util.concurrent.Executors; /** *

@@ -562,33 +561,7 @@ class ActivityChooserModel extends DataSetObservable { } } - private static final SerialExecutor SERIAL_EXECUTOR = new SerialExecutor(); - - private static class SerialExecutor implements Executor { - final LinkedList mTasks = new LinkedList(); - Runnable mActive; - - public synchronized void execute(final Runnable r) { - mTasks.offer(new Runnable() { - public void run() { - try { - r.run(); - } finally { - scheduleNext(); - } - } - }); - if (mActive == null) { - scheduleNext(); - } - } - - protected synchronized void scheduleNext() { - if ((mActive = mTasks.poll()) != null) { - mActive.run(); - } - } - } + private static final Executor SERIAL_EXECUTOR = Executors.newSingleThreadExecutor(); /** * Persists the history data to the backing file if the latter diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/widget/ActivityChooserView.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/widget/ActivityChooserView.java index da13bc99f..e19ea9e9e 100644 --- a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/widget/ActivityChooserView.java +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/widget/ActivityChooserView.java @@ -405,7 +405,11 @@ class ActivityChooserView extends ViewGroup implements ActivityChooserModelClien super.onDetachedFromWindow(); ActivityChooserModel dataModel = mAdapter.getDataModel(); if (dataModel != null) { - dataModel.unregisterObserver(mModelDataSetOberver); + try { + dataModel.unregisterObserver(mModelDataSetOberver); + } catch (IllegalStateException e) { + //Oh, well... fixes issue #557 + } } ViewTreeObserver viewTreeObserver = getViewTreeObserver(); if (viewTreeObserver.isAlive()) { @@ -526,6 +530,7 @@ class ActivityChooserView extends ViewGroup implements ActivityChooserModelClien mActivityChooserContent.setBackgroundDrawable(mActivityChooserContentBackground); } else { mActivityChooserContent.setBackgroundDrawable(null); + mActivityChooserContent.setPadding(0, 0, 0, 0); } } @@ -648,7 +653,11 @@ class ActivityChooserView extends ViewGroup implements ActivityChooserModelClien public void setDataModel(ActivityChooserModel dataModel) { ActivityChooserModel oldDataModel = mAdapter.getDataModel(); if (oldDataModel != null && isShown()) { - oldDataModel.unregisterObserver(mModelDataSetOberver); + try { + oldDataModel.unregisterObserver(mModelDataSetOberver); + } catch (IllegalStateException e) { + //Oh, well... fixes issue #557 + } } mDataModel = dataModel; if (dataModel != null && isShown()) { diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/widget/SearchView.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/widget/SearchView.java new file mode 100644 index 000000000..c9e7897d4 --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/widget/SearchView.java @@ -0,0 +1,1811 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.actionbarsherlock.widget; + +import android.app.PendingIntent; +import android.app.SearchManager; +import android.app.SearchableInfo; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.Cursor; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.ResultReceiver; +import android.speech.RecognizerIntent; +import android.support.v4.view.KeyEventCompat; +import android.support.v4.widget.CursorAdapter; +import android.text.Editable; +import android.text.InputType; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.style.ImageSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.AutoCompleteTextView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; +import com.actionbarsherlock.R; +import com.actionbarsherlock.view.CollapsibleActionView; + +import java.lang.reflect.Method; +import java.util.WeakHashMap; + +import static com.actionbarsherlock.widget.SuggestionsAdapter.getColumnString; + +/** + * A widget that provides a user interface for the user to enter a search query and submit a request + * to a search provider. Shows a list of query suggestions or results, if available, and allows the + * user to pick a suggestion or result to launch into. + * + *

+ * When the SearchView is used in an ActionBar as an action view for a collapsible menu item, it + * needs to be set to iconified by default using {@link #setIconifiedByDefault(boolean) + * setIconifiedByDefault(true)}. This is the default, so nothing needs to be done. + *

+ *

+ * If you want the search field to always be visible, then call setIconifiedByDefault(false). + *

+ * + *
+ *

Developer Guides

+ *

For information about using {@code SearchView}, read the + * Search developer guide.

+ *
+ * + * @see android.view.MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW + * @attr ref android.R.styleable#SearchView_iconifiedByDefault + * @attr ref android.R.styleable#SearchView_imeOptions + * @attr ref android.R.styleable#SearchView_inputType + * @attr ref android.R.styleable#SearchView_maxWidth + * @attr ref android.R.styleable#SearchView_queryHint + */ +public class SearchView extends LinearLayout implements CollapsibleActionView { + + private static final boolean DBG = false; + private static final String LOG_TAG = "SearchView"; + + /** + * Private constant for removing the microphone in the keyboard. + */ + private static final String IME_OPTION_NO_MICROPHONE = "nm"; + + private OnQueryTextListener mOnQueryChangeListener; + private OnCloseListener mOnCloseListener; + private OnFocusChangeListener mOnQueryTextFocusChangeListener; + private OnSuggestionListener mOnSuggestionListener; + private OnClickListener mOnSearchClickListener; + + private boolean mIconifiedByDefault; + private boolean mIconified; + private CursorAdapter mSuggestionsAdapter; + private View mSearchButton; + private View mSubmitButton; + private View mSearchPlate; + private View mSubmitArea; + private ImageView mCloseButton; + private View mSearchEditFrame; + private View mVoiceButton; + private SearchAutoComplete mQueryTextView; + private View mDropDownAnchor; + private ImageView mSearchHintIcon; + private boolean mSubmitButtonEnabled; + private CharSequence mQueryHint; + private boolean mQueryRefinement; + private boolean mClearingFocus; + private int mMaxWidth; + private boolean mVoiceButtonEnabled; + private CharSequence mOldQueryText; + private CharSequence mUserQuery; + private boolean mExpandedInActionView; + private int mCollapsedImeOptions; + + private SearchableInfo mSearchable; + private Bundle mAppSearchData; + + /* + * SearchView can be set expanded before the IME is ready to be shown during + * initial UI setup. The show operation is asynchronous to account for this. + */ + private Runnable mShowImeRunnable = new Runnable() { + public void run() { + InputMethodManager imm = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + + if (imm != null) { + showSoftInputUnchecked(SearchView.this, imm, 0); + } + } + }; + + private Runnable mUpdateDrawableStateRunnable = new Runnable() { + public void run() { + updateFocusedState(); + } + }; + + private Runnable mReleaseCursorRunnable = new Runnable() { + public void run() { + if (mSuggestionsAdapter != null && mSuggestionsAdapter instanceof SuggestionsAdapter) { + mSuggestionsAdapter.changeCursor(null); + } + } + }; + + // For voice searching + private final Intent mVoiceWebSearchIntent; + private final Intent mVoiceAppSearchIntent; + + // A weak map of drawables we've gotten from other packages, so we don't load them + // more than once. + private final WeakHashMap mOutsideDrawablesCache = + new WeakHashMap(); + + /** + * Callbacks for changes to the query text. + */ + public interface OnQueryTextListener { + + /** + * Called when the user submits the query. This could be due to a key press on the + * keyboard or due to pressing a submit button. + * The listener can override the standard behavior by returning true + * to indicate that it has handled the submit request. Otherwise return false to + * let the SearchView handle the submission by launching any associated intent. + * + * @param query the query text that is to be submitted + * + * @return true if the query has been handled by the listener, false to let the + * SearchView perform the default action. + */ + boolean onQueryTextSubmit(String query); + + /** + * Called when the query text is changed by the user. + * + * @param newText the new content of the query text field. + * + * @return false if the SearchView should perform the default action of showing any + * suggestions if available, true if the action was handled by the listener. + */ + boolean onQueryTextChange(String newText); + } + + public interface OnCloseListener { + + /** + * The user is attempting to close the SearchView. + * + * @return true if the listener wants to override the default behavior of clearing the + * text field and dismissing it, false otherwise. + */ + boolean onClose(); + } + + /** + * Callback interface for selection events on suggestions. These callbacks + * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}. + */ + public interface OnSuggestionListener { + + /** + * Called when a suggestion was selected by navigating to it. + * @param position the absolute position in the list of suggestions. + * + * @return true if the listener handles the event and wants to override the default + * behavior of possibly rewriting the query based on the selected item, false otherwise. + */ + boolean onSuggestionSelect(int position); + + /** + * Called when a suggestion was clicked. + * @param position the absolute position of the clicked item in the list of suggestions. + * + * @return true if the listener handles the event and wants to override the default + * behavior of launching any intent or submitting a search query specified on that item. + * Return false otherwise. + */ + boolean onSuggestionClick(int position); + } + + public SearchView(Context context) { + this(context, null); + } + + public SearchView(Context context, AttributeSet attrs) { + super(context, attrs); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) { + throw new IllegalStateException("SearchView is API 8+ only."); + } + + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.abs__search_view, this, true); + + mSearchButton = findViewById(R.id.abs__search_button); + mQueryTextView = (SearchAutoComplete) findViewById(R.id.abs__search_src_text); + mQueryTextView.setSearchView(this); + + mSearchEditFrame = findViewById(R.id.abs__search_edit_frame); + mSearchPlate = findViewById(R.id.abs__search_plate); + mSubmitArea = findViewById(R.id.abs__submit_area); + mSubmitButton = findViewById(R.id.abs__search_go_btn); + mCloseButton = (ImageView) findViewById(R.id.abs__search_close_btn); + mVoiceButton = findViewById(R.id.abs__search_voice_btn); + mSearchHintIcon = (ImageView) findViewById(R.id.abs__search_mag_icon); + + mSearchButton.setOnClickListener(mOnClickListener); + mCloseButton.setOnClickListener(mOnClickListener); + mSubmitButton.setOnClickListener(mOnClickListener); + mVoiceButton.setOnClickListener(mOnClickListener); + mQueryTextView.setOnClickListener(mOnClickListener); + + mQueryTextView.addTextChangedListener(mTextWatcher); + mQueryTextView.setOnEditorActionListener(mOnEditorActionListener); + mQueryTextView.setOnItemClickListener(mOnItemClickListener); + mQueryTextView.setOnItemSelectedListener(mOnItemSelectedListener); + mQueryTextView.setOnKeyListener(mTextKeyListener); + // Inform any listener of focus changes + mQueryTextView.setOnFocusChangeListener(new OnFocusChangeListener() { + + public void onFocusChange(View v, boolean hasFocus) { + if (mOnQueryTextFocusChangeListener != null) { + mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus); + } + } + }); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SherlockSearchView, 0, 0); + setIconifiedByDefault(a.getBoolean(R.styleable.SherlockSearchView_iconifiedByDefault, true)); + int maxWidth = a.getDimensionPixelSize(R.styleable.SherlockSearchView_android_maxWidth, -1); + if (maxWidth != -1) { + setMaxWidth(maxWidth); + } + CharSequence queryHint = a.getText(R.styleable.SherlockSearchView_queryHint); + if (!TextUtils.isEmpty(queryHint)) { + setQueryHint(queryHint); + } + int imeOptions = a.getInt(R.styleable.SherlockSearchView_android_imeOptions, -1); + if (imeOptions != -1) { + setImeOptions(imeOptions); + } + int inputType = a.getInt(R.styleable.SherlockSearchView_android_inputType, -1); + if (inputType != -1) { + setInputType(inputType); + } + + a.recycle(); + + boolean focusable = true; + + a = context.obtainStyledAttributes(attrs, R.styleable.SherlockView, 0, 0); + focusable = a.getBoolean(R.styleable.SherlockView_android_focusable, focusable); + a.recycle(); + setFocusable(focusable); + + // Save voice intent for later queries/launching + mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); + mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, + RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); + + mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + mDropDownAnchor = findViewById(mQueryTextView.getDropDownAnchor()); + if (mDropDownAnchor != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mDropDownAnchor.addOnLayoutChangeListener(new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + adjustDropDownSizeAndPosition(); + } + }); + } else { + mDropDownAnchor.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override public void onGlobalLayout() { + adjustDropDownSizeAndPosition(); + } + }); + } + } + + updateViewsVisibility(mIconifiedByDefault); + updateQueryHint(); + } + + /** + * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used + * to display labels, hints, suggestions, create intents for launching search results screens + * and controlling other affordances such as a voice button. + * + * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific + * activity or a global search provider. + */ + public void setSearchableInfo(SearchableInfo searchable) { + mSearchable = searchable; + if (mSearchable != null) { + updateSearchAutoComplete(); + updateQueryHint(); + } + // Cache the voice search capability + mVoiceButtonEnabled = hasVoiceSearch(); + + if (mVoiceButtonEnabled) { + // Disable the microphone on the keyboard, as a mic is displayed near the text box + // TODO: use imeOptions to disable voice input when the new API will be available + mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); + } + updateViewsVisibility(isIconified()); + } + + /** + * Sets the APP_DATA for legacy SearchDialog use. + * @param appSearchData bundle provided by the app when launching the search dialog + * @hide + */ + public void setAppSearchData(Bundle appSearchData) { + mAppSearchData = appSearchData; + } + + /** + * Sets the IME options on the query text field. + * + * @see TextView#setImeOptions(int) + * @param imeOptions the options to set on the query text field + * + * @attr ref android.R.styleable#SearchView_imeOptions + */ + public void setImeOptions(int imeOptions) { + mQueryTextView.setImeOptions(imeOptions); + } + + /** + * Returns the IME options set on the query text field. + * @return the ime options + * @see TextView#setImeOptions(int) + * + * @attr ref android.R.styleable#SearchView_imeOptions + */ + public int getImeOptions() { + return mQueryTextView.getImeOptions(); + } + + /** + * Sets the input type on the query text field. + * + * @see TextView#setInputType(int) + * @param inputType the input type to set on the query text field + * + * @attr ref android.R.styleable#SearchView_inputType + */ + public void setInputType(int inputType) { + mQueryTextView.setInputType(inputType); + } + + /** + * Returns the input type set on the query text field. + * @return the input type + * + * @attr ref android.R.styleable#SearchView_inputType + */ + public int getInputType() { + return mQueryTextView.getInputType(); + } + + /** @hide */ + @Override + public boolean requestFocus(int direction, Rect previouslyFocusedRect) { + // Don't accept focus if in the middle of clearing focus + if (mClearingFocus) return false; + // Check if SearchView is focusable. + if (!isFocusable()) return false; + // If it is not iconified, then give the focus to the text field + if (!isIconified()) { + boolean result = mQueryTextView.requestFocus(direction, previouslyFocusedRect); + if (result) { + updateViewsVisibility(false); + } + return result; + } else { + return super.requestFocus(direction, previouslyFocusedRect); + } + } + + /** @hide */ + @Override + public void clearFocus() { + mClearingFocus = true; + setImeVisibility(false); + super.clearFocus(); + mQueryTextView.clearFocus(); + mClearingFocus = false; + } + + /** + * Sets a listener for user actions within the SearchView. + * + * @param listener the listener object that receives callbacks when the user performs + * actions in the SearchView such as clicking on buttons or typing a query. + */ + public void setOnQueryTextListener(OnQueryTextListener listener) { + mOnQueryChangeListener = listener; + } + + /** + * Sets a listener to inform when the user closes the SearchView. + * + * @param listener the listener to call when the user closes the SearchView. + */ + public void setOnCloseListener(OnCloseListener listener) { + mOnCloseListener = listener; + } + + /** + * Sets a listener to inform when the focus of the query text field changes. + * + * @param listener the listener to inform of focus changes. + */ + public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) { + mOnQueryTextFocusChangeListener = listener; + } + + /** + * Sets a listener to inform when a suggestion is focused or clicked. + * + * @param listener the listener to inform of suggestion selection events. + */ + public void setOnSuggestionListener(OnSuggestionListener listener) { + mOnSuggestionListener = listener; + } + + /** + * Sets a listener to inform when the search button is pressed. This is only + * relevant when the text field is not visible by default. Calling {@link #setIconified + * setIconified(false)} can also cause this listener to be informed. + * + * @param listener the listener to inform when the search button is clicked or + * the text field is programmatically de-iconified. + */ + public void setOnSearchClickListener(OnClickListener listener) { + mOnSearchClickListener = listener; + } + + /** + * Returns the query string currently in the text field. + * + * @return the query string + */ + public CharSequence getQuery() { + return mQueryTextView.getText(); + } + + /** + * Sets a query string in the text field and optionally submits the query as well. + * + * @param query the query string. This replaces any query text already present in the + * text field. + * @param submit whether to submit the query right now or only update the contents of + * text field. + */ + public void setQuery(CharSequence query, boolean submit) { + mQueryTextView.setText(query); + if (query != null) { + mQueryTextView.setSelection(mQueryTextView.length()); + mUserQuery = query; + } + + // If the query is not empty and submit is requested, submit the query + if (submit && !TextUtils.isEmpty(query)) { + onSubmitQuery(); + } + } + + /** + * Sets the hint text to display in the query text field. This overrides any hint specified + * in the SearchableInfo. + * + * @param hint the hint text to display + * + * @attr ref android.R.styleable#SearchView_queryHint + */ + public void setQueryHint(CharSequence hint) { + mQueryHint = hint; + updateQueryHint(); + } + + /** + * Gets the hint text to display in the query text field. + * @return the query hint text, if specified, null otherwise. + * + * @attr ref android.R.styleable#SearchView_queryHint + */ + public CharSequence getQueryHint() { + if (mQueryHint != null) { + return mQueryHint; + } else if (mSearchable != null) { + CharSequence hint = null; + int hintId = mSearchable.getHintId(); + if (hintId != 0) { + hint = getContext().getString(hintId); + } + return hint; + } + return null; + } + + /** + * Sets the default or resting state of the search field. If true, a single search icon is + * shown by default and expands to show the text field and other buttons when pressed. Also, + * if the default state is iconified, then it collapses to that state when the close button + * is pressed. Changes to this property will take effect immediately. + * + *

The default value is true.

+ * + * @param iconified whether the search field should be iconified by default + * + * @attr ref android.R.styleable#SearchView_iconifiedByDefault + */ + public void setIconifiedByDefault(boolean iconified) { + if (mIconifiedByDefault == iconified) return; + mIconifiedByDefault = iconified; + updateViewsVisibility(iconified); + updateQueryHint(); + } + + /** + * Returns the default iconified state of the search field. + * @return + * + * @attr ref android.R.styleable#SearchView_iconifiedByDefault + */ + public boolean isIconfiedByDefault() { + return mIconifiedByDefault; + } + + /** + * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is + * a temporary state and does not override the default iconified state set by + * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then + * a false here will only be valid until the user closes the field. And if the default + * state is expanded, then a true here will only clear the text field and not close it. + * + * @param iconify a true value will collapse the SearchView to an icon, while a false will + * expand it. + */ + public void setIconified(boolean iconify) { + if (iconify) { + onCloseClicked(); + } else { + onSearchClicked(); + } + } + + /** + * Returns the current iconified state of the SearchView. + * + * @return true if the SearchView is currently iconified, false if the search field is + * fully visible. + */ + public boolean isIconified() { + return mIconified; + } + + /** + * Enables showing a submit button when the query is non-empty. In cases where the SearchView + * is being used to filter the contents of the current activity and doesn't launch a separate + * results activity, then the submit button should be disabled. + * + * @param enabled true to show a submit button for submitting queries, false if a submit + * button is not required. + */ + public void setSubmitButtonEnabled(boolean enabled) { + mSubmitButtonEnabled = enabled; + updateViewsVisibility(isIconified()); + } + + /** + * Returns whether the submit button is enabled when necessary or never displayed. + * + * @return whether the submit button is enabled automatically when necessary + */ + public boolean isSubmitButtonEnabled() { + return mSubmitButtonEnabled; + } + + /** + * Specifies if a query refinement button should be displayed alongside each suggestion + * or if it should depend on the flags set in the individual items retrieved from the + * suggestions provider. Clicking on the query refinement button will replace the text + * in the query text field with the text from the suggestion. This flag only takes effect + * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)} + * and not when using a custom adapter. + * + * @param enable true if all items should have a query refinement button, false if only + * those items that have a query refinement flag set should have the button. + * + * @see SearchManager#SUGGEST_COLUMN_FLAGS + * @see SearchManager#FLAG_QUERY_REFINEMENT + */ + public void setQueryRefinementEnabled(boolean enable) { + mQueryRefinement = enable; + if (mSuggestionsAdapter instanceof SuggestionsAdapter) { + ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement( + enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY); + } + } + + /** + * Returns whether query refinement is enabled for all items or only specific ones. + * @return true if enabled for all items, false otherwise. + */ + public boolean isQueryRefinementEnabled() { + return mQueryRefinement; + } + + /** + * You can set a custom adapter if you wish. Otherwise the default adapter is used to + * display the suggestions from the suggestions provider associated with the SearchableInfo. + * + * @see #setSearchableInfo(SearchableInfo) + */ + public void setSuggestionsAdapter(CursorAdapter adapter) { + mSuggestionsAdapter = adapter; + + mQueryTextView.setAdapter(mSuggestionsAdapter); + } + + /** + * Returns the adapter used for suggestions, if any. + * @return the suggestions adapter + */ + public CursorAdapter getSuggestionsAdapter() { + return mSuggestionsAdapter; + } + + /** + * Makes the view at most this many pixels wide + * + * @attr ref android.R.styleable#SearchView_maxWidth + */ + public void setMaxWidth(int maxpixels) { + mMaxWidth = maxpixels; + + requestLayout(); + } + + /** + * Gets the specified maximum width in pixels, if set. Returns zero if + * no maximum width was specified. + * @return the maximum width of the view + * + * @attr ref android.R.styleable#SearchView_maxWidth + */ + public int getMaxWidth() { + return mMaxWidth; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Let the standard measurements take effect in iconified state. + if (isIconified()) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int width = MeasureSpec.getSize(widthMeasureSpec); + + switch (widthMode) { + case MeasureSpec.AT_MOST: + // If there is an upper limit, don't exceed maximum width (explicit or implicit) + if (mMaxWidth > 0) { + width = Math.min(mMaxWidth, width); + } else { + width = Math.min(getPreferredWidth(), width); + } + break; + case MeasureSpec.EXACTLY: + // If an exact width is specified, still don't exceed any specified maximum width + if (mMaxWidth > 0) { + width = Math.min(mMaxWidth, width); + } + break; + case MeasureSpec.UNSPECIFIED: + // Use maximum width, if specified, else preferred width + width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth(); + break; + } + widthMode = MeasureSpec.EXACTLY; + super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode), heightMeasureSpec); + } + + private int getPreferredWidth() { + return getContext().getResources() + .getDimensionPixelSize(R.dimen.abs__search_view_preferred_width); + } + + private void updateViewsVisibility(final boolean collapsed) { + mIconified = collapsed; + // Visibility of views that are visible when collapsed + final int visCollapsed = collapsed ? VISIBLE : GONE; + // Is there text in the query + final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText()); + + mSearchButton.setVisibility(visCollapsed); + updateSubmitButton(hasText); + mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE); + mSearchHintIcon.setVisibility(mIconifiedByDefault ? GONE : VISIBLE); + updateCloseButton(); + updateVoiceButton(!hasText); + updateSubmitArea(); + } + + private boolean hasVoiceSearch() { + if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) { + Intent testIntent = null; + if (mSearchable.getVoiceSearchLaunchWebSearch()) { + testIntent = mVoiceWebSearchIntent; + } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { + testIntent = mVoiceAppSearchIntent; + } + if (testIntent != null) { + ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent, + PackageManager.MATCH_DEFAULT_ONLY); + return ri != null; + } + } + return false; + } + + private boolean isSubmitAreaEnabled() { + return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified(); + } + + private void updateSubmitButton(boolean hasText) { + int visibility = GONE; + if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus() + && (hasText || !mVoiceButtonEnabled)) { + visibility = VISIBLE; + } + mSubmitButton.setVisibility(visibility); + } + + private void updateSubmitArea() { + int visibility = GONE; + if (isSubmitAreaEnabled() + && (mSubmitButton.getVisibility() == VISIBLE + || mVoiceButton.getVisibility() == VISIBLE)) { + visibility = VISIBLE; + } + mSubmitArea.setVisibility(visibility); + } + + private void updateCloseButton() { + final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText()); + // Should we show the close button? It is not shown if there's no focus, + // field is not iconified by default and there is no text in it. + final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView); + mCloseButton.setVisibility(showClose ? VISIBLE : GONE); + mCloseButton.getDrawable().setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET); + } + + private void postUpdateFocusedState() { + post(mUpdateDrawableStateRunnable); + } + + private void updateFocusedState() { + boolean focused = mQueryTextView.hasFocus(); + mSearchPlate.getBackground().setState(focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET); + mSubmitArea.getBackground().setState(focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET); + invalidate(); + } + + @Override + protected void onDetachedFromWindow() { + removeCallbacks(mUpdateDrawableStateRunnable); + post(mReleaseCursorRunnable); + super.onDetachedFromWindow(); + } + + private void setImeVisibility(final boolean visible) { + if (visible) { + post(mShowImeRunnable); + } else { + removeCallbacks(mShowImeRunnable); + InputMethodManager imm = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + + if (imm != null) { + imm.hideSoftInputFromWindow(getWindowToken(), 0); + } + } + } + + /** + * Called by the SuggestionsAdapter + * @hide + */ + /* package */void onQueryRefine(CharSequence queryText) { + setQuery(queryText); + } + + private final OnClickListener mOnClickListener = new OnClickListener() { + + public void onClick(View v) { + if (v == mSearchButton) { + onSearchClicked(); + } else if (v == mCloseButton) { + onCloseClicked(); + } else if (v == mSubmitButton) { + onSubmitQuery(); + } else if (v == mVoiceButton) { + onVoiceClicked(); + } else if (v == mQueryTextView) { + forceSuggestionQuery(); + } + } + }; + + /** + * Handles the key down event for dealing with action keys. + * + * @param keyCode This is the keycode of the typed key, and is the same value as + * found in the KeyEvent parameter. + * @param event The complete event record for the typed key + * + * @return true if the event was handled here, or false if not. + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mSearchable == null) { + return false; + } + + // if it's an action specified by the searchable activity, launch the + // entered query with the action key + // TODO SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); + // TODO if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { + // TODO launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView.getText() + // TODO .toString()); + // TODO return true; + // TODO } + + return super.onKeyDown(keyCode, event); + } + + /** + * React to the user typing "enter" or other hardwired keys while typing in + * the search box. This handles these special keys while the edit box has + * focus. + */ + View.OnKeyListener mTextKeyListener = new View.OnKeyListener() { + public boolean onKey(View v, int keyCode, KeyEvent event) { + // guard against possible race conditions + if (mSearchable == null) { + return false; + } + + if (DBG) { + Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: " + + mQueryTextView.getListSelection()); + } + + // If a suggestion is selected, handle enter, search key, and action keys + // as presses on the selected suggestion + if (mQueryTextView.isPopupShowing() + && mQueryTextView.getListSelection() != ListView.INVALID_POSITION) { + return onSuggestionsKey(v, keyCode, event); + } + + // If there is text in the query box, handle enter, and action keys + // The search key is handled by the dialog's onKeyDown(). + if (!mQueryTextView.isEmpty() && KeyEventCompat.hasNoModifiers(event)) { + if (event.getAction() == KeyEvent.ACTION_UP) { + if (keyCode == KeyEvent.KEYCODE_ENTER) { + v.cancelLongPress(); + + // Launch as a regular search. + launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mQueryTextView.getText() + .toString()); + return true; + } + } + if (event.getAction() == KeyEvent.ACTION_DOWN) { + // TODO SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); + // TODO if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { + // TODO launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView + // TODO .getText().toString()); + // TODO return true; + // TODO } + } + } + return false; + } + }; + + /** + * React to the user typing while in the suggestions list. First, check for + * action keys. If not handled, try refocusing regular characters into the + * EditText. + */ + private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) { + // guard against possible race conditions (late arrival after dismiss) + if (mSearchable == null) { + return false; + } + if (mSuggestionsAdapter == null) { + return false; + } + if (event.getAction() == KeyEvent.ACTION_DOWN && KeyEventCompat.hasNoModifiers(event)) { + // First, check for enter or search (both of which we'll treat as a + // "click") + if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH + || keyCode == KeyEvent.KEYCODE_TAB) { + int position = mQueryTextView.getListSelection(); + return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null); + } + + // Next, check for left/right moves, which we use to "return" the + // user to the edit view + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + // give "focus" to text editor, with cursor at the beginning if + // left key, at end if right key + // TODO: Reverse left/right for right-to-left languages, e.g. + // Arabic + int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView + .length(); + mQueryTextView.setSelection(selPoint); + mQueryTextView.setListSelection(0); + mQueryTextView.clearListSelection(); + ensureImeVisible(mQueryTextView, true); + + return true; + } + + // Next, check for an "up and out" move + if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mQueryTextView.getListSelection()) { + // TODO: restoreUserQuery(); + // let ACTV complete the move + return false; + } + + // Next, check for an "action key" + // TODO SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); + // TODO if ((actionKey != null) + // TODO && ((actionKey.getSuggestActionMsg() != null) || (actionKey + // TODO .getSuggestActionMsgColumn() != null))) { + // TODO // launch suggestion using action key column + // TODO int position = mQueryTextView.getListSelection(); + // TODO if (position != ListView.INVALID_POSITION) { + // TODO Cursor c = mSuggestionsAdapter.getCursor(); + // TODO if (c.moveToPosition(position)) { + // TODO final String actionMsg = getActionKeyMessage(c, actionKey); + // TODO if (actionMsg != null && (actionMsg.length() > 0)) { + // TODO return onItemClicked(position, keyCode, actionMsg); + // TODO } + // TODO } + // TODO } + // TODO } + } + return false; + } + + /** + * For a given suggestion and a given cursor row, get the action message. If + * not provided by the specific row/column, also check for a single + * definition (for the action key). + * + * @param c The cursor providing suggestions + * @param actionKey The actionkey record being examined + * + * @return Returns a string, or null if no action key message for this + * suggestion + */ + // TODO private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) { + // TODO String result = null; + // TODO // check first in the cursor data, for a suggestion-specific message + // TODO final String column = actionKey.getSuggestActionMsgColumn(); + // TODO if (column != null) { + // TODO result = SuggestionsAdapter.getColumnString(c, column); + // TODO } + // TODO // If the cursor didn't give us a message, see if there's a single + // TODO // message defined + // TODO // for the actionkey (for all suggestions) + // TODO if (result == null) { + // TODO result = actionKey.getSuggestActionMsg(); + // TODO } + // TODO return result; + // TODO } + + private int getSearchIconId() { + TypedValue outValue = new TypedValue(); + getContext().getTheme().resolveAttribute(R.attr.searchViewSearchIcon, + outValue, true); + return outValue.resourceId; + } + + private CharSequence getDecoratedHint(CharSequence hintText) { + // If the field is always expanded, then don't add the search icon to the hint + if (!mIconifiedByDefault) return hintText; + + SpannableStringBuilder ssb = new SpannableStringBuilder(" "); // for the icon + ssb.append(hintText); + Drawable searchIcon = getContext().getResources().getDrawable(getSearchIconId()); + int textSize = (int) (mQueryTextView.getTextSize() * 1.25); + searchIcon.setBounds(0, 0, textSize, textSize); + ssb.setSpan(new ImageSpan(searchIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + return ssb; + } + + private void updateQueryHint() { + if (mQueryHint != null) { + mQueryTextView.setHint(getDecoratedHint(mQueryHint)); + } else if (mSearchable != null) { + CharSequence hint = null; + int hintId = mSearchable.getHintId(); + if (hintId != 0) { + hint = getContext().getString(hintId); + } + if (hint != null) { + mQueryTextView.setHint(getDecoratedHint(hint)); + } + } else { + mQueryTextView.setHint(getDecoratedHint("")); + } + } + + /** + * Updates the auto-complete text view. + */ + private void updateSearchAutoComplete() { + // TODO mQueryTextView.setDropDownAnimationStyle(0); // no animation + mQueryTextView.setThreshold(mSearchable.getSuggestThreshold()); + mQueryTextView.setImeOptions(mSearchable.getImeOptions()); + int inputType = mSearchable.getInputType(); + // We only touch this if the input type is set up for text (which it almost certainly + // should be, in the case of search!) + if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) { + // The existence of a suggestions authority is the proxy for "suggestions + // are available here" + inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; + if (mSearchable.getSuggestAuthority() != null) { + inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; + // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing + // auto-completion based on its own semantics, which it will present to the user + // as they type. This generally means that the input method should not show its + // own candidates, and the spell checker should not be in action. The text editor + // supplies its candidates by calling InputMethodManager.displayCompletions(), + // which in turn will call InputMethodSession.displayCompletions(). + inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; + } + } + mQueryTextView.setInputType(inputType); + if (mSuggestionsAdapter != null) { + mSuggestionsAdapter.changeCursor(null); + } + // attach the suggestions adapter, if suggestions are available + // The existence of a suggestions authority is the proxy for "suggestions available here" + if (mSearchable.getSuggestAuthority() != null) { + mSuggestionsAdapter = new SuggestionsAdapter(getContext(), + this, mSearchable, mOutsideDrawablesCache); + mQueryTextView.setAdapter(mSuggestionsAdapter); + ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement( + mQueryRefinement ? SuggestionsAdapter.REFINE_ALL + : SuggestionsAdapter.REFINE_BY_ENTRY); + } + } + + /** + * Update the visibility of the voice button. There are actually two voice search modes, + * either of which will activate the button. + * @param empty whether the search query text field is empty. If it is, then the other + * criteria apply to make the voice button visible. + */ + private void updateVoiceButton(boolean empty) { + int visibility = GONE; + if (mVoiceButtonEnabled && !isIconified() && empty) { + visibility = VISIBLE; + mSubmitButton.setVisibility(GONE); + } + mVoiceButton.setVisibility(visibility); + } + + private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() { + + /** + * Called when the input method default action key is pressed. + */ + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + onSubmitQuery(); + return true; + } + }; + + private void onTextChanged(CharSequence newText) { + CharSequence text = mQueryTextView.getText(); + mUserQuery = text; + boolean hasText = !TextUtils.isEmpty(text); + updateSubmitButton(hasText); + updateVoiceButton(!hasText); + updateCloseButton(); + updateSubmitArea(); + if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) { + mOnQueryChangeListener.onQueryTextChange(newText.toString()); + } + mOldQueryText = newText.toString(); + } + + private void onSubmitQuery() { + CharSequence query = mQueryTextView.getText(); + if (query != null && TextUtils.getTrimmedLength(query) > 0) { + if (mOnQueryChangeListener == null + || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) { + if (mSearchable != null) { + launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString()); + setImeVisibility(false); + } + dismissSuggestions(); + } + } + } + + private void dismissSuggestions() { + mQueryTextView.dismissDropDown(); + } + + private void onCloseClicked() { + CharSequence text = mQueryTextView.getText(); + if (TextUtils.isEmpty(text)) { + if (mIconifiedByDefault) { + // If the app doesn't override the close behavior + if (mOnCloseListener == null || !mOnCloseListener.onClose()) { + // hide the keyboard and remove focus + clearFocus(); + // collapse the search field + updateViewsVisibility(true); + } + } + } else { + mQueryTextView.setText(""); + mQueryTextView.requestFocus(); + setImeVisibility(true); + } + + } + + private void onSearchClicked() { + updateViewsVisibility(false); + mQueryTextView.requestFocus(); + setImeVisibility(true); + if (mOnSearchClickListener != null) { + mOnSearchClickListener.onClick(this); + } + } + + private void onVoiceClicked() { + // guard against possible race conditions + if (mSearchable == null) { + return; + } + SearchableInfo searchable = mSearchable; + try { + if (searchable.getVoiceSearchLaunchWebSearch()) { + Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent, + searchable); + getContext().startActivity(webSearchIntent); + } else if (searchable.getVoiceSearchLaunchRecognizer()) { + Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent, + searchable); + getContext().startActivity(appSearchIntent); + } + } catch (ActivityNotFoundException e) { + // Should not happen, since we check the availability of + // voice search before showing the button. But just in case... + Log.w(LOG_TAG, "Could not find voice search activity"); + } + } + + void onTextFocusChanged() { + updateViewsVisibility(isIconified()); + // Delayed update to make sure that the focus has settled down and window focus changes + // don't affect it. A synchronous update was not working. + postUpdateFocusedState(); + if (mQueryTextView.hasFocus()) { + forceSuggestionQuery(); + } + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + + postUpdateFocusedState(); + } + + /** + * {@inheritDoc} + */ + @Override + public void onActionViewCollapsed() { + clearFocus(); + updateViewsVisibility(true); + mQueryTextView.setImeOptions(mCollapsedImeOptions); + mExpandedInActionView = false; + } + + /** + * {@inheritDoc} + */ + @Override + public void onActionViewExpanded() { + if (mExpandedInActionView) return; + + mExpandedInActionView = true; + mCollapsedImeOptions = mQueryTextView.getImeOptions(); + mQueryTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN); + mQueryTextView.setText(""); + setIconified(false); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(SearchView.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(SearchView.class.getName()); + } + + private void adjustDropDownSizeAndPosition() { + if (mDropDownAnchor.getWidth() > 1) { + Resources res = getContext().getResources(); + int anchorPadding = mSearchPlate.getPaddingLeft(); + Rect dropDownPadding = new Rect(); + int iconOffset = mIconifiedByDefault + ? res.getDimensionPixelSize(R.dimen.abs__dropdownitem_icon_width) + + res.getDimensionPixelSize(R.dimen.abs__dropdownitem_text_padding_left) + : 0; + mQueryTextView.getDropDownBackground().getPadding(dropDownPadding); + mQueryTextView.setDropDownHorizontalOffset(-(dropDownPadding.left + iconOffset) + + anchorPadding); + mQueryTextView.setDropDownWidth(mDropDownAnchor.getWidth() + dropDownPadding.left + + dropDownPadding.right + iconOffset - (anchorPadding)); + } + } + + private boolean onItemClicked(int position, int actionKey, String actionMsg) { + if (mOnSuggestionListener == null + || !mOnSuggestionListener.onSuggestionClick(position)) { + launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null); + setImeVisibility(false); + dismissSuggestions(); + return true; + } + return false; + } + + private boolean onItemSelected(int position) { + if (mOnSuggestionListener == null + || !mOnSuggestionListener.onSuggestionSelect(position)) { + rewriteQueryFromSuggestion(position); + return true; + } + return false; + } + + private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() { + + /** + * Implements OnItemClickListener + */ + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position); + onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null); + } + }; + + private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() { + + /** + * Implements OnItemSelectedListener + */ + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position); + SearchView.this.onItemSelected(position); + } + + /** + * Implements OnItemSelectedListener + */ + public void onNothingSelected(AdapterView parent) { + if (DBG) + Log.d(LOG_TAG, "onNothingSelected()"); + } + }; + + /** + * Query rewriting. + */ + private void rewriteQueryFromSuggestion(int position) { + CharSequence oldQuery = mQueryTextView.getText(); + Cursor c = mSuggestionsAdapter.getCursor(); + if (c == null) { + return; + } + if (c.moveToPosition(position)) { + // Get the new query from the suggestion. + CharSequence newQuery = mSuggestionsAdapter.convertToString(c); + if (newQuery != null) { + // The suggestion rewrites the query. + // Update the text field, without getting new suggestions. + setQuery(newQuery); + } else { + // The suggestion does not rewrite the query, restore the user's query. + setQuery(oldQuery); + } + } else { + // We got a bad position, restore the user's query. + setQuery(oldQuery); + } + } + + /** + * Launches an intent based on a suggestion. + * + * @param position The index of the suggestion to create the intent from. + * @param actionKey The key code of the action key that was pressed, + * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. + * @param actionMsg The message for the action key that was pressed, + * or null if none. + * @return true if a successful launch, false if could not (e.g. bad position). + */ + private boolean launchSuggestion(int position, int actionKey, String actionMsg) { + Cursor c = mSuggestionsAdapter.getCursor(); + if ((c != null) && c.moveToPosition(position)) { + + Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); + + // launch the intent + launchIntent(intent); + + return true; + } + return false; + } + + /** + * Launches an intent, including any special intent handling. + */ + private void launchIntent(Intent intent) { + if (intent == null) { + return; + } + try { + // If the intent was created from a suggestion, it will always have an explicit + // component here. + getContext().startActivity(intent); + } catch (RuntimeException ex) { + Log.e(LOG_TAG, "Failed launch activity: " + intent, ex); + } + } + + /** + * Sets the text in the query box, without updating the suggestions. + */ + private void setQuery(CharSequence query) { + setText(mQueryTextView, query, true); + // Move the cursor to the end + mQueryTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length()); + } + + private void launchQuerySearch(int actionKey, String actionMsg, String query) { + String action = Intent.ACTION_SEARCH; + Intent intent = createIntent(action, null, null, query, actionKey, actionMsg); + getContext().startActivity(intent); + } + + /** + * Constructs an intent from the given information and the search dialog state. + * + * @param action Intent action. + * @param data Intent data, or null. + * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or null. + * @param query Intent query, or null. + * @param actionKey The key code of the action key that was pressed, + * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. + * @param actionMsg The message for the action key that was pressed, + * or null if none. + * @return The intent. + */ + private Intent createIntent(String action, Uri data, String extraData, String query, + int actionKey, String actionMsg) { + // Now build the Intent + Intent intent = new Intent(action); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // We need CLEAR_TOP to avoid reusing an old task that has other activities + // on top of the one we want. We don't want to do this in in-app search though, + // as it can be destructive to the activity stack. + if (data != null) { + intent.setData(data); + } + intent.putExtra(SearchManager.USER_QUERY, mUserQuery); + if (query != null) { + intent.putExtra(SearchManager.QUERY, query); + } + if (extraData != null) { + intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); + } + if (mAppSearchData != null) { + intent.putExtra(SearchManager.APP_DATA, mAppSearchData); + } + if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { + intent.putExtra(SearchManager.ACTION_KEY, actionKey); + intent.putExtra(SearchManager.ACTION_MSG, actionMsg); + } + intent.setComponent(mSearchable.getSearchActivity()); + return intent; + } + + /** + * Create and return an Intent that can launch the voice search activity for web search. + */ + private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) { + Intent voiceIntent = new Intent(baseIntent); + ComponentName searchActivity = searchable.getSearchActivity(); + voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null + : searchActivity.flattenToShortString()); + return voiceIntent; + } + + /** + * Create and return an Intent that can launch the voice search activity, perform a specific + * voice transcription, and forward the results to the searchable activity. + * + * @param baseIntent The voice app search intent to start from + * @return A completely-configured intent ready to send to the voice search activity + */ + private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) { + ComponentName searchActivity = searchable.getSearchActivity(); + + // create the necessary intent to set up a search-and-forward operation + // in the voice search system. We have to keep the bundle separate, + // because it becomes immutable once it enters the PendingIntent + Intent queryIntent = new Intent(Intent.ACTION_SEARCH); + queryIntent.setComponent(searchActivity); + PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent, + PendingIntent.FLAG_ONE_SHOT); + + // Now set up the bundle that will be inserted into the pending intent + // when it's time to do the search. We always build it here (even if empty) + // because the voice search activity will always need to insert "QUERY" into + // it anyway. + Bundle queryExtras = new Bundle(); + + // Now build the intent to launch the voice search. Add all necessary + // extras to launch the voice recognizer, and then all the necessary extras + // to forward the results to the searchable activity + Intent voiceIntent = new Intent(baseIntent); + + // Add all of the configuration options supplied by the searchable's metadata + String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM; + String prompt = null; + String language = null; + int maxResults = 1; + + Resources resources = getResources(); + if (searchable.getVoiceLanguageModeId() != 0) { + languageModel = resources.getString(searchable.getVoiceLanguageModeId()); + } + if (searchable.getVoicePromptTextId() != 0) { + prompt = resources.getString(searchable.getVoicePromptTextId()); + } + if (searchable.getVoiceLanguageId() != 0) { + language = resources.getString(searchable.getVoiceLanguageId()); + } + if (searchable.getVoiceMaxResults() != 0) { + maxResults = searchable.getVoiceMaxResults(); + } + voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); + voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); + voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); + voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); + voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null + : searchActivity.flattenToShortString()); + + // Add the values that configure forwarding the results + voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending); + voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras); + + return voiceIntent; + } + + /** + * When a particular suggestion has been selected, perform the various lookups required + * to use the suggestion. This includes checking the cursor for suggestion-specific data, + * and/or falling back to the XML for defaults; It also creates REST style Uri data when + * the suggestion includes a data id. + * + * @param c The suggestions cursor, moved to the row of the user's selection + * @param actionKey The key code of the action key that was pressed, + * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. + * @param actionMsg The message for the action key that was pressed, + * or null if none. + * @return An intent for the suggestion at the cursor's position. + */ + private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) { + try { + // use specific action if supplied, or default action if supplied, or fixed default + String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION); + + if (action == null) { + action = mSearchable.getSuggestIntentAction(); + } + if (action == null) { + action = Intent.ACTION_SEARCH; + } + + // use specific data if supplied, or default data if supplied + String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA); + if (data == null) { + data = mSearchable.getSuggestIntentData(); + } + // then, if an ID was provided, append it. + if (data != null) { + String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); + if (id != null) { + data = data + "/" + Uri.encode(id); + } + } + Uri dataUri = (data == null) ? null : Uri.parse(data); + + String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); + String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); + + return createIntent(action, dataUri, extraData, query, actionKey, actionMsg); + } catch (RuntimeException e ) { + int rowNum; + try { // be really paranoid now + rowNum = c.getPosition(); + } catch (RuntimeException e2 ) { + rowNum = -1; + } + Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum + + " returned exception.", e); + return null; + } + } + + private void forceSuggestionQuery() { + try { + Method before = SearchAutoComplete.class.getMethod("doBeforeTextChanged"); + Method after = SearchAutoComplete.class.getMethod("doAfterTextChanged"); + before.setAccessible(true); + after.setAccessible(true); + before.invoke(mQueryTextView); + after.invoke(mQueryTextView); + } catch (Exception e) { + // Oh well... + } + } + + static boolean isLandscapeMode(Context context) { + return context.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; + } + + /** + * Callback to watch the text field for empty/non-empty + */ + private TextWatcher mTextWatcher = new TextWatcher() { + + public void beforeTextChanged(CharSequence s, int start, int before, int after) { } + + public void onTextChanged(CharSequence s, int start, + int before, int after) { + SearchView.this.onTextChanged(s); + } + + public void afterTextChanged(Editable s) { + } + }; + + /** + * Local subclass for AutoCompleteTextView. + * @hide + */ + public static class SearchAutoComplete extends AutoCompleteTextView { + + private int mThreshold; + private SearchView mSearchView; + + public SearchAutoComplete(Context context) { + super(context); + mThreshold = getThreshold(); + } + + public SearchAutoComplete(Context context, AttributeSet attrs) { + super(context, attrs); + mThreshold = getThreshold(); + } + + public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mThreshold = getThreshold(); + } + + void setSearchView(SearchView searchView) { + mSearchView = searchView; + } + + @Override + public void setThreshold(int threshold) { + super.setThreshold(threshold); + mThreshold = threshold; + } + + /** + * Returns true if the text field is empty, or contains only whitespace. + */ + private boolean isEmpty() { + return TextUtils.getTrimmedLength(getText()) == 0; + } + + /** + * We override this method to avoid replacing the query box text when a + * suggestion is clicked. + */ + @Override + protected void replaceText(CharSequence text) { + } + + /** + * We override this method to avoid an extra onItemClick being called on + * the drop-down's OnItemClickListener by + * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is + * clicked with the trackball. + */ + @Override + public void performCompletion() { + } + + /** + * We override this method to be sure and show the soft keyboard if + * appropriate when the TextView has focus. + */ + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + + if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) { + InputMethodManager inputManager = (InputMethodManager) getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + inputManager.showSoftInput(this, 0); + // If in landscape mode, then make sure that + // the ime is in front of the dropdown. + if (isLandscapeMode(getContext())) { + ensureImeVisible(this, true); + } + } + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(focused, direction, previouslyFocusedRect); + mSearchView.onTextFocusChanged(); + } + + /** + * We override this method so that we can allow a threshold of zero, + * which ACTV does not. + */ + @Override + public boolean enoughToFilter() { + return mThreshold <= 0 || super.enoughToFilter(); + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + // special case for the back key, we do not even try to send it + // to the drop down list but instead, consume it immediately + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + KeyEvent.DispatcherState state = getKeyDispatcherState(); + if (state != null) { + state.startTracking(event, this); + } + return true; + } else if (event.getAction() == KeyEvent.ACTION_UP) { + KeyEvent.DispatcherState state = getKeyDispatcherState(); + if (state != null) { + state.handleUpEvent(event); + } + if (event.isTracking() && !event.isCanceled()) { + mSearchView.clearFocus(); + mSearchView.setImeVisibility(false); + return true; + } + } + } + return super.onKeyPreIme(keyCode, event); + } + + } + + private static void ensureImeVisible(AutoCompleteTextView view, boolean visible) { + try { + Method method = AutoCompleteTextView.class.getMethod("ensureImeVisible", boolean.class); + method.setAccessible(true); + method.invoke(view, visible); + } catch (Exception e) { + //Oh well... + } + } + + private static void showSoftInputUnchecked(View view, InputMethodManager imm, int flags) { + try { + Method method = imm.getClass().getMethod("showSoftInputUnchecked", int.class, ResultReceiver.class); + method.setAccessible(true); + method.invoke(imm, flags, null); + } catch (Exception e) { + //Fallback to public API which hopefully does mostly the same thing + imm.showSoftInput(view, flags); + } + } + + private static void setText(AutoCompleteTextView view, CharSequence text, boolean filter) { + try { + Method method = AutoCompleteTextView.class.getMethod("setText", CharSequence.class, boolean.class); + method.setAccessible(true); + method.invoke(view, text, filter); + } catch (Exception e) { + //Fallback to public API which hopefully does mostly the same thing + view.setText(text); + } + } +} diff --git a/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/widget/SuggestionsAdapter.java b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/widget/SuggestionsAdapter.java new file mode 100644 index 000000000..bd5cbd718 --- /dev/null +++ b/APG/android-libs/ActionBarSherlock/src/com/actionbarsherlock/widget/SuggestionsAdapter.java @@ -0,0 +1,733 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.actionbarsherlock.widget; + +import android.app.SearchManager; +import android.app.SearchableInfo; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.widget.ResourceCursorAdapter; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import com.actionbarsherlock.R; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.WeakHashMap; + +/** + * Provides the contents for the suggestion drop-down list. + * + * @hide + */ +class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListener { + + private static final boolean DBG = false; + private static final String LOG_TAG = "SuggestionsAdapter"; + private static final int QUERY_LIMIT = 50; + + static final int REFINE_NONE = 0; + static final int REFINE_BY_ENTRY = 1; + static final int REFINE_ALL = 2; + + private SearchManager mSearchManager; + private SearchView mSearchView; + private Context mProviderContext; + private WeakHashMap mOutsideDrawablesCache; + private boolean mClosed = false; + private int mQueryRefinement = REFINE_BY_ENTRY; + + // URL color + private ColorStateList mUrlColor; + + static final int INVALID_INDEX = -1; + + // Cached column indexes, updated when the cursor changes. + private int mText1Col = INVALID_INDEX; + private int mText2Col = INVALID_INDEX; + private int mText2UrlCol = INVALID_INDEX; + private int mIconName1Col = INVALID_INDEX; + private int mIconName2Col = INVALID_INDEX; + private int mFlagsCol = INVALID_INDEX; + + // private final Runnable mStartSpinnerRunnable; + // private final Runnable mStopSpinnerRunnable; + + /** + * The amount of time we delay in the filter when the user presses the delete key. + */ + //private static final long DELETE_KEY_POST_DELAY = 500L; + + public SuggestionsAdapter(Context context, SearchView searchView, + SearchableInfo mSearchable, WeakHashMap outsideDrawablesCache) { + super(context, + R.layout.abs__search_dropdown_item_icons_2line, + null, // no initial cursor + true); // auto-requery + mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE); + mProviderContext = mContext; + mSearchView = searchView; + + mOutsideDrawablesCache = outsideDrawablesCache; + + // mStartSpinnerRunnable = new Runnable() { + // public void run() { + // // mSearchView.setWorking(true); // TODO: + // } + // }; + // + // mStopSpinnerRunnable = new Runnable() { + // public void run() { + // // mSearchView.setWorking(false); // TODO: + // } + // }; + + // delay 500ms when deleting +// TODO getFilter().setDelayer(new Filter.Delayer() { +// +// private int mPreviousLength = 0; +// +// public long getPostingDelay(CharSequence constraint) { +// if (constraint == null) return 0; +// +// long delay = constraint.length() < mPreviousLength ? DELETE_KEY_POST_DELAY : 0; +// mPreviousLength = constraint.length(); +// return delay; +// } +// }); + } + + /** + * Enables query refinement for all suggestions. This means that an additional icon + * will be shown for each entry. When clicked, the suggested text on that line will be + * copied to the query text field. + *

+ * + * @param refineWhat which queries to refine. Possible values are {@link #REFINE_NONE}, + * {@link #REFINE_BY_ENTRY}, and {@link #REFINE_ALL}. + */ + public void setQueryRefinement(int refineWhat) { + mQueryRefinement = refineWhat; + } + + /** + * Returns the current query refinement preference. + * @return value of query refinement preference + */ + public int getQueryRefinement() { + return mQueryRefinement; + } + + /** + * Overridden to always return false, since we cannot be sure that + * suggestion sources return stable IDs. + */ + @Override + public boolean hasStableIds() { + return false; + } + + /** + * Use the search suggestions provider to obtain a live cursor. This will be called + * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions). + * The results will be processed in the UI thread and changeCursor() will be called. + */ + @Override + public Cursor runQueryOnBackgroundThread(CharSequence constraint) { + if (DBG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")"); + String query = (constraint == null) ? "" : constraint.toString(); + /** + * for in app search we show the progress spinner until the cursor is returned with + * the results. + */ + Cursor cursor = null; + if (mSearchView.getVisibility() != View.VISIBLE + || mSearchView.getWindowVisibility() != View.VISIBLE) { + return null; + } + //mSearchView.getWindow().getDecorView().post(mStartSpinnerRunnable); // TODO: + try { + cursor = getSuggestions(query, QUERY_LIMIT); + // trigger fill window so the spinner stays up until the results are copied over and + // closer to being ready + if (cursor != null) { + cursor.getCount(); + return cursor; + } + } catch (RuntimeException e) { + Log.w(LOG_TAG, "Search suggestions query threw an exception.", e); + } + // If cursor is null or an exception was thrown, stop the spinner and return null. + // changeCursor doesn't get called if cursor is null + // mSearchView.getWindow().getDecorView().post(mStopSpinnerRunnable); // TODO: + return null; + } + + public Cursor getSuggestions(String query, int limit) { + Uri.Builder uriBuilder = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel() + .fragment(""); // TODO: Remove, workaround for a bug in Uri.writeToParcel() + + // append standard suggestion query path + uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY); + + // inject query, either as selection args or inline + uriBuilder.appendPath(query); + + if (limit > 0) { + uriBuilder.appendQueryParameter(SearchManager.SUGGEST_PARAMETER_LIMIT, String.valueOf(limit)); + } + + Uri uri = uriBuilder.build(); + + // finally, make the query + return mContext.getContentResolver().query(uri, null, null, null, null); + } + + public void close() { + if (DBG) Log.d(LOG_TAG, "close()"); + changeCursor(null); + mClosed = true; + } + + @Override + public void notifyDataSetChanged() { + if (DBG) Log.d(LOG_TAG, "notifyDataSetChanged"); + super.notifyDataSetChanged(); + + // mSearchView.onDataSetChanged(); // TODO: + + updateSpinnerState(getCursor()); + } + + @Override + public void notifyDataSetInvalidated() { + if (DBG) Log.d(LOG_TAG, "notifyDataSetInvalidated"); + super.notifyDataSetInvalidated(); + + updateSpinnerState(getCursor()); + } + + private void updateSpinnerState(Cursor cursor) { + Bundle extras = cursor != null ? cursor.getExtras() : null; + if (DBG) { + Log.d(LOG_TAG, "updateSpinnerState - extra = " + + (extras != null + ? extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS) + : null)); + } + // Check if the Cursor indicates that the query is not complete and show the spinner + if (extras != null + && extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)) { + // mSearchView.getWindow().getDecorView().post(mStartSpinnerRunnable); // TODO: + return; + } + // If cursor is null or is done, stop the spinner + // mSearchView.getWindow().getDecorView().post(mStopSpinnerRunnable); // TODO: + } + + /** + * Cache columns. + */ + @Override + public void changeCursor(Cursor c) { + if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")"); + + if (mClosed) { + Log.w(LOG_TAG, "Tried to change cursor after adapter was closed."); + if (c != null) c.close(); + return; + } + + try { + super.changeCursor(c); + + if (c != null) { + mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1); + mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2); + mText2UrlCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL); + mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1); + mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2); + mFlagsCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_FLAGS); + } + } catch (Exception e) { + Log.e(LOG_TAG, "error changing cursor and caching columns", e); + } + } + + /** + * Tags the view with cached child view look-ups. + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + View v = super.newView(context, cursor, parent); + v.setTag(new ChildViewCache(v)); + return v; + } + + /** + * Cache of the child views of drop-drown list items, to avoid looking up the children + * each time the contents of a list item are changed. + */ + private final static class ChildViewCache { + public final TextView mText1; + public final TextView mText2; + public final ImageView mIcon1; + public final ImageView mIcon2; + public final ImageView mIconRefine; + + public ChildViewCache(View v) { + mText1 = (TextView) v.findViewById(android.R.id.text1); + mText2 = (TextView) v.findViewById(android.R.id.text2); + mIcon1 = (ImageView) v.findViewById(android.R.id.icon1); + mIcon2 = (ImageView) v.findViewById(android.R.id.icon2); + mIconRefine = (ImageView) v.findViewById(R.id.edit_query); + } + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + ChildViewCache views = (ChildViewCache) view.getTag(); + + int flags = 0; + if (mFlagsCol != INVALID_INDEX) { + flags = cursor.getInt(mFlagsCol); + } + if (views.mText1 != null) { + String text1 = getStringOrNull(cursor, mText1Col); + setViewText(views.mText1, text1); + } + if (views.mText2 != null) { + // First check TEXT_2_URL + CharSequence text2 = getStringOrNull(cursor, mText2UrlCol); + if (text2 != null) { + text2 = formatUrl(text2); + } else { + text2 = getStringOrNull(cursor, mText2Col); + } + + // If no second line of text is indicated, allow the first line of text + // to be up to two lines if it wants to be. + if (TextUtils.isEmpty(text2)) { + if (views.mText1 != null) { + views.mText1.setSingleLine(false); + views.mText1.setMaxLines(2); + } + } else { + if (views.mText1 != null) { + views.mText1.setSingleLine(true); + views.mText1.setMaxLines(1); + } + } + setViewText(views.mText2, text2); + } + + if (views.mIcon1 != null) { + setViewDrawable(views.mIcon1, getIcon1(cursor), View.INVISIBLE); + } + if (views.mIcon2 != null) { + setViewDrawable(views.mIcon2, getIcon2(cursor), View.GONE); + } + if (mQueryRefinement == REFINE_ALL + || (mQueryRefinement == REFINE_BY_ENTRY + && (flags & SearchManager.FLAG_QUERY_REFINEMENT) != 0)) { + views.mIconRefine.setVisibility(View.VISIBLE); + views.mIconRefine.setTag(views.mText1.getText()); + views.mIconRefine.setOnClickListener(this); + } else { + views.mIconRefine.setVisibility(View.GONE); + } + } + + public void onClick(View v) { + Object tag = v.getTag(); + if (tag instanceof CharSequence) { + mSearchView.onQueryRefine((CharSequence) tag); + } + } + + private CharSequence formatUrl(CharSequence url) { + if (mUrlColor == null) { + // Lazily get the URL color from the current theme. + TypedValue colorValue = new TypedValue(); + mContext.getTheme().resolveAttribute(R.attr.textColorSearchUrl, colorValue, true); + mUrlColor = mContext.getResources().getColorStateList(colorValue.resourceId); + } + + SpannableString text = new SpannableString(url); + text.setSpan(new TextAppearanceSpan(null, 0, 0, mUrlColor, null), + 0, url.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + return text; + } + + private void setViewText(TextView v, CharSequence text) { + // Set the text even if it's null, since we need to clear any previous text. + v.setText(text); + + if (TextUtils.isEmpty(text)) { + v.setVisibility(View.GONE); + } else { + v.setVisibility(View.VISIBLE); + } + } + + private Drawable getIcon1(Cursor cursor) { + if (mIconName1Col == INVALID_INDEX) { + return null; + } + String value = cursor.getString(mIconName1Col); + Drawable drawable = getDrawableFromResourceValue(value); + if (drawable != null) { + return drawable; + } + return getDefaultIcon1(cursor); + } + + private Drawable getIcon2(Cursor cursor) { + if (mIconName2Col == INVALID_INDEX) { + return null; + } + String value = cursor.getString(mIconName2Col); + return getDrawableFromResourceValue(value); + } + + /** + * Sets the drawable in an image view, makes sure the view is only visible if there + * is a drawable. + */ + private void setViewDrawable(ImageView v, Drawable drawable, int nullVisibility) { + // Set the icon even if the drawable is null, since we need to clear any + // previous icon. + v.setImageDrawable(drawable); + + if (drawable == null) { + v.setVisibility(nullVisibility); + } else { + v.setVisibility(View.VISIBLE); + + // This is a hack to get any animated drawables (like a 'working' spinner) + // to animate. You have to setVisible true on an AnimationDrawable to get + // it to start animating, but it must first have been false or else the + // call to setVisible will be ineffective. We need to clear up the story + // about animated drawables in the future, see http://b/1878430. + drawable.setVisible(false, false); + drawable.setVisible(true, false); + } + } + + /** + * Gets the text to show in the query field when a suggestion is selected. + * + * @param cursor The Cursor to read the suggestion data from. The Cursor should already + * be moved to the suggestion that is to be read from. + * @return The text to show, or null if the query should not be + * changed when selecting this suggestion. + */ + @Override + public CharSequence convertToString(Cursor cursor) { + if (cursor == null) { + return null; + } + + String query = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_QUERY); + if (query != null) { + return query; + } + + return null; + } + + /** + * This method is overridden purely to provide a bit of protection against + * flaky content providers. + * + * @see android.widget.ListAdapter#getView(int, View, ViewGroup) + */ + @Override + public View getView(int position, View convertView, ViewGroup parent) { + try { + return super.getView(position, convertView, parent); + } catch (RuntimeException e) { + Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e); + // Put exception string in item title + View v = newView(mContext, mCursor, parent); + if (v != null) { + ChildViewCache views = (ChildViewCache) v.getTag(); + TextView tv = views.mText1; + tv.setText(e.toString()); + } + return v; + } + } + + /** + * Gets a drawable given a value provided by a suggestion provider. + * + * This value could be just the string value of a resource id + * (e.g., "2130837524"), in which case we will try to retrieve a drawable from + * the provider's resources. If the value is not an integer, it is + * treated as a Uri and opened with + * {@link ContentResolver#openOutputStream(android.net.Uri, String)}. + * + * All resources and URIs are read using the suggestion provider's context. + * + * If the string is not formatted as expected, or no drawable can be found for + * the provided value, this method returns null. + * + * @param drawableId a string like "2130837524", + * "android.resource://com.android.alarmclock/2130837524", + * or "content://contacts/photos/253". + * @return a Drawable, or null if none found + */ + private Drawable getDrawableFromResourceValue(String drawableId) { + if (drawableId == null || drawableId.length() == 0 || "0".equals(drawableId)) { + return null; + } + try { + // First, see if it's just an integer + int resourceId = Integer.parseInt(drawableId); + // It's an int, look for it in the cache + String drawableUri = ContentResolver.SCHEME_ANDROID_RESOURCE + + "://" + mProviderContext.getPackageName() + "/" + resourceId; + // Must use URI as cache key, since ints are app-specific + Drawable drawable = checkIconCache(drawableUri); + if (drawable != null) { + return drawable; + } + // Not cached, find it by resource ID + drawable = mProviderContext.getResources().getDrawable(resourceId); + // Stick it in the cache, using the URI as key + storeInIconCache(drawableUri, drawable); + return drawable; + } catch (NumberFormatException nfe) { + // It's not an integer, use it as a URI + Drawable drawable = checkIconCache(drawableId); + if (drawable != null) { + return drawable; + } + Uri uri = Uri.parse(drawableId); + drawable = getDrawable(uri); + storeInIconCache(drawableId, drawable); + return drawable; + } catch (Resources.NotFoundException nfe) { + // It was an integer, but it couldn't be found, bail out + Log.w(LOG_TAG, "Icon resource not found: " + drawableId); + return null; + } + } + + /** + * Gets a drawable by URI, without using the cache. + * + * @return A drawable, or {@code null} if the drawable could not be loaded. + */ + private Drawable getDrawable(Uri uri) { + try { + String scheme = uri.getScheme(); + if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { + // Load drawables through Resources, to get the source density information + try { + return getTheDrawable(uri); + } catch (Resources.NotFoundException ex) { + throw new FileNotFoundException("Resource does not exist: " + uri); + } + } else { + // Let the ContentResolver handle content and file URIs. + InputStream stream = mProviderContext.getContentResolver().openInputStream(uri); + if (stream == null) { + throw new FileNotFoundException("Failed to open " + uri); + } + try { + return Drawable.createFromStream(stream, null); + } finally { + try { + stream.close(); + } catch (IOException ex) { + Log.e(LOG_TAG, "Error closing icon stream for " + uri, ex); + } + } + } + } catch (FileNotFoundException fnfe) { + Log.w(LOG_TAG, "Icon not found: " + uri + ", " + fnfe.getMessage()); + return null; + } + } + + public Drawable getTheDrawable(Uri uri) throws FileNotFoundException { + String authority = uri.getAuthority(); + Resources r; + if (TextUtils.isEmpty(authority)) { + throw new FileNotFoundException("No authority: " + uri); + } else { + try { + r = mContext.getPackageManager().getResourcesForApplication(authority); + } catch (NameNotFoundException ex) { + throw new FileNotFoundException("No package found for authority: " + uri); + } + } + List path = uri.getPathSegments(); + if (path == null) { + throw new FileNotFoundException("No path: " + uri); + } + int len = path.size(); + int id; + if (len == 1) { + try { + id = Integer.parseInt(path.get(0)); + } catch (NumberFormatException e) { + throw new FileNotFoundException("Single path segment is not a resource ID: " + uri); + } + } else if (len == 2) { + id = r.getIdentifier(path.get(1), path.get(0), authority); + } else { + throw new FileNotFoundException("More than two path segments: " + uri); + } + if (id == 0) { + throw new FileNotFoundException("No resource found for: " + uri); + } + return r.getDrawable(id); + } + + private Drawable checkIconCache(String resourceUri) { + Drawable.ConstantState cached = mOutsideDrawablesCache.get(resourceUri); + if (cached == null) { + return null; + } + if (DBG) Log.d(LOG_TAG, "Found icon in cache: " + resourceUri); + return cached.newDrawable(); + } + + private void storeInIconCache(String resourceUri, Drawable drawable) { + if (drawable != null) { + mOutsideDrawablesCache.put(resourceUri, drawable.getConstantState()); + } + } + + /** + * Gets the left-hand side icon that will be used for the current suggestion + * if the suggestion contains an icon column but no icon or a broken icon. + * + * @param cursor A cursor positioned at the current suggestion. + * @return A non-null drawable. + */ + private Drawable getDefaultIcon1(Cursor cursor) { + // Fall back to a default icon + return mContext.getPackageManager().getDefaultActivityIcon(); + } + + /** + * Gets the activity or application icon for an activity. + * Uses the local icon cache for fast repeated lookups. + * + * @param component Name of an activity. + * @return A drawable, or {@code null} if neither the activity nor the application + * has an icon set. + */ + private Drawable getActivityIconWithCache(ComponentName component) { + // First check the icon cache + String componentIconKey = component.flattenToShortString(); + // Using containsKey() since we also store null values. + if (mOutsideDrawablesCache.containsKey(componentIconKey)) { + Drawable.ConstantState cached = mOutsideDrawablesCache.get(componentIconKey); + return cached == null ? null : cached.newDrawable(mProviderContext.getResources()); + } + // Then try the activity or application icon + Drawable drawable = getActivityIcon(component); + // Stick it in the cache so we don't do this lookup again. + Drawable.ConstantState toCache = drawable == null ? null : drawable.getConstantState(); + mOutsideDrawablesCache.put(componentIconKey, toCache); + return drawable; + } + + /** + * Gets the activity or application icon for an activity. + * + * @param component Name of an activity. + * @return A drawable, or {@code null} if neither the acitivy or the application + * have an icon set. + */ + private Drawable getActivityIcon(ComponentName component) { + PackageManager pm = mContext.getPackageManager(); + final ActivityInfo activityInfo; + try { + activityInfo = pm.getActivityInfo(component, PackageManager.GET_META_DATA); + } catch (NameNotFoundException ex) { + Log.w(LOG_TAG, ex.toString()); + return null; + } + int iconId = activityInfo.getIconResource(); + if (iconId == 0) return null; + String pkg = component.getPackageName(); + Drawable drawable = pm.getDrawable(pkg, iconId, activityInfo.applicationInfo); + if (drawable == null) { + Log.w(LOG_TAG, "Invalid icon resource " + iconId + " for " + + component.flattenToShortString()); + return null; + } + return drawable; + } + + /** + * Gets the value of a string column by name. + * + * @param cursor Cursor to read the value from. + * @param columnName The name of the column to read. + * @return The value of the given column, or null + * if the cursor does not contain the given column. + */ + public static String getColumnString(Cursor cursor, String columnName) { + int col = cursor.getColumnIndex(columnName); + return getStringOrNull(cursor, col); + } + + private static String getStringOrNull(Cursor cursor, int col) { + if (col == INVALID_INDEX) { + return null; + } + try { + return cursor.getString(col); + } catch (Exception e) { + Log.e(LOG_TAG, + "unexpected error retrieving valid column from cursor, " + + "did the remote process die?", e); + return null; + } + } +} diff --git a/APG/android-libs/ActionBarSherlock/test/com/actionbarsherlock/internal/ManifestParsingTest.java b/APG/android-libs/ActionBarSherlock/test/com/actionbarsherlock/internal/ManifestParsingTest.java index 1314248a4..47475c574 100644 --- a/APG/android-libs/ActionBarSherlock/test/com/actionbarsherlock/internal/ManifestParsingTest.java +++ b/APG/android-libs/ActionBarSherlock/test/com/actionbarsherlock/internal/ManifestParsingTest.java @@ -1,13 +1,11 @@ package com.actionbarsherlock.internal; +import org.junit.Test; + +import static com.actionbarsherlock.internal.ActionBarSherlockCompat.cleanActivityName; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; -import static com.actionbarsherlock.internal.ActionBarSherlockCompat.cleanActivityName; -import com.xtremelabs.robolectric.RobolectricTestRunner; -import org.junit.Test; -import org.junit.runner.RunWith; -@RunWith(RobolectricTestRunner.class) public class ManifestParsingTest { @Test public void testFullyQualifiedClassName() {