am 14ca4235: Make dream setting ui fit to system screen.

* commit '14ca4235805189e33093ff728aa40a538e2e28a6':
  Make dream setting ui fit to system screen.
diff --git a/res/menu/cities_menu.xml b/res/menu/cities_menu.xml
deleted file mode 100644
index a9b18f4..0000000
--- a/res/menu/cities_menu.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2012 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.
--->
-
-<menu
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:ex="http://schemas.android.com/apk/res-auto">
-
-    <item android:id="@+id/menu_item_search"
-        android:title="@android:string/search_go"
-        android:icon="@android:drawable/ic_menu_search"
-        android:imeOptions="actionSearch"
-        android:orderInCategory="1"
-        ex:showAsAction="ifRoom"
-        ex:actionViewClass="android.support.v7.widget.SearchView" />
-
-    <item android:id="@+id/menu_item_sort"
-        android:title="@string/menu_item_sort_by_gmt_offset"
-        ex:showAsAction="never" />
-
-    <item android:id="@+id/menu_item_settings"
-        android:title="@string/menu_item_settings"
-        android:icon="@android:drawable/ic_menu_preferences"
-        ex:showAsAction="never"/>
-
-    <item android:id="@+id/menu_item_help"
-        android:title="@string/menu_item_help"
-        android:icon="@android:drawable/ic_menu_preferences"
-        ex:showAsAction="never"/>
-
-</menu>
-
-
diff --git a/res/menu/desk_clock_menu.xml b/res/menu/desk_clock_menu.xml
index b242050..8907301 100644
--- a/res/menu/desk_clock_menu.xml
+++ b/res/menu/desk_clock_menu.xml
@@ -14,8 +14,19 @@
      limitations under the License.
 -->
 
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
     <group android:id="@+id/menu_items">
+        <item android:id="@+id/menu_item_search"
+            android:title="@android:string/search_go"
+            android:icon="@android:drawable/ic_menu_search"
+            android:imeOptions="actionSearch"
+            android:orderInCategory="1"
+            app:showAsAction="ifRoom"
+            app:actionViewClass="android.support.v7.widget.SearchView" />
+        <item android:id="@+id/menu_item_sort"
+            android:title="@string/menu_item_sort_by_gmt_offset"
+            app:showAsAction="never" />
         <item android:id="@+id/menu_item_night_mode"
             android:title="@string/menu_item_night_mode"
             android:icon="@android:drawable/ic_menu_preferences"/>
diff --git a/res/menu/settings_menu.xml b/res/menu/settings_menu.xml
deleted file mode 100644
index 399e393..0000000
--- a/res/menu/settings_menu.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:id="@+id/menu_item_help"
-        android:title="@string/menu_item_help"
-        android:icon="@android:drawable/ic_menu_preferences"/>
-</menu>
-
-
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 2acefaf..2edb374 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -818,7 +818,6 @@
 
      <!-- Header in the preferences settings for the section pertaining to alarms -->
      <string name="alarm_settings">Alarms</string>
-     <string name="desk_clock_help_url" translatable="false"></string>
      <!-- Describes the running service for the stopwatch -->
      <string name="stopwatch_service_desc">Stopwatch service to run the notification.</string>
      <!-- Desription for the stopped stop watch -->
diff --git a/src/com/android/deskclock/AlarmClockFragment.java b/src/com/android/deskclock/AlarmClockFragment.java
index 522692d..68e9a7d 100644
--- a/src/com/android/deskclock/AlarmClockFragment.java
+++ b/src/com/android/deskclock/AlarmClockFragment.java
@@ -169,7 +169,6 @@
         mAlarmUpdateHandler.hideUndoBar(false, null);
     }
 
-
     public void setLabel(Alarm alarm, String label) {
         alarm.label = label;
         mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true);
diff --git a/src/com/android/deskclock/DeskClock.java b/src/com/android/deskclock/DeskClock.java
index f71d190..cf7e37d 100644
--- a/src/com/android/deskclock/DeskClock.java
+++ b/src/com/android/deskclock/DeskClock.java
@@ -18,7 +18,6 @@
 
 import android.app.Fragment;
 import android.app.FragmentManager;
-import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
@@ -42,6 +41,11 @@
 import android.widget.ImageButton;
 import android.widget.ImageView;
 
+import com.android.deskclock.actionbarmenu.ActionBarMenuManager;
+import com.android.deskclock.actionbarmenu.MenuItemController;
+import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
+import com.android.deskclock.actionbarmenu.NightModeMenuItemController;
+import com.android.deskclock.actionbarmenu.SettingMenuItemController;
 import com.android.deskclock.alarms.AlarmStateManager;
 import com.android.deskclock.events.Events;
 import com.android.deskclock.provider.Alarm;
@@ -72,16 +76,16 @@
     private static final String KEY_SELECTED_TAB = "selected_tab";
     public static final String SELECT_TAB_INTENT_EXTRA = "deskclock.select.tab";
 
