Add conditionals to Settings

Also add Airplane Mode and Hotspot conditionals (more to come soon)

Change-Id: I11f206db59f7c715f416fb5852b8f0fcb857a247
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 3bbc667..c83002a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2656,5 +2656,22 @@
             </intent-filter>
         </activity>
 
+        <!-- Conditional receivers, only enabled during silenced state, default off-->
+        <receiver
+            android:name=".dashboard.conditional.HotspotCondition$Receiver"
+            android:enabled="false">
+            <intent-filter>
+                 <action android:name="android.net.wifi.WIFI_AP_STATE_CHANGED" />
+            </intent-filter>
+       </receiver>
+
+        <receiver
+            android:name=".dashboard.conditional.AirplaneModeCondition$Receiver"
+            android:enabled="false">
+            <intent-filter>
+                 <action android:name="android.intent.action.AIRPLANE_MODE" />
+            </intent-filter>
+       </receiver>
+
     </application>
 </manifest>
diff --git a/res/drawable/ic_airplane.xml b/res/drawable/ic_airplane.xml
new file mode 100644
index 0000000..b3e0ba0
--- /dev/null
+++ b/res/drawable/ic_airplane.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:name="root"
+    android:alpha="1.0"
+    android:height="48dp"
+    android:width="48dp"
+    android:viewportHeight="48"
+    android:viewportWidth="48" >
+    <group
+        android:name="ic_signal_airplane"
+        android:translateX="21.9995"
+        android:translateY="25.73401" >
+        <group
+            android:name="ic_signal_airplane_pivot"
+            android:translateX="-23.21545"
+            android:translateY="-18.86649" >
+            <clip-path
+                android:name="mask"
+                android:pathData="M 37.8337860107,-40.4599914551 c 0.0,0.0 -35.8077850342,31.5523681641 -35.8077850342,31.5523681641 c 0.0,0.0 9.55097961426,9.55285644531 9.55097961426,9.55285644531 c 0.0,0.0 -2.61698913574,2.09387207031 -2.61698913574,2.09387207031 c 0.0,0.0 -9.75096130371,-9.56428527832 -9.75096130371,-9.56428527832 c 0.0,0.0 -34.6200408936,25.4699249268 -34.6200408936,25.4699249268 c 0.0,0.0 55.9664764404,69.742401123 55.9664764404,69.742401123 c 0.0,0.0 73.2448120117,-59.1047973633 73.2448120117,-59.1047973633 c 0.0,0.0 -55.9664916992,-69.7423400879 -55.9664916992,-69.7423400879 Z" />
+            <group
+                android:name="cross" >
+                <path
+                    android:name="cross_1"
+                    android:pathData="M 7.54049682617,3.9430847168 c 0.0,0.0 0.324981689453,0.399978637695 0.324981689453,0.399978637695 "
+                    android:strokeColor="#FFFFFFFF"
+                    android:strokeAlpha="0"
+                    android:strokeWidth="3.5"
+                    android:fillColor="#00000000" />
+            </group>
+            <group
+                android:name="plane"
+                android:translateX="23.481"
+                android:translateY="18.71151" >
+                <path
+                    android:name="plane_1"
+                    android:pathData="M 18.9439849854,7.98849487305 c 0.0,0.0 0.0,-4.0 0.0,-4.0 c 0.0,0.0 -16.0,-10.0 -16.0,-10.0 c 0.0,0.0 0.0,-11.0 0.0,-11.0 c 0.0,-1.70001220703 -1.30000305176,-3.0 -3.0,-3.0 c -1.69999694824,0.0 -3.0,1.29998779297 -3.0,3.0 c 0.0,0.0 0.0,11.0 0.0,11.0 c 0.0,0.0 -16.0,10.0 -16.0,10.0 c 0.0,0.0 0.0,4.0 0.0,4.0 c 0.0,0.0 16.0,-5.0 16.0,-5.0 c 0.0,0.0 0.0,11.0 0.0,11.0 c 0.0,0.0 -4.0,3.0 -4.0,3.0 c 0.0,0.0 0.0,3.0 0.0,3.0 c 0.0,0.0 7.0,-2.0 7.0,-2.0 c 0.0,0.0 7.0,2.0 7.0,2.0 c 0.0,0.0 0.0,-3.0 0.0,-3.0 c 0.0,0.0 -4.0,-3.0 -4.0,-3.0 c 0.0,0.0 0.0,-11.0 0.0,-11.0 c 0.0,0.0 16.0,5.0 16.0,5.0 Z"
+                    android:fillColor="#FFFFFFFF"
+                    android:fillAlpha="1" />
+            </group>
+        </group>
+    </group>
+</vector>
diff --git a/res/drawable/ic_expand_less.xml b/res/drawable/ic_expand_less.xml
index 3a00faf..7ccc080 100644
--- a/res/drawable/ic_expand_less.xml
+++ b/res/drawable/ic_expand_less.xml
@@ -19,10 +19,11 @@
     android:width="24dp"
     android:height="24dp"
     android:viewportWidth="24"
-    android:viewportHeight="24">
+    android:viewportHeight="24"
+    android:tint="?android:attr/colorControlNormal">
 
     <path
-        android:fillColor="?android:attr/colorControlNormal"
+        android:fillColor="@android:color/white"
         android:pathData="M12.0,8.0l-6.0,6.0 1.41,1.41L12.0,10.83l4.59,4.58L18.0,14.0z"/>
 
 </vector>
diff --git a/res/drawable/ic_expand_more.xml b/res/drawable/ic_expand_more.xml
index 64d2242..609cf36 100644
--- a/res/drawable/ic_expand_more.xml
+++ b/res/drawable/ic_expand_more.xml
@@ -19,10 +19,11 @@
     android:width="24dp"
     android:height="24dp"
     android:viewportWidth="24"
-    android:viewportHeight="24">
+    android:viewportHeight="24"
+    android:tint="?android:attr/colorControlNormal">
 
     <path