-    // Request code used when SettingsActivity is launched.
-    private static final int REQUEST_CHANGE_SETTINGS = 1;
-
     public static final int ALARM_TAB_INDEX = 0;
     public static final int CLOCK_TAB_INDEX = 1;
     public static final int TIMER_TAB_INDEX = 2;
     public static final int STOPWATCH_TAB_INDEX = 3;
 
+    private final ActionBarMenuManager mActionBarMenuManager = new ActionBarMenuManager(this);
+    private final MenuItemController nightModeMenuItemController =
+            new NightModeMenuItemController(this);
+
     private TabLayout mTabLayout;
-    private Menu mMenu;
     private RtlViewPager mViewPager;
     private ImageView mFab;
     private ImageButton mLeftButton;
@@ -178,6 +182,10 @@
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
+        mActionBarMenuManager.addMenuItemController(new SettingMenuItemController(this))
+                .addMenuItemController(MenuItemControllerFactory.getInstance()
+                        .buildMenuItemControllers(this))
+                .addMenuItemController(nightModeMenuItemController);
         setVolumeControlStream(AudioManager.STREAM_ALARM);
 
         if (icicle != null) {
@@ -249,80 +257,34 @@
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
-        // We only want to show it as a menu in landscape, and only for clock/alarm fragment.
-        mMenu = menu;
-        // Clear the menu so that it doesn't get duplicate items in case onCreateOptionsMenu
-        // was called multiple times.
-        menu.clear();
-        getMenuInflater().inflate(R.menu.desk_clock_menu, menu);
-        // Always return true, regardless of whether we've inflated the menu, so
-        // that when we switch tabs this method will get called and we can inflate the menu.
+        mActionBarMenuManager.createOptionsMenu(menu, getMenuInflater());
         return true;
     }
 
     @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
-        updateMenu(menu);
+        super.onPrepareOptionsMenu(menu);
+        mActionBarMenuManager.prepareShowMenu(menu);
         return true;
     }
 
-    private void updateMenu(Menu menu) {
-        // Hide "help" if we don't have a URI for it.
-        MenuItem help = menu.findItem(R.id.menu_item_help);
-        if (help != null) {
-            Utils.prepareHelpMenuItem(this, help);
-        }
-
-        // Hide "lights out" for timer.
-        MenuItem nightMode = menu.findItem(R.id.menu_item_night_mode);
-        if (mTabLayout.getSelectedTabPosition() == CLOCK_TAB_INDEX) {
-            nightMode.setVisible(true);
-        } else {
-            nightMode.setVisible(false);
-        }
-    }
-
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
-        if (processMenuClick(item)) {
+        if (mActionBarMenuManager.handleMenuItemClick(item)) {
             return true;
         }
-
         return super.onOptionsItemSelected(item);
     }
 
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         // Recreate the activity if any settings have been changed
-        if (requestCode == REQUEST_CHANGE_SETTINGS && resultCode == RESULT_OK) {
+        if (requestCode == SettingMenuItemController.REQUEST_CHANGE_SETTINGS
+                && resultCode == RESULT_OK) {
             recreate();
         }
     }
 
-    private boolean processMenuClick(MenuItem item) {
-        switch (item.getItemId()) {
-            case R.id.menu_item_settings:
-                startActivityForResult(new Intent(DeskClock.this, SettingsActivity.class),
-                        REQUEST_CHANGE_SETTINGS);
-                return true;
-            case R.id.menu_item_help:
-                Intent i = item.getIntent();
-                if (i != null) {
-                    try {
-                        startActivity(i);
-                    } catch (ActivityNotFoundException e) {
-                        // No activity found to match the intent - ignore
-                    }
-                }
-                return true;
-            case R.id.menu_item_night_mode:
-                startActivity(new Intent(DeskClock.this, ScreensaverActivity.class));
-            default:
-                break;
-        }
-        return true;
-    }
-
     /**
      * Insert the local time zone as the Home Time Zone if one is not set
      */
@@ -441,13 +403,8 @@
             mTabLayout.getTabAt(position).select();
             notifyPageChanged(position);
 
-            // Only show the overflow menu for alarm and world clock.
-            if (mMenu != null) {
-                // Make sure the menu's been initialized.
-                mMenu.setGroupVisible(R.id.menu_items, true);
-                onCreateOptionsMenu(mMenu);
-            }
             mSelectedTab = position;
+            nightModeMenuItemController.setEnabled(mSelectedTab == CLOCK_TAB_INDEX);
 
             if (mActivityResumed) {
                 switch (mSelectedTab) {
@@ -507,7 +464,6 @@
         public void unregisterPageChangedListener(DeskClockFragment frag) {
             mFragmentTags.remove(frag.getTag());
         }
-
     }
 
     /**
diff --git a/src/com/android/deskclock/Utils.java b/src/com/android/deskclock/Utils.java
index 75e4b7a..d142dd2 100644
--- a/src/com/android/deskclock/Utils.java
+++ b/src/com/android/deskclock/Utils.java
@@ -76,18 +76,6 @@
 import java.util.TimeZone;
 
 public class Utils {
-    private final static String PARAM_LANGUAGE_CODE = "hl";
-
-    /**
-     * Help URL query parameter key for the app version.
-     */
-    private final static String PARAM_VERSION = "version";
-
-    /**
-     * Cached version code to prevent repeated calls to the package manager.
-     */
-    private static String sCachedVersionCode = null;
-
     // Single-char version of day name, e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
     private static String[] sShortWeekdays = null;
     private static final String DATE_FORMAT_SHORT = "ccccc";
@@ -184,64 +172,6 @@
         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
     }
 