-        android:fillColor="?android:attr/colorControlNormal"
+        android:fillColor="@android:color/white"
         android:pathData="M16.59,8.59L12.0,13.17 7.41,8.59 6.0,10.0l6.0,6.0 6.0,-6.0z"/>
 
 </vector>
diff --git a/res/drawable/ic_hotspot.xml b/res/drawable/ic_hotspot.xml
new file mode 100644
index 0000000..d8528dd
--- /dev/null
+++ b/res/drawable/ic_hotspot.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:name="root"
+    android:alpha="1.0"
+    android:height="48dp"
+    android:width="48dp"
+    android:viewportHeight="48"
+    android:viewportWidth="48" >
+    <group
+        android:name="ic_hotspot"
+        android:translateX="23.9778"
+        android:translateY="24.26443" >
+        <group
+            android:name="ic_hotspot_pivot"
+            android:translateX="-23.21545"
+            android:translateY="-18.86649" >
+            <clip-path
+                android:name="mask"
+                android:pathData="M 38.8337860107,-40.3974914551 c 0.0,0.0 -38.4077911377,30.8523712158 -38.4077911377,30.8523712158 c 0.0,0.0 6.97125244141,7.33258056641 6.97125244141,7.33258056641 c 0.0,0.0 -2.4169921875,2.57838439941 -2.4169921875,2.57838439941 c 0.0,0.0 -6.77128601074,-6.82850646973 -6.77128601074,-6.82850646973 c 0.0,0.0 -32.6199798584,25.1699066162 -32.6199798584,25.1699066162 c 0.0,0.0 55.9664764404,69.742401123 55.9664764404,69.742401123 c 0.0,0.0 27.6589050293,-22.6579437256 27.6589050293,-22.6579437256 c 0.0,0.0 -30.8645172119,-34.00390625 -30.8645172119,-34.00390625 c 0.0,0.0 2.70756530762,-1.99278259277 2.70756530762,-1.99278259277 c 0.0,0.0 1.53030395508,-0.876571655273 1.53030395508,-0.876571655274 c 0.0,0.0 2.85780334473,-3.12069702148 2.85780334473,-3.12069702148 c 0.0,0.0 0.659332275391,0.664688110352 0.659332275391,0.664688110351 c 0.0,0.0 -3.13299560547,2.82977294922 -3.13299560547,2.82977294922 c 0.0,0.0 29.0108337402,34.4080963135 29.0108337402,34.4080963135 c 0.0,0.0 42.8175811768,-34.3554534912 42.8175811768,-34.3554534912 c 0.0,0.0 -55.9664916992,-69.7423400879 -55.9664916992,-69.7423400879 Z" />
+            <group
+                android:name="cross" >
+                <path
+                    android:name="cross_1"
+                    android:pathData="M 4.44044494629,2.24310302734 c 0.0,0.0 0.0875396728516,0.112457275391 0.0875396728516,0.112457275391 "
+                    android:strokeColor="#FFFFFFFF"
+                    android:strokeAlpha="0"
+                    android:strokeWidth="3.5"
+                    android:fillColor="#00000000" />
+            </group>
+            <group
+                android:name="hotspot"
+                android:translateX="23.481"
+                android:translateY="18.71151" >
+                <group
+                    android:name="circles"
+                    android:translateX="-0.23909"
+                    android:translateY="-0.10807" >
+                    <path
+                        android:name="path_3"
+                        android:pathData="M -0.0042724609375,-2.64895629883 c -2.20922851562,0.0 -4.0,1.791015625 -4.0,4.0 c 0.0,2.20922851562 1.79077148438,4.0 4.0,4.0 c 2.208984375,0.0 4.0,-1.79077148438 4.0,-4.0 c 0.0,-2.208984375 -1.791015625,-4.0 -4.0,-4.0 Z M 11.9957275391,1.35104370117 c 0.0,-6.626953125 -5.373046875,-12.0 -12.0,-12.0 c -6.62719726562,0.0 -12.0,5.373046875 -12.0,12.0 c 0.0,4.43603515625 2.41381835938,8.30004882812 5.99194335938,10.3771972656 c 0.0,0.0 2.01586914062,-3.48217773438 2.01586914062,-3.48217773438 c -2.38500976562,-1.38500976562 -4.0078125,-3.93798828125 -4.0078125,-6.89501953125 c 0.0,-4.41796875 3.58178710938,-8.0 8.0,-8.0 c 4.41796875,0.0 8.0,3.58203125 8.0,8.0 c 0.0,2.95703125 -1.623046875,5.51000976562 -4.00805664062,6.89501953125 c 0.0,0.0 2.01586914062,3.48217773438 2.01586914062,3.48217773438 c 3.578125,-2.0771484375 5.9921875,-5.94116210938 5.9921875,-10.3771972656 Z M -0.0042724609375,-18.6489562988 c -11.0451660156,0.0 -20.0,8.9541015625 -20.0,20.0 c 0.0,7.39306640625 4.02099609375,13.8330078125 9.98779296875,17.2951660156 c 0.0,0.0 2.00219726562,-3.458984375 2.00219726562,-3.458984375 c -4.77319335938,-2.77001953125 -7.98999023438,-7.92211914062 -7.98999023438,-13.8361816406 c 0.0,-8.8369140625 7.16381835938,-16.0 16.0,-16.0 c 8.83595275879,0.0 16.0000152588,7.1630859375 16.0000152588,16.0 c 0.0,5.9140625 -3.21704101562,11.0661621094 -7.990234375,13.8361816406 c 0.0,0.0 2.00219726562,3.458984375 2.00219726563,3.458984375 c 5.966796875,-3.46215820312 9.98803710937,-9.90209960938 9.98803710938,-17.2951660156 c 0.0,-11.0458984375 -8.955078125,-20.0 -20.0000152588,-20.0 Z"
+                        android:fillColor="#FFFFFFFF"
+                        android:fillAlpha="1" />
+                </group>
+            </group>
+        </group>
+    </group>
+</vector>
diff --git a/res/layout/condition_card.xml b/res/layout/condition_card.xml
new file mode 100644
index 0000000..482d5b6
--- /dev/null
+++ b/res/layout/condition_card.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:background="?android:attr/colorAccent"
+    android:elevation="3dp"
+    android:clickable="true">
+
+    <LinearLayout
+        android:id="@+id/collapsed_group"
+        android:layout_width="match_parent"
+        android:layout_height="56dp"
+        android:orientation="horizontal"
+        android:gravity="center">
+
+        <ImageView
+            android:id="@android:id/icon"
+            android:layout_width="24dp"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="36dp"
+            android:tint="?android:attr/textColorPrimaryInverse" />
+
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:textColor="?android:attr/textColorPrimaryInverse" />
+
+        <ImageView
+            android:id="@+id/expand_indicator"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:tint="?android:attr/textColorPrimaryInverse"
+            android:padding="?android:attr/listPreferredItemPaddingEnd"
+            android:clickable="true"
+            android:background="?android:attr/selectableItemBackground" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/detail_group"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingStart="60dp"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@android:id/summary"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+            android:paddingBottom="16dp"
+            android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+            android:textColor="?android:attr/textColorPrimaryInverse" />
+
+        <!-- TODO: Better background -->
+        <View
+            android:layout_width="match_parent"
+            android:layout_height=".25dp"
+            android:background="@android:color/white" />
+
+        <com.android.internal.widget.ButtonBarLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingTop="8dp"
+            android:paddingBottom="8dp"
+            style="?attr/buttonBarStyle"
+            android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+            <Button
+                android:id="@+id/first_action"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingStart="0dp"
+                android:textColor="?android:attr/textColorPrimaryInverse"
+                style="?android:attr/buttonBarButtonStyle" />
+
+            <Button
+                android:id="@+id/second_action"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textColor="?android:attr/textColorPrimaryInverse"
+                style="?android:attr/buttonBarButtonStyle" />
+
+        </com.android.internal.widget.ButtonBarLayout>
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/dashboard.xml b/res/layout/dashboard.xml
index ae5be68..93261a53 100644
--- a/res/layout/dashboard.xml
+++ b/res/layout/dashboard.xml
@@ -14,7 +14,7 @@
      limitations under the License.
 -->
 