-    public static void prepareHelpMenuItem(Context context, MenuItem helpMenuItem) {
-        String helpUrlString = context.getResources().getString(R.string.desk_clock_help_url);
-        if (TextUtils.isEmpty(helpUrlString)) {
-            // The help url string is empty or null, so set the help menu item to be invisible.
-            helpMenuItem.setVisible(false);
-            return;
-        }
-        // The help url string exists, so first add in some extra query parameters.  87
-        final Uri fullUri = uriWithAddedParameters(context, Uri.parse(helpUrlString));
-
-        // Then, create an intent that will be fired when the user
-        // selects this help menu item.
-        Intent intent = new Intent(Intent.ACTION_VIEW, fullUri);
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
-                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
-
-        // Set the intent to the help menu item, show the help menu item in the overflow
-        // menu, and make it visible.
-        helpMenuItem.setIntent(intent);
-        helpMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
-        helpMenuItem.setVisible(true);
-    }
-
-    /**
-     * Adds two query parameters into the Uri, namely the language code and the version code
-     * of the application's package as gotten via the context.
-     * @return the uri with added query parameters
-     */
-    private static Uri uriWithAddedParameters(Context context, Uri baseUri) {
-        Uri.Builder builder = baseUri.buildUpon();
-
-        // Add in the preferred language
-        builder.appendQueryParameter(PARAM_LANGUAGE_CODE, Locale.getDefault().toString());
-
-        // Add in the package version code
-        if (sCachedVersionCode == null) {
-            // There is no cached version code, so try to get it from the package manager.
-            try {
-                // cache the version code
-                PackageInfo info = context.getPackageManager().getPackageInfo(
-                        context.getPackageName(), 0);
-                sCachedVersionCode = Integer.toString(info.versionCode);
-
-                // append the version code to the uri
-                builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode);
-            } catch (NameNotFoundException e) {
-                // Cannot find the package name, so don't add in the version parameter
-                // This shouldn't happen.
-                LogUtils.wtf("Invalid package name for context " + e);
-            }
-        } else {
-            builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode);
-        }
-
-        // Build the full uri and return it
-        return builder.build();
-    }
-
     public static long getTimeNow() {
         return SystemClock.elapsedRealtime();
     }
diff --git a/src/com/android/deskclock/actionbarmenu/AbstractMenuItemController.java b/src/com/android/deskclock/actionbarmenu/AbstractMenuItemController.java
new file mode 100644
index 0000000..047d816
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/AbstractMenuItemController.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2015 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.android.deskclock.actionbarmenu;
+
+import android.view.Menu;
+
+/**
+ * Base of all {@link MenuItemController}. It contains basic implementation for enabling controller.
+ */
+public abstract class AbstractMenuItemController implements MenuItemController {
+    // Whether or not the controller is enabled. By default it's enabled.
+    private boolean mEnabled = true;
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        mEnabled = enabled;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+
+    @Override
+    public void setInitialState(Menu menu) {
+        // By default, there is nothing to initialize.
+    }
+}
diff --git a/src/com/android/deskclock/actionbarmenu/ActionBarMenuManager.java b/src/com/android/deskclock/actionbarmenu/ActionBarMenuManager.java
new file mode 100644
index 0000000..f470e5b
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/ActionBarMenuManager.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 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.android.deskclock.actionbarmenu;
+
+import android.app.Activity;
+import android.util.ArrayMap;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import com.android.deskclock.R;
+
+/**
+ * Activity scoped singleton that manages action bar menus. Each menu item is controlled by a
+ * {@link MenuItemController} instance.
+ * <p/>
+ * This class needs to be instantiated before or during activity's onCreate event.
+ */
+public final class ActionBarMenuManager {
+    // A map of all menu item controllers, keyed by menu item id.
+    private final ArrayMap<Integer, MenuItemController> mControllers;
+
+    public ActionBarMenuManager(Activity activity) {
+        mControllers = new ArrayMap<>();
+    }
+
+    /**
+     * Add one or more {@link MenuItemController} to the actionbar menu.
+     * <p/>
+     * This should be called before activity's onPrepareOptionsMenu event.
+     */
+    public ActionBarMenuManager addMenuItemController(MenuItemController... menuItemControllers) {
+        if (menuItemControllers != null) {
+            for (MenuItemController controller : menuItemControllers) {
+                mControllers.put(controller.getId(), controller);
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Inflates {@link Menu} for the activity.
+     * <p/>
+     * This method should be called during activity's onCreateOptionsMenu method.
+     */
+    public void createOptionsMenu(Menu menu, MenuInflater inflater) {
+        if (menu.size() > 0) {
+            throw new IllegalStateException("Menu has already been inflated.");
+        }
+        inflater.inflate(R.menu.desk_clock_menu, menu);
+
+        final int controllerSize = mControllers.size();
+        for (int i = 0; i < controllerSize; i++) {
+            final MenuItemController controller = mControllers.valueAt(i);
+            if (controller.isEnabled()) {
+                controller.setInitialState(menu);
+            }
+        }
+    }
+
+    /**
+     * Prepares the popup to displays all required menu items.
+     * <p/>
+     * This method should be called during activity's onPrepareOptionsMenu method.
+     */
+    public void prepareShowMenu(Menu menu) {
+        final int menuSize = menu.size();
+        for (int i = 0; i < menuSize; i++) {
+            menu.getItem(i).setVisible(false);
+        }
+        final int controllerSize = mControllers.size();
+        for (int i = 0; i < controllerSize; i++) {
+            final MenuItemController controller = mControllers.valueAt(i);
+            if (controller.isEnabled()) {
+                controller.showMenuItem(menu);
+            }
+        }
+    }
+
+    /**
+     * Handles click action for a menu item.
+     * <p/>
+     * This method should be called during activity's onOptionsItemSelected method.
+     */
+    public boolean handleMenuItemClick(MenuItem item) {
+        final int itemId = item.getItemId();
+        return mControllers.get(itemId).handleMenuItemClick(item);
+    }
+}
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemController.java b/src/com/android/deskclock/actionbarmenu/MenuItemController.java
new file mode 100644
index 0000000..d7cb06f
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/MenuItemController.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2015 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.android.deskclock.actionbarmenu;
+
+import android.view.Menu;
+import android.view.MenuItem;
+
+/**
+ * Interface for handling a single menu item in action bar.
+ */
+public interface MenuItemController {
+
+    /**
+     * Sets whether or not the controller is enabled.
+     */
+    void setEnabled(boolean enabled);
+
+    /**
+     * Returns true if the controller is currently enabled.
+     */
+    boolean isEnabled();
+
+    /**
+     * Returns the menu item id that the controller is responsible for.
+     */
+    int getId();
+
+    /**
+     * Sets the initial state for the menu item.
+     */
+    void setInitialState(Menu menu);
+
+    /**
+     * Find the menu item this controller cares about, and make it visible.
+     *
+     * @param menu The menu object containing an item that controller can handle.
+     */
+    void showMenuItem(Menu menu);
+
+    /**
+     * Attempts to handle the click action.
+     *
+     * @param item The menu item being clicked.
+     * @return True if the action is handled by this controller, false otherwise.
+     */
+    boolean handleMenuItemClick(MenuItem item);
+}
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.java b/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.java
new file mode 100644
index 0000000..3598f16
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2015 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.android.deskclock.actionbarmenu;
+
+import android.app.Activity;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Factory that builds optional {@link MenuItemController} instances.
+ */
+public final class MenuItemControllerFactory {
+
+    private static final MenuItemControllerFactory INSTANCE = new MenuItemControllerFactory();
+
+    public static MenuItemControllerFactory getInstance() {
+        return INSTANCE;
+    }
+
+    private final List<MenuItemProvider> mMenuItemProviders;
+
+    private MenuItemControllerFactory() {
+        mMenuItemProviders = new ArrayList<>();
+    }
+
+    public MenuItemControllerFactory addMenuItemProvider(MenuItemProvider provider) {
+        mMenuItemProviders.add(provider);
+        return this;
+    }
+
+    public MenuItemController[] buildMenuItemControllers(Activity activity) {
+        final int providerSize = mMenuItemProviders.size();
+        final MenuItemController[] controllers = new MenuItemController[providerSize];
+        for (int i = 0; i < providerSize; i++) {
+            controllers[i] = mMenuItemProviders.get(i).provide(activity);
+        }
+        return controllers;
+    }
+}
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemProvider.java b/src/com/android/deskclock/actionbarmenu/MenuItemProvider.java
new file mode 100644
index 0000000..c3e460d
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/MenuItemProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2015 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.android.deskclock.actionbarmenu;
+
+import android.app.Activity;
+
+/**
+ * Provider for a {@link MenuItemController} instances.
+ */
+public interface MenuItemProvider {
+
+    /**
+     * provides a {@link MenuItemController} that handles menu item.
+     */
+    MenuItemController provide(Activity activity);
+}
diff --git a/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.java b/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.java
new file mode 100644
index 0000000..a2aebbf
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 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.android.deskclock.actionbarmenu;
+
+import android.app.Activity;
+import android.view.Menu;
+import android.view.MenuItem;
+
+/**
+ * {@link MenuItemController} for handling navigation up button in actionbar. It is a special
+ * menu item because it's not inflated through menu.xml, and has its own predefined id.
+ */
+public final class NavUpMenuItemController extends AbstractMenuItemController {
+
+    private final Activity mActivity;
+
+    public NavUpMenuItemController(Activity activity) {
+        mActivity = activity;
+    }
+
+    @Override
+    public int getId() {
+        return android.R.id.home;
+    }
+
+    @Override
+    public void showMenuItem(Menu menu) {
+        // Intentionally left empty, because back button in actionbar is not shown by inflating
+        // menu layouts. It's directly controlled by actionbar.
+    }
+
+    @Override
+    public boolean handleMenuItemClick(MenuItem item) {
+        mActivity.finish();
+        return true;
+    }
+}
diff --git a/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.java b/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.java
new file mode 100644
index 0000000..0a98621
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2015 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.android.deskclock.actionbarmenu;
+
+import android.content.Context;
+import android.content.Intent;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.deskclock.R;
+import com.android.deskclock.ScreensaverActivity;
+
+/**
+ * {@link MenuItemController} for controlling night mode display.
+ */
+public final class NightModeMenuItemController extends AbstractMenuItemController {
+
+    private static final int NIGHT_MODE_MENU_RES_ID = R.id.menu_item_night_mode;
+
+    private final Context mContext;
+
+    public NightModeMenuItemController(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public int getId() {
+        return NIGHT_MODE_MENU_RES_ID;
+    }
+
+    @Override
+    public void showMenuItem(Menu menu) {
+        menu.findItem(NIGHT_MODE_MENU_RES_ID).setVisible(true);
+    }
+
+    @Override
+    public boolean handleMenuItemClick(MenuItem item) {
+        mContext.startActivity(new Intent(mContext, ScreensaverActivity.class));
+        return true;
+    }
+}
diff --git a/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.java b/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.java
new file mode 100644
index 0000000..fbba78d
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2015 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.android.deskclock.actionbarmenu;
+
+import android.os.Bundle;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.widget.SearchView;
+import android.support.v7.widget.SearchView.OnQueryTextListener;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+
+import com.android.deskclock.R;
+
+/**
+ * {@link MenuItemController} for search menu.
+ */
+public final class SearchMenuItemController extends AbstractMenuItemController {
+
+    private static final String KEY_SEARCH_QUERY = "search_query";
+    private static final String KEY_SEARCH_MODE = "search_mode";
+    private static final int SEARCH_MENU_RES_ID = R.id.menu_item_search;
+    private final SearchView.OnQueryTextListener mQueryListener;
+    private final SearchModeChangeListener mSearchModeChangeListener;
+    private String mQuery = "";
+    private boolean mSearchMode;
+
+    public SearchMenuItemController(OnQueryTextListener queryListener, Bundle savedState) {
+        mSearchModeChangeListener = new SearchModeChangeListener();
+        mQueryListener = queryListener;
+        if (savedState != null) {
+            mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE, false);
+            mQuery = savedState.getString(KEY_SEARCH_QUERY, "");
+        }
+    }
+
+    public void saveInstance(Bundle outState) {
+        outState.putString(KEY_SEARCH_QUERY, mQuery);
+        outState.putBoolean(KEY_SEARCH_MODE, mSearchMode);
+    }
+
+    @Override
+    public int getId() {
+        return SEARCH_MENU_RES_ID;
+    }
+
+    @Override
+    public void setInitialState(Menu menu) {
+        super.setInitialState(menu);
+        final MenuItem search = menu.findItem(SEARCH_MENU_RES_ID);
+        final SearchView searchView = (SearchView) MenuItemCompat.getActionView(search);
+        searchView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
+        searchView.setQuery(mQuery, false);
+        searchView.setOnCloseListener(mSearchModeChangeListener);
+        searchView.setOnSearchClickListener(mSearchModeChangeListener);
+        searchView.setOnQueryTextListener(mQueryListener);
+        if (mSearchMode) {
+            searchView.requestFocus();
+            searchView.setIconified(false);
+        }
+    }
+
+    @Override
+    public void showMenuItem(Menu menu) {
+        menu.findItem(SEARCH_MENU_RES_ID).setVisible(true);
+    }
+
+    @Override
+    public boolean handleMenuItemClick(MenuItem item) {
+        // The search view is handled by {@link #mSearchListener}. Skip handling here.
+        return false;
+    }
+
+    public String getQueryText() {
+        return mQuery;
+    }
+
+    public void setQueryText(String query) {
+        mQuery = query;
+    }
+
+    /**
+     * Listener for user actions on search view.
+     */
+    private final class SearchModeChangeListener implements View.OnClickListener,
+            SearchView.OnCloseListener {
+        @Override
+        public void onClick(View v) {
+            mSearchMode = true;
+        }
+
+        @Override
+        public boolean onClose() {
+            mSearchMode = false;
+            return false;
+        }
+    }
+}
diff --git a/src/com/android/deskclock/actionbarmenu/SettingMenuItemController.java b/src/com/android/deskclock/actionbarmenu/SettingMenuItemController.java
new file mode 100644
index 0000000..a984451
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/SettingMenuItemController.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2015 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.android.deskclock.actionbarmenu;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.deskclock.R;
+import com.android.deskclock.settings.SettingsActivity;
+
+/**
+ * {@link MenuItemController} for setting menu.
+ */
+public final class SettingMenuItemController extends AbstractMenuItemController {
+
+    public static final int REQUEST_CHANGE_SETTINGS = 1;
+
+    private static final int SETTING_MENU_RES_ID = R.id.menu_item_settings;
+    private final Activity mActivity;
+
+    public SettingMenuItemController(Activity activity) {
+        mActivity = activity;
+    }
+
+    @Override
+    public int getId() {
+        return SETTING_MENU_RES_ID;
+    }
+
+    @Override
+    public void showMenuItem(Menu menu) {
+        menu.findItem(SETTING_MENU_RES_ID).setVisible(true);
+    }
+
+    @Override
+    public boolean handleMenuItemClick(MenuItem item) {
+        Intent settingIntent = new Intent(mActivity, SettingsActivity.class);
+        mActivity.startActivityForResult(settingIntent, REQUEST_CHANGE_SETTINGS);
+        return true;
+    }
+}
diff --git a/src/com/android/deskclock/settings/SettingsActivity.java b/src/com/android/deskclock/settings/SettingsActivity.java
index 777fe72..6e3266a 100644
--- a/src/com/android/deskclock/settings/SettingsActivity.java
+++ b/src/com/android/deskclock/settings/SettingsActivity.java
@@ -36,6 +36,9 @@
 import com.android.deskclock.LogUtils;
 import com.android.deskclock.R;
 import com.android.deskclock.Utils;
+import com.android.deskclock.actionbarmenu.ActionBarMenuManager;
+import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
+import com.android.deskclock.actionbarmenu.NavUpMenuItemController;
 import com.android.deskclock.data.DataModel;
 
 import java.util.ArrayList;
@@ -68,36 +71,38 @@
     public static final String VOLUME_BEHAVIOR_SNOOZE = "1";
     public static final String VOLUME_BEHAVIOR_DISMISS = "2";
 
+    private final ActionBarMenuManager mActionBarMenuManager = new ActionBarMenuManager(this);
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setVolumeControlStream(AudioManager.STREAM_ALARM);
         setContentView(R.layout.settings);
+        mActionBarMenuManager.addMenuItemController(new NavUpMenuItemController(this))
+            .addMenuItemController(MenuItemControllerFactory.getInstance()
+                    .buildMenuItemControllers(this));
     }
 
     @Override
-    public boolean onOptionsItemSelected (MenuItem item) {
-        switch (item.getItemId()) {
-            case android.R.id.home:
-                finish();
-                return true;
-            default:
-                break;
+    public boolean onCreateOptionsMenu(Menu menu) {
+        mActionBarMenuManager.createOptionsMenu(menu, getMenuInflater());
+        return true;
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        mActionBarMenuManager.prepareShowMenu(menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (mActionBarMenuManager.handleMenuItemClick(item)) {
+            return true;
         }
         return super.onOptionsItemSelected(item);
     }
 
-    @Override
-    public boolean onCreateOptionsMenu (Menu menu) {
-        getMenuInflater().inflate(R.menu.settings_menu, menu);
-        MenuItem help = menu.findItem(R.id.menu_item_help);
-        if (help != null) {
-            Utils.prepareHelpMenuItem(this, help);
-        }
-        return super.onCreateOptionsMenu(menu);
-    }
-
-
     public static class PrefsFragment extends PreferenceFragment
             implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener {
 
@@ -150,7 +155,7 @@
                     break;
                 case KEY_WEEK_START:
                     final ListPreference weekStartPref = (ListPreference)
-                        findPreference(KEY_WEEK_START);
+                            findPreference(KEY_WEEK_START);
                     idx = weekStartPref.findIndexOfValue((String) newValue);
                     weekStartPref.setSummary(weekStartPref.getEntries()[idx]);
                     break;
diff --git a/src/com/android/deskclock/timer/TimerFragment.java b/src/com/android/deskclock/timer/TimerFragment.java
index 10a017f..6bbd6e9 100644
--- a/src/com/android/deskclock/timer/TimerFragment.java
+++ b/src/com/android/deskclock/timer/TimerFragment.java
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.android.deskclock.timer;
 
 import android.animation.Animator;
diff --git a/src/com/android/deskclock/worldclock/CitySelectionActivity.java b/src/com/android/deskclock/worldclock/CitySelectionActivity.java
index 726cf45..2af0ce0 100644
--- a/src/com/android/deskclock/worldclock/CitySelectionActivity.java
+++ b/src/com/android/deskclock/worldclock/CitySelectionActivity.java
@@ -16,12 +16,9 @@
 
 package com.android.deskclock.worldclock;
 
-import android.content.ActivityNotFoundException;
 import android.content.Context;
-import android.content.Intent;
 import android.media.AudioManager;
 import android.os.Bundle;
-import android.support.v4.view.MenuItemCompat;
 import android.support.v7.widget.SearchView;
 import android.text.TextUtils;
 import android.text.format.DateFormat;
@@ -32,7 +29,6 @@
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.inputmethod.EditorInfo;
 import android.widget.BaseAdapter;
 import android.widget.CheckBox;
 import android.widget.CompoundButton;
@@ -43,9 +39,14 @@
 import com.android.deskclock.BaseActivity;
 import com.android.deskclock.R;
 import com.android.deskclock.Utils;
+import com.android.deskclock.actionbarmenu.AbstractMenuItemController;
+import com.android.deskclock.actionbarmenu.ActionBarMenuManager;
+import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
+import com.android.deskclock.actionbarmenu.NavUpMenuItemController;
+import com.android.deskclock.actionbarmenu.SearchMenuItemController;
+import com.android.deskclock.actionbarmenu.SettingMenuItemController;
 import com.android.deskclock.data.City;
 import com.android.deskclock.data.DataModel;
-import com.android.deskclock.settings.SettingsActivity;
 
 import java.util.ArrayList;
 import java.util.Calendar;
@@ -72,35 +73,45 @@
  */
 public final class CitySelectionActivity extends BaseActivity {
 
-    /** Key in the state bundle by which the user's query is stored. */
-    private static final String KEY_SEARCH_QUERY = "search_query";
-
-    /** Key in the state bundle by which the user's search mode is stored. */
-    private static final String KEY_SEARCH_MODE = "search_mode";
-
-    /** {@code true} when the user has entered search mode. */
-    private boolean mSearchMode = false;
-
     /** The list of all selected and unselected cities, indexed and possibly filtered. */
     private ListView mCitiesList;
 
     /** The adapter that presents all of the selected and unselected cities. */
     private CityAdapter mCitiesAdapter;
 
+    /** Manages all action bar menu display and click handling. */
+    private final ActionBarMenuManager mActionBarMenuManager = new ActionBarMenuManager(this);
+
+    /** Menu item controller for search view. */
+    private SearchMenuItemController mSearchMenuItemController;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setVolumeControlStream(AudioManager.STREAM_ALARM);
 
-        mCitiesAdapter = new CityAdapter(this);
-
-        // Restore state information if it exists.
-        if (savedInstanceState != null) {
-            mSearchMode = savedInstanceState.getBoolean(KEY_SEARCH_MODE);
-            mCitiesAdapter.setQueryText(savedInstanceState.getString(KEY_SEARCH_QUERY));
-        }
-
         setContentView(R.layout.cities_activity);
+        mSearchMenuItemController =
+                new SearchMenuItemController(new SearchView.OnQueryTextListener() {
+                    @Override
+                    public boolean onQueryTextSubmit(String query) {
+                        return false;
+                    }
+
+                    @Override
+                    public boolean onQueryTextChange(String query) {
+                        mCitiesAdapter.filter(query);
+                        updateFastScrolling();
+                        return true;
+                    }
+                }, savedInstanceState);
+        mCitiesAdapter = new CityAdapter(this, mSearchMenuItemController);
+        mActionBarMenuManager.addMenuItemController(new NavUpMenuItemController(this))
+                .addMenuItemController(mSearchMenuItemController)
+                .addMenuItemController(new SortOrderMenuItemController())
+                .addMenuItemController(new SettingMenuItemController(this))
+                .addMenuItemController(MenuItemControllerFactory.getInstance()
+                        .buildMenuItemControllers(this));
         mCitiesList = (ListView) findViewById(R.id.cities_list);
         mCitiesList.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET);
         mCitiesList.setAdapter(mCitiesAdapter);
@@ -111,8 +122,7 @@
     @Override
     public void onSaveInstanceState(Bundle bundle) {
         super.onSaveInstanceState(bundle);
-        bundle.putString(KEY_SEARCH_QUERY, mCitiesAdapter.getQueryText());
-        bundle.putBoolean(KEY_SEARCH_MODE, mSearchMode);
+        mSearchMenuItemController.saveInstance(bundle);
     }
 
     @Override
@@ -133,70 +143,20 @@
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
-        getMenuInflater().inflate(R.menu.cities_menu, menu);
-
-        final MenuItem help = menu.findItem(R.id.menu_item_help);
-        Utils.prepareHelpMenuItem(this, help);
-
-        final SearchListener searchListener = new SearchListener();
-        final MenuItem search = menu.findItem(R.id.menu_item_search);
-        final SearchView searchView = (SearchView) MenuItemCompat.getActionView(search);
-        searchView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
-        searchView.setQuery(mCitiesAdapter.getQueryText(), false);
-        searchView.setOnCloseListener(searchListener);
-        searchView.setOnQueryTextListener(searchListener);
-        searchView.setOnSearchClickListener(searchListener);
-
-        if (mSearchMode) {
-            searchView.requestFocus();
-            searchView.setIconified(false);
-        }
-
-        return super.onCreateOptionsMenu(menu);
+        mActionBarMenuManager.createOptionsMenu(menu, getMenuInflater());
+        return true;
     }
 
     @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
-        final MenuItem sort = menu.findItem(R.id.menu_item_sort);
-
-        // Adjust the title of the sort menu item to the opposite of the current sort order.
-        if (DataModel.getDataModel().getCitySort() == DataModel.CitySort.NAME) {
-            sort.setTitle(getString(R.string.menu_item_sort_by_gmt_offset));
-        } else {
-            sort.setTitle(getString(R.string.menu_item_sort_by_name));
-        }
-        return super.onPrepareOptionsMenu(menu);
+        mActionBarMenuManager.prepareShowMenu(menu);
+        return true;
     }
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
-        switch (item.getItemId()) {
-            case android.R.id.home:
-                finish();
-                return true;
-            case R.id.menu_item_sort:
-                // Save the new sort order.
-                DataModel.getDataModel().toggleCitySort();
-
-                // Section headers are influenced by sort order and must be cleared.
-                mCitiesAdapter.clearSectionHeaders();
-
-                // Honor the new sort order in the adapter.
-                mCitiesAdapter.filter(mCitiesAdapter.getQueryText());
-                return true;
-            case R.id.menu_item_settings:
-                startActivity(new Intent(this, SettingsActivity.class));
-                return true;
-            case R.id.menu_item_help:
-                final Intent i = item.getIntent();
-                if (i != null) {
-                    try {
-                        startActivity(i);
-                    } catch (ActivityNotFoundException e) {
-                        // No activity found to match the intent - ignore
-                    }
-                }
-                return true;
+        if (mActionBarMenuManager.handleMenuItemClick(item)) {
+            return true;
         }
         return super.onOptionsItemSelected(item);
     }
@@ -211,35 +171,6 @@
     }
 
     /**
-     * Listeners that update the UI state in response to changes in the search component.
-     */
-    private final class SearchListener implements View.OnClickListener, SearchView.OnCloseListener,
-            SearchView.OnQueryTextListener {
-        @Override
-        public void onClick(View v) {
-            mSearchMode = true;
-        }
-
-        @Override
-        public boolean onClose() {
-            mSearchMode = false;
-            return false;
-        }
-
-        @Override
-        public boolean onQueryTextChange(String queryText) {
-            mCitiesAdapter.filter(queryText);
-            updateFastScrolling();
-            return true;
-        }
-
-        @Override
-        public boolean onQueryTextSubmit(String s) {
-            return false;
-        }
-    }
-
-    /**
      * This adapter presents data in 2 possible modes. If selected cities exist the format is:
      *
      * <pre>
@@ -306,11 +237,12 @@
         /** The corresponding location of each precomputed section header. */
         private Integer[] mSectionHeaderPositions;
 
-        /** The query text currently filtering the cities. */
-        private String mQueryText = "";
+        /** Menu item controller for search. Search query is maintained here. */
+        private final SearchMenuItemController mSearchMenuItemController;
 
-        public CityAdapter(Context context) {
+        public CityAdapter(Context context, SearchMenuItemController searchMenuItemController) {
             mContext = context;
+            mSearchMenuItemController = searchMenuItemController;
             mInflater = LayoutInflater.from(context);
 
             mCalendar = Calendar.getInstance();
@@ -522,15 +454,15 @@
             clearSectionHeaders();
 
             // Recompute filtered cities.
-            filter(getQueryText());
+            filter(mSearchMenuItemController.getQueryText());
         }
 
         /**
          * Filter the cities using the given {@code queryText}.
          */
         private void filter(String queryText) {
-            setQueryText(queryText);
-            final String query = mQueryText.trim().toUpperCase();
+            mSearchMenuItemController.setQueryText(queryText);
+            final String query = queryText.trim().toUpperCase();
 
             // Compute the filtered list of cities.
             final List<City> filteredCities;
@@ -551,9 +483,10 @@
             notifyDataSetChanged();
         }
 
-        private String getQueryText() { return mQueryText; }
-        private void setQueryText(String queryText) { mQueryText = queryText; }
-        private boolean isFiltering() { return !TextUtils.isEmpty(mQueryText.trim()); }
+        private boolean isFiltering() {
+            return !TextUtils.isEmpty(mSearchMenuItemController.getQueryText().trim());
+        }
+
         private Collection<City> getSelectedCities() { return mUserSelectedCities; }
         private boolean hasHeader() { return !isFiltering() && mOriginalUserSelectionCount > 0; }
 
@@ -622,4 +555,40 @@
             }
         }
     }
-}
\ No newline at end of file
+
+    private final class SortOrderMenuItemController extends AbstractMenuItemController {
+
+        private static final int SORT_MENU_RES_ID = R.id.menu_item_sort;
+
+        @Override
+        public int getId() {
+            return SORT_MENU_RES_ID;
+        }
+
+        @Override
+        public void showMenuItem(Menu menu) {
+            final MenuItem sortMenuItem = menu.findItem(SORT_MENU_RES_ID);
+            final String title;
+            if (DataModel.getDataModel().getCitySort() == DataModel.CitySort.NAME) {
+                title = getString(R.string.menu_item_sort_by_gmt_offset);
+            } else {
+                title = getString(R.string.menu_item_sort_by_name);
+            }
+            sortMenuItem.setTitle(title);
+            sortMenuItem.setVisible(true);
+        }
+
+        @Override
+        public boolean handleMenuItemClick(MenuItem item) {
+            // Save the new sort order.
+            DataModel.getDataModel().toggleCitySort();
+
+            // Section headers are influenced by sort order and must be cleared.
+            mCitiesAdapter.clearSectionHeaders();
+
+            // Honor the new sort order in the adapter.
+            mCitiesAdapter.filter(mSearchMenuItemController.getQueryText());
+            return true;
+        }
+    }
+}