-<android.support.v7.widget.RecyclerView
+<com.android.settings.dashboard.conditional.FocusRecyclerView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/dashboard_container"
     android:layout_width="match_parent"
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 86030ff..539dc05 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -101,7 +101,7 @@
     <dimen name="dashboard_category_padding_end">0dp</dimen>
 
     <!-- Dashboard category panel elevation -->
-    <dimen name="dashboard_category_elevation">4dp</dimen>
+    <dimen name="dashboard_category_elevation">2dp</dimen>
 
     <!-- Dashboard category title layout height -->
     <dimen name="dashboard_category_title_height">48dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7120163..8d1f69a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -6691,4 +6691,20 @@
     <string name="default_organisation_name">organisation</string>
     <string name="default_admin_support_msg">Contact them to learn more.</string>
     <string name="list_of_administrators">List of administrators</string>
+
+    <!-- Turn off a conditional state of the device (e.g. airplane mode, or hotspot) [CHAR LIMIT=30] -->
+    <string name="condition_turn_off">Turn off</string>
+
+    <!-- Title of condition that hotspot is on [CHAR LIMIT=30] -->
+    <string name="condition_hotspot_title">Hotspot is on</string>
+
+    <!-- Summary of condition that hotspot is on [CHAR LIMIT=NONE] -->
+    <string name="condition_hotspot_summary">Portable Wi-Fi hotspot <xliff:g name="ap_name" example="AndroidAP">%1$s</xliff:g> is active, Wi-Fi for this device is turned off.</string>
+
+    <!-- Title of condition that airplane mode is on [CHAR LIMIT=30] -->
+    <string name="condition_airplane_title">Airplane mode is on</string>
+
+    <!-- Summary of condition that airplane mode is on [CHAR LIMIT=NONE] -->
+    <string name="condition_airplane_summary">Wi-Fi, Bluetooth, and cellular network are turned off. You can\'t make phone calls or connect to the Internet.</string>
+
 </resources>
diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java
index 5a54ba4..ddea92b 100644
--- a/src/com/android/settings/Utils.java
+++ b/src/com/android/settings/Utils.java
@@ -74,6 +74,7 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
+import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -83,8 +84,6 @@
 import android.widget.ListView;
 import android.widget.TabWidget;
 import com.android.internal.util.UserIcons;
-import com.android.settingslib.drawer.UserAdapter;
-import com.android.settingslib.drawer.UserAdapter.UserDetails;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -1058,5 +1057,11 @@
             return UserHandle.myUserId();
         }
     }
+
+    public static int resolveResource(Context context, int attr) {
+        TypedValue value = new TypedValue();
+        context.getTheme().resolveAttribute(attr, value, true);
+        return value.resourceId;
+    }
 }
 
diff --git a/src/com/android/settings/dashboard/DashboardAdapter.java b/src/com/android/settings/dashboard/DashboardAdapter.java
index 6228bbc..3e0b9dd 100644
--- a/src/com/android/settings/dashboard/DashboardAdapter.java
+++ b/src/com/android/settings/dashboard/DashboardAdapter.java
@@ -18,7 +18,6 @@
 import android.content.Context;
 import android.support.v7.widget.RecyclerView;
 import android.text.TextUtils;
-import android.util.Log;
 import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -26,40 +25,52 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 import com.android.internal.util.ArrayUtils;
+import com.android.settings.R;
 import com.android.settings.SettingsActivity;
+import com.android.settings.dashboard.conditional.Condition;
+import com.android.settings.dashboard.conditional.ConditionAdapterUtils;
 import com.android.settingslib.drawer.DashboardCategory;
 import com.android.settingslib.drawer.DashboardTile;
 
 import java.util.ArrayList;
 import java.util.List;
 
-public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder> {
+public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder> implements View.OnClickListener {
     public static final String TAG = "DashboardAdapter";
 
     private final List<Object> mItems = new ArrayList<>();
     private final List<Integer> mTypes = new ArrayList<>();
     private final List<Integer> mIds = new ArrayList<>();
 
-    private final List<DashboardCategory> mCategories;
     private final Context mContext;
 
+    private List<DashboardCategory> mCategories;
+    private List<Condition> mConditions;
+
     private boolean mIsShowingAll;
     // Used for counting items;
     private int mId;
 
-    public DashboardAdapter(Context context, List<DashboardCategory> categories) {
+    private Condition mExpandedCondition = null;
+
+    public DashboardAdapter(Context context) {
         mContext = context;
+
+        setHasStableIds(true);
+    }
+
+    public void setCategories(List<DashboardCategory> categories) {
         mCategories = categories;
 
         // TODO: Better place for tinting?
         TypedValue tintColor = new TypedValue();
-        context.getTheme().resolveAttribute(com.android.internal.R.attr.colorAccent,
+        mContext.getTheme().resolveAttribute(com.android.internal.R.attr.colorAccent,
                 tintColor, true);
         for (int i = 0; i < categories.size(); i++) {
             for (int j = 0; j < categories.get(i).tiles.size(); j++) {
                 DashboardTile tile = categories.get(i).tiles.get(j);
 
-                if (!context.getPackageName().equals(
+                if (!mContext.getPackageName().equals(
                         tile.intent.getComponent().getPackageName())) {
                     // If this drawable is coming from outside Settings, tint it to match the
                     // color.
@@ -67,9 +78,12 @@
                 }
             }
         }
+        setShowingAll(mIsShowingAll);
+    }
 
-        setShowingAll(false);
-        setHasStableIds(true);
+    public void setConditions(List<Condition> conditions) {
+        mConditions = conditions;
+        setShowingAll(mIsShowingAll);
     }
 
     public boolean isShowingAll() {
@@ -88,19 +102,21 @@
     public void setShowingAll(boolean showingAll) {
         mIsShowingAll = showingAll;
         reset();
-        countItem(null, com.android.settings.R.layout.dashboard_spacer, true);
-        for (int i = 0; i < mCategories.size(); i++) {
+        for (int i = 0; mConditions != null && i < mConditions.size(); i++) {
+            countItem(mConditions.get(i), R.layout.condition_card, mConditions.get(i).shouldShow());
+        }
+        countItem(null, R.layout.dashboard_spacer, true);
+        for (int i = 0; mCategories != null && i < mCategories.size(); i++) {
             DashboardCategory category = mCategories.get(i);
-            countItem(category, com.android.settings.R.layout.dashboard_category, mIsShowingAll);
+            countItem(category, R.layout.dashboard_category, mIsShowingAll);
             for (int j = 0; j < category.tiles.size(); j++) {
                 DashboardTile tile = category.tiles.get(j);
-                Log.d(TAG, "Maybe adding " + tile.intent.getComponent().getClassName());
-                countItem(tile, com.android.settings.R.layout.dashboard_tile, mIsShowingAll
+                countItem(tile, R.layout.dashboard_tile, mIsShowingAll
                         || ArrayUtils.contains(DashboardSummary.INITIAL_ITEMS,
                         tile.intent.getComponent().getClassName()));
             }
         }
-        countItem(null, com.android.settings.R.layout.see_all, true);
+        countItem(null, R.layout.see_all, true);
         notifyDataSetChanged();
     }
 
@@ -129,10 +145,10 @@
     @Override
     public void onBindViewHolder(DashboardItemHolder holder, int position) {
         switch (mTypes.get(position)) {
-            case com.android.settings.R.layout.dashboard_category:
+            case R.layout.dashboard_category:
                 onBindCategory(holder, (DashboardCategory) mItems.get(position));
                 break;
-            case com.android.settings.R.layout.dashboard_tile:
+            case R.layout.dashboard_tile:
                 final DashboardTile tile = (DashboardTile) mItems.get(position);
                 onBindTile(holder, tile);
                 holder.itemView.setOnClickListener(new View.OnClickListener() {
@@ -142,7 +158,7 @@
                     }
                 });
                 break;
-            case com.android.settings.R.layout.see_all:
+            case R.layout.see_all:
                 onBindSeeAll(holder);
                 holder.itemView.setOnClickListener(new View.OnClickListener() {
                     @Override
@@ -151,6 +167,16 @@
                     }
                 });
                 break;
+            case R.layout.condition_card:
+                ConditionAdapterUtils.bindViews((Condition) mItems.get(position), holder,
+                        mItems.get(position) == mExpandedCondition, this,
+                        new View.OnClickListener() {
+                            @Override
+                            public void onClick(View v) {
+                                onExpandClick(v);
+                            }
+                        });
+                break;
         }
     }
 
@@ -170,8 +196,8 @@
     }
 
     private void onBindSeeAll(DashboardItemHolder holder) {
-        holder.title.setText(mIsShowingAll ? com.android.settings.R.string.see_less
-                : com.android.settings.R.string.see_all);
+        holder.title.setText(mIsShowingAll ? R.string.see_less
+                : R.string.see_all);
     }
 
     @Override
@@ -189,10 +215,38 @@
         return mIds.size();
     }
 
+    @Override
+    public void onClick(View v) {
+        if (v.getTag() == mExpandedCondition) {
+            mExpandedCondition.onPrimaryClick();
+        } else {
+            mExpandedCondition = (Condition) v.getTag();
+            notifyDataSetChanged();
+        }
+    }
+
+    public void onExpandClick(View v) {
+        if (v.getTag() == mExpandedCondition) {
+            mExpandedCondition = null;
+        } else {
+            mExpandedCondition = (Condition) v.getTag();
+        }
+        notifyDataSetChanged();
+    }
+
+    public Object getItem(long itemId) {
+        for (int i = 0; i < mIds.size(); i++) {
+            if (mIds.get(i) == itemId) {
+                return mItems.get(i);
+            }
+        }
+        return null;
+    }
+
     public static class DashboardItemHolder extends RecyclerView.ViewHolder {
-        private final ImageView icon;
-        private final TextView title;
-        private final TextView summary;
+        public final ImageView icon;
+        public final TextView title;
+        public final TextView summary;
 
         public DashboardItemHolder(View itemView) {
             super(itemView);
diff --git a/src/com/android/settings/dashboard/DashboardSummary.java b/src/com/android/settings/dashboard/DashboardSummary.java
index 64eb356..95170a4 100644
--- a/src/com/android/settings/dashboard/DashboardSummary.java
+++ b/src/com/android/settings/dashboard/DashboardSummary.java
@@ -18,7 +18,6 @@
 
 import android.os.Bundle;
 import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -31,13 +30,17 @@
 import com.android.settings.R;
 import com.android.settings.Settings;
 import com.android.settings.SettingsActivity;
+import com.android.settings.dashboard.conditional.ConditionAdapterUtils;
+import com.android.settings.dashboard.conditional.ConditionManager;
+import com.android.settings.dashboard.conditional.FocusRecyclerView;
 import com.android.settingslib.drawer.DashboardCategory;
 import com.android.settingslib.drawer.SettingsDrawerActivity;
 
 import java.util.List;
 
 public class DashboardSummary extends InstrumentedFragment
-        implements SettingsDrawerActivity.CategoryListener {
+        implements SettingsDrawerActivity.CategoryListener, ConditionManager.ConditionListener,
+        FocusRecyclerView.FocusListener {
     public static final boolean DEBUG = false;
     private static final boolean DEBUG_TIMING = false;
     private static final String TAG = "DashboardSummary";
@@ -51,11 +54,10 @@
             Settings.StorageSettingsActivity.class.getName(),
     };
 
-    private static final int MSG_REBUILD_UI = 1;
-
-    private RecyclerView mDashboard;
+    private FocusRecyclerView mDashboard;
     private DashboardAdapter mAdapter;
     private SummaryLoader mSummaryLoader;
+    private ConditionManager mConditionManager;
 
     @Override
     protected int getMetricsCategory() {
@@ -73,6 +75,7 @@
         setHasOptionsMenu(true);
         if (DEBUG_TIMING) Log.d(TAG, "onCreate took " + (System.currentTimeMillis() - startTime)
                 + " ms");
+        mConditionManager = ConditionManager.get(getContext());
     }
 
     @Override
@@ -95,6 +98,7 @@
 
         ((SettingsDrawerActivity) getActivity()).addCategoryListener(this);
         mSummaryLoader.setListening(true);
+        Log.d(TAG, "onResume");
     }
 
     @Override
@@ -106,6 +110,16 @@
     }
 
     @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        if (hasWindowFocus) {
+            mConditionManager.addListener(this);
+            mConditionManager.refreshAll();
+        } else {
+            mConditionManager.remListener(this);
+        }
+    }
+
+    @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
         return inflater.inflate(R.layout.dashboard, container, false);
@@ -113,11 +127,16 @@
 
     @Override
     public void onViewCreated(View view, Bundle bundle) {
-        mDashboard = (RecyclerView) view.findViewById(R.id.dashboard_container);
+        mDashboard = (FocusRecyclerView) view.findViewById(R.id.dashboard_container);
         LinearLayoutManager llm = new LinearLayoutManager(getContext());
         llm.setOrientation(LinearLayoutManager.VERTICAL);
         mDashboard.setLayoutManager(llm);
         mDashboard.setHasFixedSize(true);
+        mDashboard.setListener(this);
+        mAdapter = new DashboardAdapter(getContext());
+        mAdapter.setConditions(mConditionManager.getConditions());
+        mSummaryLoader.setAdapter(mAdapter);
+        ConditionAdapterUtils.addDismiss(mDashboard);
 
         rebuildUI();
     }
@@ -132,10 +151,7 @@
         // TODO: Cache summaries from old categories somehow.
         List<DashboardCategory> categories =
                 ((SettingsActivity) getActivity()).getDashboardCategories();
-        boolean showingAll = mAdapter != null && mAdapter.isShowingAll();
-        mAdapter = new DashboardAdapter(getContext(), categories);
-        mSummaryLoader.setAdapter(mAdapter);
-        mAdapter.setShowingAll(showingAll);
+        mAdapter.setCategories(categories);
         mDashboard.setAdapter(mAdapter);
 
         long delta = System.currentTimeMillis() - start;
@@ -146,4 +162,10 @@
     public void onCategoriesChanged() {
         rebuildUI();
     }
+
+    @Override
+    public void onConditionsChanged() {
+        Log.d(TAG, "onConditionsChanged");
+        mAdapter.setConditions(mConditionManager.getConditions());
+    }
 }
diff --git a/src/com/android/settings/dashboard/conditional/AirplaneModeCondition.java b/src/com/android/settings/dashboard/conditional/AirplaneModeCondition.java
new file mode 100644
index 0000000..b839631
--- /dev/null
+++ b/src/com/android/settings/dashboard/conditional/AirplaneModeCondition.java
@@ -0,0 +1,95 @@
+/*
+ * 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.settings.dashboard.conditional;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Icon;
+import android.net.ConnectivityManager;
+import com.android.settings.R;
+import com.android.settings.Settings;
+import com.android.settingslib.WirelessUtils;
+
+public class AirplaneModeCondition extends Condition {
+
+    public AirplaneModeCondition(ConditionManager conditionManager) {
+        super(conditionManager);
+    }
+
+    @Override
+    public void refreshState() {
+        setActive(WirelessUtils.isAirplaneModeOn(mManager.getContext()));
+    }
+
+    @Override
+    protected void onSilenceChanged(boolean silenced) {
+        // Only need to listen for airplane mode changes when its been silenced.
+        PackageManager pm = mManager.getContext().getPackageManager();
+        pm.setComponentEnabledSetting(new ComponentName(mManager.getContext(), Receiver.class),
+                silenced ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+                        : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+                PackageManager.DONT_KILL_APP);
+    }
+
+    @Override
+    public Icon getIcon() {
+        return Icon.createWithResource(mManager.getContext(), R.drawable.ic_airplane);
+    }
+
+    @Override
+    public CharSequence getTitle() {
+        return mManager.getContext().getString(R.string.condition_airplane_title);
+    }
+
+    @Override
+    public CharSequence getSummary() {
+        return mManager.getContext().getString(R.string.condition_airplane_summary);
+    }
+
+    @Override
+    public CharSequence[] getActions() {
+        return new CharSequence[] { mManager.getContext().getString(R.string.condition_turn_off) };
+    }
+
+    @Override
+    public void onPrimaryClick() {
+        mManager.getContext().startActivity(new Intent(mManager.getContext(),
+                Settings.WirelessSettings.class));
+    }
+
+    @Override
+    public void onActionClick(int index) {
+        if (index == 0) {
+            ConnectivityManager.from(mManager.getContext()).setAirplaneMode(false);
+            setActive(false);
+        } else {
+            throw new IllegalArgumentException("Unexpected index " + index);
+        }
+    }
+
+    public static class Receiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction())) {
+                ConditionManager.get(context).getCondition(AirplaneModeCondition.class)
+                        .refreshState();
+            }
+        }
+    }
+}
diff --git a/src/com/android/settings/dashboard/conditional/Condition.java b/src/com/android/settings/dashboard/conditional/Condition.java
new file mode 100644
index 0000000..7a6fed5
--- /dev/null
+++ b/src/com/android/settings/dashboard/conditional/Condition.java
@@ -0,0 +1,107 @@
+/*
+ * 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.settings.dashboard.conditional;
+
+import android.graphics.drawable.Icon;
+import android.os.PersistableBundle;
+import android.util.Log;
+
+public abstract class Condition {
+
+    private static final String KEY_SILENCE = "silence";
+    private static final String KEY_ACTIVE = "active";
+    private static final String KEY_LAST_STATE = "last_state";
+
+    protected final ConditionManager mManager;
+
+    private boolean mIsSilenced;
+    private boolean mIsActive;
+    private long mLastStateChange;
+
+    public Condition(ConditionManager manager) {
+        mManager = manager;
+    }
+
+    void restoreState(PersistableBundle bundle) {
+        mIsSilenced = bundle.getBoolean(KEY_SILENCE);
+        mIsActive = bundle.getBoolean(KEY_ACTIVE);
+        mLastStateChange = bundle.getLong(KEY_LAST_STATE);
+    }
+
+    void saveState(PersistableBundle bundle) {
+        bundle.putBoolean(KEY_SILENCE, mIsSilenced);
+        bundle.putBoolean(KEY_ACTIVE, mIsActive);
+        bundle.putLong(KEY_LAST_STATE, mLastStateChange);
+    }
+
+    protected void notifyChanged() {
+        mManager.notifyChanged(this);
+    }
+
+    public boolean isSilenced() {
+        return mIsSilenced;
+    }
+
+    public boolean isActive() {
+        return mIsActive;
+    }
+
+    protected void setActive(boolean active) {
+        if (mIsActive == active) {
+            return;
+        }
+        mIsActive = active;
+        mLastStateChange = System.currentTimeMillis();
+        if (mIsSilenced && !active) {
+            mIsSilenced = false;
+            onSilenceChanged(mIsSilenced);
+        }
+        notifyChanged();
+    }
+
+    public void silence() {
+        if (!mIsSilenced) {
+            mIsSilenced = true;
+            onSilenceChanged(mIsSilenced);
+            notifyChanged();
+        }
+    }
+
+    protected void onSilenceChanged(boolean state) {
+        // Optional enable/disable receivers based on silence state.
+    }
+
+    public boolean shouldShow() {
+        return isActive() && !isSilenced();
+    }
+
+    long getLastChange() {
+        return mLastStateChange;
+    }
+
+    // State.
+    public abstract void refreshState();
+
+    // UI.
+    public abstract Icon getIcon();
+    public abstract CharSequence getTitle();
+    public abstract CharSequence getSummary();
+    public abstract CharSequence[] getActions();
+
+    public abstract void onPrimaryClick();
+    public abstract void onActionClick(int index);
+}
diff --git a/src/com/android/settings/dashboard/conditional/ConditionAdapterUtils.java b/src/com/android/settings/dashboard/conditional/ConditionAdapterUtils.java
new file mode 100644
index 0000000..e390782
--- /dev/null
+++ b/src/com/android/settings/dashboard/conditional/ConditionAdapterUtils.java
@@ -0,0 +1,93 @@
+/*
+ * 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.settings.dashboard.conditional;
+
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.helper.ItemTouchHelper;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import com.android.settings.R;
+import com.android.settings.dashboard.DashboardAdapter;
+
+public class ConditionAdapterUtils {
+
+    public static void addDismiss(final RecyclerView recyclerView) {
+        ItemTouchHelper.SimpleCallback callback = new ItemTouchHelper.SimpleCallback(0,
+                ItemTouchHelper.START | ItemTouchHelper.END) {
+            @Override
+            public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
+                    RecyclerView.ViewHolder target) {
+                return true;
+            }
+
+            @Override
+            public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
+                return viewHolder.getItemViewType() == R.layout.condition_card
+                        ? super.getSwipeDirs(recyclerView, viewHolder) : 0;
+            }
+
+            @Override
+            public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
+                DashboardAdapter adapter = (DashboardAdapter) recyclerView.getAdapter();
+                Object item = adapter.getItem(viewHolder.getItemId());
+                if (item instanceof Condition) {
+                    ((Condition) item).silence();
+                }
+            }
+        };
+        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
+        itemTouchHelper.attachToRecyclerView(recyclerView);
+    }
+
+    public static void bindViews(final Condition condition,
+            DashboardAdapter.DashboardItemHolder view, boolean isExpanded,
+            View.OnClickListener onClickListener, View.OnClickListener onExpandListener) {
+        view.itemView.setTag(condition);
+        view.itemView.setOnClickListener(onClickListener);
+        view.icon.setImageIcon(condition.getIcon());
+        view.title.setText(condition.getTitle());
+        ImageView expand = (ImageView) view.itemView.findViewById(R.id.expand_indicator);
+        expand.setTag(condition);
+        expand.setImageResource(isExpanded ? R.drawable.ic_expand_less : R.drawable.ic_expand_more);
+        expand.setOnClickListener(onExpandListener);
+
+        View detailGroup = view.itemView.findViewById(R.id.detail_group);
+        // TODO: Animate expand/collapse
+        detailGroup.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
+        if (isExpanded) {
+            view.summary.setText(condition.getSummary());
+            CharSequence[] actions = condition.getActions();
+            for (int i = 0; i < 2; i++) {
+                Button button = (Button) detailGroup.findViewById(i == 0
+                        ? R.id.first_action : R.id.second_action);
+                if (actions.length > i) {
+                    button.setVisibility(View.VISIBLE);
+                    button.setText(actions[i]);
+                    final int index = i;
+                    button.setOnClickListener(new View.OnClickListener() {
+                        @Override
+                        public void onClick(View v) {
+                            condition.onActionClick(index);
+                        }
+                    });
+                } else {
+                    button.setVisibility(View.GONE);
+                }
+            }
+        }
+    }
+}
diff --git a/src/com/android/settings/dashboard/conditional/ConditionManager.java b/src/com/android/settings/dashboard/conditional/ConditionManager.java
new file mode 100644
index 0000000..3ef4f5b
--- /dev/null
+++ b/src/com/android/settings/dashboard/conditional/ConditionManager.java
@@ -0,0 +1,220 @@
+/*
+ * 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.settings.dashboard.conditional;
+
+import android.content.Context;
+import android.os.PersistableBundle;
+import android.util.Log;
+import android.util.Xml;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class ConditionManager {
+
+    private static final String TAG = "ConditionManager";
+
+    private static final boolean DEBUG = true;
+
+    private static final String FILE_NAME = "condition_state.xml";
+    private static final String TAG_CONDITIONS = "conditions";
+    private static final String TAG_CONDITION = "condition";
+    private static final String ATTR_CLASS = "class";
+
+    private static ConditionManager sInstance;
+
+    private final Context mContext;
+    private final ArrayList<Condition> mConditions;
+    private final File mXmlFile;
+
+    private final ArrayList<ConditionListener> mListeners = new ArrayList<>();
+
+    private ConditionManager(Context context) {
+        mContext = context;
+        mConditions = new ArrayList<Condition>();
+        mXmlFile = new File(context.getFilesDir(), FILE_NAME);
+        if (mXmlFile.exists()) {
+            readFromXml();
+        }
+        addMissingConditions();
+    }
+
+    public void refreshAll() {
+        final int N = mConditions.size();
+        for (int i = 0; i < N; i++) {
+            mConditions.get(i).refreshState();
+        }
+    }
+
+    private void readFromXml() {
+        if (DEBUG) Log.d(TAG, "Reading from " + mXmlFile.toString());
+        try {
+            XmlPullParser parser = Xml.newPullParser();
+            FileReader in = new FileReader(mXmlFile);
+            parser.setInput(in);
+            int state = parser.getEventType();
+
+            while (state != XmlPullParser.END_DOCUMENT) {
+                if (TAG_CONDITION.equals(parser.getName())) {
+                    int depth = parser.getDepth();
+                    String clz = parser.getAttributeValue("", ATTR_CLASS);
+                    Condition condition = createCondition(Class.forName(clz));
+                    PersistableBundle bundle = PersistableBundle.restoreFromXml(parser);
+                    if (DEBUG) Log.d(TAG, "Reading " + clz + " -- " + bundle);
+                    condition.restoreState(bundle);
+                    mConditions.add(condition);
+                    while (parser.getDepth() > depth) {
+                        parser.next();
+                    }
+                }
+                state = parser.next();
+            }
+            in.close();
+        } catch (XmlPullParserException | IOException | ClassNotFoundException e) {
+            Log.w(TAG, "Problem reading " + FILE_NAME, e);
+        }
+    }
+
+    private void saveToXml() {
+        if (DEBUG) Log.d(TAG, "Writing to " + mXmlFile.toString());
+        try {
+            XmlSerializer serializer = Xml.newSerializer();
+            FileWriter writer = new FileWriter(mXmlFile);
+            serializer.setOutput(writer);
+
+            serializer.startDocument("UTF-8", true);
+            serializer.startTag("", TAG_CONDITIONS);
+
+            final int N = mConditions.size();
+            for (int i = 0; i < N; i++) {
+                serializer.startTag("", TAG_CONDITION);
+                serializer.attribute("", ATTR_CLASS, mConditions.get(i).getClass().getName());
+                PersistableBundle bundle = new PersistableBundle();
+                mConditions.get(i).saveState(bundle);
+                bundle.saveToXml(serializer);
+                serializer.endTag("", TAG_CONDITION);
+            }
+
+            serializer.endTag("", TAG_CONDITIONS);
+            serializer.flush();
+            writer.close();
+        } catch (XmlPullParserException | IOException e) {
+            Log.w(TAG, "Problem writing " + FILE_NAME, e);
+        }
+    }
+
+    private void addMissingConditions() {
+        addIfMissing(AirplaneModeCondition.class);
+        addIfMissing(HotspotCondition.class);
+    }
+
+    private void addIfMissing(Class<? extends Condition> clz) {
+        if (getCondition(clz) == null) {
+            if (DEBUG) Log.d(TAG, "Adding missing " + clz.getName());
+            mConditions.add(createCondition(clz));
+        }
+    }
+
+    private Condition createCondition(Class<?> clz) {
+        if (AirplaneModeCondition.class == clz) {
+            return new AirplaneModeCondition(this);
+        } else if (HotspotCondition.class == clz) {
+            return new HotspotCondition(this);
+        }
+        try {
+            Constructor<?> constructor = clz.getConstructor(ConditionManager.class);
+            return (Condition) constructor.newInstance(this);
+        } catch (NoSuchMethodException | IllegalAccessException | InstantiationException
+                | InvocationTargetException e) {
+        }
+        return null;
+    }
+
+    Context getContext() {
+        return mContext;
+    }
+
+    public <T extends Condition> T getCondition(Class<T> clz) {
+        final int N = mConditions.size();
+        for (int i = 0; i < N; i++) {
+            if (clz.equals(mConditions.get(i).getClass())) {
+                return (T) mConditions.get(i);
+            }
+        }
+        return null;
+    }
+
+    public List<Condition> getConditions() {
+        return mConditions;
+    }
+
+    public List<Condition> getVisibleConditions() {
+        List<Condition> conditions = new ArrayList<>();
+        final int N = mConditions.size();
+        for (int i = 0; i < N; i++) {
+            if (mConditions.get(i).shouldShow()) {
+                conditions.add(mConditions.get(i));
+            }
+        }
+        Collections.sort(conditions, CONDITION_COMPARATOR);
+        return conditions;
+    }
+
+    public void notifyChanged(Condition condition) {
+        saveToXml();
+        final int N = mListeners.size();
+        for (int i = 0; i < N; i++) {
+            mListeners.get(i).onConditionsChanged();
+        }
+    }
+
+    public void addListener(ConditionListener listener) {
+        mListeners.add(listener);
+    }
+
+    public void remListener(ConditionListener listener) {
+        mListeners.remove(listener);
+    }
+
+    public static ConditionManager get(Context context) {
+        if (sInstance == null) {
+            sInstance = new ConditionManager(context);
+        }
+        return sInstance;
+    }
+
+    public interface ConditionListener {
+        void onConditionsChanged();
+    }
+
+    private static final Comparator<Condition> CONDITION_COMPARATOR = new Comparator<Condition>() {
+        @Override
+        public int compare(Condition lhs, Condition rhs) {
+            return Long.compare(lhs.getLastChange(), rhs.getLastChange());
+        }
+    };
+}
diff --git a/src/com/android/settings/dashboard/conditional/FocusRecyclerView.java b/src/com/android/settings/dashboard/conditional/FocusRecyclerView.java
new file mode 100644
index 0000000..af51ed5
--- /dev/null
+++ b/src/com/android/settings/dashboard/conditional/FocusRecyclerView.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 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.settings.dashboard.conditional;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+
+/**
+ * Version of RecyclerView that can have listeners for onWindowFocusChanged.
+ */
+public class FocusRecyclerView extends RecyclerView {
+
+    private FocusListener mListener;
+
+    public FocusRecyclerView(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+        if (mListener != null) {
+            mListener.onWindowFocusChanged(hasWindowFocus);
+        }
+    }
+
+    public void setListener(FocusListener listener) {
+        mListener = listener;
+    }
+
+    public interface FocusListener {
+        void onWindowFocusChanged(boolean hasWindowFocus);
+    }
+}
diff --git a/src/com/android/settings/dashboard/conditional/HotspotCondition.java b/src/com/android/settings/dashboard/conditional/HotspotCondition.java
new file mode 100644
index 0000000..0f185c6
--- /dev/null
+++ b/src/com/android/settings/dashboard/conditional/HotspotCondition.java
@@ -0,0 +1,111 @@
+/*
+ * 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.settings.dashboard.conditional;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Icon;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import com.android.settings.R;
+import com.android.settings.TetherSettings;
+import com.android.settings.Utils;
+import com.android.settingslib.TetherUtil;
+
+public class HotspotCondition extends Condition {
+
+    private final WifiManager mWifiManager;
+
+    public HotspotCondition(ConditionManager manager) {
+        super(manager);
+        mWifiManager = mManager.getContext().getSystemService(WifiManager.class);
+    }
+
+    @Override
+    public void refreshState() {
+        boolean wifiTetherEnabled = TetherUtil.isWifiTetherEnabled(mManager.getContext());
+        setActive(wifiTetherEnabled);
+    }
+
+    @Override
+    protected void onSilenceChanged(boolean silenced) {
+        // Only need to listen for hotspot changes when hotspot has been silenced.
+        PackageManager pm = mManager.getContext().getPackageManager();
+        pm.setComponentEnabledSetting(new ComponentName(mManager.getContext(), Receiver.class),
+                silenced ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+                        : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+                PackageManager.DONT_KILL_APP);
+    }
+
+    @Override
+    public Icon getIcon() {
+        return Icon.createWithResource(mManager.getContext(), R.drawable.ic_hotspot);
+    }
+
+    private String getSsid() {
+        WifiConfiguration wifiConfig = mWifiManager.getWifiApConfiguration();
+        if (wifiConfig == null) {
+            return mManager.getContext().getString(
+                    com.android.internal.R.string.wifi_tether_configure_ssid_default);
+        } else {
+            return wifiConfig.SSID;
+        }
+    }
+
+    @Override
+    public CharSequence getTitle() {
+        return mManager.getContext().getString(R.string.condition_hotspot_title);
+    }
+
+    @Override
+    public CharSequence getSummary() {
+        return mManager.getContext().getString(R.string.condition_hotspot_summary, getSsid());
+    }
+
+    @Override
+    public CharSequence[] getActions() {
+        return new CharSequence[] { mManager.getContext().getString(R.string.condition_turn_off) };
+    }
+
+    @Override
+    public void onPrimaryClick() {
+        Utils.startWithFragment(mManager.getContext(), TetherSettings.class.getName(), null, null,
+                0, R.string.tether_settings_title_all, null);
+    }
+
+    @Override
+    public void onActionClick(int index) {
+        if (index == 0) {
+            TetherUtil.setWifiTethering(false, mManager.getContext());
+            setActive(false);
+        } else {
+            throw new IllegalArgumentException("Unexpected index " + index);
+        }
+    }
+
+    public static class Receiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (WifiManager.WIFI_AP_STATE_CHANGED_ACTION.equals(intent.getAction())) {
+                ConditionManager.get(context).getCondition(HotspotCondition.class)
+                        .refreshState();
+            }
+        }
+    }
+}